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

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
  FormHistoryAutoCompleteResult:
    "resource://gre/modules/FormHistoryAutoComplete.sys.mjs",
  FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
  GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

XPCOMUtils.defineLazyPreferenceGetter(lazy, "gDebug", "browser.formfill.debug");
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "gEnabled",
  "browser.formfill.enable"
);

function log(message) {
  if (!lazy.gDebug) {
    return;
  }
  Services.console.logStringMessage("satchelFormListener: " + message);
}

export class FormHistoryChild extends JSWindowActorChild {
  handleEvent(event) {
    switch (event.type) {
      case "DOMFormBeforeSubmit":
        this.#onDOMFormBeforeSubmit(event.target);
        break;
      default:
        throw new Error("Unexpected event");
    }
  }

  static getInputName(input) {
    return input.name || input.id;
  }

  #onDOMFormBeforeSubmit(form) {
    if (
      !lazy.gEnabled ||
      lazy.PrivateBrowsingUtils.isContentWindowPrivate(form.ownerGlobal)
    ) {
      return;
    }

    log("Form submit observer notified.");

    if (form.getAttribute("autocomplete")?.toLowerCase() == "off") {
      return;
    }

    const entries = [];
    for (const input of form.elements) {
      if (!HTMLInputElement.isInstance(input)) {
        continue;
      }

      // Only use inputs that hold text values (not including type="password")
      if (!input.mozIsTextField(true)) {
        continue;
      }

      // Don't save fields that were previously type=password such as on sites
      // that allow the user to toggle password visibility.
      if (input.hasBeenTypePassword) {
        continue;
      }

      // Bug 1780571, Bug 394612: If Login Manager marked this input, don't save it.
      // The login manager will deal with remembering it.
      if (this.manager.getActor("LoginManager")?.isLoginManagerField(input)) {
        continue;
      }

      // Don't save values when @autocomplete is "off" or has a sensitive field name.
      const autocompleteInfo = input.getAutocompleteInfo();
      if (autocompleteInfo?.canAutomaticallyPersist === false) {
        continue;
      }

      const value = input.lastInteractiveValue?.trim();

      // Only save user entered values even if they match the default value.
      // Any script input is ignored.
      // See Bug 1642570 for details.
      if (!value) {
        continue;
      }

      // Save only when user input was last.
      if (value != input.value.trim()) {
        continue;
      }

      // Don't save credit card numbers.
      if (lazy.CreditCard.isValidNumber(value)) {
        log("skipping saving a credit card number");
        continue;
      }

      const name = FormHistoryChild.getInputName(input);
      if (!name) {
        continue;
      }

      if (name == "searchbar-history") {
        log('addEntry for input name "' + name + '" is denied');
        continue;
      }

      // Limit stored data to 200 characters.
      if (name.length > 200 || value.length > 200) {
        log("skipping input that has a name/value too large");
        continue;
      }

      entries.push({ name, value });

      // Limit number of fields stored per form.
      if (entries.length >= 100) {
        log("not saving any more entries for this form.");
        break;
      }
    }

    if (entries.length) {
      log("sending entries to parent process for form " + form.id);
      this.sendAsyncMessage("FormHistory:FormSubmitEntries", entries);
    }
  }

  get actorName() {
    return "FormHistory";
  }

  /**
   * Get the search options when searching for autocomplete entries in the parent
   *
   * @param {HTMLInputElement} input - The input element to search for autocomplete entries
   * @returns {object} the search options for the input
   */
  getAutoCompleteSearchOption(input) {
    const inputName = FormHistoryChild.getInputName(input);
    const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
      ? "SignUpFormScenario"
      : "";

    return { inputName, scenarioName };
  }

  /**
   * Ask the provider whether it might have autocomplete entry to show
   * for the given input.
   *
   * @param {HTMLInputElement} input - The input element to search for autocomplete entries
   * @returns {boolean} true if we shold search for autocomplete entries
   */
  shouldSearchForAutoComplete(input) {
    if (!lazy.gEnabled) {
      return false;
    }

    const inputName = FormHistoryChild.getInputName(input);
    // Don't allow form inputs (aField != null) to get results from
    // search bar history.
    if (inputName == "searchbar-history") {
      log(`autoCompleteSearch for input name "${inputName}" is denied`);
      return false;
    }

    if (input.autocomplete == "off" || input.form?.autocomplete == "off") {
      log("autoCompleteSearch not allowed due to autcomplete=off");
      return false;
    }

    return true;
  }

  /**
   * Convert the search result to autocomplete results
   *
   * @param {string} searchString - The string to search for
   * @param {HTMLInputElement} input - The input element to search for autocomplete entries
   * @param {Array<object>} records - autocomplete records
   * @returns {AutocompleteResult}
   */
  searchResultToAutoCompleteResult(searchString, input, records) {
    const inputName = FormHistoryChild.getInputName(input);
    const acResult = new lazy.FormHistoryAutoCompleteResult(
      input,
      [],
      inputName,
      searchString
    );

    acResult.fixedEntries = this.getDataListSuggestions(input);
    if (!records) {
      return acResult;
    }

    const entries = records.formHistoryEntries;
    const externalEntries = records.externalEntries;

    if (input?.maxLength > -1) {
      acResult.entries = entries.filter(
        el => el.text.length <= input.maxLength
      );
    } else {
      acResult.entries = entries;
    }

    acResult.externalEntries.push(
      ...externalEntries.map(
        entry =>
          new lazy.GenericAutocompleteItem(
            entry.image,
            entry.label,
            entry.secondary,
            entry.fillMessageName,
            entry.fillMessageData
          )
      )
    );

    acResult.removeDuplicateHistoryEntries();
    return acResult;
  }

  #isTextControl(input) {
    return [
      "text",
      "email",
      "search",
      "tel",
      "url",
      "number",
      "month",
      "week",
      "password",
    ].includes(input.type);
  }

  getDataListSuggestions(input) {
    const items = [];

    if (!this.#isTextControl(input) || !input.list) {
      return items;
    }

    const upperFieldValue = input.value.toUpperCase();

    for (const option of input.list.options) {
      const label = option.label || option.text || option.value || "";

      if (!label.toUpperCase().includes(upperFieldValue)) {
        continue;
      }

      items.push({
        label,
        value: option.value,
      });
    }

    return items;
  }
}
