/* 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 { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs",
  { global: "contextual" }
);
loader.lazyRequireGetter(
  this,
  "WorkerDescriptorActor",
  "resource://devtools/server/actors/descriptors/worker.js",
  true
);

XPCOMUtils.defineLazyServiceGetter(
  this,
  "wdm",
  "@mozilla.org/dom/workers/workerdebuggermanager;1",
  "nsIWorkerDebuggerManager"
);

function matchWorkerDebugger(dbg, options) {
  if ("type" in options && dbg.type !== options.type) {
    return false;
  }
  if ("window" in options) {
    let window = dbg.window;
    while (window !== null && window.parent !== window) {
      window = window.parent;
    }

    if (window !== options.window) {
      return false;
    }
  }

  return true;
}

function matchServiceWorker(dbg, origin) {
  return (
    dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE &&
    new URL(dbg.url).origin == origin
  );
}

// When a new worker appears, in some cases (i.e. the debugger is running) we
// want it to pause during registration until a later time (i.e. the debugger
// finishes attaching to the worker). This is an optional WorkderDebuggerManager
// listener that can be installed in addition to the WorkerDescriptorActorList
// listener. It always listens to new workers and pauses any matching filters
// which have been set on it.
//
// Two kinds of filters are supported:
//
// setPauseMatching(true) will pause all workers which match the options strcut
// passed in on creation.
//
// setPauseServiceWorkers(origin) will pause all service workers which have the
// specified origin.
//
// FIXME Bug 1601279 separate WorkerPauser from WorkerDescriptorActorList and give
// it a more consistent interface.
class WorkerPauser {
  constructor(options) {
    this._options = options;
    this._pauseMatching = null;
    this._pauseServiceWorkerOrigin = null;

    this.onRegister = this._onRegister.bind(this);
    this.onUnregister = () => {};

    wdm.addListener(this);
  }

  destroy() {
    wdm.removeListener(this);
  }

  _onRegister(dbg) {
    if (
      (this._pauseMatching && matchWorkerDebugger(dbg, this._options)) ||
      (this._pauseServiceWorkerOrigin &&
        matchServiceWorker(dbg, this._pauseServiceWorkerOrigin))
    ) {
      // Prevent the debuggee from executing in this worker until the debugger
      // has finished attaching to it.
      dbg.setDebuggerReady(false);
    }
  }

  setPauseMatching(shouldPause) {
    this._pauseMatching = shouldPause;
  }

  setPauseServiceWorkers(origin) {
    this._pauseServiceWorkerOrigin = origin;
  }
}

class WorkerDescriptorActorList {
  constructor(conn, options) {
    this._conn = conn;
    this._options = options;
    this._actors = new Map();
    this._onListChanged = null;
    this._workerPauser = null;
    this._mustNotify = false;
    this.onRegister = this.onRegister.bind(this);
    this.onUnregister = this.onUnregister.bind(this);
  }

  destroy() {
    this.onListChanged = null;
    if (this._workerPauser) {
      this._workerPauser.destroy();
      this._workerPauser = null;
    }
  }

  getList() {
    // Create a set of debuggers.
    const dbgs = new Set();
    for (const dbg of wdm.getWorkerDebuggerEnumerator()) {
      if (matchWorkerDebugger(dbg, this._options)) {
        dbgs.add(dbg);
      }
    }

    // Delete each actor for which we don't have a debugger.
    for (const [dbg] of this._actors) {
      if (!dbgs.has(dbg)) {
        this._actors.delete(dbg);
      }
    }

    // Create an actor for each debugger for which we don't have one.
    for (const dbg of dbgs) {
      if (!this._actors.has(dbg)) {
        this._actors.set(dbg, new WorkerDescriptorActor(this._conn, dbg));
      }
    }

    const actors = [];
    for (const [, actor] of this._actors) {
      actors.push(actor);
    }

    if (!this._mustNotify) {
      if (this._onListChanged !== null) {
        wdm.addListener(this);
      }
      this._mustNotify = true;
    }

    return Promise.resolve(actors);
  }

  get onListChanged() {
    return this._onListChanged;
  }

  set onListChanged(onListChanged) {
    if (typeof onListChanged !== "function" && onListChanged !== null) {
      throw new Error("onListChanged must be either a function or null.");
    }
    if (onListChanged === this._onListChanged) {
      return;
    }

    if (this._mustNotify) {
      if (this._onListChanged === null && onListChanged !== null) {
        wdm.addListener(this);
      }
      if (this._onListChanged !== null && onListChanged === null) {
        wdm.removeListener(this);
      }
    }
    this._onListChanged = onListChanged;
  }

  _notifyListChanged() {
    this._onListChanged();

    if (this._onListChanged !== null) {
      wdm.removeListener(this);
    }
    this._mustNotify = false;
  }

  onRegister(dbg) {
    if (matchWorkerDebugger(dbg, this._options)) {
      this._notifyListChanged();
    }
  }

  onUnregister(dbg) {
    if (matchWorkerDebugger(dbg, this._options)) {
      this._notifyListChanged();
    }
  }

  get workerPauser() {
    if (!this._workerPauser) {
      this._workerPauser = new WorkerPauser(this._options);
    }
    return this._workerPauser;
  }
}

exports.WorkerDescriptorActorList = WorkerDescriptorActorList;
