
package app.crossword.yourealwaysbe.forkyz.util;

import java.util.Locale;

import android.content.Context;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import androidx.core.view.inputmethod.EditorInfoCompat;

import app.crossword.yourealwaysbe.forkyz.versions.AndroidVersionUtils;

public class BoxInputConnection extends BaseInputConnection {

    private static final int INPUT_TYPE =
        InputType.TYPE_CLASS_TEXT
        | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
        | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;

    private AndroidVersionUtils utils;
    private BoardEditable boardEditable;
    private View targetView;
    private BoxInputListener listener;
    private boolean forceCaps;
    private int updateExtractedTextToken = -1;
    private int batchEditNestingDepth = 0;
    private String pendingResponse = null;

    public interface BoxInputListener {
        default void onNewResponse(String response) { }
        default void onDeleteResponse() { }
    }

    /**
     * Create BoxInputConnection
     *
     * @param forceCaps whether to convert all input into uppercase (input is
     * always requested in uppercase, but not always enforced by input method,
     * this setting converts it)
     */
    public BoxInputConnection(
        View targetView,
        AndroidVersionUtils utils,
        String initialResponse,
        BoxInputListener listener,
        boolean forceCaps
    ) {
        super(targetView, true);
        this.targetView = targetView;
        this.utils = utils;
        this.boardEditable = new BoardEditable(initialResponse);
        this.listener = listener;
        this.forceCaps = forceCaps;
    }

    public void setOutAttrs(EditorInfo outAttrs) {
        outAttrs.fieldId = targetView.getId();
        outAttrs.inputType = INPUT_TYPE;
        outAttrs.imeOptions = EditorInfo.IME_NULL;
        outAttrs.initialSelStart
            = boardEditable.getSpanStart(Selection.SELECTION_START);
        outAttrs.initialSelEnd
            = boardEditable.getSpanStart(Selection.SELECTION_END);
        outAttrs.packageName = targetView.getContext().getPackageName();
        outAttrs.initialCapsMode = TextUtils.CAP_MODE_CHARACTERS;
        EditorInfoCompat.setInitialSurroundingText(
            outAttrs, boardEditable.toString()
        );
    }

    @Override
    public Editable getEditable() {
        return boardEditable;
    }

    @Override
    public ExtractedText getExtractedText(
        ExtractedTextRequest request, int flags
    ) {
        // call with null internally for convenience
        if (request != null) {
            if ((flags & InputConnection.GET_EXTRACTED_TEXT_MONITOR) != 0)
                updateExtractedTextToken = request.token;
        }

        ExtractedText extracted = new ExtractedText();
        extracted.flags = 0;
        extracted.partialStartOffset = -1;
        extracted.selectionStart = 0;
        extracted.startOffset = 0;
        String response = boardEditable.toString();
        extracted.text = response;
        extracted.selectionEnd = response.length();

        return extracted;
    }

    public void setResponse(String response) {
        boardEditable.setResponse(response);
    }

    public void refreshInput() {
        InputMethodManager imm = getInputMethodManager();
        // Don't use invalidateInput -- it is more efficient but leads
        // to what looks like a race condition with showSoftInput. It
        // starts a new session ID but showSoftInput tries to connect to
        // the wrong one and keyboard entry breaks. This is in the logs:
        // "RemoteInputConnectionImpl: Session id mismatch
        // header.sessionId: 0 currentSessionId: 1"
        imm.restartInput(targetView);
        //utils.invalidateInput(imm, targetView);
    }

    @Override
    public boolean beginBatchEdit() {
        batchEditNestingDepth += 1;
        return true;
    }

    @Override
    public boolean endBatchEdit() {
        if (batchEditNestingDepth > 0)
            batchEditNestingDepth -= 1;

        if (isBatchEdit()) {
            return true;
        } else {
            if (pendingResponse != null) {
                // need to reset pending as some connections call
                // begin/endBatchEdit during an update, so
                // pendingResponse needs to be cleared before updating
                // else infinite loop
                String savedPendingResponse = pendingResponse;
                pendingResponse = null;
                updateListenerWithResponse(savedPendingResponse);
            }
            return false;
        }
    }

    private boolean isBatchEdit() {
        return batchEditNestingDepth > 0;
    }

    /**
     * Register an update to response with listener
     *
     * If in a batch edit, the response will be delayed until batch
     * done. Subsequent updates will overwrite previous pending.
     */
    private void updateListenerWithResponse(String response) {
        if (isBatchEdit()) {
            pendingResponse = response;
        } else {
            if (listener != null) {
                // if removing characters, delete
                if (response.isEmpty()) {
                    listener.onDeleteResponse();
                } else {
                    if (isForceCaps())
                        response = response.toUpperCase(Locale.getDefault());
                    // give whole response so emojis work
                    listener.onNewResponse(response);
                }
            }
        }
    }

    private InputMethodManager getInputMethodManager() {
        return (InputMethodManager) targetView
            .getContext()
            .getSystemService(Context.INPUT_METHOD_SERVICE);
    }

    private boolean isForceCaps() { return forceCaps; }

    private class BoardEditable implements Editable {
        SpannableStringBuilder spannable;

        public BoardEditable(String initialResponse) {
            spannable = new SpannableStringBuilder(initialResponse);
            Selection.selectAll(spannable);
        }

        @Override
        public Editable append(char text) {
            return append(String.valueOf(text));
        }

        @Override
        public Editable append(CharSequence text, int start, int end) {
            return replace(length(), length(), text, start, end);
        }

        @Override
        public Editable append(CharSequence text) {
            return replace(length(), length(), text, 0, text.length());
        }

        @Override
        public void clear() {
            replace(0, length(), "", 0, 0);
        }

        @Override
        public void clearSpans() {
            spannable.clearSpans();
        }

        @Override
        public Editable delete(int st, int en) {
            return replace(st, en, "", 0, 0);
        }

        @Override
        public InputFilter[] getFilters() {
            return spannable.getFilters();
        }

        @Override
        public Editable insert(int where, CharSequence text) {
            return replace(where, where, text, 0, text.length());
        }

        @Override
        public Editable insert(
            int where, CharSequence text, int start, int end
        ) {
            return replace(where, where, text, start, end);
        }

        @Override
        public Editable replace(
            int st, int en, CharSequence source, int start, int end
        ) {
            spannable.replace(st, en, source, start, end);
            updateListenerWithResponse(spannable.toString());
            return this;
        }

        @Override
        public Editable replace(int st, int en, CharSequence text) {
            return replace(st, en, text, 0, text.length());
        }

        @Override
        public void setFilters(InputFilter[] filters) {
            spannable.setFilters(filters);
        }

        @Override
        public char charAt(int index) {
            return spannable.charAt(index);
        }

        @Override
        public int length() {
            return spannable.length();
        }

        @Override
        public CharSequence subSequence(int start, int end) {
            return spannable.subSequence(start, end);
        }

        @Override
        public String toString() {
            return spannable.toString();
        }

        @Override
        public void getChars(
            int start, int end, char[] dest, int destoff
        ) {
            spannable.getChars(start, end, dest, destoff);
        }

        @Override
        public void removeSpan(Object what) {
            spannable.removeSpan(what);
        }

        @Override
        public void setSpan(Object what, int start, int end, int flags) {
            spannable.setSpan(what, start, end, flags);
        }

        @Override
        public int getSpanEnd(Object tag) {
            return spannable.getSpanEnd(tag);
        }

        @Override
        public int getSpanFlags(Object tag) {
            return spannable.getSpanFlags(tag);
        }

        @Override
        public int getSpanStart(Object tag) {
            return spannable.getSpanStart(tag);
        }

        @Override
        public <T> T[] getSpans(int start, int end, Class<T> type) {
            return spannable.getSpans(start, end, type);
        }

        @Override
        @SuppressWarnings("rawtypes")
        public int nextSpanTransition(int start, int limit, Class type) {
            return spannable.nextSpanTransition(start, limit, type);
        }

        public int getCompositionStart() {
            return getComposingSpanStart(spannable);
        }

        public int getCompositionEnd() {
            return getComposingSpanEnd(spannable);
        }

        public void setResponse(String response) {
            spannable.replace(0, spannable.length(), response);
            Selection.selectAll(spannable);

            // have to do this because the text being edited has changed
            // this causes the keyboard to reset too, which is annoying,
            // but no way around it for a box-by-box model of input (see
            // documentation of InputMethodManager.restartInput).
            refreshInput();

            // It would be nicer if we could get away with the code
            // below, but the input method stops reporting deletion. This
            // is code we should run when the selection changes, but
            // there's no point if we're going to reset the connection
            // anyway!
            //
            // InputMethodManager imm = getInputMethodManager();
            // imm.updateSelection(
            //     targetView,
            //     0, spannable.length(),
            //     getComposingSpanStart(spannable), getComposingSpanEnd(spannable)
            // );
            // if (updateExtractedTextToken > -1) {
            //     imm.updateExtractedText(
            //         targetView,
            //         updateExtractedTextToken,
            //         getExtractedText(null, 0)
            //     );
            // }
        }
    };
}
