/* 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 {
  Component,
  createFactory,
} = require("resource://devtools/client/shared/vendor/react.mjs");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
const { div, button } = dom;
const MenuButton = createFactory(
  require("resource://devtools/client/shared/components/menu/MenuButton.js")
);
const ToolboxTabs = createFactory(
  require("resource://devtools/client/framework/components/ToolboxTabs.js")
);
loader.lazyGetter(this, "MeatballMenu", function () {
  return createFactory(
    require("resource://devtools/client/framework/components/MeatballMenu.js")
  );
});
loader.lazyGetter(this, "MenuItem", function () {
  return createFactory(
    require("resource://devtools/client/shared/components/menu/MenuItem.js")
  );
});
loader.lazyGetter(this, "MenuList", function () {
  return createFactory(
    require("resource://devtools/client/shared/components/menu/MenuList.js")
  );
});
loader.lazyGetter(this, "LocalizationProvider", function () {
  return createFactory(
    require("resource://devtools/client/shared/vendor/fluent-react.js")
      .LocalizationProvider
  );
});
loader.lazyGetter(this, "DebugTargetInfo", () =>
  createFactory(
    require("resource://devtools/client/framework/components/DebugTargetInfo.js")
  )
);
loader.lazyGetter(this, "ChromeDebugToolbar", () =>
  createFactory(
    require("resource://devtools/client/framework/components/ChromeDebugToolbar.js")
  )
);

loader.lazyRequireGetter(
  this,
  "getUnicodeUrl",
  "resource://devtools/client/shared/unicode-url.js",
  true
);

/**
 * This is the overall component for the toolbox toolbar. It is designed to not know how
 * the state is being managed, and attempts to be as pure as possible. The
 * ToolboxController component controls the changing state, and passes in everything as
 * props.
 */
class ToolboxToolbar extends Component {
  static get propTypes() {
    return {
      // The currently focused item (for arrow keyboard navigation)
      // This ID determines the tabindex being 0 or -1.
      focusedButton: PropTypes.string,
      // List of command button definitions.
      toolboxButtons: PropTypes.array,
      // The id of the currently selected tool, e.g. "inspector"
      currentToolId: PropTypes.string,
      // An optionally highlighted tools, e.g. "inspector" (used by ToolboxTabs
      // component).
      highlightedTools: PropTypes.instanceOf(Set),
      // List of tool panel definitions (used by ToolboxTabs component).
      panelDefinitions: PropTypes.array,
      // List of possible docking options.
      hostTypes: PropTypes.arrayOf(
        PropTypes.shape({
          position: PropTypes.string.isRequired,
          switchHost: PropTypes.func.isRequired,
        })
      ),
      // Current docking type. Typically one of the position values in
      // |hostTypes| but this is not always the case (e.g. for "browsertoolbox").
      currentHostType: PropTypes.string,
      // Are docking options enabled? They are not enabled in certain situations
      // like when the toolbox is opened in a tab.
      areDockOptionsEnabled: PropTypes.bool,
      // Do we need to add UI for closing the toolbox? We don't when the
      // toolbox is undocked, for example.
      canCloseToolbox: PropTypes.bool,
      // Is the split console currently visible?
      isSplitConsoleActive: PropTypes.bool,
      // Are we disabling the behavior where pop-ups are automatically closed
      // when clicking outside them?
      //
      // This is a tri-state value that may be true/false or undefined where
      // undefined means that the option is not relevant in this context
      // (i.e. we're not in a browser toolbox).
      disableAutohide: PropTypes.bool,
      // Are we displaying the window always on top?
      //
      // This is a tri-state value that may be true/false or undefined where
      // undefined means that the option is not relevant in this context
      // (i.e. we're not in a local web extension toolbox).
      alwaysOnTop: PropTypes.bool,
      // Is the toolbox currently focused?
      //
      // This will only be defined when alwaysOnTop is true.
      focusedState: PropTypes.bool,
      // Function to turn the options panel on / off.
      toggleOptions: PropTypes.func.isRequired,
      // Function to turn the split console on / off.
      toggleSplitConsole: PropTypes.func,
      // Function to turn the disable pop-up autohide behavior on / off.
      toggleNoAutohide: PropTypes.func,
      // Function to turn the always on top behavior on / off.
      toggleAlwaysOnTop: PropTypes.func,
      // Function to completely close the toolbox.
      closeToolbox: PropTypes.func,
      // Keep a record of what button is focused.
      focusButton: PropTypes.func,
      // Hold off displaying the toolbar until enough information is ready for
      // it to render nicely.
      canRender: PropTypes.bool,
      // Localization interface.
      L10N: PropTypes.object.isRequired,
      // The devtools toolbox
      toolbox: PropTypes.object,
      // Call back function to detect tabs order updated.
      onTabsOrderUpdated: PropTypes.func.isRequired,
      // Count of visible toolbox buttons which is used by ToolboxTabs component
      // to recognize that the visibility of toolbox buttons were changed.
      // Because in the component we cannot compare the visibility since the
      // button definition instance in toolboxButtons will be unchanged.
      visibleToolboxButtonCount: PropTypes.number,
      // Data to show debug target info, if needed
      debugTargetData: PropTypes.shape({
        runtimeInfo: PropTypes.object.isRequired,
        descriptorType: PropTypes.string.isRequired,
      }),
      // The loaded Fluent localization bundles.
      fluentBundles: PropTypes.array.isRequired,
    };
  }

  constructor(props) {
    super(props);

    this.hideMenu = this.hideMenu.bind(this);
    this.createFrameList = this.createFrameList.bind(this);
    this.highlightFrame = this.highlightFrame.bind(this);
  }

  componentDidMount() {
    this.props.toolbox.on("panel-changed", this.hideMenu);
  }

  componentWillUnmount() {
    this.props.toolbox.off("panel-changed", this.hideMenu);
  }

  hideMenu() {
    if (this.refs.meatballMenuButton) {
      this.refs.meatballMenuButton.hideMenu();
    }

    if (this.refs.frameMenuButton) {
      this.refs.frameMenuButton.hideMenu();
    }
  }

  /**
   * A little helper function to call renderToolboxButtons for buttons at the start
   * of the toolbox.
   */
  renderToolboxButtonsStart() {
    return this.renderToolboxButtons(true);
  }

  /**
   * A little helper function to call renderToolboxButtons for buttons at the end
   * of the toolbox.
   */
  renderToolboxButtonsEnd() {
    return this.renderToolboxButtons(false);
  }

  /**
   * Render all of the tabs, this takes in a list of toolbox button states. These are plain
   * objects that have all of the relevant information needed to render the button.
   * See Toolbox.prototype._createButtonState in devtools/client/framework/toolbox.js for
   * documentation on this object.
   *
   * @param {String} focusedButton - The id of the focused button.
   * @param {Array} toolboxButtons - Array of objects that define the command buttons.
   * @param {Function} focusButton - Keep a record of the currently focused button.
   * @param {boolean} isStart - Render either the starting buttons, or ending buttons.
   */
  renderToolboxButtons(isStart) {
    const { focusedButton, toolboxButtons, focusButton } = this.props;
    const visibleButtons = toolboxButtons.filter(command => {
      const { isVisible, isInStartContainer } = command;
      return isVisible && (isStart ? isInStartContainer : !isInStartContainer);
    });

    if (visibleButtons.length === 0) {
      return null;
    }

    // The RDM button, if present, should always go last
    const rdmIndex = visibleButtons.findIndex(
      button => button.id === "command-button-responsive"
    );
    if (rdmIndex !== -1 && rdmIndex !== visibleButtons.length - 1) {
      const rdm = visibleButtons.splice(rdmIndex, 1)[0];
      visibleButtons.push(rdm);
    }

    const renderedButtons = visibleButtons.map(command => {
      const {
        id,
        description,
        disabled,
        onClick,
        isChecked,
        isToggle,
        className: buttonClass,
        onKeyDown,
      } = command;

      // If button is frame button, create menu button in order to
      // use the doorhanger menu.
      if (id === "command-button-frames") {
        return this.renderFrameButton(command);
      }

      if (id === "command-button-errorcount") {
        return this.renderErrorIcon(command);
      }

      return button({
        id,
        title: description,
        disabled,
        "aria-pressed": !isToggle ? null : isChecked,
        className: `devtools-tabbar-button command-button ${
          buttonClass || ""
        } ${isChecked ? "checked" : ""}`,
        onClick: event => {
          onClick(event);
          focusButton(id);
        },
        onFocus: () => focusButton(id),
        tabIndex: id === focusedButton ? "0" : "-1",
        onKeyDown: event => {
          onKeyDown(event);
        },
        onContextMenu: event => {
          const menu = command.getContextMenu();
          if (!menu) {
            return;
          }

          event.preventDefault();
          event.stopPropagation();

          menu.popup(event.screenX, event.screenY, window.parent.document);
        },
      });
    });

    // Add the appropriate separator, if needed.
    const children = renderedButtons;
    if (renderedButtons.length) {
      if (isStart) {
        children.push(this.renderSeparator());
        // For the end group we add a separator *before* the RDM button if it
        // exists, but only if it is not the only button.
      } else if (rdmIndex !== -1 && renderedButtons.length > 1) {
        children.splice(children.length - 1, 0, this.renderSeparator());
      }
    }

    return div(
      { id: `toolbox-buttons-${isStart ? "start" : "end"}` },
      ...children
    );
  }

  renderFrameButton(command) {
    const { id, isChecked, disabled, description } = command;

    const { toolbox } = this.props;

    return MenuButton(
      {
        id,
        disabled,
        menuId: id + "-panel",
        toolboxDoc: toolbox.doc,
        className: `devtools-tabbar-button command-button ${
          isChecked ? "checked" : ""
        }`,
        ref: "frameMenuButton",
        title: description,
        onCloseButton: async () => {
          // Only try to unhighlight if the inspectorFront has been created already
          const inspectorFront = toolbox.target.getCachedFront("inspector");
          if (inspectorFront) {
            const highlighter = toolbox.getHighlighter();
            await highlighter.unhighlight();
          }
        },
      },
      this.createFrameList
    );
  }

  renderErrorIcon(command) {
    let { errorCount, id } = command;

    if (!errorCount) {
      return null;
    }

    if (errorCount > 99) {
      errorCount = "99+";
    }

    const errorIconTooltip = this.props.toolbox.isSplitConsoleEnabled()
      ? this.props.L10N.getStr("toolbox.errorCountButton.tooltip")
      : this.props.L10N.getStr("toolbox.errorCountButtonConsoleTab.tooltip");

    return button(
      {
        id,
        className: "devtools-tabbar-button command-button toolbox-error",
        onClick: () => {
          if (this.props.currentToolId !== "webconsole") {
            this.props.toolbox.openSplitConsole();
          }
        },
        title:
          this.props.currentToolId !== "webconsole" ? errorIconTooltip : null,
      },
      errorCount
    );
  }

  highlightFrame(id) {
    const { toolbox } = this.props;
    if (!id) {
      return;
    }

    toolbox.onHighlightFrame(id);
  }

  createFrameList() {
    const { toolbox } = this.props;
    if (toolbox.frameMap.size < 1) {
      return null;
    }

    const items = [];
    toolbox.frameMap.forEach(frame => {
      const label = toolbox.commands.descriptorFront.isWebExtensionDescriptor
        ? toolbox.getExtensionPathName(frame.url)
        : getUnicodeUrl(frame.url);

      const item = MenuItem({
        id: frame.id.toString(),
        key: "toolbox-frame-key-" + frame.id,
        label,
        checked: frame.id === toolbox.selectedFrameId,
        onClick: () => toolbox.onIframePickerFrameSelected(frame.id),
      });

      // Always put the top level frame at the top
      if (frame.isTopLevel) {
        items.unshift(item);
      } else {
        items.push(item);
      }
    });

    return MenuList(
      {
        id: "toolbox-frame-menu",
        onHighlightedChildChange: this.highlightFrame,
      },
      items
    );
  }

  /**
   * Render a separator.
   */
  renderSeparator() {
    return div({ className: "devtools-separator" });
  }

  /**
   * Render the toolbox control buttons. The following props are expected:
   *
   * @param {string} props.focusedButton
   *        The id of the focused button.
   * @param {string} props.currentToolId
   *        The id of the currently selected tool, e.g. "inspector".
   * @param {Object[]} props.hostTypes
   *        Array of host type objects.
   * @param {string} props.hostTypes[].position
   *        Position name.
   * @param {Function} props.hostTypes[].switchHost
   *        Function to switch the host.
   * @param {string} props.currentHostType
   *        The current docking configuration.
   * @param {boolean} props.areDockOptionsEnabled
   *        They are not enabled in certain situations like when the toolbox is
   *        in a tab.
   * @param {boolean} props.canCloseToolbox
   *        Do we need to add UI for closing the toolbox? We don't when the
   *        toolbox is undocked, for example.
   * @param {boolean} props.isSplitConsoleActive
   *         Is the split console currently visible?
   *        toolbox is undocked, for example.
   * @param {boolean|undefined} props.disableAutohide
   *        Are we disabling the behavior where pop-ups are automatically
   *        closed when clicking outside them?
   *        (Only defined for the browser toolbox.)
   * @param {Function} props.selectTool
   *        Function to select a tool based on its id.
   * @param {Function} props.toggleOptions
   *        Function to turn the options panel on / off.
   * @param {Function} props.toggleSplitConsole
   *        Function to turn the split console on / off.
   * @param {Function} props.toggleNoAutohide
   *        Function to turn the disable pop-up autohide behavior on / off.
   * @param {Function} props.toggleAlwaysOnTop
   *        Function to turn the always on top behavior on / off.
   * @param {Function} props.closeToolbox
   *        Completely close the toolbox.
   * @param {Function} props.focusButton
   *        Keep a record of the currently focused button.
   * @param {Object} props.L10N
   *        Localization interface.
   * @param {Object} props.toolbox
   *        The devtools toolbox. Used by the MenuButton component to display
   *        the menu popup.
   * @param {Object} refs
   *        The components refs object. Used to keep a reference to the MenuButton
   *        for the meatball menu so that we can tell it to resize its contents
   *        when they change.
   */
  renderToolboxControls() {
    const {
      focusedButton,
      canCloseToolbox,
      closeToolbox,
      focusButton,
      L10N,
      toolbox,
    } = this.props;

    const meatballMenuButtonId = "toolbox-meatball-menu-button";

    const meatballMenuButton = MenuButton(
      {
        id: meatballMenuButtonId,
        menuId: meatballMenuButtonId + "-panel",
        toolboxDoc: toolbox.doc,
        onFocus: () => focusButton(meatballMenuButtonId),
        className: "devtools-tabbar-button",
        title: L10N.getStr("toolbox.meatballMenu.button.tooltip"),
        tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1",
        ref: "meatballMenuButton",
      },
      MeatballMenu({
        ...this.props,
        hostTypes: this.props.areDockOptionsEnabled ? this.props.hostTypes : [],
        onResize: () => {
          this.refs.meatballMenuButton.resizeContent();
        },
      })
    );

    const closeButtonId = "toolbox-close";

    const closeButton = canCloseToolbox
      ? button({
          id: closeButtonId,
          onFocus: () => focusButton(closeButtonId),
          className: "devtools-tabbar-button",
          title: L10N.getStr("toolbox.closebutton.tooltip"),
          onClick: () => closeToolbox(),
          tabIndex: focusedButton === "toolbox-close" ? "0" : "-1",
        })
      : null;

    return div({ id: "toolbox-controls" }, meatballMenuButton, closeButton);
  }

  /**
   * The render function is kept fairly short for maintainability. See the individual
   * render functions for how each of the sections is rendered.
   */
  render() {
    const { L10N, debugTargetData, toolbox, fluentBundles } = this.props;
    const classnames = ["devtools-tabbar"];
    const startButtons = this.renderToolboxButtonsStart();
    const endButtons = this.renderToolboxButtonsEnd();

    if (!startButtons) {
      classnames.push("devtools-tabbar-has-start");
    }
    if (!endButtons) {
      classnames.push("devtools-tabbar-has-end");
    }

    const toolbar = this.props.canRender
      ? div(
          {
            className: classnames.join(" "),
          },
          startButtons,
          ToolboxTabs(this.props),
          endButtons,
          this.renderToolboxControls()
        )
      : div({ className: classnames.join(" ") });

    const debugTargetInfo = debugTargetData
      ? DebugTargetInfo({
          alwaysOnTop: this.props.alwaysOnTop,
          focusedState: this.props.focusedState,
          toggleAlwaysOnTop: this.props.toggleAlwaysOnTop,
          debugTargetData,
          L10N,
          toolbox,
        })
      : null;

    // Display the toolbar in the MBT and about:debugging MBT if we have server support for it.
    const chromeDebugToolbar = toolbox.commands.targetCommand.descriptorFront
      .isBrowserProcessDescriptor
      ? ChromeDebugToolbar()
      : null;

    return LocalizationProvider(
      { bundles: fluentBundles },
      div({}, chromeDebugToolbar, debugTargetInfo, toolbar)
    );
  }
}

module.exports = ToolboxToolbar;
