/* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils");
const EventEmitter = require("devtools/shared/event-emitter");

/**
 * A partial implementation of the Menu API provided by electron:
 * https://github.com/electron/electron/blob/master/docs/api/menu.md.
 *
 * Extra features:
 *  - Emits an 'open' and 'close' event when the menu is opened/closed

 * @param String id (non standard)
 *        Needed so tests can confirm the XUL implementation is working
 */
function Menu({ id = null } = {}) {
  this.menuitems = [];
  this.id = id;

  Object.defineProperty(this, "items", {
    get() {
      return this.menuitems;
    },
  });

  EventEmitter.decorate(this);
}

/**
 * Add an item to the end of the Menu
 *
 * @param {MenuItem} menuItem
 */
Menu.prototype.append = function(menuItem) {
  this.menuitems.push(menuItem);
};

/**
 * Remove all items from the Menu
 */
Menu.prototype.clear = function() {
  this.menuitems = [];
};

/**
 * Add an item to a specified position in the menu
 *
 * @param {int} pos
 * @param {MenuItem} menuItem
 */
Menu.prototype.insert = function(pos, menuItem) {
  throw Error("Not implemented");
};

/**
 * Show the Menu next to the provided target. Anchor point is bottom-left.
 *
 * @param {Element} target
 *        The element to use as anchor.
 */
Menu.prototype.popupAtTarget = function(target) {
  const rect = target.getBoundingClientRect();
  const doc = target.ownerDocument;
  const defaultView = doc.defaultView;
  const x = rect.left + defaultView.mozInnerScreenX;
  const y = rect.bottom + defaultView.mozInnerScreenY;

  this.popup(x, y, doc);
};

/**
 * Hide an existing menu, if there's any.
 *
 * @param {Document} doc
 *        The document that should own the context menu.
 */
Menu.prototype.hide = function(doc) {
  const win = doc.defaultView;
  doc = DevToolsUtils.getTopWindow(win).document;
  const popup = doc.querySelector('popupset menupopup[menu-api="true"]');
  if (!popup) {
    return;
  }
  popup.hidePopup();
};

/**
 * Show the Menu at a specified location on the screen
 *
 * Missing features:
 *   - browserWindow - BrowserWindow (optional) - Default is null.
 *   - positioningItem Number - (optional) OS X
 *
 * @param {int} screenX
 * @param {int} screenY
 * @param {Document} doc
 *        The document that should own the context menu.
 */
Menu.prototype.popup = function(screenX, screenY, doc) {
  // See bug 1285229, on Windows, opening the same popup multiple times in a
  // row ends up duplicating the popup. The newly inserted popup doesn't
  // dismiss the old one. So remove any previously displayed popup before
  // opening a new one.
  this.hide(doc);

  // The context-menu will be created in the topmost window to preserve keyboard
  // navigation (see Bug 1543940).
  // Keep a reference on the window owning the menu to hide the popup on unload.
  const win = doc.defaultView;
  const topWin = DevToolsUtils.getTopWindow(win);

  // Convert coordinates from win's CSS coordinate space to topWin's
  const winToTopWinCssScale = win.devicePixelRatio / topWin.devicePixelRatio;
  screenX = screenX * winToTopWinCssScale;
  screenY = screenY * winToTopWinCssScale;

  doc = topWin.document;

  let popupset = doc.querySelector("popupset");
  if (!popupset) {
    popupset = doc.createXULElement("popupset");
    doc.documentElement.appendChild(popupset);
  }

  const popup = doc.createXULElement("menupopup");
  popup.setAttribute("menu-api", "true");
  popup.setAttribute("consumeoutsideclicks", "false");
  popup.setAttribute("incontentshell", "false");

  if (this.id) {
    popup.id = this.id;
  }
  this._createMenuItems(popup);

  // The context menu will be created in the topmost chrome window. Hide it manually when
  // the owner document is unloaded.
  const onWindowUnload = () => popup.hidePopup();
  win.addEventListener("unload", onWindowUnload);

  // Remove the menu from the DOM once it's hidden.
  popup.addEventListener("popuphidden", e => {
    if (e.target === popup) {
      win.removeEventListener("unload", onWindowUnload);
      popup.remove();
      this.emit("close");
    }
  });

  popup.addEventListener("popupshown", e => {
    if (e.target === popup) {
      this.emit("open");
    }
  });

  popupset.appendChild(popup);
  popup.openPopupAtScreen(screenX, screenY, true);
};

Menu.prototype._createMenuItems = function(parent) {
  const doc = parent.ownerDocument;
  this.menuitems.forEach(item => {
    if (!item.visible) {
      return;
    }

    if (item.submenu) {
      const menupopup = doc.createXULElement("menupopup");
      menupopup.setAttribute("incontentshell", "false");

      item.submenu._createMenuItems(menupopup);

      const menu = doc.createXULElement("menu");
      menu.appendChild(menupopup);
      applyItemAttributesToNode(item, menu);
      parent.appendChild(menu);
    } else if (item.type === "separator") {
      const menusep = doc.createXULElement("menuseparator");
      parent.appendChild(menusep);
    } else {
      const menuitem = doc.createXULElement("menuitem");
      applyItemAttributesToNode(item, menuitem);

      menuitem.addEventListener("command", () => {
        item.click();
      });
      menuitem.addEventListener("DOMMenuItemActive", () => {
        item.hover();
      });

      parent.appendChild(menuitem);
    }
  });
};

Menu.getMenuElementById = function(id, doc) {
  const menuDoc = DevToolsUtils.getTopWindow(doc.defaultView).document;
  return menuDoc.getElementById(id);
};

Menu.setApplicationMenu = () => {
  throw Error("Not implemented");
};

Menu.sendActionToFirstResponder = () => {
  throw Error("Not implemented");
};

Menu.buildFromTemplate = () => {
  throw Error("Not implemented");
};

function applyItemAttributesToNode(item, node) {
  if (item.l10nID) {
    node.setAttribute("data-l10n-id", item.l10nID);
  } else {
    node.setAttribute("label", item.label);
    if (item.accelerator) {
      node.setAttribute("acceltext", item.accelerator);
    }
    if (item.accesskey) {
      node.setAttribute("accesskey", item.accesskey);
    }
  }
  if (item.type === "checkbox") {
    node.setAttribute("type", "checkbox");
  }
  if (item.type === "radio") {
    node.setAttribute("type", "radio");
  }
  if (item.disabled) {
    node.setAttribute("disabled", "true");
  }
  if (item.checked) {
    node.setAttribute("checked", "true");
  }
  if (item.image) {
    node.setAttribute("image", item.image);
  }
  if (item.id) {
    node.id = item.id;
  }
}

module.exports = Menu;
