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

/**
 * A factory to generate LoginForm objects that represent a set of login fields
 * which aren't necessarily marked up with a <form> element.
 */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  FormLikeFactory: "resource://gre/modules/FormLikeFactory.sys.mjs",
  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
  Logic: "resource://gre/modules/LoginManager.shared.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "log", () => {
  return lazy.LoginHelper.createLogger("LoginFormFactory");
});

export const LoginFormFactory = {
  /**
   * WeakMap of the root element of a LoginForm to the LoginForm representing its fields.
   *
   * This is used to be able to lookup an existing LoginForm for a given root element since multiple
   * calls to LoginFormFactory.createFrom* won't give the exact same object. When batching fills we don't always
   * want to use the most recent list of elements for a LoginForm since we may end up doing multiple
   * fills for the same set of elements when a field gets added between arming and running the
   * DeferredTask.
   *
   * @type {WeakMap}
   */
  _loginFormsByRootElement: new WeakMap(),

  /**
   * Maps all DOM content documents in this content process, including those in
   * frames, to a WeakSet of LoginForm.rootElement for the document.
   */
  _loginFormRootElementsByDocument: new WeakMap(),

  /**
   * Create a LoginForm object from a <form>.
   *
   * @param {HTMLFormElement} aForm
   * @return {LoginForm}
   * @throws Error if aForm isn't an HTMLFormElement
   */
  createFromForm(aForm) {
    let formLike = lazy.FormLikeFactory.createFromForm(aForm);
    formLike.action = lazy.Logic.getFormActionOrigin(aForm);

    this._addLoginFormToRootElementsSet(formLike);

    return formLike;
  },

  /**
   * Create a LoginForm object from an elememt that is the root of the document
   *
   * Currently all <input> not in a <form> are one LoginForm but this
   * shouldn't be relied upon as the heuristics may change to detect multiple
   * "forms" (e.g. registration and login) on one page with a <form>.
   *
   * @param {HTMLElement} aDocumentRoot
   * @return {LoginForm}
   * @throws Error if aDocumentRoot is null
   */
  createFromDocumentRoot(aDocumentRoot) {
    const formLike = lazy.FormLikeFactory.createFromDocumentRoot(aDocumentRoot);
    formLike.action = lazy.Logic.getLoginOrigin(aDocumentRoot.baseURI);

    lazy.log.debug(
      "Created non-form LoginForm for rootElement:",
      aDocumentRoot
    );

    this._addLoginFormToRootElementsSet(formLike);

    return formLike;
  },

  /**
   * Create a LoginForm object from a password or username field.
   *
   * If the field is in a <form>, construct the LoginForm from the form.
   * Otherwise, create a LoginForm with a rootElement (wrapper) according to
   * heuristics. Currently all <input> not in a <form> are one LoginForm but this
   * shouldn't be relied upon as the heuristics may change to detect multiple
   * "forms" (e.g. registration and login) on one page with a <form>.
   *
   * Note that two LoginForms created from the same field won't return the same LoginForm object.
   * Use the `rootElement` property on the LoginForm as a key instead.
   *
   * @param {HTMLInputElement} aField - a password or username field in a document
   * @return {LoginForm}
   * @throws Error if aField isn't a password or username field in a document
   */
  createFromField(aField) {
    if (
      !HTMLInputElement.isInstance(aField) ||
      (!aField.hasBeenTypePassword &&
        !lazy.Logic.isUsernameFieldType(aField)) ||
      !aField.ownerDocument
    ) {
      throw new Error(
        "createFromField requires a password or username field in a document"
      );
    }

    let form =
      aField.form ||
      lazy.FormLikeFactory.closestFormIgnoringShadowRoots(aField);
    if (form) {
      return this.createFromForm(form);
    } else if (aField.hasAttribute("form")) {
      lazy.log.debug(
        "createFromField: field has form attribute but no form: ",
        aField.getAttribute("form")
      );
    }

    let formLike = lazy.FormLikeFactory.createFromField(aField);
    formLike.action = lazy.Logic.getLoginOrigin(aField.ownerDocument.baseURI);
    lazy.log.debug(
      "Created non-form LoginForm for rootElement:",
      aField.ownerDocument.documentElement
    );

    this._addLoginFormToRootElementsSet(formLike);
    return formLike;
  },

  getRootElementsWeakSetForDocument(aDocument) {
    let rootElementsSet = this._loginFormRootElementsByDocument.get(aDocument);
    if (!rootElementsSet) {
      rootElementsSet = new WeakSet();
      this._loginFormRootElementsByDocument.set(aDocument, rootElementsSet);
    }
    return rootElementsSet;
  },

  getForRootElement(aRootElement) {
    return this._loginFormsByRootElement.get(aRootElement);
  },

  setForRootElement(aRootElement, aLoginForm) {
    return this._loginFormsByRootElement.set(aRootElement, aLoginForm);
  },

  _addLoginFormToRootElementsSet(formLike) {
    let rootElementsSet = this.getRootElementsWeakSetForDocument(
      formLike.ownerDocument
    );
    rootElementsSet.add(formLike.rootElement);
    lazy.log.debug(
      "adding",
      formLike.rootElement,
      "to root elements for",
      formLike.ownerDocument
    );

    this._loginFormsByRootElement.set(formLike.rootElement, formLike);
  },
};
