/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "nsFormFillController.h"

#include "mozilla/dom/Element.h"
#include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
#include "nsIFormAutoComplete.h"
#include "nsIInputListAutoComplete.h"
#include "nsIAutoCompleteSimpleResult.h"
#include "nsString.h"
#include "nsReadableUtils.h"
#include "nsIServiceManager.h"
#include "nsIInterfaceRequestor.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIDocShellTreeItem.h"
#include "nsPIDOMWindow.h"
#include "nsIWebNavigation.h"
#include "nsIContentViewer.h"
#include "nsIDOMKeyEvent.h"
#include "nsIDOMDocument.h"
#include "nsIDOMElement.h"
#include "nsIFormControl.h"
#include "nsIDocument.h"
#include "nsIContent.h"
#include "nsIPresShell.h"
#include "nsRect.h"
#include "nsIDOMHTMLFormElement.h"
#include "nsILoginManager.h"
#include "nsIDOMMouseEvent.h"
#include "mozilla/ModuleUtils.h"
#include "nsToolkitCompsCID.h"
#include "nsEmbedCID.h"
#include "nsIDOMNSEditableElement.h"
#include "nsContentUtils.h"
#include "nsILoadContext.h"
#include "nsIFrame.h"
#include "nsIScriptSecurityManager.h"
#include "nsFocusManager.h"

using namespace mozilla::dom;

NS_IMPL_CYCLE_COLLECTION(nsFormFillController,
                         mController, mLoginManager, mFocusedPopup, mDocShells,
                         mPopups, mLastListener, mLastFormAutoComplete)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFormFillController)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFormFillController)
  NS_INTERFACE_MAP_ENTRY(nsIFormFillController)
  NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteInput)
  NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteSearch)
  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
  NS_INTERFACE_MAP_ENTRY(nsIFormAutoCompleteObserver)
  NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFormFillController)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFormFillController)



nsFormFillController::nsFormFillController() :
  mFocusedInput(nullptr),
  mFocusedInputNode(nullptr),
  mListNode(nullptr),
  mTimeout(50),
  mMinResultsForPopup(1),
  mMaxRows(0),
  mContextMenuFiredBeforeFocus(false),
  mDisableAutoComplete(false),
  mCompleteDefaultIndex(false),
  mCompleteSelectedIndex(false),
  mForceComplete(false),
  mSuppressOnInput(false)
{
  mController = do_GetService("@mozilla.org/autocomplete/controller;1");
  MOZ_ASSERT(mController);
}

nsFormFillController::~nsFormFillController()
{
  if (mListNode) {
    mListNode->RemoveMutationObserver(this);
    mListNode = nullptr;
  }
  if (mFocusedInputNode) {
    MaybeRemoveMutationObserver(mFocusedInputNode);
    mFocusedInputNode = nullptr;
    mFocusedInput = nullptr;
  }
  RemoveForDocument(nullptr);

  // Remove ourselves as a focus listener from all cached docShells
  uint32_t count = mDocShells.Length();
  for (uint32_t i = 0; i < count; ++i) {
    nsCOMPtr<nsPIDOMWindowOuter> window = GetWindowForDocShell(mDocShells[i]);
    RemoveWindowListeners(window);
  }
}

////////////////////////////////////////////////////////////////////////
//// nsIMutationObserver
//

void
nsFormFillController::AttributeChanged(nsIDocument* aDocument,
                                       mozilla::dom::Element* aElement,
                                       int32_t aNameSpaceID,
                                       nsIAtom* aAttribute, int32_t aModType,
                                       const nsAttrValue* aOldValue)
{
  if ((aAttribute == nsGkAtoms::type || aAttribute == nsGkAtoms::readonly ||
       aAttribute == nsGkAtoms::autocomplete) &&
      aNameSpaceID == kNameSpaceID_None) {
    nsCOMPtr<nsIDOMHTMLInputElement> focusedInput(mFocusedInput);
    // Reset the current state of the controller, unconditionally.
    StopControllingInput();
    // Then restart based on the new values.  We have to delay this
    // to avoid ending up in an endless loop due to re-registering our
    // mutation observer (which would notify us again for *this* event).
    nsCOMPtr<nsIRunnable> event =
      mozilla::NewRunnableMethod<nsCOMPtr<nsIDOMHTMLInputElement>>
      (this, &nsFormFillController::MaybeStartControllingInput, focusedInput);
    NS_DispatchToCurrentThread(event);
  }

  if (mListNode && mListNode->Contains(aElement)) {
    RevalidateDataList();
  }
}

void
nsFormFillController::ContentAppended(nsIDocument* aDocument,
                                      nsIContent* aContainer,
                                      nsIContent* aChild,
                                      int32_t aIndexInContainer)
{
  if (mListNode && mListNode->Contains(aContainer)) {
    RevalidateDataList();
  }
}

void
nsFormFillController::ContentInserted(nsIDocument* aDocument,
                                      nsIContent* aContainer,
                                      nsIContent* aChild,
                                      int32_t aIndexInContainer)
{
  if (mListNode && mListNode->Contains(aContainer)) {
    RevalidateDataList();
  }
}

void
nsFormFillController::ContentRemoved(nsIDocument* aDocument,
                                     nsIContent* aContainer,
                                     nsIContent* aChild,
                                     int32_t aIndexInContainer,
                                     nsIContent* aPreviousSibling)
{
  if (mListNode && mListNode->Contains(aContainer)) {
    RevalidateDataList();
  }
}

void
nsFormFillController::CharacterDataWillChange(nsIDocument* aDocument,
                                              nsIContent* aContent,
                                              CharacterDataChangeInfo* aInfo)
{
}

void
nsFormFillController::CharacterDataChanged(nsIDocument* aDocument,
                                           nsIContent* aContent,
                                           CharacterDataChangeInfo* aInfo)
{
}

void
nsFormFillController::AttributeWillChange(nsIDocument* aDocument,
                                          mozilla::dom::Element* aElement,
                                          int32_t aNameSpaceID,
                                          nsIAtom* aAttribute, int32_t aModType,
                                          const nsAttrValue* aNewValue)
{
}

void
nsFormFillController::NativeAnonymousChildListChange(nsIDocument* aDocument,
                                                     nsIContent* aContent,
                                                     bool aIsRemove)
{
}

void
nsFormFillController::ParentChainChanged(nsIContent* aContent)
{
}

void
nsFormFillController::NodeWillBeDestroyed(const nsINode* aNode)
{
  mPwmgrInputs.Remove(aNode);
  if (aNode == mListNode) {
    mListNode = nullptr;
    RevalidateDataList();
  } else if (aNode == mFocusedInputNode) {
    mFocusedInputNode = nullptr;
    mFocusedInput = nullptr;
  }
}

void
nsFormFillController::MaybeRemoveMutationObserver(nsINode* aNode)
{
  // Nodes being tracked in mPwmgrInputs will have their observers removed when
  // they stop being tracked.
  if (!mPwmgrInputs.Get(aNode)) {
    aNode->RemoveMutationObserver(this);
  }
}

////////////////////////////////////////////////////////////////////////
//// nsIFormFillController

NS_IMETHODIMP
nsFormFillController::AttachToBrowser(nsIDocShell *aDocShell, nsIAutoCompletePopup *aPopup)
{
  NS_ENSURE_TRUE(aDocShell && aPopup, NS_ERROR_ILLEGAL_VALUE);

  mDocShells.AppendElement(aDocShell);
  mPopups.AppendElement(aPopup);

  // Listen for focus events on the domWindow of the docShell
  nsCOMPtr<nsPIDOMWindowOuter> window = GetWindowForDocShell(aDocShell);
  AddWindowListeners(window);

  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::DetachFromBrowser(nsIDocShell *aDocShell)
{
  int32_t index = GetIndexOfDocShell(aDocShell);
  NS_ENSURE_TRUE(index >= 0, NS_ERROR_FAILURE);

  // Stop listening for focus events on the domWindow of the docShell
  nsCOMPtr<nsPIDOMWindowOuter> window =
    GetWindowForDocShell(mDocShells.SafeElementAt(index));
  RemoveWindowListeners(window);

  mDocShells.RemoveElementAt(index);
  mPopups.RemoveElementAt(index);

  return NS_OK;
}


NS_IMETHODIMP
nsFormFillController::MarkAsLoginManagerField(nsIDOMHTMLInputElement *aInput)
{
  /*
   * The Login Manager can supply autocomplete results for username fields,
   * when a user has multiple logins stored for a site. It uses this
   * interface to indicate that the form manager shouldn't handle the
   * autocomplete. The form manager also checks for this tag when saving
   * form history (so it doesn't save usernames).
   */
  nsCOMPtr<nsINode> node = do_QueryInterface(aInput);
  NS_ENSURE_STATE(node);

  // If the field was already marked, we don't want to show the popup again.
  if (mPwmgrInputs.Get(node)) {
    return NS_OK;
  }

  mPwmgrInputs.Put(node, true);
  node->AddMutationObserverUnlessExists(this);

  nsFocusManager *fm = nsFocusManager::GetFocusManager();
  if (fm) {
    nsCOMPtr<nsIContent> focusedContent = fm->GetFocusedContent();
    if (SameCOMIdentity(focusedContent, node)) {
      nsCOMPtr<nsIDOMHTMLInputElement> input = do_QueryInterface(node);
      if (!mFocusedInput) {
        MaybeStartControllingInput(input);
      }
    }
  }

  if (!mLoginManager)
    mLoginManager = do_GetService("@mozilla.org/login-manager;1");

  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetFocusedInput(nsIDOMHTMLInputElement **aInput) {
  *aInput = mFocusedInput;
  NS_IF_ADDREF(*aInput);
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsIAutoCompleteInput

NS_IMETHODIMP
nsFormFillController::GetPopup(nsIAutoCompletePopup **aPopup)
{
  *aPopup = mFocusedPopup;
  NS_IF_ADDREF(*aPopup);
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetController(nsIAutoCompleteController **aController)
{
  *aController = mController;
  NS_IF_ADDREF(*aController);
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetPopupOpen(bool *aPopupOpen)
{
  if (mFocusedPopup)
    mFocusedPopup->GetPopupOpen(aPopupOpen);
  else
    *aPopupOpen = false;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::SetPopupOpen(bool aPopupOpen)
{
  if (mFocusedPopup) {
    if (aPopupOpen) {
      // make sure input field is visible before showing popup (bug 320938)
      nsCOMPtr<nsIContent> content = do_QueryInterface(mFocusedInput);
      NS_ENSURE_STATE(content);
      nsCOMPtr<nsIDocShell> docShell = GetDocShellForInput(mFocusedInput);
      NS_ENSURE_STATE(docShell);
      nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell();
      NS_ENSURE_STATE(presShell);
      presShell->ScrollContentIntoView(content,
                                       nsIPresShell::ScrollAxis(
                                         nsIPresShell::SCROLL_MINIMUM,
                                         nsIPresShell::SCROLL_IF_NOT_VISIBLE),
                                       nsIPresShell::ScrollAxis(
                                         nsIPresShell::SCROLL_MINIMUM,
                                         nsIPresShell::SCROLL_IF_NOT_VISIBLE),
                                       nsIPresShell::SCROLL_OVERFLOW_HIDDEN);
      // mFocusedPopup can be destroyed after ScrollContentIntoView, see bug 420089
      if (mFocusedPopup) {
        nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput);
        mFocusedPopup->OpenAutocompletePopup(this, element);
      }
    } else
      mFocusedPopup->ClosePopup();
  }

  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetDisableAutoComplete(bool *aDisableAutoComplete)
{
  *aDisableAutoComplete = mDisableAutoComplete;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::SetDisableAutoComplete(bool aDisableAutoComplete)
{
  mDisableAutoComplete = aDisableAutoComplete;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetCompleteDefaultIndex(bool *aCompleteDefaultIndex)
{
  *aCompleteDefaultIndex = mCompleteDefaultIndex;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::SetCompleteDefaultIndex(bool aCompleteDefaultIndex)
{
  mCompleteDefaultIndex = aCompleteDefaultIndex;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetCompleteSelectedIndex(bool *aCompleteSelectedIndex)
{
  *aCompleteSelectedIndex = mCompleteSelectedIndex;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::SetCompleteSelectedIndex(bool aCompleteSelectedIndex)
{
  mCompleteSelectedIndex = aCompleteSelectedIndex;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetForceComplete(bool *aForceComplete)
{
  *aForceComplete = mForceComplete;
  return NS_OK;
}

NS_IMETHODIMP nsFormFillController::SetForceComplete(bool aForceComplete)
{
  mForceComplete = aForceComplete;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetMinResultsForPopup(uint32_t *aMinResultsForPopup)
{
  *aMinResultsForPopup = mMinResultsForPopup;
  return NS_OK;
}

NS_IMETHODIMP nsFormFillController::SetMinResultsForPopup(uint32_t aMinResultsForPopup)
{
  mMinResultsForPopup = aMinResultsForPopup;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetMaxRows(uint32_t *aMaxRows)
{
  *aMaxRows = mMaxRows;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::SetMaxRows(uint32_t aMaxRows)
{
  mMaxRows = aMaxRows;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetShowImageColumn(bool *aShowImageColumn)
{
  *aShowImageColumn = false;
  return NS_OK;
}

NS_IMETHODIMP nsFormFillController::SetShowImageColumn(bool aShowImageColumn)
{
  return NS_ERROR_NOT_IMPLEMENTED;
}


NS_IMETHODIMP
nsFormFillController::GetShowCommentColumn(bool *aShowCommentColumn)
{
  *aShowCommentColumn = false;
  return NS_OK;
}

NS_IMETHODIMP nsFormFillController::SetShowCommentColumn(bool aShowCommentColumn)
{
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
nsFormFillController::GetTimeout(uint32_t *aTimeout)
{
  *aTimeout = mTimeout;
  return NS_OK;
}

NS_IMETHODIMP nsFormFillController::SetTimeout(uint32_t aTimeout)
{
  mTimeout = aTimeout;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::SetSearchParam(const nsAString &aSearchParam)
{
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
nsFormFillController::GetSearchParam(nsAString &aSearchParam)
{
  if (!mFocusedInput) {
    NS_WARNING("mFocusedInput is null for some reason! avoiding a crash. should find out why... - ben");
    return NS_ERROR_FAILURE; // XXX why? fix me.
  }

  mFocusedInput->GetName(aSearchParam);
  if (aSearchParam.IsEmpty()) {
    nsCOMPtr<nsIDOMHTMLElement> element = do_QueryInterface(mFocusedInput);
    element->GetId(aSearchParam);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetSearchCount(uint32_t *aSearchCount)
{
  *aSearchCount = 1;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetSearchAt(uint32_t index, nsACString & _retval)
{
  _retval.AssignLiteral("form-history");
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetTextValue(nsAString & aTextValue)
{
  if (mFocusedInput) {
    nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput;
    input->GetValue(aTextValue);
  } else {
    aTextValue.Truncate();
  }
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::SetTextValue(const nsAString & aTextValue)
{
  nsCOMPtr<nsIDOMNSEditableElement> editable = do_QueryInterface(mFocusedInput);
  if (editable) {
    mSuppressOnInput = true;
    editable->SetUserInput(aTextValue);
    mSuppressOnInput = false;
  }
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::SetTextValueWithReason(const nsAString & aTextValue,
                                             uint16_t aReason)
{
  return SetTextValue(aTextValue);
}

NS_IMETHODIMP
nsFormFillController::GetSelectionStart(int32_t *aSelectionStart)
{
  if (mFocusedInput) {
    nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput;
    input->GetSelectionStart(aSelectionStart);
  }
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetSelectionEnd(int32_t *aSelectionEnd)
{
  if (mFocusedInput) {
    nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput;
    input->GetSelectionEnd(aSelectionEnd);
  }
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::SelectTextRange(int32_t aStartIndex, int32_t aEndIndex)
{
 if (mFocusedInput) {
   nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput;
   input->SetSelectionRange(aStartIndex, aEndIndex, EmptyString());
 }
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::OnSearchBegin()
{
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::OnSearchComplete()
{
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::OnTextEntered(nsIDOMEvent* aEvent,
                                    bool* aPrevent)
{
  NS_ENSURE_ARG(aPrevent);
  NS_ENSURE_TRUE(mFocusedInput, NS_OK);
  // Fire off a DOMAutoComplete event
  nsCOMPtr<nsIDOMDocument> domDoc;
  nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput);
  element->GetOwnerDocument(getter_AddRefs(domDoc));
  NS_ENSURE_STATE(domDoc);

  nsCOMPtr<nsIDOMEvent> event;
  domDoc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event));
  NS_ENSURE_STATE(event);

  event->InitEvent(NS_LITERAL_STRING("DOMAutoComplete"), true, true);

  // XXXjst: We mark this event as a trusted event, it's up to the
  // callers of this to ensure that it's only called from trusted
  // code.
  event->SetTrusted(true);

  nsCOMPtr<EventTarget> targ = do_QueryInterface(mFocusedInput);

  bool defaultActionEnabled;
  targ->DispatchEvent(event, &defaultActionEnabled);
  *aPrevent = !defaultActionEnabled;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::OnTextReverted(bool *_retval)
{
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetConsumeRollupEvent(bool *aConsumeRollupEvent)
{
  *aConsumeRollupEvent = false;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetInPrivateContext(bool *aInPrivateContext)
{
  if (!mFocusedInput) {
    *aInPrivateContext = false;
    return NS_OK;
  }

  nsCOMPtr<nsIDOMDocument> inputDoc;
  nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput);
  element->GetOwnerDocument(getter_AddRefs(inputDoc));
  nsCOMPtr<nsIDocument> doc = do_QueryInterface(inputDoc);
  nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext();
  *aInPrivateContext = loadContext && loadContext->UsePrivateBrowsing();
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetNoRollupOnCaretMove(bool *aNoRollupOnCaretMove)
{
  *aNoRollupOnCaretMove = false;
  return NS_OK;
}

NS_IMETHODIMP
nsFormFillController::GetUserContextId(uint32_t* aUserContextId)
{
  *aUserContextId = nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID;
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsIAutoCompleteSearch

NS_IMETHODIMP
nsFormFillController::StartSearch(const nsAString &aSearchString, const nsAString &aSearchParam,
                                  nsIAutoCompleteResult *aPreviousResult, nsIAutoCompleteObserver *aListener)
{
  nsresult rv;
  nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(mFocusedInputNode);

  // If the login manager has indicated it's responsible for this field, let it
  // handle the autocomplete. Otherwise, handle with form history.
  // This method is sometimes called in unit tests and from XUL without a focused node.
  if (mFocusedInputNode && (mPwmgrInputs.Get(mFocusedInputNode) ||
                            formControl->GetType() == NS_FORM_INPUT_PASSWORD)) {

    // Handle the case where a password field is focused but
    // MarkAsLoginManagerField wasn't called because password manager is disabled.
    if (!mLoginManager) {
      mLoginManager = do_GetService("@mozilla.org/login-manager;1");
    }

    if (NS_WARN_IF(!mLoginManager)) {
      return NS_ERROR_FAILURE;
    }

    // XXX aPreviousResult shouldn't ever be a historyResult type, since we're not letting
    // satchel manage the field?
    mLastListener = aListener;
    rv = mLoginManager->AutoCompleteSearchAsync(aSearchString,
                                                aPreviousResult,
                                                mFocusedInput,
                                                this);
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    mLastListener = aListener;

    nsCOMPtr<nsIAutoCompleteResult> datalistResult;
    if (mFocusedInput) {
      rv = PerformInputListAutoComplete(aSearchString,
                                        getter_AddRefs(datalistResult));
      NS_ENSURE_SUCCESS(rv, rv);
    }

    nsCOMPtr <nsIFormAutoComplete> formAutoComplete =
      do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);

    formAutoComplete->AutoCompleteSearchAsync(aSearchParam,
                                              aSearchString,
                                              mFocusedInput,
                                              aPreviousResult,
                                              datalistResult,
                                              this);
    mLastFormAutoComplete = formAutoComplete;
  }

  return NS_OK;
}

nsresult
nsFormFillController::PerformInputListAutoComplete(const nsAString& aSearch,
                                                   nsIAutoCompleteResult** aResult)
{
  // If an <input> is focused, check if it has a list="<datalist>" which can
  // provide the list of suggestions.

  MOZ_ASSERT(!mPwmgrInputs.Get(mFocusedInputNode));
  nsresult rv;

  nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete =
    do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = inputListAutoComplete->AutoCompleteSearch(aSearch,
                                                 mFocusedInput,
                                                 aResult);
  NS_ENSURE_SUCCESS(rv, rv);

  if (mFocusedInput) {
    nsCOMPtr<nsIDOMHTMLElement> list;
    mFocusedInput->GetList(getter_AddRefs(list));

    // Add a mutation observer to check for changes to the items in the <datalist>
    // and update the suggestions accordingly.
    nsCOMPtr<nsINode> node = do_QueryInterface(list);
    if (mListNode != node) {
      if (mListNode) {
        mListNode->RemoveMutationObserver(this);
        mListNode = nullptr;
      }
      if (node) {
        node->AddMutationObserverUnlessExists(this);
        mListNode = node;
      }
    }
  }

  return NS_OK;
}

class UpdateSearchResultRunnable : public mozilla::Runnable
{
public:
  UpdateSearchResultRunnable(nsIAutoCompleteObserver* aObserver,
                             nsIAutoCompleteSearch* aSearch,
                             nsIAutoCompleteResult* aResult)
    : mObserver(aObserver)
    , mSearch(aSearch)
    , mResult(aResult)
  {
    MOZ_ASSERT(mResult, "Should have a valid result");
    MOZ_ASSERT(mObserver, "You shouldn't call this runnable with a null observer!");
  }

  NS_IMETHOD Run() override {
    mObserver->OnUpdateSearchResult(mSearch, mResult);
    return NS_OK;
  }

private:
  nsCOMPtr<nsIAutoCompleteObserver> mObserver;
  nsCOMPtr<nsIAutoCompleteSearch> mSearch;
  nsCOMPtr<nsIAutoCompleteResult> mResult;
};

void nsFormFillController::RevalidateDataList()
{
  if (!mLastListener) {
    return;
  }

  if (XRE_IsContentProcess()) {
    nsCOMPtr<nsIAutoCompleteController> controller(do_QueryInterface(mLastListener));
    if (!controller) {
      return;
    }

    controller->StartSearch(mLastSearchString);
    return;
  }

  nsresult rv;
  nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete =
    do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv);

  nsCOMPtr<nsIAutoCompleteResult> result;

  rv = inputListAutoComplete->AutoCompleteSearch(mLastSearchString,
                                                 mFocusedInput,
                                                 getter_AddRefs(result));

  nsCOMPtr<nsIRunnable> event =
    new UpdateSearchResultRunnable(mLastListener, this, result);
  NS_DispatchToCurrentThread(event);
}

NS_IMETHODIMP
nsFormFillController::StopSearch()
{
  // Make sure to stop and clear this, otherwise the controller will prevent
  // mLastFormAutoComplete from being deleted.
  if (mLastFormAutoComplete) {
    mLastFormAutoComplete->StopAutoCompleteSearch();
    mLastFormAutoComplete = nullptr;
  } else if (mLoginManager) {
    mLoginManager->StopSearch();
  }
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsIFormAutoCompleteObserver

NS_IMETHODIMP
nsFormFillController::OnSearchCompletion(nsIAutoCompleteResult *aResult)
{
  nsAutoString searchString;
  aResult->GetSearchString(searchString);

  mLastSearchString = searchString;

  if (mLastListener) {
    mLastListener->OnSearchResult(this, aResult);
  }

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsIDOMEventListener

NS_IMETHODIMP
nsFormFillController::HandleEvent(nsIDOMEvent* aEvent)
{
  nsAutoString type;
  aEvent->GetType(type);

  if (type.EqualsLiteral("focus")) {
    return Focus(aEvent);
  }
  if (type.EqualsLiteral("mousedown")) {
    return MouseDown(aEvent);
  }
  if (type.EqualsLiteral("keypress")) {
    return KeyPress(aEvent);
  }
  if (type.EqualsLiteral("input")) {
    bool unused = false;
    return (!mSuppressOnInput && mController && mFocusedInput) ?
           mController->HandleText(&unused) : NS_OK;
  }
  if (type.EqualsLiteral("blur")) {
    if (mFocusedInput)
      StopControllingInput();
    return NS_OK;
  }
  if (type.EqualsLiteral("compositionstart")) {
    NS_ASSERTION(mController, "should have a controller!");
    if (mController && mFocusedInput)
      mController->HandleStartComposition();
    return NS_OK;
  }
  if (type.EqualsLiteral("compositionend")) {
    NS_ASSERTION(mController, "should have a controller!");
    if (mController && mFocusedInput)
      mController->HandleEndComposition();
    return NS_OK;
  }
  if (type.EqualsLiteral("contextmenu")) {
    mContextMenuFiredBeforeFocus = true;
    if (mFocusedPopup)
      mFocusedPopup->ClosePopup();
    return NS_OK;
  }
  if (type.EqualsLiteral("pagehide")) {

    nsCOMPtr<nsIDocument> doc = do_QueryInterface(
      aEvent->InternalDOMEvent()->GetTarget());
    if (!doc)
      return NS_OK;

    if (mFocusedInput) {
      if (doc == mFocusedInputNode->OwnerDoc())
        StopControllingInput();
    }

    RemoveForDocument(doc);
  }

  return NS_OK;
}

void
nsFormFillController::RemoveForDocument(nsIDocument* aDoc)
{
  for (auto iter = mPwmgrInputs.Iter(); !iter.Done(); iter.Next()) {
    const nsINode* key = iter.Key();
    if (key && (!aDoc || key->OwnerDoc() == aDoc)) {
      // mFocusedInputNode's observer is tracked separately, so don't remove it
      // here.
      if (key != mFocusedInputNode) {
        const_cast<nsINode*>(key)->RemoveMutationObserver(this);
      }
      iter.Remove();
    }
  }
}

void
nsFormFillController::MaybeStartControllingInput(nsIDOMHTMLInputElement* aInput)
{
  nsCOMPtr<nsINode> inputNode = do_QueryInterface(aInput);
  if (!inputNode)
    return;

  nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(aInput);
  if (!formControl || !formControl->IsSingleLineTextControl(false))
    return;

  bool isReadOnly = false;
  aInput->GetReadOnly(&isReadOnly);
  if (isReadOnly)
    return;

  bool autocomplete = nsContentUtils::IsAutocompleteEnabled(aInput);

  nsCOMPtr<nsIDOMHTMLElement> datalist;
  aInput->GetList(getter_AddRefs(datalist));
  bool hasList = datalist != nullptr;

  bool isPwmgrInput = false;
  if (mPwmgrInputs.Get(inputNode) ||
      formControl->GetType() == NS_FORM_INPUT_PASSWORD) {
    isPwmgrInput = true;
  }

  if (isPwmgrInput || hasList || autocomplete) {
    StartControllingInput(aInput);
  }
}

nsresult
nsFormFillController::Focus(nsIDOMEvent* aEvent)
{
  nsCOMPtr<nsIDOMHTMLInputElement> input = do_QueryInterface(
    aEvent->InternalDOMEvent()->GetTarget());
  MaybeStartControllingInput(input);

  // Bail if we didn't start controlling the input.
  if (!mFocusedInputNode) {
    mContextMenuFiredBeforeFocus = false;
    return NS_OK;
  }

#ifndef ANDROID
  nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(mFocusedInputNode);
  MOZ_ASSERT(formControl);

  // If this focus doesn't immediately follow a contextmenu event then show
  // the autocomplete popup for all password fields.
  if (!mContextMenuFiredBeforeFocus
      && formControl->GetType() == NS_FORM_INPUT_PASSWORD) {
    ShowPopup();
  }
#endif

  mContextMenuFiredBeforeFocus = false;
  return NS_OK;
}

nsresult
nsFormFillController::KeyPress(nsIDOMEvent* aEvent)
{
  NS_ASSERTION(mController, "should have a controller!");
  if (!mFocusedInput || !mController)
    return NS_OK;

  nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent);
  if (!keyEvent)
    return NS_ERROR_FAILURE;

  bool cancel = false;
  bool unused = false;

  uint32_t k;
  keyEvent->GetKeyCode(&k);
  switch (k) {
  case nsIDOMKeyEvent::DOM_VK_DELETE:
#ifndef XP_MACOSX
    mController->HandleDelete(&cancel);
    break;
  case nsIDOMKeyEvent::DOM_VK_BACK_SPACE:
    mController->HandleText(&unused);
    break;
#else
  case nsIDOMKeyEvent::DOM_VK_BACK_SPACE:
    {
      bool isShift = false;
      keyEvent->GetShiftKey(&isShift);

      if (isShift) {
        mController->HandleDelete(&cancel);
      } else {
        mController->HandleText(&unused);
      }

      break;
    }
#endif
  case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
  case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
    {
      bool isCtrl, isAlt, isMeta;
      keyEvent->GetCtrlKey(&isCtrl);
      keyEvent->GetAltKey(&isAlt);
      keyEvent->GetMetaKey(&isMeta);
      if (isCtrl || isAlt || isMeta)
        break;
    }
    MOZ_FALLTHROUGH;
  case nsIDOMKeyEvent::DOM_VK_UP:
  case nsIDOMKeyEvent::DOM_VK_DOWN:
  case nsIDOMKeyEvent::DOM_VK_LEFT:
  case nsIDOMKeyEvent::DOM_VK_RIGHT:
    {
      // Get the writing-mode of the relevant input element,
      // so that we can remap arrow keys if necessary.
      mozilla::WritingMode wm;
      if (mFocusedInputNode && mFocusedInputNode->IsElement()) {
        mozilla::dom::Element *elem = mFocusedInputNode->AsElement();
        nsIFrame *frame = elem->GetPrimaryFrame();
        if (frame) {
          wm = frame->GetWritingMode();
        }
      }
      if (wm.IsVertical()) {
        switch (k) {
        case nsIDOMKeyEvent::DOM_VK_LEFT:
          k = wm.IsVerticalLR() ? nsIDOMKeyEvent::DOM_VK_UP
                                : nsIDOMKeyEvent::DOM_VK_DOWN;
          break;
        case nsIDOMKeyEvent::DOM_VK_RIGHT:
          k = wm.IsVerticalLR() ? nsIDOMKeyEvent::DOM_VK_DOWN
                                : nsIDOMKeyEvent::DOM_VK_UP;
          break;
        case nsIDOMKeyEvent::DOM_VK_UP:
          k = nsIDOMKeyEvent::DOM_VK_LEFT;
          break;
        case nsIDOMKeyEvent::DOM_VK_DOWN:
          k = nsIDOMKeyEvent::DOM_VK_RIGHT;
          break;
        }
      }
    }
    mController->HandleKeyNavigation(k, &cancel);
    break;
  case nsIDOMKeyEvent::DOM_VK_ESCAPE:
    mController->HandleEscape(&cancel);
    break;
  case nsIDOMKeyEvent::DOM_VK_TAB:
    mController->HandleTab();
    cancel = false;
    break;
  case nsIDOMKeyEvent::DOM_VK_RETURN:
    mController->HandleEnter(false, aEvent, &cancel);
    break;
  }

  if (cancel) {
    aEvent->PreventDefault();
    // Don't let the page see the RETURN event when the popup is open
    // (indicated by cancel=true) so sites don't manually submit forms
    // (e.g. via submit.click()) without the autocompleted value being filled.
    // Bug 286933 will fix this for other key events.
    if (k == nsIDOMKeyEvent::DOM_VK_RETURN) {
      aEvent->StopPropagation();
    }
  }

  return NS_OK;
}

nsresult
nsFormFillController::MouseDown(nsIDOMEvent* aEvent)
{
  nsCOMPtr<nsIDOMMouseEvent> mouseEvent(do_QueryInterface(aEvent));
  if (!mouseEvent)
    return NS_ERROR_FAILURE;

  nsCOMPtr<nsIDOMHTMLInputElement> targetInput = do_QueryInterface(
    aEvent->InternalDOMEvent()->GetTarget());
  if (!targetInput)
    return NS_OK;

  int16_t button;
  mouseEvent->GetButton(&button);
  if (button != 0)
    return NS_OK;

  return ShowPopup();
}

NS_IMETHODIMP
nsFormFillController::ShowPopup()
{
  bool isOpen = false;
  GetPopupOpen(&isOpen);
  if (isOpen) {
    return SetPopupOpen(false);
  }

  nsCOMPtr<nsIAutoCompleteInput> input;
  mController->GetInput(getter_AddRefs(input));
  if (!input)
    return NS_OK;

  nsAutoString value;
  input->GetTextValue(value);
  if (value.Length() > 0) {
    // Show the popup with a filtered result set
    mController->SetSearchString(EmptyString());
    bool unused = false;
    mController->HandleText(&unused);
  } else {
    // Show the popup with the complete result set.  Can't use HandleText()
    // because it doesn't display the popup if the input is blank.
    bool cancel = false;
    mController->HandleKeyNavigation(nsIDOMKeyEvent::DOM_VK_DOWN, &cancel);
  }

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsFormFillController

void
nsFormFillController::AddWindowListeners(nsPIDOMWindowOuter* aWindow)
{
  if (!aWindow)
    return;

  EventTarget* target = aWindow->GetChromeEventHandler();

  if (!target)
    return;

  target->AddEventListener(NS_LITERAL_STRING("focus"), this,
                           true, false);
  target->AddEventListener(NS_LITERAL_STRING("blur"), this,
                           true, false);
  target->AddEventListener(NS_LITERAL_STRING("pagehide"), this,
                           true, false);
  target->AddEventListener(NS_LITERAL_STRING("mousedown"), this,
                           true, false);
  target->AddEventListener(NS_LITERAL_STRING("input"), this,
                           true, false);
  target->AddEventListener(NS_LITERAL_STRING("keypress"), this, true, false);
  target->AddEventListener(NS_LITERAL_STRING("compositionstart"), this,
                           true, false);
  target->AddEventListener(NS_LITERAL_STRING("compositionend"), this,
                           true, false);
  target->AddEventListener(NS_LITERAL_STRING("contextmenu"), this,
                           true, false);

  // Note that any additional listeners added should ensure that they ignore
  // untrusted events, which might be sent by content that's up to no good.
}

void
nsFormFillController::RemoveWindowListeners(nsPIDOMWindowOuter* aWindow)
{
  if (!aWindow)
    return;

  StopControllingInput();

  nsCOMPtr<nsIDocument> doc = aWindow->GetDoc();
  RemoveForDocument(doc);

  EventTarget* target = aWindow->GetChromeEventHandler();

  if (!target)
    return;

  target->RemoveEventListener(NS_LITERAL_STRING("focus"), this, true);
  target->RemoveEventListener(NS_LITERAL_STRING("blur"), this, true);
  target->RemoveEventListener(NS_LITERAL_STRING("pagehide"), this, true);
  target->RemoveEventListener(NS_LITERAL_STRING("mousedown"), this, true);
  target->RemoveEventListener(NS_LITERAL_STRING("input"), this, true);
  target->RemoveEventListener(NS_LITERAL_STRING("keypress"), this, true);
  target->RemoveEventListener(NS_LITERAL_STRING("compositionstart"), this,
                              true);
  target->RemoveEventListener(NS_LITERAL_STRING("compositionend"), this,
                              true);
  target->RemoveEventListener(NS_LITERAL_STRING("contextmenu"), this, true);
}

void
nsFormFillController::StartControllingInput(nsIDOMHTMLInputElement *aInput)
{
  // Make sure we're not still attached to an input
  StopControllingInput();

  if (!mController) {
    return;
  }

  // Find the currently focused docShell
  nsCOMPtr<nsIDocShell> docShell = GetDocShellForInput(aInput);
  int32_t index = GetIndexOfDocShell(docShell);
  if (index < 0)
    return;

  // Cache the popup for the focused docShell
  mFocusedPopup = mPopups.SafeElementAt(index);

  nsCOMPtr<nsINode> node = do_QueryInterface(aInput);
  if (!node) {
    return;
  }

  node->AddMutationObserverUnlessExists(this);
  mFocusedInputNode = node;
  mFocusedInput = aInput;

  nsCOMPtr<nsIDOMHTMLElement> list;
  mFocusedInput->GetList(getter_AddRefs(list));
  nsCOMPtr<nsINode> listNode = do_QueryInterface(list);
  if (listNode) {
    listNode->AddMutationObserverUnlessExists(this);
    mListNode = listNode;
  }

  mController->SetInput(this);
}

void
nsFormFillController::StopControllingInput()
{
  if (mListNode) {
    mListNode->RemoveMutationObserver(this);
    mListNode = nullptr;
  }

  if (mController) {
    // Reset the controller's input, but not if it has been switched
    // to another input already, which might happen if the user switches
    // focus by clicking another autocomplete textbox
    nsCOMPtr<nsIAutoCompleteInput> input;
    mController->GetInput(getter_AddRefs(input));
    if (input == this)
      mController->SetInput(nullptr);
  }

  if (mFocusedInputNode) {
    MaybeRemoveMutationObserver(mFocusedInputNode);

    nsresult rv;
    nsCOMPtr <nsIFormAutoComplete> formAutoComplete =
      do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv);
    if (formAutoComplete) {
      formAutoComplete->StopControllingInput(mFocusedInput);
    }

    mFocusedInputNode = nullptr;
    mFocusedInput = nullptr;
  }

  if (mFocusedPopup) {
    mFocusedPopup->ClosePopup();
  }
  mFocusedPopup = nullptr;
}

nsIDocShell *
nsFormFillController::GetDocShellForInput(nsIDOMHTMLInputElement *aInput)
{
  nsCOMPtr<nsINode> node = do_QueryInterface(aInput);
  NS_ENSURE_TRUE(node, nullptr);

  nsCOMPtr<nsPIDOMWindowOuter> win = node->OwnerDoc()->GetWindow();
  NS_ENSURE_TRUE(win, nullptr);

  return win->GetDocShell();
}

nsPIDOMWindowOuter*
nsFormFillController::GetWindowForDocShell(nsIDocShell *aDocShell)
{
  nsCOMPtr<nsIContentViewer> contentViewer;
  aDocShell->GetContentViewer(getter_AddRefs(contentViewer));
  NS_ENSURE_TRUE(contentViewer, nullptr);

  nsCOMPtr<nsIDOMDocument> domDoc;
  contentViewer->GetDOMDocument(getter_AddRefs(domDoc));
  nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
  NS_ENSURE_TRUE(doc, nullptr);

  return doc->GetWindow();
}

int32_t
nsFormFillController::GetIndexOfDocShell(nsIDocShell *aDocShell)
{
  if (!aDocShell)
    return -1;

  // Loop through our cached docShells looking for the given docShell
  uint32_t count = mDocShells.Length();
  for (uint32_t i = 0; i < count; ++i) {
    if (mDocShells[i] == aDocShell)
      return i;
  }

  // Recursively check the parent docShell of this one
  nsCOMPtr<nsIDocShellTreeItem> treeItem = do_QueryInterface(aDocShell);
  nsCOMPtr<nsIDocShellTreeItem> parentItem;
  treeItem->GetParent(getter_AddRefs(parentItem));
  if (parentItem) {
    nsCOMPtr<nsIDocShell> parentShell = do_QueryInterface(parentItem);
    return GetIndexOfDocShell(parentShell);
  }

  return -1;
}

NS_GENERIC_FACTORY_CONSTRUCTOR(nsFormFillController)

NS_DEFINE_NAMED_CID(NS_FORMFILLCONTROLLER_CID);

static const mozilla::Module::CIDEntry kSatchelCIDs[] = {
  { &kNS_FORMFILLCONTROLLER_CID, false, nullptr, nsFormFillControllerConstructor },
  { nullptr }
};

static const mozilla::Module::ContractIDEntry kSatchelContracts[] = {
  { "@mozilla.org/satchel/form-fill-controller;1", &kNS_FORMFILLCONTROLLER_CID },
  { NS_FORMHISTORYAUTOCOMPLETE_CONTRACTID, &kNS_FORMFILLCONTROLLER_CID },
  { nullptr }
};

static const mozilla::Module kSatchelModule = {
  mozilla::Module::kVersion,
  kSatchelCIDs,
  kSatchelContracts
};

NSMODULE_DEFN(satchel) = &kSatchelModule;