/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko;

import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;

import org.json.JSONObject;

import android.content.Context;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout;
import android.widget.TextView;

public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnClickListener, GeckoEventListener  {
    private static final String LOGTAG = "GeckoFindInPageBar";
    private static final String REQUEST_ID = "FindInPageBar";

    private final Context mContext;
    private CustomEditText mFindText;
    private TextView mStatusText;
    private boolean mInflated;

    public FindInPageBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        setFocusable(true);
    }

    public void inflateContent() {
        LayoutInflater inflater = LayoutInflater.from(mContext);
        View content = inflater.inflate(R.layout.find_in_page_content, this);

        content.findViewById(R.id.find_prev).setOnClickListener(this);
        content.findViewById(R.id.find_next).setOnClickListener(this);
        content.findViewById(R.id.find_close).setOnClickListener(this);

        // Capture clicks on the rest of the view to prevent them from
        // leaking into other views positioned below.
        content.setOnClickListener(this);

        mFindText = (CustomEditText) content.findViewById(R.id.find_text);
        mFindText.addTextChangedListener(this);
        mFindText.setOnKeyPreImeListener(new CustomEditText.OnKeyPreImeListener() {
            @Override
            public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_BACK) {
                    hide();
                    return true;
                }
                return false;
            }
        });

        mStatusText = (TextView) content.findViewById(R.id.find_status);

        mInflated = true;
        GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
            "FindInPage:MatchesCountResult",
            "TextSelection:Data");
    }

    public void show() {
        if (!mInflated)
            inflateContent();

        setVisibility(VISIBLE);
        mFindText.requestFocus();

        // handleMessage() receives response message and determines initial state of softInput
        GeckoAppShell.notifyObservers("TextSelection:Get", REQUEST_ID);
        GeckoAppShell.notifyObservers("FindInPage:Opened", null);
    }

    public void hide() {
        if (!mInflated || getVisibility() == View.GONE) {
            // There's nothing to hide yet.
            return;
        }

        // Always clear the Find string, primarily for privacy.
        mFindText.setText("");

        // Only close the IMM if its EditText is the one with focus.
        if (mFindText.isFocused()) {
          getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
        }

        // Close the FIPB / FindHelper state.
        setVisibility(GONE);
        GeckoAppShell.notifyObservers("FindInPage:Closed", null);
    }

    private InputMethodManager getInputMethodManager(View view) {
        Context context = view.getContext();
        return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
     }

    public void onDestroy() {
        if (!mInflated) {
            return;
        }
        GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
            "FindInPage:MatchesCountResult",
            "TextSelection:Data");
    }

    private void onMatchesCountResult(final int total, final int current, final int limit, final String searchString) {
        if (total == -1) {
            updateResult(Integer.toString(limit) + "+");
        } else if (total > 0) {
            updateResult(Integer.toString(current) + "/" + Integer.toString(total));
        } else if (TextUtils.isEmpty(searchString)) {
            updateResult("");
        } else {
            // We display 0/0, when there were no
            // matches found, or if matching has been turned off by setting
            // pref accessibility.typeaheadfind.matchesCountLimit to 0.
            updateResult("0/0");
        }
    }

    private void updateResult(final String statusText) {
        ThreadUtils.postToUiThread(new Runnable() {
            @Override
            public void run() {
                mStatusText.setVisibility(statusText.isEmpty() ? View.GONE : View.VISIBLE);
                mStatusText.setText(statusText);
            }
        });
    }

    // TextWatcher implementation

    @Override
    public void afterTextChanged(Editable s) {
        sendRequestToFinderHelper("FindInPage:Find", s.toString());
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // ignore
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // ignore
    }

    // View.OnClickListener implementation

    @Override
    public void onClick(View v) {
        final int viewId = v.getId();

        String extras = getResources().getResourceEntryName(viewId);
        Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, extras);

        if (viewId == R.id.find_prev) {
            sendRequestToFinderHelper("FindInPage:Prev", mFindText.getText().toString());
            getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
            return;
        }

        if (viewId == R.id.find_next) {
            sendRequestToFinderHelper("FindInPage:Next", mFindText.getText().toString());
            getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
            return;
        }

        if (viewId == R.id.find_close) {
            hide();
        }
    }

    // GeckoEventListener implementation

    @Override
    public void handleMessage(String event, JSONObject message) {
        if (event.equals("FindInPage:MatchesCountResult")) {
            onMatchesCountResult(message.optInt("total", 0),
                message.optInt("current", 0),
                message.optInt("limit", 0),
                message.optString("searchString"));
            return;
        }

        if (!event.equals("TextSelection:Data") || !REQUEST_ID.equals(message.optString("requestId"))) {
            return;
        }

        final String text = message.optString("text");

        // Populate an initial find string, virtual keyboard not required.
        if (!TextUtils.isEmpty(text)) {
            // Populate initial selection
            ThreadUtils.postToUiThread(new Runnable() {
                @Override
                public void run() {
                    mFindText.setText(text);
                }
            });
            return;
        }

        // Show the virtual keyboard.
        if (mFindText.hasWindowFocus()) {
            getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
        } else {
            // showSoftInput won't work until after the window is focused.
            mFindText.setOnWindowFocusChangeListener(new CustomEditText.OnWindowFocusChangeListener() {
                @Override
                public void onWindowFocusChanged(boolean hasFocus) {
                    if (!hasFocus)
                        return;

                    mFindText.setOnWindowFocusChangeListener(null);
                    getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
               }
            });
        }
    }

    /**
     * Request find operation, and update matchCount results (current count and total).
     */
    private void sendRequestToFinderHelper(final String request, final String searchString) {
        GeckoAppShell.sendRequestToGecko(new GeckoRequest(request, searchString) {
            @Override
            public void onResponse(NativeJSObject nativeJSObject) {
                // We don't care about the return value, because `onMatchesCountResult`
                // does the heavy lifting.
            }

            @Override
            public void onError(NativeJSObject error) {
                // Gecko didn't respond due to state change, javascript error, etc.
                Log.d(LOGTAG, "No response from Gecko on request to match string: [" +
                    searchString + "]");
                updateResult("");
            }
        });
    }
}