/*
 * Copyright (C) 2012 The CyanogenMod Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cyanogenmod.filemanager.ui.widgets;

import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.ScrollingMovementMethod;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.cyanogenmod.filemanager.R;
import com.cyanogenmod.filemanager.ui.ThemeManager;
import com.cyanogenmod.filemanager.ui.ThemeManager.Theme;
import com.cyanogenmod.filemanager.util.DialogHelper;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A widget that performs an inline autocomplete for an {@link EditText}
 * control.
 */
public class InlineAutocompleteTextView extends RelativeLayout
            implements View.OnClickListener, View.OnLongClickListener,
            View.OnKeyListener {

    /**
     * An interface to communicate events when a text value is changed.
     */
    public interface OnTextChangedListener {
        /**
         * Method invoked when the value of the initial directory was changed.
         *
         * @param newValue The new value of the text
         * @param currentFilterData The current set of string for filter
         */
        void onTextChanged(String newValue, List<String> currentFilterData);
    }

    private List<String> mData;
    private OnTextChangedListener mOnTextChangedListener;

    private EditText mBackgroundText;
    private EditText mForegroundText;
    private String mCompletionString = null;

    private int mFilter = 0;
    private FilteredTextWatcher mTextWatcher;

    /**
     * Constructor of <code>InlineAutocompleteTextView</code>.
     *
     * @param context The current context
     */
    public InlineAutocompleteTextView(Context context) {
        super(context);
        init();
    }

    /**
     * Constructor of <code>InlineAutocompleteTextView</code>.
     *
     * @param context The current context
     * @param attrs The attributes of the XML tag that is inflating the view.
     */
    public InlineAutocompleteTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * Constructor of <code>InlineAutocompleteTextView</code>.
     *
     * @param context The current context
     * @param attrs The attributes of the XML tag that is inflating the view.
     * @param defStyle The default style to apply to this view. If 0, no style
     *        will be applied (beyond what is included in the theme). This may
     *        either be an attribute resource, whose value will be retrieved
     *        from the current theme, or an explicit style resource.
     */
    public InlineAutocompleteTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    /**
     * Method that initializes the view. This method loads all the necessary
     * information and create an appropriate layout for the view
     */
    private void init() {
        //Initialize data
        this.mData = new ArrayList<String>();
        this.mOnTextChangedListener = null;
        this.mTextWatcher = new FilteredTextWatcher();

        //Inflate the view
        ViewGroup v = (ViewGroup)inflate(getContext(), R.layout.inline_autocomplete, null);
        addView(v);

        //Retrieve views
        this.mBackgroundText = (EditText)findViewById(R.id.inline_autocomplete_bg_text);
        this.mBackgroundText.setOnKeyListener(this);
        this.mForegroundText = (EditText)findViewById(R.id.inline_autocomplete_fg_text);
        this.mForegroundText.setMovementMethod(new ScrollingMovementMethod());
        this.mForegroundText.addTextChangedListener(this.mTextWatcher);
        this.mForegroundText.setOnKeyListener(this);
        this.mForegroundText.requestFocus();
        this.mForegroundText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView view, int actionId, KeyEvent event) {
                doDone(true);
                return false;
            }
        });

        View button = findViewById(R.id.inline_autocomplete_button_tab);
        button.setOnClickListener(this);
        button.setOnLongClickListener(this);

        // Apply the theme
        applyTheme();

        //Initialize text views
        setText(""); //$NON-NLS-1$
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        // Show the soft keyboard (only if the device has't a hardware keyboard)
        Configuration config = getContext().getResources().getConfiguration();
        if (config.keyboard == Configuration.KEYBOARD_NOKEYS) {
            Activity activity = (Activity)getContext();
            InputMethodManager imm =
                    (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.showSoftInputFromInputMethod(
                    this.mForegroundText.getWindowToken(), 0);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        // Hide the soft keyboard
        Configuration config = getContext().getResources().getConfiguration();
        if (config.keyboard == Configuration.KEYBOARD_NOKEYS) {
            Activity activity = (Activity)getContext();
            InputMethodManager imm =
                    (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.hideSoftInputFromWindow(this.mForegroundText.getWindowToken(), 0);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_ENTER:
            case KeyEvent.FLAG_EDITOR_ACTION:
                //Consume the event
                doDone(false);
                return true;
            case KeyEvent.KEYCODE_TAB:
                //Do tab, and consume the event
                doTab();
                return true;
            default:
                break;
        }
        return false;
    }

    /**
     * Method that sets the current text value.
     *
     * @param value The value to set to current text value
     */
    public void setText(String value) {
        this.mBackgroundText.setVisibility(View.INVISIBLE);
        this.mForegroundText.setText(value);
        this.mBackgroundText.setText(""); //$NON-NLS-1$
        onTextChanged(value);
        this.mForegroundText.requestFocus();
        this.mForegroundText.setSelection(value.length());
        int lines = this.mBackgroundText.getLineCount();
        this.mForegroundText.setLines(lines <= 0 ? 1 : lines);
    }

    /**
     * Method that returns the current text value.
     *
     * @return The current text value
     */
    public String getText() {
        return this.mForegroundText.getText().toString();
    }

    /**
     * Method that sets the listener for send text change events.
     *
     * @param onTextChangedListener The text changed listener
     */
    public void setOnTextChangedListener(OnTextChangedListener onTextChangedListener) {
        this.mOnTextChangedListener = onTextChangedListener;
    }

    /**
     * Method that returns a string for autocomplete.
     *
     * @return String The autocomplete string
     */
    public String getCompletionString() {
        return this.mCompletionString;
    }

    /**
     * Method that sets a string for autocomplete.
     *
     * @param completionString The autocomplete string
     */
    public void setCompletionString(String completionString) {
        this.mCompletionString = completionString;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.inline_autocomplete_button_tab:
                doTab();
                break;
            default:
                break;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onLongClick(View v) {
        switch (v.getId()) {
            case R.id.inline_autocomplete_button_tab:
                //Complete with current suggestion
                String filter = this.mBackgroundText.getText().toString();
                if (this.mBackgroundText.getVisibility() == View.VISIBLE && filter.length() > 0) {
                    setText(filter);
                } else {
                    post(new Runnable() {
                        @Override
                        public void run() {
                            DialogHelper.showToast(
                                    getContext(),
                                    R.string.inline_autocomplete_tab_nothing_to_complete_msg,
                                    Toast.LENGTH_SHORT);
                        }
                    });
                }
                return true;

            default:
                break;
        }
        return false;
    }

    /**
     * Method that need to be invoked when a string was changed
     *
     * @param value The new string
     * @hide
     */
    void onTextChanged(String value) {
        this.mFilter = 0;
        if (this.mOnTextChangedListener != null) {
            //Communicate the change
            this.mOnTextChangedListener.onTextChanged(
                                value.toString(), this.mData);

            if (this.mCompletionString != null &&
                    !value.toString().endsWith(this.mCompletionString)) {
                //Autocomplete
                autocomplete();
            } else {
                this.mBackgroundText.setVisibility(View.INVISIBLE);
            }
        }
    }

    /**
     * Method that autocompletes the text, showing the best matches.
     */
    private void autocomplete() {
        final String currentText = getText();
        if (!this.mData.isEmpty()) {
            Iterator<String> it = this.mData.iterator();
            while (it.hasNext()) {
                String filterData = it.next();
                if (filterData.startsWith(currentText)) {
                    this.mBackgroundText.setText(filterData);
                    this.mBackgroundText.setVisibility(View.VISIBLE);
                    int lines = this.mBackgroundText.getLineCount();
                    this.mForegroundText.setLines(lines <= 0 ? 1 : lines);
                    return;
                }
            }
        }
        this.mBackgroundText.setVisibility(View.INVISIBLE);
    }

    /**
     * Method invoked when a tab key event is requested (button or keyboard)
     * @hide
     */
    void doTab() {
        //Complete with current text
        String current = this.mForegroundText.getText().toString();
        if (current.length() == 0) {
            return;
        }

        //Get the data
        List<String> filteredData = filter(this.mData, current);
        if (filteredData.size() <= this.mFilter) {
            this.mFilter = 0;
        }
        if (filteredData.size() == 1 && this.mFilter == 0) {
            //Autocomplete with the only autocomplete option
            setText(filteredData.get(this.mFilter));
        } else {
            //Show the autocomplete options
            if (filteredData.size() > 0) {
                this.mBackgroundText.setText(filteredData.get(this.mFilter));
                this.mBackgroundText.setVisibility(View.VISIBLE);
                this.mFilter++;
            }
        }
    }

    /**
     * Method that creates a temporary filter based in the current text
     *
     * @param data The global data array
     * @param current The current text
     * @return The filtered data array
     */
    private static List<String> filter(List<String> data, String current) {
        List<String> filter = new ArrayList<String>(data);
        int size = filter.size();
        for (int i = size-1; i >= 0; i--) {
            String s = filter.get(i);
            if (!s.startsWith(current)) {
                filter.remove(i);
            }
        }
        return filter;
    }

    /**
     * Method invoked when a enter key event is requested (button or keyboard)
     *
     * @param fromEditorAction It this method was invoked from editor action
     * @hide
     */
    void doDone(boolean fromEditorAction) {
        if (fromEditorAction) {
            // Hide the soft keyboard
            Configuration config = getContext().getResources().getConfiguration();
            if (config.keyboard == Configuration.KEYBOARD_NOKEYS) {
                Activity activity = (Activity)getContext();
                InputMethodManager imm =
                        (InputMethodManager) activity.getSystemService(
                                                Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(this.mForegroundText.getWindowToken(), 0);
            }
        }
    }

    /**
     * A class for filter the text introduced by the user
     */
    private class FilteredTextWatcher implements TextWatcher {

        private String mText;
        private int mStart;
        private int mCount;

        public FilteredTextWatcher() {/**NON BLOCK**/}

        /**
         * {@inheritDoc}
         */
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            /**NON BLOCK**/
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            this.mStart = start;
            this.mCount = count;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void afterTextChanged(Editable s) {
            // Enter and Tab are not allowed, and have their own treatment
            final String orig = s.toString();
            String text = orig;
            final int start = this.mStart;
            final int count = this.mCount;

            // Avoid recursive calls
            if (this.mText != null && this.mText.compareTo(orig) == 0) {
                return;
            }

            // Force to ignore Tab and Enter keys
            text = replaceKeyEvent(text, '\t', true);
            text = replaceKeyEvent(text, '\r', false);
            text = replaceKeyEvent(text, '\n', false);
            if (text.compareTo(orig) != 0) {
                // Some key has internally changed
                this.mText = text;
                s.replace(0, orig.length(), text);
            }

            // Does the user input enter or tab keys?
            boolean tab = false;
            boolean enter = false;
            String userInput = orig.substring(start, start + count);
            tab = (userInput.indexOf("\t") != -1); //$NON-NLS-1$
            enter = (userInput.indexOf("\r") != -1 ||  //$NON-NLS-1$
                     userInput.indexOf("\n") != -1); //$NON-NLS-1$

            // Check events
            if (enter) {
                // Broadcast enter event
                doDone(false);
                return;
            }
            if (tab) {
                // Broadcast enter event
                doTab();
                return;
            }

            // Broadcast the new text value
            InlineAutocompleteTextView.this.onTextChanged(text);
        }

        /**
         * Method that replace a key char
         *
         * @param value The string in which search
         * @param key The key char to search
         * @param removeAfter If true the string will truncate after the first key char found
         * @return String The replaced string
         */
        private String replaceKeyEvent(String value, char key, boolean removeAfter) {
            String text = value;
            int pos = text.indexOf(key);
            while (pos != -1) {
                if (removeAfter) {
                    text = text.substring(0, pos);
                } else {
                    text = text.replaceAll(String.valueOf(key), ""); //$NON-NLS-1$
                }
                pos = text.indexOf(key);
            }
            return text;
        }
    }

    /**
     * Method that applies the current theme to the widget components
     */
    public void applyTheme() {
        Theme theme = ThemeManager.getCurrentTheme(getContext());
        View v = findViewById(R.id.inline_autocomplete_bg_text);
        theme.setTextColor(getContext(), (TextView)v, "text_color"); //$NON-NLS-1$
        v = findViewById(R.id.inline_autocomplete_fg_text);
        theme.setTextColor(getContext(), (TextView)v, "text_color"); //$NON-NLS-1$
        v = findViewById(R.id.inline_autocomplete_button_tab);
        theme.setImageDrawable(
                getContext(), (ImageView)v, "ab_tab_drawable"); //$NON-NLS-1$
    }

}
