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

const lazy = {};

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

/**
 * Password manager object for the browser contextual menu.
 */
export const LoginManagerContextMenu = {
  /**
   * Look for login items and add them to the contextual menu.
   *
   * @param {Object} inputElementIdentifier
   *        An identifier generated for the input element via ContentDOMReference.
   * @param {xul:browser} browser
   *        The browser for the document the context menu was open on.
   * @param {string} formOrigin
   *        The origin of the document that the context menu was activated from.
   *        This isn't the same as the browser's top-level document origin
   *        when subframes are involved.
   * @returns {DocumentFragment} a document fragment with all the login items.
   */
  addLoginsToMenu(inputElementIdentifier, browser, formOrigin) {
    let foundLogins = this._findLogins(formOrigin);

    if (!foundLogins.length) {
      return null;
    }

    let fragment = browser.ownerDocument.createDocumentFragment();
    let duplicateUsernames = this._findDuplicates(foundLogins);
    for (let login of foundLogins) {
      let item = fragment.ownerDocument.createXULElement("menuitem");

      let username = login.username;
      // If login is empty or duplicated we want to append a modification date to it.
      if (!username || duplicateUsernames.has(username)) {
        if (!username) {
          username = this._getLocalizedString("noUsername");
        }
        let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
        let time = this.dateAndTimeFormatter.format(
          new Date(meta.timePasswordChanged)
        );
        username = this._getLocalizedString("loginHostAge", [username, time]);
      }
      item.setAttribute("label", username);
      item.setAttribute("class", "context-login-item");

      // login is bound so we can keep the reference to each object.
      item.addEventListener(
        "command",
        function (login, _event) {
          this._fillTargetField(
            login,
            inputElementIdentifier,
            browser,
            formOrigin
          );
        }.bind(this, login)
      );

      fragment.appendChild(item);
    }

    return fragment;
  },

  /**
   * Undoes the work of addLoginsToMenu for the same menu.
   *
   * @param {Document}
   *        The context menu owner document.
   */
  clearLoginsFromMenu(document) {
    let loginItems = document.getElementsByClassName("context-login-item");
    while (loginItems.item(0)) {
      loginItems.item(0).remove();
    }
  },

  /**
   * Show the password autocomplete UI with the generation option forced to appear.
   */
  async useGeneratedPassword(inputElementIdentifier) {
    let browsingContextId = inputElementIdentifier.browsingContextId;
    let browsingContext = BrowsingContext.get(browsingContextId);
    let actor = browsingContext.currentWindowGlobal.getActor("LoginManager");

    actor.sendAsyncMessage("PasswordManager:useGeneratedPassword", {
      inputElementIdentifier,
    });
  },

  /**
   * Find logins for the specified origin..
   *
   * @param {string} formOrigin
   *        Origin of the logins we want to find that has be sanitized by `getLoginOrigin`.
   *        This isn't the same as the browser's top-level document URI
   *        when subframes are involved.
   *
   * @returns {nsILoginInfo[]} a login list
   */
  _findLogins(formOrigin) {
    let searchParams = {
      origin: formOrigin,
      schemeUpgrades: lazy.LoginHelper.schemeUpgrades,
    };
    let logins = lazy.LoginHelper.searchLoginsWithObject(searchParams);
    let resolveBy = ["scheme", "timePasswordChanged"];
    logins = lazy.LoginHelper.dedupeLogins(
      logins,
      ["username", "password"],
      resolveBy,
      formOrigin
    );

    // Sort logins in alphabetical order and by date.
    logins.sort((loginA, loginB) => {
      // Sort alphabetically
      let result = loginA.username.localeCompare(loginB.username);
      if (result) {
        // Forces empty logins to be at the end
        if (!loginA.username) {
          return 1;
        }
        if (!loginB.username) {
          return -1;
        }
        return result;
      }

      // Same username logins are sorted by last change date
      let metaA = loginA.QueryInterface(Ci.nsILoginMetaInfo);
      let metaB = loginB.QueryInterface(Ci.nsILoginMetaInfo);
      return metaB.timePasswordChanged - metaA.timePasswordChanged;
    });

    return logins;
  },

  /**
   * Find duplicate usernames in a login list.
   *
   * @param {nsILoginInfo[]} loginList
   *        A list of logins we want to look for duplicate usernames.
   *
   * @returns {Set} a set with the duplicate usernames.
   */
  _findDuplicates(loginList) {
    let seen = new Set();
    let duplicates = new Set();
    for (let login of loginList) {
      if (seen.has(login.username)) {
        duplicates.add(login.username);
      }
      seen.add(login.username);
    }
    return duplicates;
  },

  /**
   * @param {nsILoginInfo} login
   *        The login we want to fill the form with.
   * @param {Object} inputElementIdentifier
   *        An identifier generated for the input element via ContentDOMReference.
   * @param {xul:browser} browser
   *        The target tab browser.
   * @param {string} formOrigin
   *        Origin of the document we're filling after sanitization via
   *        `getLoginOrigin`.
   *        This isn't the same as the browser's top-level
   *        origin when subframes are involved.
   */
  _fillTargetField(login, inputElementIdentifier, browser, formOrigin) {
    let browsingContextId = inputElementIdentifier.browsingContextId;
    let browsingContext = BrowsingContext.get(browsingContextId);
    if (!browsingContext) {
      return;
    }

    let actor = browsingContext.currentWindowGlobal.getActor("LoginManager");
    if (!actor) {
      return;
    }

    actor
      .fillForm({
        browser,
        inputElementIdentifier,
        loginFormOrigin: formOrigin,
        login,
      })
      .catch(console.error);
  },

  /**
   * @param {string} key
   *        The localized string key
   * @param {string[]} formatArgs
   *        An array of formatting argument string
   *
   * @returns {string} the localized string for the specified key,
   *          formatted with arguments if required.
   */
  _getLocalizedString(key, formatArgs) {
    if (formatArgs) {
      return this._stringBundle.formatStringFromName(key, formatArgs);
    }
    return this._stringBundle.GetStringFromName(key);
  },
};

ChromeUtils.defineLazyGetter(
  LoginManagerContextMenu,
  "_stringBundle",
  function () {
    return Services.strings.createBundle(
      "chrome://passwordmgr/locale/passwordmgr.properties"
    );
  }
);

ChromeUtils.defineLazyGetter(
  LoginManagerContextMenu,
  "dateAndTimeFormatter",
  function () {
    return new Services.intl.DateTimeFormat(undefined, {
      dateStyle: "medium",
    });
  }
);
