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

/*
 * Target actor for all resources in a content process of Firefox (chrome sandboxes, frame
 * scripts, documents, etc.)
 *
 * See devtools/docs/contributor/backend/actor-hierarchy.md for more details about all the targets.
 */

const { ThreadActor } = require("resource://devtools/server/actors/thread.js");
const {
  WebConsoleActor,
} = require("resource://devtools/server/actors/webconsole.js");
const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
const { Pool } = require("resource://devtools/shared/protocol.js");
const { assert } = require("resource://devtools/shared/DevToolsUtils.js");
const {
  SourcesManager,
} = require("resource://devtools/server/actors/utils/sources-manager.js");
const {
  contentProcessTargetSpec,
} = require("resource://devtools/shared/specs/targets/content-process.js");
const Targets = require("resource://devtools/server/actors/targets/index.js");
const Resources = require("resource://devtools/server/actors/resources/index.js");
const {
  BaseTargetActor,
} = require("resource://devtools/server/actors/targets/base-target-actor.js");
const { TargetActorRegistry } = ChromeUtils.importESModule(
  "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
  { global: "shared" }
);

loader.lazyRequireGetter(
  this,
  "WorkerDescriptorActorList",
  "resource://devtools/server/actors/worker/worker-descriptor-actor-list.js",
  true
);
loader.lazyRequireGetter(
  this,
  "MemoryActor",
  "resource://devtools/server/actors/memory.js",
  true
);
loader.lazyRequireGetter(
  this,
  "TracerActor",
  "resource://devtools/server/actors/tracer.js",
  true
);

class ContentProcessTargetActor extends BaseTargetActor {
  constructor(conn, { isXpcShellTarget = false, sessionContext } = {}) {
    super(conn, Targets.TYPES.PROCESS, contentProcessTargetSpec);

    this.threadActor = null;
    this.isXpcShellTarget = isXpcShellTarget;
    this.sessionContext = sessionContext;

    // Use a see-everything debugger
    this.makeDebugger = makeDebugger.bind(null, {
      findDebuggees: dbg =>
        dbg.findAllGlobals().map(g => g.unsafeDereference()),
      shouldAddNewGlobalAsDebuggee: () => true,
    });

    const sandboxPrototype = {
      get tabs() {
        return Array.from(
          Services.ww.getWindowEnumerator(),
          win => win.docShell.messageManager
        );
      },
    };

    // Scope into which the webconsole executes:
    // A sandbox with chrome privileges with a `tabs` getter.
    const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
      Ci.nsIPrincipal
    );
    const sandbox = Cu.Sandbox(systemPrincipal, {
      sandboxPrototype,
      wantGlobalProperties: ["ChromeUtils"],
    });
    this._consoleScope = sandbox;

    this._workerList = null;
    this._workerDescriptorActorPool = null;
    this._onWorkerListChanged = this._onWorkerListChanged.bind(this);

    // Try to destroy the Content Process Target when the content process shuts down.
    // The parent process can't communicate during shutdown as the communication channel
    // is already down (message manager or JS Window Actor API).
    // So that we have to observe to some event fired from this process.
    // While such cleanup doesn't sound ultimately necessary (the process will be completely destroyed)
    // mochitests are asserting that there is no leaks during process shutdown.
    // Do not override destroy as Protocol.js may override it when calling destroy,
    // and we won't be able to call removeObserver correctly.
    this.destroyObserver = this.destroy.bind(this);
    Services.obs.addObserver(this.destroyObserver, "xpcom-shutdown");
    if (this.isXpcShellTarget) {
      TargetActorRegistry.registerXpcShellTargetActor(this);
    }
  }

  get isRootActor() {
    return true;
  }

  get url() {
    return undefined;
  }

  get window() {
    return this._consoleScope;
  }

  get targetGlobal() {
    return this._consoleScope;
  }

  get sourcesManager() {
    if (!this._sourcesManager) {
      assert(
        this.threadActor,
        "threadActor should exist when creating SourcesManager."
      );
      this._sourcesManager = new SourcesManager(this.threadActor);
    }
    return this._sourcesManager;
  }

  /*
   * Return a Debugger instance or create one if there is none yet
   */
  get dbg() {
    if (!this._dbg) {
      this._dbg = this.makeDebugger();
    }
    return this._dbg;
  }

  form() {
    if (!this._consoleActor) {
      this._consoleActor = new WebConsoleActor(this.conn, this);
      this.manage(this._consoleActor);
    }

    if (!this.threadActor) {
      this.threadActor = new ThreadActor(this);
      this.manage(this.threadActor);
    }
    if (!this.memoryActor) {
      this.memoryActor = new MemoryActor(this.conn, this);
      this.manage(this.memoryActor);
    }
    if (!this.tracerActor) {
      this.tracerActor = new TracerActor(this.conn, this);
      this.manage(this.tracerActor);
    }

    return {
      actor: this.actorID,
      targetType: this.targetType,

      isXpcShellTarget: this.isXpcShellTarget,
      processID: Services.appinfo.processID,
      remoteType: Services.appinfo.remoteType,

      consoleActor: this._consoleActor.actorID,
      memoryActor: this.memoryActor.actorID,
      threadActor: this.threadActor.actorID,
      tracerActor: this.tracerActor.actorID,

      traits: {
        networkMonitor: false,
        // See trait description in browsing-context.js
        supportsTopLevelTargetFlag: false,
      },
    };
  }

  ensureWorkerList() {
    if (!this._workerList) {
      this._workerList = new WorkerDescriptorActorList(this.conn, {});
    }
    return this._workerList;
  }

  listWorkers() {
    return this.ensureWorkerList()
      .getList()
      .then(actors => {
        const pool = new Pool(this.conn, "workers");
        for (const actor of actors) {
          pool.manage(actor);
        }

        // Do not destroy the pool before transfering ownership to the newly created
        // pool, so that we do not accidentally destroy actors that are still in use.
        if (this._workerDescriptorActorPool) {
          this._workerDescriptorActorPool.destroy();
        }

        this._workerDescriptorActorPool = pool;
        this._workerList.onListChanged = this._onWorkerListChanged;

        return { workers: actors };
      });
  }

  _onWorkerListChanged() {
    this.conn.send({ from: this.actorID, type: "workerListChanged" });
    this._workerList.onListChanged = null;
  }

  pauseMatchingServiceWorkers(request) {
    this.ensureWorkerList().workerPauser.setPauseServiceWorkers(request.origin);
  }

  destroy({ isModeSwitching } = {}) {
    // Avoid reentrancy. We will destroy the Transport when emitting "destroyed",
    // which will force destroying all actors.
    if (this.destroying) {
      return;
    }
    this.destroying = true;

    // Unregistering watchers first is important
    // otherwise you might have leaks reported when running browser_browser_toolbox_netmonitor.js in debug builds
    Resources.unwatchAllResources(this);

    this.emit("destroyed", { isModeSwitching });

    super.destroy();

    if (this.threadActor) {
      this.threadActor = null;
    }

    // Tell the live lists we aren't watching any more.
    if (this._workerList) {
      this._workerList.destroy();
      this._workerList = null;
    }

    if (this._sourcesManager) {
      this._sourcesManager.destroy();
      this._sourcesManager = null;
    }

    if (this._dbg) {
      this._dbg.disable();
      this._dbg = null;
    }

    Services.obs.removeObserver(this.destroyObserver, "xpcom-shutdown");

    if (this.isXpcShellTarget) {
      TargetActorRegistry.unregisterXpcShellTargetActor(this);
    }
  }
}

exports.ContentProcessTargetActor = ContentProcessTargetActor;
