/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.views.textinput;

import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionWrapper;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.events.EventDispatcher;

/**
 * A class to implement the TextInput 'onKeyPress' API on android for soft keyboards. It is
 * instantiated in {@link ReactEditText#onCreateInputConnection(EditorInfo)}.
 *
 * <p>Android IMEs interface with EditText views through the {@link InputConnection} interface, so
 * any observable change in state of the EditText via the soft-keyboard, should be a side effect of
 * one or more of the methods in {@link InputConnectionWrapper}.
 *
 * <p>{@link InputConnection#setComposingText(CharSequence, int)} is used to set the composing
 * region (the underlined text) in the {@link android.widget.EditText} view, i.e. when React
 * Native's TextInput has the property 'autoCorrect' set to true. When text is being composed in the
 * composing state within the EditText, each key press will result in a call to {@link
 * InputConnection#setComposingText(CharSequence, int)} with a CharSequence argument equal to that
 * of the entire composing region, rather than a single character diff. We can reason about the
 * keyPress based on the resultant cursor position changes of the EditText after applying this
 * change. For example if the cursor moved backwards by one character when composing, it's likely it
 * was a delete; if it moves forward by a character, likely to be a key press of that character.
 *
 * <p>IMEs can also call {@link InputConnection#beginBatchEdit()} to signify a batch of operations.
 * One such example is committing a word currently in composing state with the press of the space
 * key. It is IME dependent but the stock Android keyboard behavior seems to be to commit the
 * currently composing text with {@link InputConnection#setComposingText(CharSequence, int)} and
 * commits a space character with a separate call to {@link
 * InputConnection#setComposingText(CharSequence, int)}. Here we chose to emit the last input of a
 * batch edit as that tends to be the user input, but it's completely arbitrary.
 *
 * <p>Another function of this class is to detect backspaces when the cursor at the beginning of the
 * {@link android.widget.EditText}, i.e no text is deleted.
 *
 * <p>N.B. this class is only applicable for soft keyboards behavior. For hardware keyboards {@link
 * android.view.View#onKeyDown(int, KeyEvent)} can be overridden to obtain the keycode of the key
 * pressed.
 */
class ReactEditTextInputConnectionWrapper extends InputConnectionWrapper {
  public static final String NEWLINE_RAW_VALUE = "\n";
  public static final String BACKSPACE_KEY_VALUE = "Backspace";
  public static final String ENTER_KEY_VALUE = "Enter";

  private ReactEditText mEditText;
  private EventDispatcher mEventDispatcher;
  private boolean mIsBatchEdit;
  private @Nullable String mKey = null;

  public ReactEditTextInputConnectionWrapper(
      InputConnection target,
      final ReactContext reactContext,
      final ReactEditText editText,
      EventDispatcher eventDispatcher) {
    super(target, false);
    mEventDispatcher = eventDispatcher;
    mEditText = editText;
  }

  @Override
  public boolean beginBatchEdit() {
    mIsBatchEdit = true;
    return super.beginBatchEdit();
  }

  @Override
  public boolean endBatchEdit() {
    mIsBatchEdit = false;
    if (mKey != null) {
      dispatchKeyEvent(mKey);
      mKey = null;
    }
    return super.endBatchEdit();
  }

  @Override
  public boolean setComposingText(CharSequence text, int newCursorPosition) {
    int previousSelectionStart = mEditText.getSelectionStart();
    int previousSelectionEnd = mEditText.getSelectionEnd();
    String key;
    boolean consumed = super.setComposingText(text, newCursorPosition);
    int currentSelectionStart = mEditText.getSelectionStart();
    boolean noPreviousSelection = previousSelectionStart == previousSelectionEnd;
    boolean cursorDidNotMove = currentSelectionStart == previousSelectionStart;
    boolean cursorMovedBackwardsOrAtBeginningOfInput =
        (currentSelectionStart < previousSelectionStart) || currentSelectionStart <= 0;
    if (cursorMovedBackwardsOrAtBeginningOfInput || (!noPreviousSelection && cursorDidNotMove)) {
      key = BACKSPACE_KEY_VALUE;
    } else {
      key = String.valueOf(mEditText.getText().charAt(currentSelectionStart - 1));
    }
    dispatchKeyEventOrEnqueue(key);
    return consumed;
  }

  @Override
  public boolean commitText(CharSequence text, int newCursorPosition) {
    String key = text.toString();
    // Assume not a keyPress if length > 1 (or 2 if unicode)
    if (key.length() <= 2) {
      if (key.equals("")) {
        key = BACKSPACE_KEY_VALUE;
      }
      dispatchKeyEventOrEnqueue(key);
    }

    return super.commitText(text, newCursorPosition);
  }

  @Override
  public boolean deleteSurroundingText(int beforeLength, int afterLength) {
    dispatchKeyEvent(BACKSPACE_KEY_VALUE);
    return super.deleteSurroundingText(beforeLength, afterLength);
  }

  // Called by SwiftKey when cursor at beginning of input when there is a delete
  // or when enter is pressed anywhere in the text. Whereas stock Android Keyboard calls
  // {@link InputConnection#deleteSurroundingText} & {@link InputConnection#commitText}
  // in each case, respectively.
  @Override
  public boolean sendKeyEvent(KeyEvent event) {
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
      boolean isNumberKey = event.getUnicodeChar() < 58 && event.getUnicodeChar() > 47;
      if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
        dispatchKeyEvent(BACKSPACE_KEY_VALUE);
      } else if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
        dispatchKeyEvent(ENTER_KEY_VALUE);
      } else if (isNumberKey) {
        dispatchKeyEvent(String.valueOf(event.getNumber()));
      }
    }
    return super.sendKeyEvent(event);
  }

  private void dispatchKeyEventOrEnqueue(String key) {
    if (mIsBatchEdit) {
      mKey = key;
    } else {
      dispatchKeyEvent(key);
    }
  }

  private void dispatchKeyEvent(String key) {
    if (key.equals(NEWLINE_RAW_VALUE)) {
      key = ENTER_KEY_VALUE;
    }
    mEventDispatcher.dispatchEvent(new ReactTextInputKeyPressEvent(mEditText.getId(), key));
  }
}
