/* 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";

const { Ci } = require("chrome");
const { rootSpec } = require("devtools/shared/specs/root");
const {
  FrontClassWithSpec,
  registerFront,
} = require("devtools/shared/protocol");

loader.lazyRequireGetter(this, "getFront", "devtools/shared/protocol", true);

class RootFront extends FrontClassWithSpec(rootSpec) {
  constructor(client, targetFront, parentFront) {
    super(client, targetFront, parentFront);

    // Cache root form as this will always be the same value.
    Object.defineProperty(this, "rootForm", {
      get() {
        delete this.rootForm;
        this.rootForm = this.getRoot();
        return this.rootForm;
      },
      configurable: true,
    });

    // Cache of already created global scoped fronts
    // [typeName:string => Front instance]
    this.fronts = new Map();

    this._client = client;
  }

  form(form) {
    // Root Front is a special Front. It is the only one to set its actor ID manually
    // out of the form object returned by RootActor.sayHello which is called when calling
    // DevToolsClient.connect().
    this.actorID = form.from;

    this.applicationType = form.applicationType;
    this.traits = form.traits;
  }
  /**
   * Retrieve all service worker registrations with their corresponding workers.
   * @param {Array} [workerTargets] (optional)
   *        Array containing the result of a call to `listAllWorkerTargets`.
   *        (this exists to avoid duplication of calls to that method)
   * @return {Object[]} result - An Array of Objects with the following format
   *         - {result[].registration} - The registration front
   *         - {result[].workers} Array of form-like objects for service workers
   */
  async listAllServiceWorkers(workerTargets) {
    const result = [];
    const { registrations } = await this.listServiceWorkerRegistrations();
    const allWorkers = workerTargets
      ? workerTargets
      : await this.listAllWorkerTargets();

    for (const registrationFront of registrations) {
      // workers from the registration, ordered from most recent to older
      const workers = [
        registrationFront.activeWorker,
        registrationFront.waitingWorker,
        registrationFront.installingWorker,
        registrationFront.evaluatingWorker,
      ]
        // filter out non-existing workers
        .filter(w => !!w)
        // build a worker object with its WorkerDescriptorFront
        .map(workerFront => {
          const workerDescriptorFront = allWorkers.find(
            targetFront => targetFront.id === workerFront.id
          );

          return {
            id: workerFront.id,
            name: workerFront.url,
            state: workerFront.state,
            stateText: workerFront.stateText,
            url: workerFront.url,
            workerDescriptorFront,
          };
        });

      // TODO: return only the worker targets. See Bug 1620605
      result.push({
        registration: registrationFront,
        workers,
      });
    }

    return result;
  }

  /**
   * Retrieve all service worker registrations as well as workers from the parent and
   * content processes. Listing service workers involves merging information coming from
   * registrations and workers, this method will combine this information to present a
   * unified array of serviceWorkers. If you are only interested in other workers, use
   * listWorkers.
   *
   * @return {Object}
   *         - {Array} service
   *           array of form-like objects for serviceworkers
   *         - {Array} shared
   *           Array of WorkerTargetActor forms, containing shared workers.
   *         - {Array} other
   *           Array of WorkerTargetActor forms, containing other workers.
   */
  async listAllWorkers() {
    const allWorkers = await this.listAllWorkerTargets();
    const serviceWorkers = await this.listAllServiceWorkers(allWorkers);

    // NOTE: listAllServiceWorkers() now returns all the workers belonging to
    //       a registration. To preserve the usual behavior at about:debugging,
    //       in which we show only the most recent one, we grab the first
    //       worker in the array only.
    const result = {
      service: serviceWorkers
        .map(({ registration, workers }) => {
          return workers.slice(0, 1).map(worker => {
            return Object.assign(worker, {
              registrationFront: registration,
              fetch: registration.fetch,
            });
          });
        })
        .flat(),
      shared: [],
      other: [],
    };

    allWorkers.forEach(front => {
      const worker = {
        id: front.id,
        url: front.url,
        name: front.url,
        workerDescriptorFront: front,
      };

      switch (front.type) {
        case Ci.nsIWorkerDebugger.TYPE_SERVICE:
          // do nothing, since we already fetched them in `serviceWorkers`
          break;
        case Ci.nsIWorkerDebugger.TYPE_SHARED:
          result.shared.push(worker);
          break;
        default:
          result.other.push(worker);
      }
    });

    return result;
  }

  /** Get the target fronts for all worker threads running in any process. */
  async listAllWorkerTargets() {
    const listParentWorkers = async () => {
      const { workers } = await this.listWorkers();
      return workers;
    };
    const listChildWorkers = async () => {
      const processes = await this.listProcesses();
      const processWorkers = await Promise.all(
        processes.map(async processDescriptorFront => {
          // Ignore parent process
          if (processDescriptorFront.isParentProcessDescriptor) {
            return [];
          }
          try {
            const front = await processDescriptorFront.getTarget();
            if (!front) {
              return [];
            }
            const response = await front.listWorkers();
            return response.workers;
          } catch (e) {
            if (e.message.includes("Connection closed")) {
              return [];
            }
            throw e;
          }
        })
      );

      return processWorkers.flat();
    };

    const [parentWorkers, childWorkers] = await Promise.all([
      listParentWorkers(),
      listChildWorkers(),
    ]);

    return parentWorkers.concat(childWorkers);
  }

  /**
   * Fetch the ProcessDescriptorFront for the main process.
   *
   * `getProcess` requests allows to fetch the descriptor for any process and
   * the main process is having the process ID zero.
   */
  getMainProcess() {
    return this.getProcess(0);
  }

  /**
   * Fetch the tab descriptor for the currently selected tab, or for a specific
   * tab given as first parameter.
   *
   * @param [optional] object filter
   *        A dictionary object with following optional attributes:
   *         - browserId: use to match any tab
   *         - tab: a reference to xul:tab element (used for local tab debugging)
   *         - isWebExtension: an optional boolean to flag TabDescriptors
   *        If nothing is specified, returns the actor for the currently
   *        selected tab.
   */
  async getTab(filter) {
    const packet = {};
    if (filter) {
      if (typeof filter.browserId == "number") {
        packet.browserId = filter.browserId;
      } else if ("tab" in filter) {
        const browser = filter.tab.linkedBrowser;
        packet.browserId = browser.browserId;
      } else {
        // Throw if a filter object have been passed but without
        // any clearly idenfified filter.
        throw new Error("Unsupported argument given to getTab request");
      }
    }

    const descriptorFront = await super.getTab(packet);

    // Will flag TabDescriptor used by WebExtension codebase.
    if (filter?.isWebExtension) {
      descriptorFront.setIsForWebExtension(true);
    }

    // If the tab is a local tab, forward it to the descriptor.
    if (filter?.tab?.tagName == "tab") {
      // Ignore the fake `tab` object we receive, where there is only a
      // `linkedBrowser` attribute, but this isn't a real <tab> element.
      // devtools/client/framework/test/browser_toolbox_target.js is passing such
      // a fake tab.
      descriptorFront.setLocalTab(filter.tab);
    }

    return descriptorFront;
  }

  /**
   * Fetch the target front for a given add-on.
   * This is just an helper on top of `listAddons` request.
   *
   * @param object filter
   *        A dictionary object with following attribute:
   *         - id: used to match the add-on to connect to.
   */
  async getAddon({ id }) {
    const addons = await this.listAddons();
    const webextensionDescriptorFront = addons.find(addon => addon.id === id);
    return webextensionDescriptorFront;
  }

  /**
   * Fetch the target front for a given worker.
   * This is just an helper on top of `listAllWorkers` request.
   *
   * @param id
   */
  async getWorker(id) {
    const { service, shared, other } = await this.listAllWorkers();
    const worker = [...service, ...shared, ...other].find(w => w.id === id);
    if (!worker) {
      return null;
    }
    return worker.workerDescriptorFront || worker.registrationFront;
  }

  /**
   * Test request that returns the object passed as first argument.
   *
   * `echo` is special as all the property of the given object have to be passed
   * on the packet object. That's not something that can be achieve by requester helper.
   */

  echo(packet) {
    packet.type = "echo";
    return this.request(packet);
  }

  /*
   * This function returns a protocol.js Front for any root actor.
   * i.e. the one directly served from RootActor.listTabs or getRoot.
   *
   * @param String typeName
   *        The type name used in protocol.js's spec for this actor.
   */
  async getFront(typeName) {
    let front = this.fronts.get(typeName);
    if (front) {
      return front;
    }
    const rootForm = await this.rootForm;
    front = getFront(this._client, typeName, rootForm);
    this.fronts.set(typeName, front);
    return front;
  }

  /*
   * This function returns true if the root actor has a registered global actor
   * with a given name.
   * @param {String} actorName
   *        The name of a global actor.
   *
   * @return {Boolean}
   */
  async hasActor(actorName) {
    const rootForm = await this.rootForm;
    return !!rootForm[actorName + "Actor"];
  }
}
exports.RootFront = RootFront;
registerFront(RootFront);
