/* 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/. */

#include "nsAutoCompleteController.h"
#include "nsAutoCompleteSimpleResult.h"

#include "nsNetCID.h"
#include "nsIIOService.h"
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
#include "nsIScriptSecurityManager.h"
#include "nsIObserverService.h"
#include "nsServiceManagerUtils.h"
#include "mozilla/Services.h"
#include "mozilla/dom/KeyboardEventBinding.h"
#include "mozilla/dom/Event.h"

static const char* kAutoCompleteSearchCID =
    "@mozilla.org/autocomplete/search;1?name=";

using namespace mozilla;

NS_IMPL_CYCLE_COLLECTION_CLASS(nsAutoCompleteController)

MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsAutoCompleteController)
  MOZ_KnownLive(tmp)->SetInput(nullptr);
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsAutoCompleteController)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInput)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSearches)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResults)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResultCache)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(nsAutoCompleteController)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsAutoCompleteController)
NS_INTERFACE_TABLE_HEAD(nsAutoCompleteController)
  NS_INTERFACE_TABLE(nsAutoCompleteController, nsIAutoCompleteController,
                     nsIAutoCompleteObserver, nsITimerCallback, nsINamed)
  NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(nsAutoCompleteController)
NS_INTERFACE_MAP_END

nsAutoCompleteController::nsAutoCompleteController()
    : mDefaultIndexCompleted(false),
      mPopupClosedByCompositionStart(false),
      mProhibitAutoFill(false),
      mUserClearedAutoFill(false),
      mCompositionState(eCompositionState_None),
      mSearchStatus(nsAutoCompleteController::STATUS_NONE),
      mMatchCount(0),
      mSearchesOngoing(0),
      mSearchesFailed(0),
      mCompletedSelectionIndex(-1) {}

nsAutoCompleteController::~nsAutoCompleteController() { SetInput(nullptr); }

void nsAutoCompleteController::SetValueOfInputTo(const nsString& aValue) {
  mSetValue = aValue;
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  input->SetTextValue(aValue);
}

////////////////////////////////////////////////////////////////////////
//// nsIAutoCompleteController

NS_IMETHODIMP
nsAutoCompleteController::GetSearchStatus(uint16_t* aSearchStatus) {
  *aSearchStatus = mSearchStatus;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetMatchCount(uint32_t* aMatchCount) {
  *aMatchCount = mMatchCount;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetInput(nsIAutoCompleteInput** aInput) {
  *aInput = mInput;
  NS_IF_ADDREF(*aInput);
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::SetInitiallySelectedIndex(int32_t aSelectedIndex) {
  // First forward to the popup.
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  NS_ENSURE_STATE(input);

  nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
  NS_ENSURE_STATE(popup);
  popup->SetSelectedIndex(aSelectedIndex);

  // Now take care of internal stuff.
  bool completeSelection;
  if (NS_SUCCEEDED(input->GetCompleteSelectedIndex(&completeSelection)) &&
      completeSelection) {
    mCompletedSelectionIndex = aSelectedIndex;
  }
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::SetInput(nsIAutoCompleteInput* aInput) {
  // Don't do anything if the input isn't changing.
  if (mInput == aInput) return NS_OK;

  (void)ResetInternalState();
  if (mInput) {
    mSearches.Clear();
    ClosePopup();
  }

  mInput = aInput;

  // Nothing more to do if the input was just being set to null.
  if (!mInput) {
    return NS_OK;
  }
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  // Reset the current search string.
  nsAutoString value;
  input->GetTextValue(value);
  SetSearchStringInternal(value);

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::ResetInternalState() {
  // Clear out the current search context
  if (mInput) {
    nsAutoString value;
    mInput->GetTextValue(value);
    // Stop all searches in case they are async.
    (void)StopSearch();
    (void)ClearResults();
    SetSearchStringInternal(value);
  }

  mPlaceholderCompletionString.Truncate();
  mDefaultIndexCompleted = false;
  mProhibitAutoFill = false;
  mSearchStatus = nsIAutoCompleteController::STATUS_NONE;
  mMatchCount = 0;
  mCompletedSelectionIndex = -1;

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::StartSearch(const nsAString& aSearchString) {
  // If composition is ongoing don't start searching yet, until it is committed.
  if (mCompositionState == eCompositionState_Composing) {
    return NS_OK;
  }

  SetSearchStringInternal(aSearchString);
  StartSearches();
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::HandleText(bool* _retval) {
  *_retval = false;
  // Note: the events occur in the following order when IME is used.
  // 1. a compositionstart event(HandleStartComposition)
  // 2. some input events (HandleText), eCompositionState_Composing
  // 3. a compositionend event(HandleEndComposition)
  // 4. an input event(HandleText), eCompositionState_Committing
  // We should do nothing during composition.
  if (mCompositionState == eCompositionState_Composing) {
    return NS_OK;
  }

  bool handlingCompositionCommit =
      (mCompositionState == eCompositionState_Committing);
  bool popupClosedByCompositionStart = mPopupClosedByCompositionStart;
  if (handlingCompositionCommit) {
    mCompositionState = eCompositionState_None;
    mPopupClosedByCompositionStart = false;
  }

  if (!mInput) {
    // Stop all searches in case they are async.
    StopSearch();
    // Note: if now is after blur and IME end composition,
    // check mInput before calling.
    // See https://bugzilla.mozilla.org/show_bug.cgi?id=193544#c31
    NS_ERROR(
        "Called before attaching to the control or after detaching from the "
        "control");
    return NS_OK;
  }

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  nsAutoString newValue;
  input->GetTextValue(newValue);

  // Stop all searches in case they are async.
  StopSearch();

  if (!mInput) {
    // StopSearch() can call PostSearchCleanup() which might result
    // in a blur event, which could null out mInput, so we need to check it
    // again.  See bug #395344 for more details
    return NS_OK;
  }

  bool disabled;
  input->GetDisableAutoComplete(&disabled);
  NS_ENSURE_TRUE(!disabled, NS_OK);

  // Usually we don't search again if the new string is the same as the last
  // one. However, if this is called immediately after compositionend event, we
  // need to search the same value again since the search was canceled at
  // compositionstart event handler. The new string might also be the same as
  // the last search if the autofilled portion was cleared. In this case, we may
  // want to search again.

  // Whether the user removed some text at the end.
  bool userRemovedText =
      newValue.Length() < mSearchString.Length() &&
      Substring(mSearchString, 0, newValue.Length()).Equals(newValue);

  // Whether the user is repeating the previous search.
  bool repeatingPreviousSearch =
      !userRemovedText && newValue.Equals(mSearchString);

  mUserClearedAutoFill =
      repeatingPreviousSearch &&
      newValue.Length() < mPlaceholderCompletionString.Length() &&
      Substring(mPlaceholderCompletionString, 0, newValue.Length())
          .Equals(newValue);

  if (!handlingCompositionCommit && newValue.Length() > 0 &&
      repeatingPreviousSearch) {
    return NS_OK;
  }

  if (userRemovedText) {
    // We need to throw away previous results so we don't try to search
    // through them again.
    ClearResults();
    mProhibitAutoFill = true;
    mPlaceholderCompletionString.Truncate();
  } else {
    mProhibitAutoFill = false;
  }

  SetSearchStringInternal(newValue);

  bool noRollupOnEmptySearch;
  nsresult rv = input->GetNoRollupOnEmptySearch(&noRollupOnEmptySearch);
  NS_ENSURE_SUCCESS(rv, rv);

  // Don't search if the value is empty
  if (newValue.Length() == 0 && !noRollupOnEmptySearch) {
    // If autocomplete popup was closed by compositionstart event handler,
    // we should reopen it forcibly even if the value is empty.
    if (popupClosedByCompositionStart && handlingCompositionCommit) {
      bool cancel;
      HandleKeyNavigation(dom::KeyboardEvent_Binding::DOM_VK_DOWN, &cancel);
      return NS_OK;
    }
    ClosePopup();
    return NS_OK;
  }

  *_retval = true;
  StartSearches();

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::HandleEnter(bool aIsPopupSelection,
                                      dom::Event* aEvent, bool* _retval) {
  *_retval = false;
  if (!mInput) return NS_OK;

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  // allow the event through unless there is something selected in the popup
  input->GetPopupOpen(_retval);
  if (*_retval) {
    nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
    if (popup) {
      int32_t selectedIndex;
      popup->GetSelectedIndex(&selectedIndex);
      *_retval = selectedIndex >= 0;
    }
  }

  // Stop the search, and handle the enter.
  StopSearch();
  // StopSearch() can call PostSearchCleanup() which might result
  // in a blur event, which could null out mInput, so we need to check it
  // again.  See bug #408463 for more details
  if (!mInput) {
    return NS_OK;
  }

  EnterMatch(aIsPopupSelection, aEvent);

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::HandleEscape(bool* _retval) {
  *_retval = false;
  if (!mInput) return NS_OK;

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  // allow the event through if the popup is closed
  input->GetPopupOpen(_retval);

  // Stop all searches in case they are async.
  StopSearch();
  ClearResults();
  RevertTextValue();
  ClosePopup();

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::HandleStartComposition() {
  NS_ENSURE_TRUE(mCompositionState != eCompositionState_Composing, NS_OK);

  mPopupClosedByCompositionStart = false;
  mCompositionState = eCompositionState_Composing;

  if (!mInput) return NS_OK;

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  bool disabled;
  input->GetDisableAutoComplete(&disabled);
  if (disabled) return NS_OK;

  // Stop all searches in case they are async.
  StopSearch();

  bool isOpen = false;
  input->GetPopupOpen(&isOpen);
  if (isOpen) {
    ClosePopup();

    bool stillOpen = false;
    input->GetPopupOpen(&stillOpen);
    mPopupClosedByCompositionStart = !stillOpen;
  }
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::HandleEndComposition() {
  NS_ENSURE_TRUE(mCompositionState == eCompositionState_Composing, NS_OK);

  // We can't yet retrieve the committed value from the editor, since it isn't
  // completely committed yet. Set mCompositionState to
  // eCompositionState_Committing, so that when HandleText() is called (in
  // response to the "input" event), we know that we should handle the
  // committed text.
  mCompositionState = eCompositionState_Committing;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::HandleTab() {
  bool cancel;
  return HandleEnter(false, nullptr, &cancel);
}

NS_IMETHODIMP
nsAutoCompleteController::HandleKeyNavigation(uint32_t aKey, bool* _retval) {
  // By default, don't cancel the event
  *_retval = false;

  if (!mInput) {
    // Stop all searches in case they are async.
    StopSearch();
    // Note: if now is after blur and IME end composition,
    // check mInput before calling.
    // See https://bugzilla.mozilla.org/show_bug.cgi?id=193544#c31
    NS_ERROR(
        "Called before attaching to the control or after detaching from the "
        "control");
    return NS_OK;
  }

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
  NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);

  bool disabled;
  input->GetDisableAutoComplete(&disabled);
  NS_ENSURE_TRUE(!disabled, NS_OK);

  if (aKey == dom::KeyboardEvent_Binding::DOM_VK_UP ||
      aKey == dom::KeyboardEvent_Binding::DOM_VK_DOWN ||
      aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_UP ||
      aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_DOWN) {
    bool isOpen = false;
    input->GetPopupOpen(&isOpen);
    if (isOpen) {
      // Prevent the input from handling up/down events, as it may move
      // the cursor to home/end on some systems
      *_retval = true;
      bool reverse = aKey == dom::KeyboardEvent_Binding::DOM_VK_UP ||
                     aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_UP;
      bool page = aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_UP ||
                  aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_DOWN;

      // Fill in the value of the textbox with whatever is selected in the popup
      // if the completeSelectedIndex attribute is set.  We check this before
      // calling SelectBy of an earlier attempt to avoid crashing.
      bool completeSelection;
      input->GetCompleteSelectedIndex(&completeSelection);

      // The user has keyed up or down to change the selection.  Stop the search
      // (if there is one) now so that the results do not change while the user
      // is making a selection.
      (void)StopSearch();

      // Instruct the result view to scroll by the given amount and direction
      popup->SelectBy(reverse, page);

      if (completeSelection) {
        int32_t selectedIndex;
        popup->GetSelectedIndex(&selectedIndex);
        if (selectedIndex >= 0) {
          //  A result is selected, so fill in its value
          nsAutoString value;
          if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) {
            // If the result is the previously autofilled string, then restore
            // the search string and selection that existed when the result was
            // autofilled.  Else, fill the result and move the caret to the end.
            int32_t start;
            if (value.Equals(mPlaceholderCompletionString,
                             nsCaseInsensitiveStringComparator)) {
              start = mSearchString.Length();
              value = mPlaceholderCompletionString;
              SetValueOfInputTo(value);
            } else {
              start = value.Length();
              SetValueOfInputTo(value);
            }

            input->SelectTextRange(start, value.Length());
          }
          mCompletedSelectionIndex = selectedIndex;
        } else {
          // Nothing is selected, so fill in the last typed value
          SetValueOfInputTo(mSearchString);
          input->SelectTextRange(mSearchString.Length(),
                                 mSearchString.Length());
          mCompletedSelectionIndex = -1;
        }
      }
    } else {
      // Only show the popup if the caret is at the start or end of the input
      // and there is no selection, so that the default defined key shortcuts
      // for up and down move to the beginning and end of the field otherwise.
      if (aKey == dom::KeyboardEvent_Binding::DOM_VK_UP ||
          aKey == dom::KeyboardEvent_Binding::DOM_VK_DOWN) {
        const bool isUp = aKey == dom::KeyboardEvent_Binding::DOM_VK_UP;

        int32_t start, end;
        input->GetSelectionStart(&start);
        input->GetSelectionEnd(&end);

        if (isUp) {
          if (start > 0 || start != end) {
            return NS_OK;
          }
        } else {
          nsAutoString text;
          input->GetTextValue(text);
          if (start != end || end < (int32_t)text.Length()) {
            return NS_OK;
          }
        }
      }

      // Some script may have changed the value of the text field since our
      // last keypress or after our focus handler and we don't want to
      // search for a stale string.
      nsAutoString value;
      input->GetTextValue(value);
      SetSearchStringInternal(value);

      // Open the popup if there has been a previous non-errored search, or
      // else kick off a new search
      bool hadPreviousSearch = false;
      for (uint32_t i = 0; i < mResults.Length(); ++i) {
        nsAutoString oldSearchString;
        uint16_t oldResult = 0;
        nsIAutoCompleteResult* oldResultObject = mResults[i];
        if (oldResultObject &&
            NS_SUCCEEDED(oldResultObject->GetSearchResult(&oldResult)) &&
            oldResult != nsIAutoCompleteResult::RESULT_FAILURE &&
            NS_SUCCEEDED(oldResultObject->GetSearchString(oldSearchString)) &&
            oldSearchString.Equals(mSearchString,
                                   nsCaseInsensitiveStringComparator)) {
          hadPreviousSearch = true;
          break;
        }
      }
      if (hadPreviousSearch) {
        if (mMatchCount) {
          OpenPopup();
        }
      } else {
        // Stop all searches in case they are async.
        StopSearch();

        if (!mInput) {
          // StopSearch() can call PostSearchCleanup() which might result
          // in a blur event, which could null out mInput, so we need to check
          // it again.  See bug #395344 for more details
          return NS_OK;
        }

        StartSearches();
      }

      bool isOpen = false;
      input->GetPopupOpen(&isOpen);
      if (isOpen) {
        // Prevent the default action if we opened the popup in any of the code
        // paths above.
        *_retval = true;
      }
    }
  } else if (aKey == dom::KeyboardEvent_Binding::DOM_VK_LEFT ||
             aKey == dom::KeyboardEvent_Binding::DOM_VK_RIGHT
#ifndef XP_MACOSX
             || aKey == dom::KeyboardEvent_Binding::DOM_VK_HOME
#endif
  ) {
    // The user hit a text-navigation key.
    bool isOpen = false;
    input->GetPopupOpen(&isOpen);

    // If minresultsforpopup > 1 and there's less matches than the minimum
    // required, the popup is not open, but the search suggestion is showing
    // inline, so we should proceed as if we had the popup.
    uint32_t minResultsForPopup;
    input->GetMinResultsForPopup(&minResultsForPopup);
    if (isOpen || (mMatchCount > 0 && mMatchCount < minResultsForPopup)) {
      // For completeSelectedIndex autocomplete fields, if the popup shouldn't
      // close when the caret is moved, don't adjust the text value or caret
      // position.
      bool completeSelection;
      input->GetCompleteSelectedIndex(&completeSelection);
      if (isOpen) {
        bool noRollup;
        input->GetNoRollupOnCaretMove(&noRollup);
        if (noRollup) {
          if (completeSelection) {
            return NS_OK;
          }
        }
      }

      int32_t selectionEnd;
      input->GetSelectionEnd(&selectionEnd);
      int32_t selectionStart;
      input->GetSelectionStart(&selectionStart);
      bool shouldCompleteSelection =
          (uint32_t)selectionEnd == mPlaceholderCompletionString.Length() &&
          selectionStart < selectionEnd;
      int32_t selectedIndex;
      popup->GetSelectedIndex(&selectedIndex);
      bool completeDefaultIndex;
      input->GetCompleteDefaultIndex(&completeDefaultIndex);
      if (completeDefaultIndex && shouldCompleteSelection) {
        // We usually try to preserve the casing of what user has typed, but
        // if he wants to autocomplete, we will replace the value with the
        // actual autocomplete result. Note that the autocomplete input can also
        // be showing e.g. "bar >> foo bar" if the search matched "bar", a
        // word not at the start of the full value "foo bar".
        // The user wants explicitely to use that result, so this ensures
        // association of the result with the autocompleted text.
        nsAutoString value;
        nsAutoString inputValue;
        input->GetTextValue(inputValue);
        if (NS_SUCCEEDED(GetDefaultCompleteValue(-1, false, value))) {
          nsAutoString suggestedValue;
          int32_t pos = inputValue.Find(u" >> ");
          if (pos > 0) {
            inputValue.Right(suggestedValue, inputValue.Length() - pos - 4);
          } else {
            suggestedValue = std::move(inputValue);
          }

          if (value.Equals(suggestedValue, nsCaseInsensitiveStringComparator)) {
            SetValueOfInputTo(value);
            input->SelectTextRange(value.Length(), value.Length());
          }
        }
      } else if (!completeDefaultIndex && !completeSelection &&
                 selectedIndex >= 0) {
        // The pop-up is open and has a selection, take its value
        nsAutoString value;
        if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) {
          SetValueOfInputTo(value);
          input->SelectTextRange(value.Length(), value.Length());
        }
      }

      // Close the pop-up even if nothing was selected
      ClearSearchTimer();
      ClosePopup();
    }
    // Update last-searched string to the current input, since the input may
    // have changed.  Without this, subsequent backspaces look like text
    // additions, not text deletions.
    nsAutoString value;
    input->GetTextValue(value);
    SetSearchStringInternal(value);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::HandleDelete(bool* _retval) {
  *_retval = false;
  if (!mInput) return NS_OK;

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  bool isOpen = false;
  input->GetPopupOpen(&isOpen);
  if (!isOpen || mMatchCount == 0) {
    // Nothing left to delete, proceed as normal
    bool unused = false;
    HandleText(&unused);
    return NS_OK;
  }

  nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
  NS_ENSURE_TRUE(popup, NS_ERROR_FAILURE);

  int32_t index, searchIndex, matchIndex;
  popup->GetSelectedIndex(&index);
  if (index == -1) {
    // No match is selected in the list
    bool unused = false;
    HandleText(&unused);
    return NS_OK;
  }

  MatchIndexToSearch(index, &searchIndex, &matchIndex);
  NS_ENSURE_TRUE(searchIndex >= 0 && matchIndex >= 0, NS_ERROR_FAILURE);

  nsIAutoCompleteResult* result = mResults.SafeObjectAt(searchIndex);
  NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);

  bool removable;
  nsresult rv = result->IsRemovableAt(matchIndex, &removable);
  NS_ENSURE_SUCCESS(rv, rv);

  if (!removable) {
    return NS_OK;
  }

  nsAutoString search;
  input->GetSearchParam(search);

  // Clear the match in our result and in the DB.
  result->RemoveValueAt(matchIndex);
  --mMatchCount;

  // We removed it, so make sure we cancel the event that triggered this call.
  *_retval = true;

  // Unselect the current item.
  popup->SetSelectedIndex(-1);

  // Adjust index, if needed.
  MOZ_ASSERT(index >= 0);  // We verified this above, after MatchIndexToSearch.
  if (static_cast<uint32_t>(index) >= mMatchCount) index = mMatchCount - 1;

  if (mMatchCount > 0) {
    // There are still matches in the popup, select the current index again.
    popup->SetSelectedIndex(index);

    // Complete to the new current value.
    bool shouldComplete = false;
    input->GetCompleteDefaultIndex(&shouldComplete);
    if (shouldComplete) {
      nsAutoString value;
      if (NS_SUCCEEDED(GetResultValueAt(index, false, value))) {
        CompleteValue(value);
      }
    }

    // Invalidate the popup.
    popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_DELETE);
  } else {
    // Nothing left in the popup, clear any pending search timers and
    // close the popup.
    ClearSearchTimer();
    uint32_t minResults;
    input->GetMinResultsForPopup(&minResults);
    if (minResults) {
      ClosePopup();
    }
  }

  return NS_OK;
}

nsresult nsAutoCompleteController::GetResultAt(int32_t aIndex,
                                               nsIAutoCompleteResult** aResult,
                                               int32_t* aMatchIndex) {
  int32_t searchIndex;
  MatchIndexToSearch(aIndex, &searchIndex, aMatchIndex);
  NS_ENSURE_TRUE(searchIndex >= 0 && *aMatchIndex >= 0, NS_ERROR_FAILURE);

  *aResult = mResults.SafeObjectAt(searchIndex);
  NS_ENSURE_TRUE(*aResult, NS_ERROR_FAILURE);
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetValueAt(int32_t aIndex, nsAString& _retval) {
  GetResultLabelAt(aIndex, _retval);

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetLabelAt(int32_t aIndex, nsAString& _retval) {
  GetResultLabelAt(aIndex, _retval);

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetCommentAt(int32_t aIndex, nsAString& _retval) {
  int32_t matchIndex;
  nsIAutoCompleteResult* result;
  nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  return result->GetCommentAt(matchIndex, _retval);
}

NS_IMETHODIMP
nsAutoCompleteController::GetStyleAt(int32_t aIndex, nsAString& _retval) {
  int32_t matchIndex;
  nsIAutoCompleteResult* result;
  nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  return result->GetStyleAt(matchIndex, _retval);
}

NS_IMETHODIMP
nsAutoCompleteController::GetImageAt(int32_t aIndex, nsAString& _retval) {
  int32_t matchIndex;
  nsIAutoCompleteResult* result;
  nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  return result->GetImageAt(matchIndex, _retval);
}

NS_IMETHODIMP
nsAutoCompleteController::GetFinalCompleteValueAt(int32_t aIndex,
                                                  nsAString& _retval) {
  int32_t matchIndex;
  nsIAutoCompleteResult* result;
  nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  return result->GetFinalCompleteValueAt(matchIndex, _retval);
}

NS_IMETHODIMP
nsAutoCompleteController::SetSearchString(const nsAString& aSearchString) {
  SetSearchStringInternal(aSearchString);
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetSearchString(nsAString& aSearchString) {
  aSearchString = mSearchString;
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsIAutoCompleteObserver

NS_IMETHODIMP
nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch* aSearch,
                                         nsIAutoCompleteResult* aResult) {
  MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch));

  uint16_t result = 0;
  if (aResult) {
    aResult->GetSearchResult(&result);
  }

  // If our results are incremental, the search is still ongoing.
  if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
      result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
    --mSearchesOngoing;
  }

  // Look up the index of the search which is returning.
  for (uint32_t i = 0; i < mSearches.Length(); ++i) {
    if (mSearches[i] == aSearch) {
      ProcessResult(i, aResult);
      break;
    }
  }

  // If a match is found in ProcessResult, PostSearchCleanup will open the popup
  PostSearchCleanup();

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsITimerCallback

MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHODIMP
nsAutoCompleteController::Notify(nsITimer* timer) {
  mTimer = nullptr;
  return DoSearches();
}

////////////////////////////////////////////////////////////////////////
//// nsINamed

NS_IMETHODIMP
nsAutoCompleteController::GetName(nsACString& aName) {
  aName.AssignLiteral("nsAutoCompleteController");
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsAutoCompleteController

nsresult nsAutoCompleteController::OpenPopup() {
  uint32_t minResults;
  mInput->GetMinResultsForPopup(&minResults);

  if (mMatchCount >= minResults) {
    nsCOMPtr<nsIAutoCompleteInput> input = mInput;
    return input->SetPopupOpen(true);
  }

  return NS_OK;
}

nsresult nsAutoCompleteController::ClosePopup() {
  if (!mInput) {
    return NS_OK;
  }

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  bool isOpen = false;
  input->GetPopupOpen(&isOpen);
  if (!isOpen) return NS_OK;

  nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
  NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
  MOZ_ALWAYS_SUCCEEDS(input->SetPopupOpen(false));
  return popup->SetSelectedIndex(-1);
}

nsresult nsAutoCompleteController::BeforeSearches() {
  NS_ENSURE_STATE(mInput);

  mSearchStatus = nsIAutoCompleteController::STATUS_SEARCHING;
  mDefaultIndexCompleted = false;

  bool invalidatePreviousResult = false;
  mInput->GetInvalidatePreviousResult(&invalidatePreviousResult);

  if (!invalidatePreviousResult) {
    // ClearResults will clear the mResults array, but we should pass the
    // previous result to each search to allow reusing it.  So we temporarily
    // cache the current results until AfterSearches().
    if (!mResultCache.AppendObjects(mResults)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
  }
  ClearResults(true);
  mSearchesOngoing = mSearches.Length();
  mSearchesFailed = 0;

  // notify the input that the search is beginning
  mInput->OnSearchBegin();

  return NS_OK;
}

nsresult nsAutoCompleteController::StartSearch() {
  NS_ENSURE_STATE(mInput);
  nsCOMPtr<nsIAutoCompleteInput> input = mInput;

  // Iterate a copy of |mSearches| so that we don't run into trouble if the
  // array is mutated while we're still in the loop. An nsIAutoCompleteSearch
  // implementation could synchronously start a new search when StartSearch()
  // is called and that would lead to assertions down the way.
  nsCOMArray<nsIAutoCompleteSearch> searchesCopy(mSearches);
  for (uint32_t i = 0; i < searchesCopy.Length(); ++i) {
    nsCOMPtr<nsIAutoCompleteSearch> search = searchesCopy[i];

    nsIAutoCompleteResult* result = mResultCache.SafeObjectAt(i);

    if (result) {
      uint16_t searchResult;
      result->GetSearchResult(&searchResult);
      if (searchResult != nsIAutoCompleteResult::RESULT_SUCCESS &&
          searchResult != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
          searchResult != nsIAutoCompleteResult::RESULT_NOMATCH)
        result = nullptr;
    }

    nsAutoString searchParam;
    nsresult rv = input->GetSearchParam(searchParam);
    if (NS_FAILED(rv)) return rv;

    uint32_t userContextId;
    rv = input->GetUserContextId(&userContextId);
    if (NS_SUCCEEDED(rv) &&
        userContextId != nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID) {
      searchParam.AppendLiteral(" user-context-id:");
      searchParam.AppendInt(userContextId, 10);
    }

    rv = search->StartSearch(mSearchString, searchParam, result,
                             static_cast<nsIAutoCompleteObserver*>(this));
    if (NS_FAILED(rv)) {
      ++mSearchesFailed;
      MOZ_ASSERT(mSearchesOngoing > 0);
      --mSearchesOngoing;
    }
    // Because of the joy of nested event loops (which can easily happen when
    // some code uses a generator for an asynchronous AutoComplete search),
    // nsIAutoCompleteSearch::StartSearch might cause us to be detached from our
    // input field.  The next time we iterate, we'd be touching something that
    // we shouldn't be, and result in a crash.
    if (!mInput) {
      // The search operation has been finished.
      return NS_OK;
    }
  }

  return NS_OK;
}

void nsAutoCompleteController::AfterSearches() {
  mResultCache.Clear();
  // if the below evaluates to true, that means mSearchesOngoing must be 0
  if (mSearchesFailed == mSearches.Length()) {
    PostSearchCleanup();
  }
}

NS_IMETHODIMP
nsAutoCompleteController::StopSearch() {
  // Stop the timer if there is one
  ClearSearchTimer();

  // Stop any ongoing asynchronous searches
  if (mSearchStatus == nsIAutoCompleteController::STATUS_SEARCHING) {
    for (uint32_t i = 0; i < mSearches.Length(); ++i) {
      nsCOMPtr<nsIAutoCompleteSearch> search = mSearches[i];
      search->StopSearch();
    }
    mSearchesOngoing = 0;
    // since we were searching, but now we've stopped,
    // we need to call PostSearchCleanup()
    PostSearchCleanup();
  }
  return NS_OK;
}

void nsAutoCompleteController::MaybeCompletePlaceholder() {
  MOZ_ASSERT(mInput);

  if (!mInput) {  // or mInput depending on what you choose
    MOZ_ASSERT_UNREACHABLE("Input should always be valid at this point");
    return;
  }

  int32_t selectionStart;
  mInput->GetSelectionStart(&selectionStart);
  int32_t selectionEnd;
  mInput->GetSelectionEnd(&selectionEnd);

  // Check if the current input should be completed with the placeholder string
  // from the last completion until the actual search results come back.
  // The new input string needs to be compatible with the last completed string.
  // E.g. if the new value is "fob", but the last completion was "foobar",
  // then the last completion is incompatible.
  // If the search string is the same as the last completion value, then don't
  // complete the value again (this prevents completion to happen e.g. if the
  // cursor is moved and StartSeaches() is invoked).
  // In addition, the selection must be at the end of the current input to
  // trigger the placeholder completion.
  bool usePlaceholderCompletion =
      !mUserClearedAutoFill && !mPlaceholderCompletionString.IsEmpty() &&
      mPlaceholderCompletionString.Length() > mSearchString.Length() &&
      selectionEnd == selectionStart &&
      selectionEnd == (int32_t)mSearchString.Length() &&
      StringBeginsWith(mPlaceholderCompletionString, mSearchString,
                       nsCaseInsensitiveStringComparator);

  if (usePlaceholderCompletion) {
    CompleteValue(mPlaceholderCompletionString);
  } else {
    mPlaceholderCompletionString.Truncate();
  }
}

nsresult nsAutoCompleteController::StartSearches() {
  // Don't create a new search timer if we're already waiting for one to fire.
  // If we don't check for this, we won't be able to cancel the original timer
  // and may crash when it fires (bug 236659).
  if (mTimer || !mInput) return NS_OK;

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  if (!mSearches.Length()) {
    // Initialize our list of search objects
    uint32_t searchCount;
    input->GetSearchCount(&searchCount);
    mResults.SetCapacity(searchCount);
    mSearches.SetCapacity(searchCount);

    const char* searchCID = kAutoCompleteSearchCID;

    for (uint32_t i = 0; i < searchCount; ++i) {
      // Use the search name to create the contract id string for the search
      // service
      nsAutoCString searchName;
      input->GetSearchAt(i, searchName);
      nsAutoCString cid(searchCID);
      cid.Append(searchName);

      // Use the created cid to get a pointer to the search service and store it
      // for later
      nsCOMPtr<nsIAutoCompleteSearch> search = do_GetService(cid.get());
      if (search) {
        mSearches.AppendObject(search);
      }
    }
  }

  // Check if the current input should be completed with the placeholder string
  // from the last completion until the actual search results come back.
  MaybeCompletePlaceholder();

  // Get the timeout for delayed searches.
  uint32_t timeout;
  input->GetTimeout(&timeout);

  if (timeout == 0) {
    // If the timeout is 0, we still have to execute the delayed searches,
    // otherwise this will be a no-op.
    return DoSearches();
  }

  // Now start the delayed searches.
  return NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, timeout,
                                 nsITimer::TYPE_ONE_SHOT);
}

nsresult nsAutoCompleteController::DoSearches() {
  nsresult rv = BeforeSearches();
  if (NS_FAILED(rv)) return rv;

  StartSearch();

  // All the searches have been started, just finish.
  AfterSearches();
  return NS_OK;
}

nsresult nsAutoCompleteController::ClearSearchTimer() {
  if (mTimer) {
    mTimer->Cancel();
    mTimer = nullptr;
  }
  return NS_OK;
}

nsresult nsAutoCompleteController::EnterMatch(bool aIsPopupSelection,
                                              dom::Event* aEvent) {
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
  NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);

  bool forceComplete;
  input->GetForceComplete(&forceComplete);

  int32_t selectedIndex;
  popup->GetSelectedIndex(&selectedIndex);

  // Ask the popup if it wants to enter a special value into the textbox
  nsAutoString value;
  nsAutoString comment;

  popup->GetOverrideValue(value);
  if (value.IsEmpty()) {
    bool shouldComplete;
    input->GetCompleteDefaultIndex(&shouldComplete);
    bool completeSelection;
    input->GetCompleteSelectedIndex(&completeSelection);

    if (selectedIndex >= 0) {
      nsAutoString inputValue;
      input->GetTextValue(inputValue);
      GetCommentAt(selectedIndex, comment);
      if (aIsPopupSelection || !completeSelection) {
        // We need to fill-in the value if:
        //  * completeselectedindex is false
        //  * A match in the popup was confirmed
        GetResultValueAt(selectedIndex, true, value);
      } else if (mDefaultIndexCompleted &&
                 inputValue.Equals(mPlaceholderCompletionString,
                                   nsCaseInsensitiveStringComparator)) {
        // We also need to fill-in the value if the default index completion was
        // confirmed, though we cannot use the selectedIndex cause the selection
        // may have been changed by the mouse in the meanwhile.
        GetFinalDefaultCompleteValue(value);
      } else if (mCompletedSelectionIndex != -1) {
        // If completeselectedindex is true, and EnterMatch was not invoked by
        // mouse-clicking a match (for example the user pressed Enter),
        // don't fill in the value as it will have already been filled in as
        // needed, unless the selected match has a final complete value that
        // differs from the user-facing value.
        nsAutoString finalValue;
        GetResultValueAt(mCompletedSelectionIndex, true, finalValue);
        if (!inputValue.Equals(finalValue)) {
          value = std::move(finalValue);
        }
      }
    } else if (shouldComplete) {
      // We usually try to preserve the casing of what user has typed, but
      // if he wants to autocomplete, we will replace the value with the
      // actual autocomplete result.
      // The user wants explicitely to use that result, so this ensures
      // association of the result with the autocompleted text.
      nsAutoString defaultIndexValue;
      if (NS_SUCCEEDED(GetFinalDefaultCompleteValue(defaultIndexValue)))
        value = std::move(defaultIndexValue);
    }

    if (forceComplete && value.IsEmpty() && shouldComplete) {
      // See if inputValue is one of the autocomplete results. It can be an
      // identical value, or if it matched the middle of a result it can be
      // something like "bar >> foobar" (user entered bar and foobar is
      // the result value).
      // If the current search matches one of the autocomplete results, we
      // should use that result, and not overwrite it with the default value.
      // It's indeed possible EnterMatch gets called a second time (for example
      // by the blur handler) and it should not overwrite the current match.
      nsAutoString inputValue;
      input->GetTextValue(inputValue);
      nsAutoString suggestedValue;
      int32_t pos = inputValue.Find(u" >> ");
      if (pos > 0) {
        inputValue.Right(suggestedValue, inputValue.Length() - pos - 4);
      } else {
        suggestedValue = std::move(inputValue);
      }

      for (uint32_t i = 0; i < mResults.Length(); ++i) {
        nsIAutoCompleteResult* result = mResults[i];
        if (result) {
          uint32_t matchCount = 0;
          result->GetMatchCount(&matchCount);
          for (uint32_t j = 0; j < matchCount; ++j) {
            nsAutoString matchValue;
            result->GetValueAt(j, matchValue);
            if (suggestedValue.Equals(matchValue,
                                      nsCaseInsensitiveStringComparator)) {
              nsAutoString finalMatchValue;
              result->GetFinalCompleteValueAt(j, finalMatchValue);
              value = std::move(finalMatchValue);
              break;
            }
          }
        }
      }
      // The value should have been set at this point. If not, then it's not
      // a value that should be autocompleted.
    } else if (forceComplete && value.IsEmpty() && completeSelection) {
      // Since nothing was selected, and forceComplete is specified, that means
      // we have to find the first default match and enter it instead.
      for (uint32_t i = 0; i < mResults.Length(); ++i) {
        nsIAutoCompleteResult* result = mResults[i];
        if (result) {
          int32_t defaultIndex;
          result->GetDefaultIndex(&defaultIndex);
          if (defaultIndex >= 0) {
            result->GetFinalCompleteValueAt(defaultIndex, value);
            break;
          }
        }
      }
    }
  }

  if (comment.IsEmpty()) {
    comment.Assign(u"{}");
  }

  nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
  NS_ENSURE_STATE(obsSvc);
  obsSvc->NotifyObservers(input, "autocomplete-will-enter-text", comment.get());

  if (!value.IsEmpty()) {
    SetValueOfInputTo(value);
    input->SelectTextRange(value.Length(), value.Length());
    SetSearchStringInternal(value);
  }

  popup->SelectEntry();

  obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr);

  input->OnTextEntered(aEvent);

  ClosePopup();

  return NS_OK;
}

nsresult nsAutoCompleteController::RevertTextValue() {
  // StopSearch() can call PostSearchCleanup() which might result
  // in a blur event, which could null out mInput, so we need to check it
  // again.  See bug #408463 for more details
  if (!mInput) return NS_OK;

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  // If current input value is different from what we have set, it means
  // somebody modified the value like JS of the web content.  In such case,
  // we shouldn't overwrite it with the old value.
  nsAutoString currentValue;
  input->GetTextValue(currentValue);
  if (currentValue != mSetValue) {
    SetSearchStringInternal(currentValue);
    return NS_OK;
  }

  bool cancel = false;
  input->OnTextReverted(&cancel);

  if (!cancel) {
    nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
    NS_ENSURE_STATE(obsSvc);
    obsSvc->NotifyObservers(input, "autocomplete-will-revert-text", nullptr);

    // Don't change the value if it is the same to prevent sending useless
    // events. NOTE: how can |RevertTextValue| be called with inputValue !=
    // oldValue?
    if (mSearchString != currentValue) {
      SetValueOfInputTo(mSearchString);
    }

    obsSvc->NotifyObservers(input, "autocomplete-did-revert-text", nullptr);
  }

  return NS_OK;
}

nsresult nsAutoCompleteController::ProcessResult(
    int32_t aSearchIndex, nsIAutoCompleteResult* aResult) {
  NS_ENSURE_STATE(mInput);
  MOZ_ASSERT(aResult, "ProcessResult should always receive a result");
  NS_ENSURE_ARG(aResult);

  uint16_t searchResult = 0;
  aResult->GetSearchResult(&searchResult);

  // The following code supports incremental updating results in 2 ways:
  //  * The search may reuse the same result, just by adding entries to it.
  //  * The search may send a new result every time.  In this case we merge
  //    the results and proceed on the same code path as before.
  // This way both mSearches and mResults can be indexed by the search index,
  // cause we'll always have only one result per search.
  if (mResults.IndexOf(aResult) == -1) {
    nsIAutoCompleteResult* oldResult = mResults.SafeObjectAt(aSearchIndex);
    if (oldResult) {
      MOZ_ASSERT(false,
                 "Passing new matches to OnSearchResult with a new "
                 "nsIAutoCompleteResult every time is deprecated, please "
                 "update the same result until the search is done");
      // Build a new nsIAutocompleteSimpleResult and merge results into it.
      RefPtr<nsAutoCompleteSimpleResult> mergedResult =
          new nsAutoCompleteSimpleResult();
      mergedResult->AppendResult(oldResult);
      mergedResult->AppendResult(aResult);
      mResults.ReplaceObjectAt(mergedResult, aSearchIndex);
    } else {
      // This inserts and grows the array if needed.
      mResults.ReplaceObjectAt(aResult, aSearchIndex);
    }
  }
  // When found the result should have the same index as the search.
  MOZ_ASSERT_IF(mResults.IndexOf(aResult) != -1,
                mResults.IndexOf(aResult) == aSearchIndex);
  MOZ_ASSERT(mResults.Count() >= aSearchIndex + 1,
             "aSearchIndex should always be valid for mResults");

  uint32_t oldMatchCount = mMatchCount;
  // If the search failed, increase the match count to include the error
  // description.
  if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) {
    nsAutoString error;
    aResult->GetErrorDescription(error);
    if (!error.IsEmpty()) {
      ++mMatchCount;
    }
  } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
             searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
    // Increase the match count for all matches in this result.
    uint32_t totalMatchCount = 0;
    for (uint32_t i = 0; i < mResults.Length(); i++) {
      nsIAutoCompleteResult* result = mResults.SafeObjectAt(i);
      if (result) {
        uint32_t matchCount = 0;
        result->GetMatchCount(&matchCount);
        totalMatchCount += matchCount;
      }
    }
    uint32_t delta = totalMatchCount - oldMatchCount;
    mMatchCount += delta;
  }

  // Try to autocomplete the default index for this search.
  // Do this before invalidating so the binding knows about it.
  CompleteDefaultIndex(aSearchIndex);

  // Refresh the popup view to display the new search results
  nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
  NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
  popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_NEW_RESULT);

  return NS_OK;
}

nsresult nsAutoCompleteController::PostSearchCleanup() {
  NS_ENSURE_STATE(mInput);
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  uint32_t minResults;
  input->GetMinResultsForPopup(&minResults);

  if (mMatchCount || minResults == 0) {
    OpenPopup();
  } else if (mSearchesOngoing == 0) {
    ClosePopup();
  }

  if (mSearchesOngoing == 0) {
    mSearchStatus = mMatchCount
                        ? nsIAutoCompleteController::STATUS_COMPLETE_MATCH
                        : nsIAutoCompleteController::STATUS_COMPLETE_NO_MATCH;
    // notify the input that the search is complete
    input->OnSearchComplete();
  }

  return NS_OK;
}

nsresult nsAutoCompleteController::ClearResults(bool aIsSearching) {
  int32_t oldMatchCount = mMatchCount;
  mMatchCount = 0;
  mResults.Clear();
  if (oldMatchCount != 0) {
    if (mInput) {
      nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
      NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
      // Clear the selection.
      popup->SetSelectedIndex(-1);
    }
  }
  return NS_OK;
}

nsresult nsAutoCompleteController::CompleteDefaultIndex(int32_t aResultIndex) {
  if (mDefaultIndexCompleted || mProhibitAutoFill ||
      mSearchString.Length() == 0 || !mInput)
    return NS_OK;

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  int32_t selectionStart;
  input->GetSelectionStart(&selectionStart);
  int32_t selectionEnd;
  input->GetSelectionEnd(&selectionEnd);

  bool isPlaceholderSelected =
      selectionEnd == (int32_t)mPlaceholderCompletionString.Length() &&
      selectionStart == (int32_t)mSearchString.Length() &&
      StringBeginsWith(mPlaceholderCompletionString, mSearchString,
                       nsCaseInsensitiveStringComparator);

  // Don't try to automatically complete to the first result if there's already
  // a selection or the cursor isn't at the end of the input. In case the
  // selection is from the current placeholder completion value, then still
  // automatically complete.
  if (!isPlaceholderSelected &&
      (selectionEnd != selectionStart ||
       selectionEnd != (int32_t)mSearchString.Length()))
    return NS_OK;

  bool shouldComplete;
  input->GetCompleteDefaultIndex(&shouldComplete);
  if (!shouldComplete) return NS_OK;

  nsAutoString resultValue;
  if (NS_SUCCEEDED(GetDefaultCompleteValue(aResultIndex, true, resultValue))) {
    CompleteValue(resultValue);
    mDefaultIndexCompleted = true;
  } else {
    // Reset the search string again, in case it was completed with
    // mPlaceholderCompletionString, but the actually received result doesn't
    // have a default index result. Only reset the input when necessary, to
    // avoid triggering unnecessary new searches.
    nsAutoString inputValue;
    input->GetTextValue(inputValue);
    if (!inputValue.Equals(mSearchString)) {
      SetValueOfInputTo(mSearchString);
      input->SelectTextRange(mSearchString.Length(), mSearchString.Length());
    }
    mPlaceholderCompletionString.Truncate();
  }

  return NS_OK;
}

nsresult nsAutoCompleteController::GetDefaultCompleteResult(
    int32_t aResultIndex, nsIAutoCompleteResult** _result,
    int32_t* _defaultIndex) {
  *_defaultIndex = -1;
  int32_t resultIndex = aResultIndex;

  // If a result index was not provided, find the first defaultIndex result.
  for (int32_t i = 0; resultIndex < 0 && i < mResults.Count(); ++i) {
    nsIAutoCompleteResult* result = mResults.SafeObjectAt(i);
    if (result && NS_SUCCEEDED(result->GetDefaultIndex(_defaultIndex)) &&
        *_defaultIndex >= 0) {
      resultIndex = i;
    }
  }
  if (resultIndex < 0) {
    return NS_ERROR_FAILURE;
  }

  *_result = mResults.SafeObjectAt(resultIndex);
  NS_ENSURE_TRUE(*_result, NS_ERROR_FAILURE);

  if (*_defaultIndex < 0) {
    // The search must explicitly provide a default index in order
    // for us to be able to complete.
    (*_result)->GetDefaultIndex(_defaultIndex);
  }

  if (*_defaultIndex < 0) {
    // We were given a result index, but that result doesn't want to
    // be autocompleted.
    return NS_ERROR_FAILURE;
  }

  // If the result wrongly notifies a RESULT_SUCCESS with no matches, or
  // provides a defaultIndex greater than its matchCount, avoid trying to
  // complete to an empty value.
  uint32_t matchCount = 0;
  (*_result)->GetMatchCount(&matchCount);
  // Here defaultIndex is surely non-negative, so can be cast to unsigned.
  if ((uint32_t)(*_defaultIndex) >= matchCount) {
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult nsAutoCompleteController::GetDefaultCompleteValue(int32_t aResultIndex,
                                                           bool aPreserveCasing,
                                                           nsAString& _retval) {
  nsIAutoCompleteResult* result;
  int32_t defaultIndex = -1;
  nsresult rv = GetDefaultCompleteResult(aResultIndex, &result, &defaultIndex);
  if (NS_FAILED(rv)) return rv;

  nsAutoString resultValue;
  result->GetValueAt(defaultIndex, resultValue);
  if (aPreserveCasing && StringBeginsWith(resultValue, mSearchString,
                                          nsCaseInsensitiveStringComparator)) {
    // We try to preserve user casing, otherwise we would end up changing
    // the case of what he typed, if we have a result with a different casing.
    // For example if we have result "Test", and user starts writing "tuna",
    // after digiting t, we would convert it to T trying to autocomplete "Test".
    // We will still complete to cased "Test" if the user explicitely choose
    // that result, by either selecting it in the results popup, or with
    // keyboard navigation or if autocompleting in the middle.
    nsAutoString casedResultValue;
    casedResultValue.Assign(mSearchString);
    // Use what the user has typed so far.
    casedResultValue.Append(
        Substring(resultValue, mSearchString.Length(), resultValue.Length()));
    _retval = std::move(casedResultValue);
  } else
    _retval = std::move(resultValue);

  return NS_OK;
}

nsresult nsAutoCompleteController::GetFinalDefaultCompleteValue(
    nsAString& _retval) {
  MOZ_ASSERT(mInput, "Must have a valid input");
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  nsIAutoCompleteResult* result;
  int32_t defaultIndex = -1;
  nsresult rv = GetDefaultCompleteResult(-1, &result, &defaultIndex);
  if (NS_FAILED(rv)) return rv;

  result->GetValueAt(defaultIndex, _retval);
  nsAutoString inputValue;
  input->GetTextValue(inputValue);
  if (!_retval.Equals(inputValue, nsCaseInsensitiveStringComparator)) {
    return NS_ERROR_FAILURE;
  }

  nsAutoString finalCompleteValue;
  rv = result->GetFinalCompleteValueAt(defaultIndex, finalCompleteValue);
  if (NS_SUCCEEDED(rv)) {
    _retval = std::move(finalCompleteValue);
  }

  return NS_OK;
}

nsresult nsAutoCompleteController::CompleteValue(nsString& aValue)
/* mInput contains mSearchString, which we want to autocomplete to aValue.  If
 * selectDifference is true, select the remaining portion of aValue not
 * contained in mSearchString. */
{
  MOZ_ASSERT(mInput, "Must have a valid input");

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  const int32_t mSearchStringLength = mSearchString.Length();
  int32_t endSelect = aValue.Length();  // By default, select all of aValue.

  if (aValue.IsEmpty() || StringBeginsWith(aValue, mSearchString,
                                           nsCaseInsensitiveStringComparator)) {
    // aValue is empty (we were asked to clear mInput), or mSearchString
    // matches the beginning of aValue.  In either case we can simply
    // autocomplete to aValue.
    mPlaceholderCompletionString = aValue;
    SetValueOfInputTo(aValue);
  } else {
    nsresult rv;
    nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
    NS_ENSURE_SUCCESS(rv, rv);
    nsAutoCString scheme;
    if (NS_SUCCEEDED(
            ios->ExtractScheme(NS_ConvertUTF16toUTF8(aValue), scheme))) {
      // Trying to autocomplete a URI from somewhere other than the beginning.
      // Only succeed if the missing portion is "http://"; otherwise do not
      // autocomplete.  This prevents us from "helpfully" autocompleting to a
      // URI that isn't equivalent to what the user expected.
      const int32_t findIndex = 7;  // length of "http://"

      if ((endSelect < findIndex + mSearchStringLength) ||
          !scheme.EqualsLiteral("http") ||
          !Substring(aValue, findIndex, mSearchStringLength)
               .Equals(mSearchString, nsCaseInsensitiveStringComparator)) {
        return NS_OK;
      }

      mPlaceholderCompletionString =
          mSearchString +
          Substring(aValue, mSearchStringLength + findIndex, endSelect);
      SetValueOfInputTo(mPlaceholderCompletionString);

      endSelect -= findIndex;  // We're skipping this many characters of aValue.
    } else {
      // Autocompleting something other than a URI from the middle.
      // Use the format "searchstring >> full string" to indicate to the user
      // what we are going to replace their search string with.
      SetValueOfInputTo(mSearchString + u" >> "_ns + aValue);

      endSelect = mSearchString.Length() + 4 + aValue.Length();

      // Reset the last search completion.
      mPlaceholderCompletionString.Truncate();
    }
  }

  input->SelectTextRange(mSearchStringLength, endSelect);

  return NS_OK;
}

nsresult nsAutoCompleteController::GetResultLabelAt(int32_t aIndex,
                                                    nsAString& _retval) {
  return GetResultValueLabelAt(aIndex, false, false, _retval);
}

nsresult nsAutoCompleteController::GetResultValueAt(int32_t aIndex,
                                                    bool aGetFinalValue,
                                                    nsAString& _retval) {
  return GetResultValueLabelAt(aIndex, aGetFinalValue, true, _retval);
}

nsresult nsAutoCompleteController::GetResultValueLabelAt(int32_t aIndex,
                                                         bool aGetFinalValue,
                                                         bool aGetValue,
                                                         nsAString& _retval) {
  NS_ENSURE_TRUE(aIndex >= 0 && static_cast<uint32_t>(aIndex) < mMatchCount,
                 NS_ERROR_ILLEGAL_VALUE);

  int32_t matchIndex;
  nsIAutoCompleteResult* result;
  nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  uint16_t searchResult;
  result->GetSearchResult(&searchResult);

  if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) {
    if (aGetValue) return NS_ERROR_FAILURE;
    result->GetErrorDescription(_retval);
  } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
             searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
    if (aGetFinalValue) {
      // Some implementations may miss finalCompleteValue, try to be backwards
      // compatible.
      if (NS_FAILED(result->GetFinalCompleteValueAt(matchIndex, _retval))) {
        result->GetValueAt(matchIndex, _retval);
      }
    } else if (aGetValue) {
      result->GetValueAt(matchIndex, _retval);
    } else {
      result->GetLabelAt(matchIndex, _retval);
    }
  }

  return NS_OK;
}

/**
 * Given the index of a match in the autocomplete popup, find the
 * corresponding nsIAutoCompleteSearch index, and sub-index into
 * the search's results list.
 */
nsresult nsAutoCompleteController::MatchIndexToSearch(int32_t aMatchIndex,
                                                      int32_t* aSearchIndex,
                                                      int32_t* aItemIndex) {
  *aSearchIndex = -1;
  *aItemIndex = -1;

  uint32_t index = 0;

  // Move index through the results of each registered nsIAutoCompleteSearch
  // until we find the given match
  for (uint32_t i = 0; i < mSearches.Length(); ++i) {
    nsIAutoCompleteResult* result = mResults.SafeObjectAt(i);
    if (!result) continue;

    uint32_t matchCount = 0;

    uint16_t searchResult;
    result->GetSearchResult(&searchResult);

    // Find out how many results were provided by the
    // current nsIAutoCompleteSearch.
    if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
        searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
      result->GetMatchCount(&matchCount);
    }

    // If the given match index is within the results range
    // of the current nsIAutoCompleteSearch then return the
    // search index and sub-index into the results array
    if ((matchCount != 0) &&
        (index + matchCount - 1 >= (uint32_t)aMatchIndex)) {
      *aSearchIndex = i;
      *aItemIndex = aMatchIndex - index;
      return NS_OK;
    }

    // Advance the popup table index cursor past the
    // results of the current search.
    index += matchCount;
  }

  return NS_OK;
}
