/* 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 browser, module */

class Injections {
  constructor(availableInjections, customFunctions) {
    this.INJECTION_PREF = "perform_injections";

    this._injectionsEnabled = true;

    this._availableInjections = availableInjections;
    this._activeInjections = new Set();
    // Only used if this.shouldUseScriptingAPI is false and we are falling back
    // to use the contentScripts API.
    this._activeInjectionHandles = new Map();
    this._customFunctions = customFunctions;

    this.shouldUseScriptingAPI =
      browser.aboutConfigPrefs.getBoolPrefSync("useScriptingAPI");
    // Debug log emit only on nightly (similarly to the debug
    // helper used in shims.js for similar purpose).
    browser.appConstants.getReleaseBranch().then(releaseBranch => {
      if (releaseBranch !== "release_or_beta") {
        console.debug(
          `WebCompat Injections will be injected using ${
            this.shouldUseScriptingAPI ? "scripting" : "contentScripts"
          } API`
        );
      }
    });
  }

  bindAboutCompatBroker(broker) {
    this._aboutCompatBroker = broker;
  }

  bootup() {
    browser.aboutConfigPrefs.onPrefChange.addListener(() => {
      this.checkInjectionPref();
    }, this.INJECTION_PREF);
    this.checkInjectionPref();
  }

  checkInjectionPref() {
    browser.aboutConfigPrefs.getPref(this.INJECTION_PREF).then(value => {
      if (value === undefined) {
        browser.aboutConfigPrefs.setPref(this.INJECTION_PREF, true);
      } else if (value === false) {
        this.unregisterContentScripts();
      } else {
        this.registerContentScripts();
      }
    });
  }

  getAvailableInjections() {
    return this._availableInjections;
  }

  isEnabled() {
    return this._injectionsEnabled;
  }

  async getPromiseRegisteredScriptIds(scriptIds) {
    let registeredScriptIds = [];

    // Try to avoid re-registering scripts already registered
    // (e.g. if the webcompat background page is restarted
    // after an extension process crash, after having registered
    // the content scripts already once), but do not prevent
    // to try registering them again if the getRegisteredContentScripts
    // method returns an unexpected rejection.
    try {
      const registeredScripts =
        await browser.scripting.getRegisteredContentScripts({
          // By default only look for script ids that belongs to Injections
          // (and ignore the ones that may belong to Shims).
          ids: scriptIds ?? this._availableInjections.map(inj => inj.id),
        });
      registeredScriptIds = registeredScripts.map(script => script.id);
    } catch (ex) {
      console.error(
        "Retrieve WebCompat GoFaster registered content scripts failed: ",
        ex
      );
    }

    return registeredScriptIds;
  }

  async registerContentScripts() {
    const platformInfo = await browser.runtime.getPlatformInfo();
    const platformMatches = [
      "all",
      platformInfo.os,
      platformInfo.os == "android" ? "android" : "desktop",
    ];

    let registeredScriptIds = this.shouldUseScriptingAPI
      ? await this.getPromiseRegisteredScriptIds()
      : [];

    for (const injection of this._availableInjections) {
      if (platformMatches.includes(injection.platform)) {
        injection.availableOnPlatform = true;
        try {
          if (injection.checkIfNeeded && !injection.checkIfNeeded()) {
            console.warn("Skipping un-needed injection for", injection.domain);
          } else {
            await this.enableInjection(injection, registeredScriptIds);
          }
        } catch (e) {
          console.error("Error enabling injection for", injection.domain, e);
        }
      }
    }

    this._injectionsEnabled = true;
    this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
      interventionsChanged: this._aboutCompatBroker.filterOverrides(
        this._availableInjections
      ),
    });
  }

  buildContentScriptRegistrations(contentScripts) {
    let finalConfig = Object.assign({}, contentScripts);

    if (!finalConfig.runAt) {
      finalConfig.runAt = "document_start";
    }

    if (this.shouldUseScriptingAPI) {
      // Don't persist the content scripts across browser restarts
      // (at least not yet, we would need to apply some more changes
      // to adjust webcompat for accounting for the scripts to be
      // already registered).
      //
      // NOTE: scripting API has been introduced in Gecko 102,
      // prior to Gecko 105 persistAcrossSessions option was required
      // and only accepted false persistAcrossSessions, after Gecko 105
      // is optional and defaults to true.

      finalConfig.persistAcrossSessions = false;

      // Convert js/css from contentScripts.register API method
      // format to scripting.registerContentScripts API method
      // format.
      if (Array.isArray(finalConfig.js)) {
        finalConfig.js = finalConfig.js.map(e => e.file);
      }

      if (Array.isArray(finalConfig.css)) {
        finalConfig.css = finalConfig.css.map(e => e.file);
      }
    }

    return finalConfig;
  }

  async enableInjection(injection, registeredScriptIds) {
    if (injection.active) {
      return undefined;
    }

    if (injection.customFunc) {
      return this.enableCustomInjection(injection);
    }

    return this.enableContentScripts(injection, registeredScriptIds);
  }

  enableCustomInjection(injection) {
    if (injection.customFunc in this._customFunctions) {
      this._customFunctions[injection.customFunc](injection);
      injection.active = true;
    } else {
      console.error(
        `Provided function ${injection.customFunc} wasn't found in functions list`
      );
    }
  }

  async enableContentScripts(injection, registeredScriptIds) {
    let injectProps;
    try {
      const { id } = injection;
      if (this.shouldUseScriptingAPI) {
        // enableContentScripts receives a registeredScriptIds already
        // pre-computed once from registerContentScripts to register all
        // the injection, whereas it does not expect to receive one when
        // it is called from the AboutCompatBroker to re-enable one specific
        // injection.
        let activeScriptIds = Array.isArray(registeredScriptIds)
          ? registeredScriptIds
          : await this.getPromiseRegisteredScriptIds([id]);
        injectProps = this.buildContentScriptRegistrations(
          injection.contentScripts
        );
        injectProps.id = id;
        if (!activeScriptIds.includes(id)) {
          await browser.scripting.registerContentScripts([injectProps]);
        }
        this._activeInjections.add(id);
      } else {
        const handle = await browser.contentScripts.register(
          this.buildContentScriptRegistrations(injection.contentScripts)
        );
        this._activeInjections.add(id);
        this._activeInjectionHandles.set(id, handle);
      }

      injection.active = true;
    } catch (ex) {
      console.error(
        "Registering WebCompat GoFaster content scripts failed: ",
        { injection, injectProps },
        ex
      );
    }
  }

  unregisterContentScripts() {
    for (const injection of this._availableInjections) {
      this.disableInjection(injection);
    }

    this._injectionsEnabled = false;
    this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
      interventionsChanged: false,
    });
  }

  async disableInjection(injection) {
    if (!injection.active) {
      return undefined;
    }

    if (injection.customFunc) {
      return this.disableCustomInjections(injection);
    }

    return this.disableContentScripts(injection);
  }

  disableCustomInjections(injection) {
    const disableFunc = injection.customFunc + "Disable";

    if (disableFunc in this._customFunctions) {
      this._customFunctions[disableFunc](injection);
      injection.active = false;
    } else {
      console.error(
        `Provided function ${disableFunc} for disabling injection wasn't found in functions list`
      );
    }
  }

  async disableContentScripts(injection) {
    if (this._activeInjections.has(injection.id)) {
      if (this.shouldUseScriptingAPI) {
        await browser.scripting.unregisterContentScripts({
          ids: [injection.id],
        });
      } else {
        const handle = this._activeInjectionHandles.get(injection.id);
        await handle.unregister();
        this._activeInjectionHandles.delete(injection.id);
      }
      this._activeInjections.delete(injection);
    }
    injection.active = false;
  }
}

module.exports = Injections;
