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

"use strict";

/* globals ExtensionAPI, Services, XPCOMUtils */

const CACHED_STYLESHEETS = new WeakMap();

ChromeUtils.defineESModuleGetters(this, {
  FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
  FormAutofillParent: "resource://autofill/FormAutofillParent.sys.mjs",
  FormAutofillStatus: "resource://autofill/FormAutofillParent.sys.mjs",
  AutoCompleteParent: "resource://gre/actors/AutoCompleteParent.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  this,
  "resProto",
  "@mozilla.org/network/protocol;1?name=resource",
  Ci.nsISubstitutingProtocolHandler
);

const RESOURCE_HOST = "formautofill";

function insertStyleSheet(domWindow, url) {
  let doc = domWindow.document;
  let styleSheetAttr = `href="${url}" type="text/css"`;
  let styleSheet = doc.createProcessingInstruction(
    "xml-stylesheet",
    styleSheetAttr
  );

  doc.insertBefore(styleSheet, doc.documentElement);

  if (CACHED_STYLESHEETS.has(domWindow)) {
    CACHED_STYLESHEETS.get(domWindow).push(styleSheet);
  } else {
    CACHED_STYLESHEETS.set(domWindow, [styleSheet]);
  }
}

function ensureCssLoaded(domWindow) {
  if (CACHED_STYLESHEETS.has(domWindow)) {
    // This window already has autofill stylesheets.
    return;
  }

  insertStyleSheet(domWindow, "chrome://formautofill/content/formautofill.css");
}

this.formautofill = class extends ExtensionAPI {
  /**
   * Adjusts and checks form autofill preferences during startup.
   *
   * @param {boolean} addressAutofillAvailable
   * @param {boolean} creditCardAutofillAvailable
   */
  adjustAndCheckFormAutofillPrefs(
    addressAutofillAvailable,
    creditCardAutofillAvailable
  ) {
    // Reset the sync prefs in case the features were previously available
    // but aren't now.
    if (!creditCardAutofillAvailable) {
      Services.prefs.clearUserPref(
        "services.sync.engine.creditcards.available"
      );
    }
    if (!addressAutofillAvailable) {
      Services.prefs.clearUserPref("services.sync.engine.addresses.available");
    }

    if (!addressAutofillAvailable && !creditCardAutofillAvailable) {
      Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
      Glean.formautofill.availability.set(false);
      return;
    }

    // This pref is used for web contents to detect the autocomplete feature.
    // When it's true, "element.autocomplete" will return tokens we currently
    // support -- otherwise it'll return an empty string.
    Services.prefs.setBoolPref("dom.forms.autocomplete.formautofill", true);
    Glean.formautofill.availability.set(true);

    // These "*.available" prefs determines whether the "addresses"/"creditcards" sync engine is
    // available (ie, whether it is shown in any UI etc) - it *does not* determine
    // whether the engine is actually enabled or not.
    if (FormAutofill.isAutofillAddressesAvailable) {
      Services.prefs.setBoolPref(
        "services.sync.engine.addresses.available",
        true
      );
    } else {
      Services.prefs.clearUserPref("services.sync.engine.addresses.available");
    }
    if (FormAutofill.isAutofillCreditCardsAvailable) {
      Services.prefs.setBoolPref(
        "services.sync.engine.creditcards.available",
        true
      );
    } else {
      Services.prefs.clearUserPref(
        "services.sync.engine.creditcards.available"
      );
    }
  }
  onStartup() {
    // We have to do this before actually determining if we're enabled, since
    // there are scripts inside of the core browser code that depend on the
    // FormAutofill JSMs being registered.
    let uri = Services.io.newURI("chrome/res/", null, this.extension.rootURI);
    resProto.setSubstitution(RESOURCE_HOST, uri);

    let aomStartup = Cc[
      "@mozilla.org/addons/addon-manager-startup;1"
    ].getService(Ci.amIAddonManagerStartup);
    const manifestURI = Services.io.newURI(
      "manifest.json",
      null,
      this.extension.rootURI
    );
    this.chromeHandle = aomStartup.registerChrome(manifestURI, [
      ["content", "formautofill", "content/"],
    ]);

    this.adjustAndCheckFormAutofillPrefs(
      FormAutofill.isAutofillAddressesAvailable,
      FormAutofill.isAutofillCreditCardsAvailable
    );

    // Listen for the autocomplete popup message
    // or the form submitted message (which may trigger a
    // doorhanger) to lazily append our stylesheets related
    // to the autocomplete feature.
    AutoCompleteParent.addPopupStateListener(ensureCssLoaded);
    FormAutofillParent.addMessageObserver(this);
    this.onFormSubmitted = (data, window) => ensureCssLoaded(window);

    FormAutofillStatus.init();

    ChromeUtils.registerWindowActor("FormAutofill", {
      parent: {
        esModuleURI: "resource://autofill/FormAutofillParent.sys.mjs",
      },
      child: {
        esModuleURI: "resource://autofill/FormAutofillChild.sys.mjs",
        events: {
          focusin: { capture: true },
          "form-changed": { createActor: false },
          "form-submission-detected": { createActor: false },
        },
      },
      allFrames: true,
    });
  }

  onShutdown(isAppShutdown) {
    if (isAppShutdown) {
      return;
    }

    resProto.setSubstitution(RESOURCE_HOST, null);

    this.chromeHandle.destruct();
    this.chromeHandle = null;

    ChromeUtils.unregisterWindowActor("FormAutofill");

    AutoCompleteParent.removePopupStateListener(ensureCssLoaded);
    FormAutofillParent.removeMessageObserver(this);

    for (let win of Services.wm.getEnumerator("navigator:browser")) {
      let cachedStyleSheets = CACHED_STYLESHEETS.get(win);

      if (!cachedStyleSheets) {
        continue;
      }

      while (cachedStyleSheets.length !== 0) {
        cachedStyleSheets.pop().remove();
      }
    }
  }
};
