/* 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 {
  VIEW_NODE_SELECTOR_TYPE,
  VIEW_NODE_PROPERTY_TYPE,
  VIEW_NODE_VALUE_TYPE,
  VIEW_NODE_IMAGE_URL_TYPE,
  VIEW_NODE_LOCATION_TYPE,
} = require("resource://devtools/client/inspector/shared/node-types.js");

loader.lazyRequireGetter(
  this,
  "Menu",
  "resource://devtools/client/framework/menu.js"
);
loader.lazyRequireGetter(
  this,
  "MenuItem",
  "resource://devtools/client/framework/menu-item.js"
);
loader.lazyRequireGetter(
  this,
  "getRuleFromNode",
  "resource://devtools/client/inspector/rules/utils/utils.js",
  true
);
loader.lazyRequireGetter(
  this,
  "clipboardHelper",
  "resource://devtools/shared/platform/clipboard.js"
);

const STYLE_INSPECTOR_PROPERTIES =
  "devtools/shared/locales/styleinspector.properties";
const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);

const PREF_ORIG_SOURCES = "devtools.source-map.client-service.enabled";

/**
 * Style inspector context menu
 */
class StyleInspectorMenu {
  /**
   * @param {RuleView|ComputedView} view
   *        RuleView or ComputedView instance controlling this menu
   * @param {object} options
   *        Option menu configuration
   */
  constructor(view, { isRuleView = false } = {}) {
    this.view = view;
    this.inspector = this.view.inspector;
    this.styleWindow = this.view.styleWindow || this.view.doc.defaultView;
    this.isRuleView = isRuleView;

    this._onCopy = this._onCopy.bind(this);
    this._onCopyColor = this._onCopyColor.bind(this);
    this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this);
    this._onCopyLocation = this._onCopyLocation.bind(this);
    this._onCopyDeclaration = this._onCopyDeclaration.bind(this);
    this._onCopyPropertyName = this._onCopyPropertyName.bind(this);
    this._onCopyPropertyValue = this._onCopyPropertyValue.bind(this);
    this._onCopyRule = this._onCopyRule.bind(this);
    this._onCopySelector = this._onCopySelector.bind(this);
    this._onCopyUrl = this._onCopyUrl.bind(this);
    this._onSelectAll = this._onSelectAll.bind(this);
    this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
  }
  /**
   * Display the style inspector context menu
   */
  show(event) {
    try {
      this._openMenu({
        target: event.target,
        screenX: event.screenX,
        screenY: event.screenY,
      });
    } catch (e) {
      console.error(e);
    }
  }

  _openMenu({ target, screenX = 0, screenY = 0 } = {}) {
    this.currentTarget = target;
    this.styleWindow.focus();

    const menu = new Menu();

    const menuitemCopy = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"),
      accesskey: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.copy.accessKey"
      ),
      click: () => {
        this._onCopy();
      },
      disabled: !this._hasTextSelected(),
    });
    const menuitemCopyLocation = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.copyLocation"
      ),
      click: () => {
        this._onCopyLocation();
      },
      visible: false,
    });
    const menuitemCopyRule = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule"),
      click: () => {
        this._onCopyRule();
      },
      visible:
        // `_onCopyRule` calls `getRuleFromNode`, which retrieves the target closest
        // ancestor element with a data-rule-id attribute. If there's no such element,
        // we're not inside a rule and we shouldn't display this context menu item.
        this.isRuleView && !!target.closest(".ruleview-rule[data-rule-id]"),
    });
    const copyColorAccessKey = "styleinspector.contextmenu.copyColor.accessKey";
    const menuitemCopyColor = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.copyColor"
      ),
      accesskey: STYLE_INSPECTOR_L10N.getStr(copyColorAccessKey),
      click: () => {
        this._onCopyColor();
      },
      visible: this._isColorPopup(),
    });
    const copyUrlAccessKey = "styleinspector.contextmenu.copyUrl.accessKey";
    const menuitemCopyUrl = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl"),
      accesskey: STYLE_INSPECTOR_L10N.getStr(copyUrlAccessKey),
      click: () => {
        this._onCopyUrl();
      },
      visible: this._isImageUrl(),
    });
    const copyImageAccessKey =
      "styleinspector.contextmenu.copyImageDataUrl.accessKey";
    const menuitemCopyImageDataUrl = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.copyImageDataUrl"
      ),
      accesskey: STYLE_INSPECTOR_L10N.getStr(copyImageAccessKey),
      click: () => {
        this._onCopyImageDataUrl();
      },
      visible: this._isImageUrl(),
    });
    const copyDeclarationLabel = "styleinspector.contextmenu.copyDeclaration";
    const menuitemCopyDeclaration = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(copyDeclarationLabel),
      click: () => {
        this._onCopyDeclaration();
      },
      visible: false,
    });
    const menuitemCopyPropertyName = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.copyPropertyName"
      ),
      click: () => {
        this._onCopyPropertyName();
      },
      visible: false,
    });
    const menuitemCopyPropertyValue = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.copyPropertyValue"
      ),
      click: () => {
        this._onCopyPropertyValue();
      },
      visible: false,
    });
    const menuitemCopySelector = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.copySelector"
      ),
      click: () => {
        this._onCopySelector();
      },
      visible: false,
    });

    this._clickedNodeInfo = this._getClickedNodeInfo();
    if (this.isRuleView && this._clickedNodeInfo) {
      switch (this._clickedNodeInfo.type) {
        case VIEW_NODE_PROPERTY_TYPE:
          menuitemCopyDeclaration.visible = true;
          menuitemCopyPropertyName.visible = true;
          break;
        case VIEW_NODE_VALUE_TYPE:
          menuitemCopyDeclaration.visible = true;
          menuitemCopyPropertyValue.visible = true;
          break;
        case VIEW_NODE_SELECTOR_TYPE:
          menuitemCopySelector.visible = true;
          break;
        case VIEW_NODE_LOCATION_TYPE:
          menuitemCopyLocation.visible = true;
          break;
      }
    }

    menu.append(menuitemCopy);
    menu.append(menuitemCopyLocation);
    menu.append(menuitemCopyRule);
    menu.append(menuitemCopyColor);
    menu.append(menuitemCopyUrl);
    menu.append(menuitemCopyImageDataUrl);
    menu.append(menuitemCopyDeclaration);
    menu.append(menuitemCopyPropertyName);
    menu.append(menuitemCopyPropertyValue);
    menu.append(menuitemCopySelector);

    menu.append(
      new MenuItem({
        type: "separator",
      })
    );

    // Select All
    const selectAllAccessKey = "styleinspector.contextmenu.selectAll.accessKey";
    const menuitemSelectAll = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.selectAll"
      ),
      accesskey: STYLE_INSPECTOR_L10N.getStr(selectAllAccessKey),
      click: () => {
        this._onSelectAll();
      },
    });
    menu.append(menuitemSelectAll);

    menu.append(
      new MenuItem({
        type: "separator",
      })
    );

    // Add new rule
    const addRuleAccessKey = "styleinspector.contextmenu.addNewRule.accessKey";
    const menuitemAddRule = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.addNewRule"
      ),
      accesskey: STYLE_INSPECTOR_L10N.getStr(addRuleAccessKey),
      click: () => this.view.addNewRule(),
      visible: this.isRuleView,
      disabled: !this.isRuleView || !this.view.canAddNewRuleForSelectedNode(),
    });
    menu.append(menuitemAddRule);

    // Show Original Sources
    const sourcesAccessKey =
      "styleinspector.contextmenu.toggleOrigSources.accessKey";
    const menuitemSources = new MenuItem({
      label: STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.contextmenu.toggleOrigSources"
      ),
      accesskey: STYLE_INSPECTOR_L10N.getStr(sourcesAccessKey),
      click: () => {
        this._onToggleOrigSources();
      },
      type: "checkbox",
      checked: Services.prefs.getBoolPref(PREF_ORIG_SOURCES),
    });
    menu.append(menuitemSources);

    menu.popup(screenX, screenY, this.inspector.toolbox.doc);
    return menu;
  }

  _hasTextSelected() {
    let hasTextSelected;
    const selection = this.styleWindow.getSelection();

    const node = this._getClickedNode();
    if (node.nodeName == "input" || node.nodeName == "textarea") {
      const { selectionStart, selectionEnd } = node;
      hasTextSelected =
        isFinite(selectionStart) &&
        isFinite(selectionEnd) &&
        selectionStart !== selectionEnd;
    } else {
      hasTextSelected = selection.toString() && !selection.isCollapsed;
    }

    return hasTextSelected;
  }

  /**
   * Get the type of the currently clicked node
   */
  _getClickedNodeInfo() {
    const node = this._getClickedNode();
    return this.view.getNodeInfo(node);
  }

  /**
   * A helper that determines if the popup was opened with a click to a color
   * value and saves the color to this._colorToCopy.
   *
   * @return {boolean}
   *         true if click on color opened the popup, false otherwise.
   */
  _isColorPopup() {
    this._colorToCopy = "";

    const container = this._getClickedNode();
    if (!container) {
      return false;
    }

    const colorNode = container.closest("[data-color]");
    if (!colorNode) {
      return false;
    }

    this._colorToCopy = colorNode.dataset.color;
    return true;
  }

  /**
   * Check if the current node (clicked node) is an image URL
   *
   * @return {boolean} true if the node is an image url
   */
  _isImageUrl() {
    const nodeInfo = this._getClickedNodeInfo();
    if (!nodeInfo) {
      return false;
    }
    return nodeInfo.type == VIEW_NODE_IMAGE_URL_TYPE;
  }

  /**
   * Get the DOM Node container for the current target node.
   * If the target node is a text node, return the parent node, otherwise return
   * the target node itself.
   *
   * @return {DOMNode}
   */
  _getClickedNode() {
    const node = this.currentTarget;

    if (!node) {
      return null;
    }

    return node.nodeType === node.TEXT_NODE ? node.parentElement : node;
  }

  /**
   * Select all text.
   */
  _onSelectAll() {
    const selection = this.styleWindow.getSelection();

    if (this.isRuleView) {
      selection.selectAllChildren(
        this.currentTarget.closest("#ruleview-container-focusable")
      );
    } else {
      selection.selectAllChildren(this.view.element);
    }
  }

  /**
   * Copy the most recently selected color value to clipboard.
   */
  _onCopy() {
    this.view.copySelection(this.currentTarget);
  }

  /**
   * Copy the most recently selected color value to clipboard.
   */
  _onCopyColor() {
    clipboardHelper.copyString(this._colorToCopy);
  }

  /*
   * Retrieve the url for the selected image and copy it to the clipboard
   */
  _onCopyUrl() {
    if (!this._clickedNodeInfo) {
      return;
    }

    clipboardHelper.copyString(this._clickedNodeInfo.value.url);
  }

  /**
   * Retrieve the image data for the selected image url and copy it to the
   * clipboard
   */
  async _onCopyImageDataUrl() {
    if (!this._clickedNodeInfo) {
      return;
    }

    let message;
    try {
      const inspectorFront = this.inspector.inspectorFront;
      const imageUrl = this._clickedNodeInfo.value.url;
      const data = await inspectorFront.getImageDataFromURL(imageUrl);
      message = await data.data.string();
    } catch (e) {
      message = STYLE_INSPECTOR_L10N.getStr(
        "styleinspector.copyImageDataUrlError"
      );
    }

    clipboardHelper.copyString(message);
  }

  /**
   * Copy the rule source location of the current clicked node.
   */
  _onCopyLocation() {
    if (!this._clickedNodeInfo) {
      return;
    }

    clipboardHelper.copyString(this._clickedNodeInfo.value);
  }

  /**
   * Copy the CSS declaration of the current clicked node.
   */
  _onCopyDeclaration() {
    if (!this._clickedNodeInfo) {
      return;
    }

    const textProp = this._clickedNodeInfo.value.textProperty;
    clipboardHelper.copyString(textProp.stringifyProperty());
  }

  /**
   * Copy the rule property name of the current clicked node.
   */
  _onCopyPropertyName() {
    if (!this._clickedNodeInfo) {
      return;
    }

    clipboardHelper.copyString(this._clickedNodeInfo.value.property);
  }

  /**
   * Copy the rule property value of the current clicked node.
   */
  _onCopyPropertyValue() {
    if (!this._clickedNodeInfo) {
      return;
    }

    clipboardHelper.copyString(this._clickedNodeInfo.value.value);
  }

  /**
   * Copy the rule of the current clicked node.
   */
  _onCopyRule() {
    const node = this._getClickedNode();
    const rule = getRuleFromNode(node, this.view.elementStyle);
    if (!rule) {
      console.error("Can't copy rule, no rule found for node", node);
      return;
    }
    clipboardHelper.copyString(rule.stringifyRule());
  }

  /**
   * Copy the rule selector of the current clicked node.
   */
  _onCopySelector() {
    if (!this._clickedNodeInfo) {
      return;
    }

    clipboardHelper.copyString(this._clickedNodeInfo.value);
  }

  /**
   * Toggle the original sources pref.
   */
  _onToggleOrigSources() {
    const isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
    Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
  }

  destroy() {
    this.currentTarget = null;
    this.view = null;
    this.inspector = null;
    this.styleWindow = null;
  }
}

module.exports = StyleInspectorMenu;
