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

/**
 * This module inject dynamically menu items into browser UI.
 *
 * Menu definitions are fetched from:
 * - devtools/client/menus for top level entires
 * - devtools/client/definitions for tool-specifics entries
 */

const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const MENUS_L10N = new LocalizationHelper(
  "devtools/client/locales/menus.properties"
);

loader.lazyRequireGetter(
  this,
  "gDevTools",
  "resource://devtools/client/framework/devtools.js",
  true
);
loader.lazyRequireGetter(
  this,
  "gDevToolsBrowser",
  "resource://devtools/client/framework/devtools-browser.js",
  true
);
loader.lazyRequireGetter(
  this,
  "Telemetry",
  "resource://devtools/client/shared/telemetry.js"
);

let telemetry = null;

// Keep list of inserted DOM Elements in order to remove them on unload
// Maps browser xul document => list of DOM Elements
const FragmentsCache = new Map();

function l10n(key) {
  return MENUS_L10N.getStr(key);
}

/**
 * Create a xul:menuitem element
 *
 * @param {HTMLDocument} doc
 *        The document to which menus are to be added.
 * @param {String} id
 *        Element id.
 * @param {String} label
 *        Menu label.
 * @param {String} accesskey (optional)
 *        Access key of the menuitem, used as shortcut while opening the menu.
 * @param {Boolean} isCheckbox (optional)
 *        If true, the menuitem will act as a checkbox and have an optional
 *        tick on its left.
 * @param {String} appMenuL10nId (optional)
 *        A Fluent key to set the appmenu-data-l10n-id attribute of the menuitem
 *        to. This can then be used to show a different string when cloning the
 *        menuitem to show in the AppMenu or panel contexts.
 *
 * @return XULMenuItemElement
 */
function createMenuItem({
  doc,
  id,
  label,
  accesskey,
  isCheckbox,
  appMenuL10nId,
}) {
  const menuitem = doc.createXULElement("menuitem");
  menuitem.id = id;
  menuitem.setAttribute("label", label);
  if (accesskey) {
    menuitem.setAttribute("accesskey", accesskey);
  }
  if (isCheckbox) {
    menuitem.setAttribute("type", "checkbox");
    menuitem.setAttribute("autocheck", "false");
  }
  if (appMenuL10nId) {
    menuitem.setAttribute("appmenu-data-l10n-id", appMenuL10nId);
  }
  return menuitem;
}

/**
 * Add a menu entry for a tool definition
 *
 * @param {Object} toolDefinition
 *        Tool definition of the tool to add a menu entry.
 * @param {HTMLDocument} doc
 *        The document to which the tool menu item is to be added.
 */
function createToolMenuElements(toolDefinition, doc) {
  const id = toolDefinition.id;
  const menuId = "menuitem_" + id;

  // Prevent multiple entries for the same tool.
  if (doc.getElementById(menuId)) {
    return null;
  }

  const oncommand = async function (id, event) {
    try {
      const window = event.target.ownerDocument.defaultView;
      await gDevToolsBrowser.selectToolCommand(window, id, Cu.now());
      sendEntryPointTelemetry(window);
    } catch (e) {
      console.error(`Exception while opening ${id}: ${e}\n${e.stack}`);
    }
  }.bind(null, id);

  const menuitem = createMenuItem({
    doc,
    id: "menuitem_" + id,
    label: toolDefinition.menuLabel || toolDefinition.label,
    accesskey: toolDefinition.accesskey,
    appMenuL10nId: toolDefinition.appMenuL10nId,
  });
  // Refer to the key in order to display the key shortcut at menu ends
  // This <key> element is being created by devtools/client/devtools-startup.js
  menuitem.setAttribute("key", "key_" + id);
  menuitem.addEventListener("command", oncommand);

  return menuitem;
}

/**
 * Send entry point telemetry explaining how the devtools were launched when
 * launched from the System Menu.. This functionality also lives inside
 * `devtools/startup/devtools-startup.js` but that codepath is only used the
 * first time a toolbox is opened for a tab.
 */
function sendEntryPointTelemetry(window) {
  if (!telemetry) {
    telemetry = new Telemetry();
  }

  telemetry.addEventProperty(window, "open", "tools", null, "shortcut", "");

  telemetry.addEventProperty(
    window,
    "open",
    "tools",
    null,
    "entrypoint",
    "SystemMenu"
  );
}

/**
 * Create xul menuitem, key elements for a given tool.
 * And then insert them into browser DOM.
 *
 * @param {HTMLDocument} doc
 *        The document to which the tool is to be registered.
 * @param {Object} toolDefinition
 *        Tool definition of the tool to register.
 * @param {Object} prevDef
 *        The tool definition after which the tool menu item is to be added.
 */
function insertToolMenuElements(doc, toolDefinition, prevDef) {
  const menuitem = createToolMenuElements(toolDefinition, doc);
  if (!menuitem) {
    return;
  }

  let ref;
  if (prevDef) {
    const menuitem = doc.getElementById("menuitem_" + prevDef.id);
    ref = menuitem?.nextSibling ? menuitem.nextSibling : null;
  } else {
    ref = doc.getElementById("menu_devtools_remotedebugging");
  }

  if (ref) {
    ref.parentNode.insertBefore(menuitem, ref);
  }
}
exports.insertToolMenuElements = insertToolMenuElements;

/**
 * Remove a tool's menuitem from a window
 *
 * @param {string} toolId
 *        Id of the tool to add a menu entry for
 * @param {HTMLDocument} doc
 *        The document to which the tool menu item is to be removed from
 */
function removeToolFromMenu(toolId, doc) {
  const key = doc.getElementById("key_" + toolId);
  if (key) {
    key.remove();
  }

  const menuitem = doc.getElementById("menuitem_" + toolId);
  if (menuitem) {
    menuitem.remove();
  }
}
exports.removeToolFromMenu = removeToolFromMenu;

/**
 * Add all tools to the developer tools menu of a window.
 *
 * @param {HTMLDocument} doc
 *        The document to which the tool items are to be added.
 */
function addAllToolsToMenu(doc) {
  const fragMenuItems = doc.createDocumentFragment();

  for (const toolDefinition of gDevTools.getToolDefinitionArray()) {
    if (!toolDefinition.inMenu) {
      continue;
    }

    const menuItem = createToolMenuElements(toolDefinition, doc);

    if (!menuItem) {
      continue;
    }

    fragMenuItems.appendChild(menuItem);
  }

  const mps = doc.getElementById("menu_devtools_remotedebugging");
  if (mps) {
    mps.parentNode.insertBefore(fragMenuItems, mps);
  }
}

/**
 * Add global menus that are not panel specific.
 *
 * @param {HTMLDocument} doc
 *        The document to which menus are to be added.
 */
function addTopLevelItems(doc) {
  const menuItems = doc.createDocumentFragment();

  const { menuitems } = require("resource://devtools/client/menus.js");
  for (const item of menuitems) {
    if (item.separator) {
      const separator = doc.createXULElement("menuseparator");
      separator.id = item.id;
      menuItems.appendChild(separator);
    } else {
      const { id, l10nKey } = item;

      // Create a <menuitem>
      const menuitem = createMenuItem({
        doc,
        id,
        label: l10n(l10nKey + ".label"),
        accesskey: l10n(l10nKey + ".accesskey"),
        isCheckbox: item.checkbox,
        appMenuL10nId: item.appMenuL10nId,
      });
      menuitem.addEventListener("command", item.oncommand);
      menuItems.appendChild(menuitem);

      if (item.keyId) {
        menuitem.setAttribute("key", "key_" + item.keyId);
      }
    }
  }

  // Cache all nodes before insertion to be able to remove them on unload
  const nodes = [];
  for (const node of menuItems.children) {
    nodes.push(node);
  }
  FragmentsCache.set(doc, nodes);

  const menu = doc.getElementById("menuWebDeveloperPopup");
  menu.appendChild(menuItems);

  // There is still "Page Source" and "Task Manager" menuitems hardcoded
  // into browser.xhtml. Instead of manually inserting everything around it,
  // move them to the expected position.
  const pageSourceMenu = doc.getElementById("menu_pageSource");
  const extensionsForDevelopersMenu = doc.getElementById(
    "extensionsForDevelopers"
  );
  menu.insertBefore(pageSourceMenu, extensionsForDevelopersMenu);

  const taskManagerMenu = doc.getElementById("menu_taskManager");
  const remoteDebuggingMenu = doc.getElementById(
    "menu_devtools_remotedebugging"
  );
  menu.insertBefore(taskManagerMenu, remoteDebuggingMenu);
}

/**
 * Remove global menus that are not panel specific.
 *
 * @param {HTMLDocument} doc
 *        The document to which menus are to be added.
 */
function removeTopLevelItems(doc) {
  const nodes = FragmentsCache.get(doc);
  if (!nodes) {
    return;
  }
  FragmentsCache.delete(doc);
  for (const node of nodes) {
    node.remove();
  }
}

/**
 * Add menus to a browser document
 *
 * @param {HTMLDocument} doc
 *        The document to which menus are to be added.
 */
exports.addMenus = function (doc) {
  addTopLevelItems(doc);

  addAllToolsToMenu(doc);
};

/**
 * Remove menus from a browser document
 *
 * @param {HTMLDocument} doc
 *        The document to which menus are to be removed.
 */
exports.removeMenus = function (doc) {
  // We only remove top level entries. Per-tool entries are removed while
  // unregistering each tool.
  removeTopLevelItems(doc);
};
