/*
 * Copyright (C) 2013 The Android Open Source 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.amnesica.kryptey.inputmethod.latin.inputlogic;

import android.os.SystemClock;
import android.text.TextUtils;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;

import com.amnesica.kryptey.inputmethod.event.Event;
import com.amnesica.kryptey.inputmethod.event.InputTransaction;
import com.amnesica.kryptey.inputmethod.latin.LatinIME;
import com.amnesica.kryptey.inputmethod.latin.RichInputConnection;
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
import com.amnesica.kryptey.inputmethod.latin.settings.SettingsValues;
import com.amnesica.kryptey.inputmethod.latin.utils.InputTypeUtils;
import com.amnesica.kryptey.inputmethod.latin.utils.RecapitalizeStatus;
import com.amnesica.kryptey.inputmethod.latin.utils.SubtypeLocaleUtils;

import java.util.TreeSet;

/**
 * This class manages the input logic.
 */
public final class InputLogic {
  // TODO : Remove this member when we can.
  final LatinIME mLatinIME;

  // This has package visibility so it can be accessed from InputLogicHandler.
  public final RichInputConnection mConnection;
  private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();

  public final TreeSet<Long> mCurrentlyPressedHardwareKeys = new TreeSet<>();

  /**
   * Create a new instance of the input logic.
   *
   * @param latinIME the instance of the parent LatinIME. We should remove this when we can.
   *                 dictionary.
   */
  public InputLogic(final LatinIME latinIME) {
    mLatinIME = latinIME;
    mConnection = new RichInputConnection(latinIME);
    // TODO handle better
    mLatinIME.setRichInputConnection(mConnection);
  }

  /**
   * Initializes the input logic for input in an editor.
   * <p>
   * Call this when input starts or restarts in some editor (typically, in onStartInputView).
   */
  public void startInput() {
    mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once
    mCurrentlyPressedHardwareKeys.clear();
  }

  /**
   * Call this when the subtype changes.
   */
  public void onSubtypeChanged() {
    startInput();
  }

  /**
   * React to a string input.
   * <p>
   * This is triggered by keys that input many characters at once, like the ".com" key or
   * some additional keys for example.
   *
   * @param settingsValues the current values of the settings.
   * @param event          the input event containing the data.
   * @return the complete transaction object
   */
  public InputTransaction onTextInput(final SettingsValues settingsValues, final Event event) {
    final String rawText = event.getTextToCommit().toString();
    final InputTransaction inputTransaction = new InputTransaction(settingsValues);
    mConnection.beginBatchEdit();
    final String text = performSpecificTldProcessingOnTextInput(rawText);
    mConnection.commitText(text, 1);
    mConnection.endBatchEdit();
    // Space state must be updated before calling updateShiftState
    inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
    return inputTransaction;
  }

  /**
   * Consider an update to the cursor position. Evaluate whether this update has happened as
   * part of normal typing or whether it was an explicit cursor move by the user. In any case,
   * do the necessary adjustments.
   *
   * @param newSelStart new selection start
   * @param newSelEnd   new selection end
   * @return whether the cursor has moved as a result of user interaction.
   */
  public boolean onUpdateSelection(final int newSelStart, final int newSelEnd) {
    resetEntireInputState(newSelStart, newSelEnd);

    // The cursor has been moved : we now accept to perform recapitalization
    mRecapitalizeStatus.enable();
    // Stop the last recapitalization, if started.
    mRecapitalizeStatus.stop();
    return true;
  }

  /**
   * React to a code input. It may be a code point to insert, or a symbolic value that influences
   * the keyboard behavior.
   * <p>
   * Typically, this is called whenever a key is pressed on the software keyboard. This is not
   * the entry point for gesture input; see the onBatchInput* family of functions for this.
   *
   * @param settingsValues the current settings values.
   * @param event          the event to handle.
   * @return the complete transaction object
   */
  public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event) {
    final InputTransaction inputTransaction = new InputTransaction(settingsValues);
    mConnection.beginBatchEdit();

    Event currentEvent = event;
    while (null != currentEvent) {
      if (currentEvent.isConsumed()) {
        handleConsumedEvent(currentEvent);
      } else if (currentEvent.isFunctionalKeyEvent()) {
        handleFunctionalEvent(currentEvent, inputTransaction);
      } else {
        handleNonFunctionalEvent(currentEvent, inputTransaction);
      }
      currentEvent = currentEvent.mNextEvent;
    }
    mConnection.endBatchEdit();
    return inputTransaction;
  }

  /**
   * Handle a consumed event.
   * <p>
   * Consumed events represent events that have already been consumed, typically by the
   * combining chain.
   *
   * @param event The event to handle.
   */
  private void handleConsumedEvent(final Event event) {
    // A consumed event may have text to commit and an update to the composing state, so
    // we evaluate both. With some combiners, it's possible than an event contains both
    // and we enter both of the following if clauses.
    final CharSequence textToCommit = event.getTextToCommit();
    if (!TextUtils.isEmpty(textToCommit)) {
      mConnection.commitText(textToCommit, 1);
    }
  }

  /**
   * Handle a functional key event.
   * <p>
   * A functional event is a special key, like delete, shift, emoji, or the settings key.
   * Non-special keys are those that generate a single code point.
   * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
   * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
   * any key that results in multiple code points like the ".com" key.
   *
   * @param event            The event to handle.
   * @param inputTransaction The transaction in progress.
   */
  private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction) {
    switch (event.mKeyCode) {
      case Constants.CODE_DELETE:
        handleBackspaceEvent(event, inputTransaction);
        // Backspace is a functional key, but it affects the contents of the editor.
        break;
      case Constants.CODE_SHIFT:
        performRecapitalization(inputTransaction.mSettingsValues);
        inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
        break;
      case Constants.CODE_CAPSLOCK:
        // Note: Changing keyboard to shift lock state is handled in
        // {@link KeyboardSwitcher#onEvent(Event)}.
        break;
      case Constants.CODE_SYMBOL_SHIFT:
        // Note: Calling back to the keyboard on the symbol Shift key is handled in
        // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
        break;
      case Constants.CODE_SWITCH_ALPHA_SYMBOL:
        // Note: Calling back to the keyboard on symbol key is handled in
        // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
        break;
      case Constants.CODE_SETTINGS:
        onSettingsKeyPressed();
        break;
      case Constants.CODE_ACTION_NEXT:
        performEditorAction(EditorInfo.IME_ACTION_NEXT);
        break;
      case Constants.CODE_ACTION_PREVIOUS:
        performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
        break;
      case Constants.CODE_LANGUAGE_SWITCH:
        handleLanguageSwitchKey();
        break;
      case Constants.CODE_SHIFT_ENTER:
        final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
            event.mKeyCode, event.mX, event.mY, event.isKeyRepeat());
        handleNonSpecialCharacterEvent(tmpEvent, inputTransaction);
        // Shift + Enter is treated as a functional key but it results in adding a new
        // line, so that does affect the contents of the editor.
        break;
      default:
        throw new RuntimeException("Unknown key code : " + event.mKeyCode);
    }
  }

  /**
   * Handle an event that is not a functional event.
   * <p>
   * These events are generally events that cause input, but in some cases they may do other
   * things like trigger an editor action.
   *
   * @param event            The event to handle.
   * @param inputTransaction The transaction in progress.
   */
  private void handleNonFunctionalEvent(final Event event,
                                        final InputTransaction inputTransaction) {
    switch (event.mCodePoint) {
      case Constants.CODE_ENTER:
        final EditorInfo editorInfo = getCurrentInputEditorInfo();
        final int imeOptionsActionId =
            InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
        if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
          // Either we have an actionLabel and we should performEditorAction with
          // actionId regardless of its value.
          performEditorAction(editorInfo.actionId);
        } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
          // We didn't have an actionLabel, but we had another action to execute.
          // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
          // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
          // means there should be an action and the app didn't bother to set a specific
          // code for it - presumably it only handles one. It does not have to be treated
          // in any specific way: anything that is not IME_ACTION_NONE should be sent to
          // performEditorAction.
          performEditorAction(imeOptionsActionId);
        } else {
          // No action label, and the action from imeOptions is NONE: this is a regular
          // enter key that should input a carriage return.
          handleNonSpecialCharacterEvent(event, inputTransaction);
        }
        break;
      default:
        handleNonSpecialCharacterEvent(event, inputTransaction);
        break;
    }
  }

  /**
   * Handle inputting a code point to the editor.
   * <p>
   * Non-special keys are those that generate a single code point.
   * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
   * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
   * any key that results in multiple code points like the ".com" key.
   *
   * @param event            The event to handle.
   * @param inputTransaction The transaction in progress.
   */
  private void handleNonSpecialCharacterEvent(final Event event,
                                              final InputTransaction inputTransaction) {
    final int codePoint = event.mCodePoint;
    if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
        || Character.getType(codePoint) == Character.OTHER_SYMBOL) {
      handleSeparatorEvent(event, inputTransaction);
    } else {
      handleNonSeparatorEvent(event);
    }
  }

  /**
   * Handle a non-separator.
   *
   * @param event The event to handle.
   */
  private void handleNonSeparatorEvent(final Event event) {
    mConnection.commitText(StringUtils.newSingleCodePointString(event.mCodePoint), 1);
  }

  /**
   * Handle input of a separator code point.
   *
   * @param event            The event to handle.
   * @param inputTransaction The transaction in progress.
   */
  private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction) {
    mConnection.commitText(StringUtils.newSingleCodePointString(event.mCodePoint), 1);

    inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
  }

  /**
   * Handle a press on the backspace key.
   *
   * @param event            The event to handle.
   * @param inputTransaction The transaction in progress.
   */
  private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction) {
    // In many cases after backspace, we need to update the shift state. Normally we need
    // to do this right away to avoid the shift state being out of date in case the user types
    // backspace then some other character very fast. However, in the case of backspace key
    // repeat, this can lead to flashiness when the cursor flies over positions where the
    // shift state should be updated, so if this is a key repeat, we update after a small delay.
    // Then again, even in the case of a key repeat, if the cursor is at start of text, it
    // can't go any further back, so we can update right away even if it's a key repeat.
    final int shiftUpdateKind =
        event.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0
            ? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW;
    inputTransaction.requireShiftUpdate(shiftUpdateKind);

    // No cancelling of commit/double space/swap: we have a regular backspace.
    // We should backspace one char and restart suggestion if at the end of a word.
    if (mConnection.hasSelection()) {
      // If there is a selection, remove it.
      // We also need to unlearn the selected text.
      final int numCharsDeleted = mConnection.getExpectedSelectionEnd()
          - mConnection.getExpectedSelectionStart();
      mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
          mConnection.getExpectedSelectionEnd());
      mConnection.deleteTextBeforeCursor(numCharsDeleted);
    } else {
      // There is no selection, just delete one character.
      if (inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()
          || Constants.NOT_A_CURSOR_POSITION
          == mConnection.getExpectedSelectionEnd()) {
        // There are three possible reasons to send a key event: either the field has
        // type TYPE_NULL, in which case the keyboard should send events, or we are
        // running in backward compatibility mode, or we don't know the cursor position.
        // Before Jelly bean, the keyboard would simulate a hardware keyboard event on
        // pressing enter or delete. This is bad for many reasons (there are race
        // conditions with commits) but some applications are relying on this behavior
        // so we continue to support it for older apps, so we retain this behavior if
        // the app has target SDK < JellyBean.
        // As for the case where we don't know the cursor position, it can happen
        // because of bugs in the framework. But the framework should know, so the next
        // best thing is to leave it to whatever it thinks is best.
        sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
      } else {
        final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
        if (codePointBeforeCursor == Constants.NOT_A_CODE) {
          // HACK for backward compatibility with broken apps that haven't realized
          // yet that hardware keyboards are not the only way of inputting text.
          // Nothing to delete before the cursor. We should not do anything, but many
          // broken apps expect something to happen in this case so that they can
          // catch it and have their broken interface react. If you need the keyboard
          // to do this, you're doing it wrong -- please fix your app.
          mConnection.deleteTextBeforeCursor(1);
          // TODO: Add a new StatsUtils method onBackspaceWhenNoText()
          return;
        }
        final int lengthToDelete =
            Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
        mConnection.deleteTextBeforeCursor(lengthToDelete);
      }
    }
  }

  /**
   * Handle a press on the language switch key (the "globe key")
   */
  private void handleLanguageSwitchKey() {
    mLatinIME.switchToNextSubtype();
  }

  /**
   * Performs a recapitalization event.
   *
   * @param settingsValues The current settings values.
   */
  private void performRecapitalization(final SettingsValues settingsValues) {
    if (!mConnection.hasSelection() || !mRecapitalizeStatus.mIsEnabled()) {
      return; // No selection or recapitalize is disabled for now
    }
    final int selectionStart = mConnection.getExpectedSelectionStart();
    final int selectionEnd = mConnection.getExpectedSelectionEnd();
    final int numCharsSelected = selectionEnd - selectionStart;
    if (numCharsSelected > Constants.MAX_CHARACTERS_FOR_RECAPITALIZATION) {
      // We bail out if we have too many characters for performance reasons. We don't want
      // to suck possibly multiple-megabyte data.
      return;
    }
    // If we have a recapitalize in progress, use it; otherwise, start a new one.
    if (!mRecapitalizeStatus.isStarted()
        || !mRecapitalizeStatus.isSetAt(selectionStart, selectionEnd)) {
      final CharSequence selectedText =
          mConnection.getSelectedText(0 /* flags, 0 for no styles */);
      if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
      mRecapitalizeStatus.start(selectionStart, selectionEnd, selectedText.toString(), mLatinIME.getCurrentLayoutLocale());
      // We trim leading and trailing whitespace.
      mRecapitalizeStatus.trim();
    }
    mConnection.finishComposingText();
    mRecapitalizeStatus.rotate();
    mConnection.setSelection(selectionEnd, selectionEnd);
    mConnection.deleteTextBeforeCursor(numCharsSelected);
    mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
    mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(),
        mRecapitalizeStatus.getNewCursorEnd());
  }

  /**
   * Gets the current auto-caps state, factoring in the space state.
   * <p>
   * This method tries its best to do this in the most efficient possible manner. It avoids
   * getting text from the editor if possible at all.
   * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it
   * needs to know auto caps state to display the right layout.
   *
   * @param settingsValues the relevant settings values
   * @param layoutSetName  the name of the current keyboard layout set
   * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF.
   */
  public int getCurrentAutoCapsState(final SettingsValues settingsValues,
                                     final String layoutSetName) {
    if (!settingsValues.mAutoCap || !layoutUsesAutoCaps(layoutSetName)) {
      return Constants.TextUtils.CAP_MODE_OFF;
    }

    final EditorInfo ei = getCurrentInputEditorInfo();
    if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
    final int inputType = ei.inputType;
    // Warning: this depends on mSpaceState, which may not be the most current value. If
    // mSpaceState gets updated later, whoever called this may need to be told about it.
    return mConnection.getCursorCapsMode(inputType, settingsValues.mSpacingAndPunctuations);
  }

  private boolean layoutUsesAutoCaps(final String layoutSetName) {
    switch (layoutSetName) {
      case SubtypeLocaleUtils.LAYOUT_ARABIC:
      case SubtypeLocaleUtils.LAYOUT_BENGALI:
      case SubtypeLocaleUtils.LAYOUT_BENGALI_AKKHOR:
      case SubtypeLocaleUtils.LAYOUT_FARSI:
      case SubtypeLocaleUtils.LAYOUT_GEORGIAN:
      case SubtypeLocaleUtils.LAYOUT_HEBREW:
      case SubtypeLocaleUtils.LAYOUT_HINDI:
      case SubtypeLocaleUtils.LAYOUT_HINDI_COMPACT:
      case SubtypeLocaleUtils.LAYOUT_KANNADA:
      case SubtypeLocaleUtils.LAYOUT_KHMER:
      case SubtypeLocaleUtils.LAYOUT_LAO:
      case SubtypeLocaleUtils.LAYOUT_MALAYALAM:
      case SubtypeLocaleUtils.LAYOUT_MARATHI:
      case SubtypeLocaleUtils.LAYOUT_NEPALI_ROMANIZED:
      case SubtypeLocaleUtils.LAYOUT_NEPALI_TRADITIONAL:
      case SubtypeLocaleUtils.LAYOUT_TAMIL:
      case SubtypeLocaleUtils.LAYOUT_TELUGU:
      case SubtypeLocaleUtils.LAYOUT_THAI:
      case SubtypeLocaleUtils.LAYOUT_URDU:
        return false;
      default:
        return true;
    }
  }

  public int getCurrentRecapitalizeState() {
    if (!mRecapitalizeStatus.isStarted()
        || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(),
        mConnection.getExpectedSelectionEnd())) {
      // Not recapitalizing at the moment
      return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
    }
    return mRecapitalizeStatus.getCurrentMode();
  }

  /**
   * @return the editor info for the current editor
   */
  private EditorInfo getCurrentInputEditorInfo() {
    return mLatinIME.getCurrentInputEditorInfo();
  }

  /**
   * @param actionId the action to perform
   */
  private void performEditorAction(final int actionId) {
    mConnection.performEditorAction(actionId);
  }

  /**
   * Perform the processing specific to inputting TLDs.
   * <p>
   * Some keys input a TLD (specifically, the ".com" key) and this warrants some specific
   * processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type
   * of character in onCodeInput, but since this gets inputted as a whole string we need to
   * do it here specifically. Then, if the last character before the cursor is a period, then
   * we cut the dot at the start of ".com". This is because humans tend to type "www.google."
   * and then press the ".com" key and instinctively don't expect to get "www.google..com".
   *
   * @param text the raw text supplied to onTextInput
   * @return the text to actually send to the editor
   */
  private String performSpecificTldProcessingOnTextInput(final String text) {
    if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
        || !Character.isLetter(text.charAt(1))) {
      // Not a tld: do nothing.
      return text;
    }
    final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
    // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT.
    if (Constants.CODE_PERIOD == codePointBeforeCursor) {
      return text.substring(1);
    }
    return text;
  }

  /**
   * Handle a press on the settings key.
   */
  private void onSettingsKeyPressed() {
    mLatinIME.launchSettings();
  }

  /**
   * Resets the whole input state to the starting state.
   * <p>
   * This will clear the composing word, reset the last composed word, clear the suggestion
   * strip and tell the input connection about it so that it can refresh its caches.
   *
   * @param newSelStart the new selection start, in java characters.
   * @param newSelEnd   the new selection end, in java characters.
   */
  // TODO: how is this different from startInput ?!
  private void resetEntireInputState(final int newSelStart, final int newSelEnd) {
    mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd);
  }

  /**
   * Sends a DOWN key event followed by an UP key event to the editor.
   * <p>
   * If possible at all, avoid using this method. It causes all sorts of race conditions with
   * the text view because it goes through a different, asynchronous binder. Also, batch edits
   * are ignored for key events. Use the normal software input methods instead.
   *
   * @param keyCode the key code to send inside the key event.
   */
  public void sendDownUpKeyEvent(final int keyCode) {
    final long eventTime = SystemClock.uptimeMillis();
    mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
        KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
        KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
    mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
        KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
        KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
  }

  /**
   * Sends a code point to the editor, using the most appropriate method.
   * <p>
   * Normally we send code points with commitText, but there are some cases (where backward
   * compatibility is a concern for example) where we want to use deprecated methods.
   *
   * @param codePoint the code point to send.
   */
  // TODO: replace these two parameters with an InputTransaction
  private void sendKeyCodePoint(final int codePoint) {
    // TODO: Remove this special handling of digit letters.
    // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
    if (codePoint >= '0' && codePoint <= '9') {
      sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0);
      return;
    }

    mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1);
  }

  /**
   * Retry resetting caches in the rich input connection.
   * <p>
   * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
   * This method handles the retry, and re-schedules a new retry if we still can't access.
   * We only retry up to 5 times before giving up.
   *
   * @param tryResumeSuggestions Whether we should resume suggestions or not.
   * @param remainingTries       How many times we may try again before giving up.
   * @return whether true if the caches were successfully reset, false otherwise.
   */
  public boolean retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions,
                                                  final int remainingTries, final LatinIME.UIHandler handler) {
    if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(
        mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd())) {
      if (0 < remainingTries) {
        handler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
        return false;
      }
      // If remainingTries is 0, we should stop waiting for new tries, however we'll still
      // return true as we need to perform other tasks (for example, loading the keyboard).
    }
    return true;
  }
}
