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

var { loader, require } = ChromeUtils.importESModule(
  "resource://devtools/shared/loader/Loader.sys.mjs"
);

var { useDistinctSystemPrincipalLoader, releaseDistinctSystemPrincipalLoader } =
  ChromeUtils.importESModule(
    "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
  );

// Require this module to setup core modules
loader.require("resource://devtools/client/framework/devtools-browser.js");

var { gDevTools } = require("resource://devtools/client/framework/devtools.js");
var { Toolbox } = require("resource://devtools/client/framework/toolbox.js");
var {
  DevToolsClient,
} = require("resource://devtools/client/devtools-client.js");
var { PrefsHelper } = require("resource://devtools/client/shared/prefs.js");
const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const L10N = new LocalizationHelper(
  "devtools/client/locales/toolbox.properties"
);

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  BrowserToolboxLauncher:
    "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs",
});

const {
  CommandsFactory,
} = require("resource://devtools/shared/commands/commands-factory.js");

// Timeout to wait before we assume that a connect() timed out without an error.
// In milliseconds. (With the Debugger pane open, this has been reported to last
// more than 10 seconds!)
const STATUS_REVEAL_TIME = 15000;

/**
 * Shortcuts for accessing various debugger preferences.
 */
var Prefs = new PrefsHelper("devtools.debugger", {
  chromeDebuggingHost: ["Char", "chrome-debugging-host"],
  chromeDebuggingWebSocket: ["Bool", "chrome-debugging-websocket"],
});

var gCommands, gToolbox, gShortcuts;

function appendStatusMessage(msg) {
  const statusMessage = document.getElementById("status-message");
  statusMessage.textContent += msg + "\n";
  if (msg.stack) {
    statusMessage.textContent += msg.stack + "\n";
  }
}

function toggleStatusMessage(visible = true) {
  document.getElementById("status-message-container").hidden = !visible;
}

function revealStatusMessage() {
  toggleStatusMessage(true);
}

function hideStatusMessage() {
  toggleStatusMessage(false);
}

var connect = async function () {
  // Initiate the connection

  // MOZ_BROWSER_TOOLBOX_INPUT_CONTEXT is set by the target Firefox instance
  // before opening the Browser Toolbox.
  // If "devtools.webconsole.input.context" is true, the variable is set to "1",
  // otherwise it is set to "0".
  Services.prefs.setBoolPref(
    "devtools.webconsole.input.context",
    Services.env.get("MOZ_BROWSER_TOOLBOX_INPUT_CONTEXT") === "1"
  );
  // Similar, but for the Browser Toolbox mode
  if (Services.env.get("MOZ_BROWSER_TOOLBOX_FORCE_MULTIPROCESS") === "1") {
    Services.prefs.setCharPref("devtools.browsertoolbox.scope", "everything");
  }

  const port = Services.env.get("MOZ_BROWSER_TOOLBOX_PORT");

  // A port needs to be passed in from the environment, for instance:
  //    MOZ_BROWSER_TOOLBOX_PORT=6080 ./mach run -chrome \
  //      chrome://devtools/content/framework/browser-toolbox/window.html
  if (!port) {
    throw new Error(
      "Must pass a port in an env variable with MOZ_BROWSER_TOOLBOX_PORT"
    );
  }

  const host = Prefs.chromeDebuggingHost;
  const webSocket = Prefs.chromeDebuggingWebSocket;
  appendStatusMessage(`Connecting to ${host}:${port}, ws: ${webSocket}`);
  const transport = await DevToolsClient.socketConnect({
    host,
    port,
    webSocket,
  });
  const client = new DevToolsClient(transport);
  appendStatusMessage("Start protocol client for connection");
  await client.connect();

  appendStatusMessage("Get root form for toolbox");
  gCommands = await CommandsFactory.forMainProcess({ client });

  // Bug 1794607: for some unexpected reason, closing the DevToolsClient
  // when the commands is destroyed by the toolbox would introduce leaks
  // when running the browser-toolbox mochitests.
  gCommands.shouldCloseClient = false;

  await openToolbox(gCommands);
};

// Certain options should be toggled since we can assume chrome debugging here
function setPrefDefaults() {
  Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
  Services.prefs.setBoolPref(
    "devtools.inspector.showAllAnonymousContent",
    true
  );
  Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
  Services.prefs.setBoolPref("devtools.console.stdout.chrome", true);
  Services.prefs.setBoolPref(
    "devtools.command-button-noautohide.enabled",
    true
  );

  // We force enabling the performance panel in the browser toolbox.
  Services.prefs.setBoolPref("devtools.performance.enabled", true);

  // Bug 1773226: Try to avoid session restore to reopen a transient browser window
  // if we ever opened a URL from the browser toolbox. (but it doesn't seem to be enough)
  Services.prefs.setBoolPref("browser.sessionstore.resume_from_crash", false);

  // Disable Safe mode as the browser toolbox is often closed brutaly by subprocess
  // and the safe mode kicks in when reopening it
  Services.prefs.setIntPref("toolkit.startup.max_resumed_crashes", -1);
}

window.addEventListener(
  "load",
  async function () {
    gShortcuts = new KeyShortcuts({ window });
    gShortcuts.on("CmdOrCtrl+W", onCloseCommand);
    gShortcuts.on("CmdOrCtrl+Alt+Shift+I", onDebugBrowserToolbox);
    gShortcuts.on("CmdOrCtrl+Alt+R", onReloadBrowser);

    const statusMessageContainer = document.getElementById(
      "status-message-title"
    );
    statusMessageContainer.textContent = L10N.getStr(
      "browserToolbox.statusMessage"
    );

    setPrefDefaults();

    // Reveal status message if connecting is slow or if an error occurs.
    const delayedStatusReveal = setTimeout(
      revealStatusMessage,
      STATUS_REVEAL_TIME
    );
    try {
      await connect();
      clearTimeout(delayedStatusReveal);
      hideStatusMessage();
    } catch (e) {
      clearTimeout(delayedStatusReveal);
      appendStatusMessage(e);
      revealStatusMessage();
      console.error(e);
    }
  },
  { once: true }
);

function onCloseCommand(event) {
  window.close();
}

/**
 * Open a Browser toolbox debugging the current browser toolbox
 *
 * This helps debugging the browser toolbox code, especially the code
 * running in the parent process. i.e. frontend code.
 */
function onDebugBrowserToolbox() {
  lazy.BrowserToolboxLauncher.init();
}

/**
 * Replicate the local-build-only key shortcut to reload the browser
 */
function onReloadBrowser() {
  gToolbox.commands.targetCommand.reloadTopLevelTarget();
}

async function openToolbox(commands) {
  const form = commands.descriptorFront._form;
  appendStatusMessage(
    `Create toolbox for target descriptor: ${JSON.stringify({ form }, null, 2)}`
  );

  // Remember the last panel that was used inside of this profile.
  // But if we are testing, then it should always open the debugger panel.
  const selectedTool = Services.prefs.getCharPref(
    "devtools.browsertoolbox.panel",
    Services.prefs.getCharPref("devtools.toolbox.selectedTool", "jsdebugger")
  );

  const toolboxOptions = { doc: document };
  appendStatusMessage(`Show toolbox with ${selectedTool} selected`);

  gToolbox = await gDevTools.showToolbox(commands, {
    toolId: selectedTool,
    hostType: Toolbox.HostType.BROWSERTOOLBOX,
    hostOptions: toolboxOptions,
  });

  bindToolboxHandlers();

  // Enable some testing features if the browser toolbox test pref is set.
  if (
    Services.prefs.getBoolPref(
      "devtools.browsertoolbox.enable-test-server",
      false
    )
  ) {
    // setup a server so that the test can evaluate messages in this process.
    installTestingServer();
  }

  await gToolbox.raise();

  // Warn the user if we started recording this browser toolbox via MOZ_BROWSER_TOOLBOX_PROFILER_STARTUP=1
  if (Services.env.get("MOZ_PROFILER_STARTUP") === "1") {
    const notificationBox = gToolbox.getNotificationBox();
    const text =
      "The profiler started recording this toolbox, open another browser toolbox to open the profile via the performance panel";
    notificationBox.appendNotification(
      text,
      null,
      null,
      notificationBox.PRIORITY_INFO_HIGH
    );
  }
}

let releaseTestLoader = null;
function installTestingServer() {
  // Install a DevToolsServer in this process and inform the server of its
  // location. Tests operating on the browser toolbox run in the server
  // (the firefox parent process) and can connect to this new server using
  // initBrowserToolboxTask(), allowing them to evaluate scripts here.

  const requester = {};
  const testLoader = useDistinctSystemPrincipalLoader(requester);
  releaseTestLoader = () => releaseDistinctSystemPrincipalLoader(requester);
  const { DevToolsServer } = testLoader.require(
    "resource://devtools/server/devtools-server.js"
  );
  const { SocketListener } = testLoader.require(
    "resource://devtools/shared/security/socket.js"
  );

  DevToolsServer.init();
  DevToolsServer.registerAllActors();
  DevToolsServer.allowChromeProcess = true;

  // Force this server to be kept alive until the browser toolbox process is closed.
  // For some reason intermittents appears on Windows when destroying the server
  // once the last connection drops.
  DevToolsServer.keepAlive = true;

  // Use a fixed port which initBrowserToolboxTask can look for.
  const socketOptions = { portOrPath: 6001 };
  const listener = new SocketListener(DevToolsServer, socketOptions);
  listener.open();
}

async function bindToolboxHandlers() {
  gToolbox.once("destroyed", quitApp);
  window.addEventListener("unload", onUnload);

  // If the remote connection drops, firefox was closed
  // In such case, force closing the browser toolbox
  gCommands.client.once("closed", quitApp);

  if (Services.appinfo.OS == "Darwin") {
    // Badge the dock icon to differentiate this process from the main application
    // process.
    updateBadgeText(false);

    gToolbox.on("toolbox-paused", () => updateBadgeText(true));
    gToolbox.on("toolbox-resumed", () => updateBadgeText(false));
  }
}

function updateBadgeText(paused) {
  const dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"].getService(
    Ci.nsIMacDockSupport
  );
  dockSupport.badgeText = paused ? "▐▐ " : " ▶";
}

function onUnload() {
  window.removeEventListener("unload", onUnload);
  gToolbox.destroy();
  if (releaseTestLoader) {
    releaseTestLoader();
    releaseTestLoader = null;
  }
}

function quitApp() {
  const quit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
    Ci.nsISupportsPRBool
  );
  Services.obs.notifyObservers(quit, "quit-application-requested");

  const shouldProceed = !quit.data;
  if (shouldProceed) {
    Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
  }
}
