/* 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/. */

/* eslint-env browser */

"use strict";

const {
  Component,
  createFactory,
  createRef,
} = require("resource://devtools/client/shared/vendor/react.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");

const Sidebar = createFactory(
  require("resource://devtools/client/shared/components/Sidebar.js")
);

loader.lazyRequireGetter(
  this,
  "Menu",
  "resource://devtools/client/framework/menu.js"
);
loader.lazyRequireGetter(
  this,
  "MenuItem",
  "resource://devtools/client/framework/menu-item.js"
);

// Shortcuts
const { div } = dom;

/**
 * Renders Tabbar component.
 */
class Tabbar extends Component {
  static get propTypes() {
    return {
      children: PropTypes.array,
      menuDocument: PropTypes.object,
      onSelect: PropTypes.func,
      showAllTabsMenu: PropTypes.bool,
      allTabsMenuButtonTooltip: PropTypes.string,
      activeTabId: PropTypes.string,
      renderOnlySelected: PropTypes.bool,
      sidebarToggleButton: PropTypes.shape({
        // Set to true if collapsed.
        collapsed: PropTypes.bool.isRequired,
        // Tooltip text used when the button indicates expanded state.
        collapsePaneTitle: PropTypes.string.isRequired,
        // Tooltip text used when the button indicates collapsed state.
        expandPaneTitle: PropTypes.string.isRequired,
        // Click callback
        onClick: PropTypes.func.isRequired,
        // align toggle button to right
        alignRight: PropTypes.bool,
        // if set to true toggle-button rotate 90
        canVerticalSplit: PropTypes.bool,
      }),
    };
  }

  static get defaultProps() {
    return {
      menuDocument: window.parent.document,
      showAllTabsMenu: false,
    };
  }

  constructor(props, context) {
    super(props, context);
    const { activeTabId, children = [] } = props;
    const tabs = this.createTabs(children);
    const activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId);

    this.state = {
      activeTab: activeTab === -1 ? 0 : activeTab,
      tabs,
    };

    // Array of queued tabs to add to the Tabbar.
    this.queuedTabs = [];

    this.createTabs = this.createTabs.bind(this);
    this.addTab = this.addTab.bind(this);
    this.addAllQueuedTabs = this.addAllQueuedTabs.bind(this);
    this.queueTab = this.queueTab.bind(this);
    this.toggleTab = this.toggleTab.bind(this);
    this.removeTab = this.removeTab.bind(this);
    this.select = this.select.bind(this);
    this.getTabIndex = this.getTabIndex.bind(this);
    this.getTabId = this.getTabId.bind(this);
    this.getCurrentTabId = this.getCurrentTabId.bind(this);
    this.onTabChanged = this.onTabChanged.bind(this);
    this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this);
    this.renderTab = this.renderTab.bind(this);
    this.tabbarRef = createRef();
  }

  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
  UNSAFE_componentWillReceiveProps(nextProps) {
    const { activeTabId, children = [] } = nextProps;
    const tabs = this.createTabs(children);
    const activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId);

    if (
      activeTab !== this.state.activeTab ||
      children !== this.props.children
    ) {
      this.setState({
        activeTab: activeTab === -1 ? 0 : activeTab,
        tabs,
      });
    }
  }

  createTabs(children) {
    return children
      .filter(panel => panel)
      .map((panel, index) =>
        Object.assign({}, children[index], {
          id: panel.props.id || index,
          panel,
          title: panel.props.title,
        })
      );
  }

  // Public API

  addTab(id, title, selected = false, panel, url, index = -1) {
    const tabs = this.state.tabs.slice();

    if (index >= 0) {
      tabs.splice(index, 0, { id, title, panel, url });
    } else {
      tabs.push({ id, title, panel, url });
    }

    const newState = Object.assign({}, this.state, {
      tabs,
    });

    if (selected) {
      newState.activeTab = index >= 0 ? index : tabs.length - 1;
    }

    this.setState(newState, () => {
      if (this.props.onSelect && selected) {
        this.props.onSelect(id);
      }
    });
  }

  addAllQueuedTabs() {
    if (!this.queuedTabs.length) {
      return;
    }

    const tabs = this.state.tabs.slice();

    // Preselect the first sidebar tab if none was explicitly selected.
    let activeTab = 0;
    let activeId = this.queuedTabs[0].id;

    for (const { id, index, panel, selected, title, url } of this.queuedTabs) {
      if (index >= 0) {
        tabs.splice(index, 0, { id, title, panel, url });
      } else {
        tabs.push({ id, title, panel, url });
      }

      if (selected) {
        activeId = id;
        activeTab = index >= 0 ? index : tabs.length - 1;
      }
    }

    const newState = Object.assign({}, this.state, {
      activeTab,
      tabs,
    });

    this.setState(newState, () => {
      if (this.props.onSelect) {
        this.props.onSelect(activeId);
      }
    });

    this.queuedTabs = [];
  }

  /**
   * Queues a tab to be added. This is more performant than calling addTab for every
   * single tab to be added since we will limit the number of renders happening when
   * a new state is set. Once all the tabs to be added have been queued, call
   * addAllQueuedTabs() to populate the TabBar with all the queued tabs.
   */
  queueTab(id, title, selected = false, panel, url, index = -1) {
    this.queuedTabs.push({
      id,
      index,
      panel,
      selected,
      title,
      url,
    });
  }

  toggleTab(tabId, isVisible) {
    const index = this.getTabIndex(tabId);
    if (index < 0) {
      return;
    }

    const tabs = this.state.tabs.slice();
    tabs[index] = Object.assign({}, tabs[index], {
      isVisible,
    });

    this.setState(
      Object.assign({}, this.state, {
        tabs,
      })
    );
  }

  removeTab(tabId) {
    const index = this.getTabIndex(tabId);
    if (index < 0) {
      return;
    }

    const tabs = this.state.tabs.slice();
    tabs.splice(index, 1);

    let activeTab = this.state.activeTab - 1;
    activeTab = activeTab === -1 ? 0 : activeTab;

    this.setState(
      Object.assign({}, this.state, {
        activeTab,
        tabs,
      }),
      () => {
        // Select the next active tab and force the select event handler to initialize
        // the panel if needed.
        if (tabs.length && this.props.onSelect) {
          this.props.onSelect(this.getTabId(activeTab));
        }
      }
    );
  }

  select(tabId) {
    const docRef = this.tabbarRef.current.ownerDocument;

    const index = this.getTabIndex(tabId);
    if (index < 0) {
      return;
    }

    const newState = Object.assign({}, this.state, {
      activeTab: index,
    });

    const tabDomElement = docRef.querySelector(`[data-tab-index="${index}"]`);

    if (tabDomElement) {
      tabDomElement.scrollIntoView();
    }

    this.setState(newState, () => {
      if (this.props.onSelect) {
        this.props.onSelect(tabId);
      }
    });
  }

  // Helpers

  getTabIndex(tabId) {
    let tabIndex = -1;
    this.state.tabs.forEach((tab, index) => {
      if (tab.id === tabId) {
        tabIndex = index;
      }
    });
    return tabIndex;
  }

  getTabId(index) {
    return this.state.tabs[index].id;
  }

  getCurrentTabId() {
    return this.state.tabs[this.state.activeTab].id;
  }

  // Event Handlers

  onTabChanged(index) {
    this.setState(
      {
        activeTab: index,
      },
      () => {
        if (this.props.onSelect) {
          this.props.onSelect(this.state.tabs[index].id);
        }
      }
    );
  }

  onAllTabsMenuClick(event) {
    const menu = new Menu();
    const target = event.target;

    // Generate list of menu items from the list of tabs.
    this.state.tabs.forEach(tab => {
      menu.append(
        new MenuItem({
          label: tab.title,
          type: "checkbox",
          checked: this.getCurrentTabId() === tab.id,
          click: () => this.select(tab.id),
        })
      );
    });

    // Show a drop down menu with frames.
    menu.popupAtTarget(target);

    return menu;
  }

  // Rendering

  renderTab(tab) {
    if (typeof tab.panel === "function") {
      return tab.panel({
        key: tab.id,
        title: tab.title,
        id: tab.id,
        url: tab.url,
      });
    }

    return tab.panel;
  }

  render() {
    const tabs = this.state.tabs.map(tab => this.renderTab(tab));

    return div(
      {
        className: "devtools-sidebar-tabs",
        ref: this.tabbarRef,
      },
      Sidebar(
        {
          onAllTabsMenuClick: this.onAllTabsMenuClick,
          renderOnlySelected: this.props.renderOnlySelected,
          showAllTabsMenu: this.props.showAllTabsMenu,
          allTabsMenuButtonTooltip: this.props.allTabsMenuButtonTooltip,
          sidebarToggleButton: this.props.sidebarToggleButton,
          activeTab: this.state.activeTab,
          onAfterChange: this.onTabChanged,
        },
        tabs
      )
    );
  }
}

module.exports = Tabbar;
