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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

import { getChromeWindow } from "resource:///modules/syncedtabs/util.sys.mjs";

function getContextMenu(window) {
  return getChromeWindow(window).document.getElementById(
    "SyncedTabsSidebarContext"
  );
}

function getTabsFilterContextMenu(window) {
  return getChromeWindow(window).document.getElementById(
    "SyncedTabsSidebarTabsFilterContext"
  );
}

/*
 * TabListView
 *
 * Given a state, this object will render the corresponding DOM.
 * It maintains no state of it's own. It listens for DOM events
 * and triggers actions that may cause the state to change and
 * ultimately the view to rerender.
 */
export function TabListView(window, props) {
  this.props = props;

  this._window = window;
  this._doc = this._window.document;

  this._tabsContainerTemplate = this._doc.getElementById(
    "tabs-container-template"
  );
  this._clientTemplate = this._doc.getElementById("client-template");
  this._emptyClientTemplate = this._doc.getElementById("empty-client-template");
  this._tabTemplate = this._doc.getElementById("tab-template");
  this.tabsFilter = this._doc.querySelector(".tabsFilter");

  this.container = this._doc.createElement("div");

  this._attachFixedListeners();

  this._setupContextMenu();
}

TabListView.prototype = {
  render(state) {
    // Don't rerender anything; just update attributes, e.g. selection
    if (state.canUpdateAll) {
      this._update(state);
      return;
    }
    // Rerender the tab list
    if (state.canUpdateInput) {
      this._updateSearchBox(state);
      this._createList(state);
      return;
    }
    // Create the world anew
    this._create(state);
  },

  // Create the initial DOM from templates
  _create(state) {
    let wrapper = this._doc.importNode(
      this._tabsContainerTemplate.content,
      true
    ).firstElementChild;
    this._clearChilden();
    this.container.appendChild(wrapper);

    this.list = this.container.querySelector(".list");

    this._createList(state);
    this._updateSearchBox(state);

    this._attachListListeners();
  },

  _createList(state) {
    this._clearChilden(this.list);
    for (let client of state.clients) {
      if (state.filter) {
        this._renderFilteredClient(client);
      } else {
        this._renderClient(client);
      }
    }
    if (this.list.firstElementChild) {
      const firstTab = this.list.firstElementChild.querySelector(
        ".item.tab:first-child .item-title"
      );
      if (firstTab) {
        firstTab.setAttribute("tabindex", 2);
      }
    }
  },

  destroy() {
    this._teardownContextMenu();
    this.container.remove();
  },

  _update(state) {
    this._updateSearchBox(state);
    for (let client of state.clients) {
      let clientNode = this._doc.getElementById("item-" + client.id);
      if (clientNode) {
        this._updateClient(client, clientNode);
      }

      client.tabs.forEach((tab, index) => {
        let tabNode = this._doc.getElementById(
          "tab-" + client.id + "-" + index
        );
        this._updateTab(tab, tabNode, index);
      });
    }
  },

  // Client rows are hidden when the list is filtered
  _renderFilteredClient(client) {
    client.tabs.forEach((tab, index) => {
      let node = this._renderTab(client, tab, index);
      this.list.appendChild(node);
    });
  },

  _updateLastSyncTitle(lastModified, itemNode) {
    let lastSync = new Date(lastModified);
    let lastSyncTitle = getChromeWindow(this._window).gSync.formatLastSyncDate(
      lastSync
    );
    itemNode.setAttribute("title", lastSyncTitle);
  },

  _renderClient(client) {
    let itemNode = client.tabs.length
      ? this._createClient(client)
      : this._createEmptyClient(client);

    itemNode.addEventListener("mouseover", () =>
      this._updateLastSyncTitle(client.lastModified, itemNode)
    );

    this._updateClient(client, itemNode);

    let tabsList = itemNode.querySelector(".item-tabs-list");
    client.tabs.forEach((tab, index) => {
      let node = this._renderTab(client, tab, index);
      tabsList.appendChild(node);
    });

    this.list.appendChild(itemNode);
    return itemNode;
  },

  _renderTab(client, tab, index) {
    let itemNode = this._createTab(tab);
    this._updateTab(tab, itemNode, index);
    return itemNode;
  },

  _createClient() {
    return this._doc.importNode(this._clientTemplate.content, true)
      .firstElementChild;
  },

  _createEmptyClient() {
    return this._doc.importNode(this._emptyClientTemplate.content, true)
      .firstElementChild;
  },

  _createTab() {
    return this._doc.importNode(this._tabTemplate.content, true)
      .firstElementChild;
  },

  _clearChilden(node) {
    let parent = node || this.container;
    while (parent.firstChild) {
      parent.firstChild.remove();
    }
  },

  // These listeners are attached only once, when we initialize the view
  _attachFixedListeners() {
    this.tabsFilter.addEventListener("input", this.onFilter.bind(this));
    this.tabsFilter.addEventListener("focus", this.onFilterFocus.bind(this));
    this.tabsFilter.addEventListener("blur", this.onFilterBlur.bind(this));
  },

  // These listeners have to be re-created every time since we re-create the list
  _attachListListeners() {
    this.list.addEventListener("click", this.onClick.bind(this));
    this.list.addEventListener("mouseup", this.onMouseUp.bind(this));
    this.list.addEventListener("keydown", this.onKeyDown.bind(this));
  },

  _updateSearchBox(state) {
    this.tabsFilter.value = state.filter;
    if (state.inputFocused) {
      this.tabsFilter.focus();
    }
  },

  /**
   * Update the element representing an item, ensuring it's in sync with the
   * underlying data.
   *
   * @param {client} item - Item to use as a source.
   * @param {Element} itemNode - Element to update.
   */
  _updateClient(item, itemNode) {
    itemNode.setAttribute("id", "item-" + item.id);
    this._updateLastSyncTitle(item.lastModified, itemNode);
    if (item.closed) {
      itemNode.classList.add("closed");
    } else {
      itemNode.classList.remove("closed");
    }
    if (item.selected) {
      itemNode.classList.add("selected");
    } else {
      itemNode.classList.remove("selected");
    }
    if (item.focused) {
      itemNode.focus();
    }
    itemNode.setAttribute("clientType", item.clientType);
    itemNode.dataset.id = item.id;
    itemNode.querySelector(".item-title").textContent = item.name;
  },

  /**
   * Update the element representing a tab, ensuring it's in sync with the
   * underlying data.
   *
   * @param {tab} item - Item to use as a source.
   * @param {Element} itemNode - Element to update.
   */
  _updateTab(item, itemNode, index) {
    itemNode.setAttribute("title", `${item.title}\n${item.url}`);
    itemNode.setAttribute("id", "tab-" + item.client + "-" + index);
    if (item.selected) {
      itemNode.classList.add("selected");
    } else {
      itemNode.classList.remove("selected");
    }
    if (item.focused) {
      itemNode.focus();
    }
    itemNode.dataset.url = item.url;

    itemNode.querySelector(".item-title").textContent = item.title;

    if (item.icon) {
      let icon = itemNode.querySelector(".item-icon-container");
      icon.style.backgroundImage = "url(" + item.icon + ")";
    }
  },

  onMouseUp(event) {
    if (event.which == 2) {
      // Middle click
      this.onClick(event);
    }
  },

  onClick(event) {
    let itemNode = this._findParentItemNode(event.target);
    if (!itemNode) {
      return;
    }

    if (itemNode.classList.contains("tab")) {
      let url = itemNode.dataset.url;
      if (url) {
        this.onOpenSelected(url, event);
      }
    }

    // Middle click on a client
    if (itemNode.classList.contains("client")) {
      let where = lazy.BrowserUtils.whereToOpenLink(event);
      if (where != "current") {
        this._openAllClientTabs(itemNode, where);
      }
    }

    if (
      event.target.classList.contains("item-twisty-container") &&
      event.which != 2
    ) {
      this.props.onToggleBranch(itemNode.dataset.id);
      return;
    }

    let position = this._getSelectionPosition(itemNode);
    this.props.onSelectRow(position);
  },

  /**
   * Handle a keydown event on the list box.
   *
   * @param {Event} event - Triggering event.
   */
  onKeyDown(event) {
    if (event.keyCode == this._window.KeyEvent.DOM_VK_DOWN) {
      event.preventDefault();
      this.props.onMoveSelectionDown();
    } else if (event.keyCode == this._window.KeyEvent.DOM_VK_UP) {
      event.preventDefault();
      this.props.onMoveSelectionUp();
    } else if (event.keyCode == this._window.KeyEvent.DOM_VK_RETURN) {
      let selectedNode = this.container.querySelector(".item.selected");
      if (selectedNode.dataset.url) {
        this.onOpenSelected(selectedNode.dataset.url, event);
      } else if (selectedNode) {
        this.props.onToggleBranch(selectedNode.dataset.id);
      }
    }
  },

  onBookmarkTab() {
    let item = this._getSelectedTabNode();
    if (item) {
      let title = item.querySelector(".item-title").textContent;
      this.props.onBookmarkTab(item.dataset.url, title);
    }
  },

  onCopyTabLocation() {
    let item = this._getSelectedTabNode();
    if (item) {
      this.props.onCopyTabLocation(item.dataset.url);
    }
  },

  onOpenSelected(url, event) {
    let where = lazy.BrowserUtils.whereToOpenLink(event);
    this.props.onOpenTab(url, where, {});
  },

  onOpenSelectedFromContextMenu(event) {
    let item = this._getSelectedTabNode();
    if (item) {
      let where = event.target.getAttribute("where");
      let params = {
        private: event.target.hasAttribute("private"),
      };
      this.props.onOpenTab(item.dataset.url, where, params);
    }
  },

  onOpenSelectedInContainerTab(event) {
    let item = this._getSelectedTabNode();
    if (item) {
      this.props.onOpenTab(item.dataset.url, "tab", {
        userContextId: parseInt(event.target?.dataset.usercontextid),
      });
    }
  },

  onOpenAllInTabs() {
    let item = this._getSelectedClientNode();
    if (item) {
      this._openAllClientTabs(item, "tab");
    }
  },

  onFilter(event) {
    let query = event.target.value;
    if (query) {
      this.props.onFilter(query);
    } else {
      this.props.onClearFilter();
    }
  },

  onFilterFocus() {
    this.props.onFilterFocus();
  },
  onFilterBlur() {
    this.props.onFilterBlur();
  },

  _getSelectedTabNode() {
    let item = this.container.querySelector(".item.selected");
    if (this._isTab(item) && item.dataset.url) {
      return item;
    }
    return null;
  },

  _getSelectedClientNode() {
    let item = this.container.querySelector(".item.selected");
    if (this._isClient(item)) {
      return item;
    }
    return null;
  },

  // Set up the custom context menu
  _setupContextMenu() {
    this._window.addEventListener("contextmenu", this, {
      mozSystemGroup: true,
    });
    for (let getMenu of [getContextMenu, getTabsFilterContextMenu]) {
      let menu = getMenu(this._window);
      menu.addEventListener("popupshowing", this, true);
      menu.addEventListener("command", this, true);
    }
  },

  _teardownContextMenu() {
    // Tear down context menu
    this._window.removeEventListener("contextmenu", this, {
      mozSystemGroup: true,
    });
    for (let getMenu of [getContextMenu, getTabsFilterContextMenu]) {
      let menu = getMenu(this._window);
      menu.removeEventListener("popupshowing", this, true);
      menu.removeEventListener("command", this, true);
    }
  },

  handleEvent(event) {
    switch (event.type) {
      case "contextmenu":
        this.handleContextMenu(event);
        break;

      case "popupshowing": {
        if (
          event.target.getAttribute("id") ==
          "SyncedTabsSidebarTabsFilterContext"
        ) {
          this.handleTabsFilterContextMenuShown(event);
        }
        break;
      }

      case "command": {
        let menu = event.target.closest("menupopup");
        switch (menu.getAttribute("id")) {
          case "SyncedTabsSidebarContext":
            this.handleContentContextMenuCommand(event);
            break;

          case "SyncedTabsOpenSelectedInContainerTabMenu":
            this.onOpenSelectedInContainerTab(event);
            break;

          case "SyncedTabsSidebarTabsFilterContext":
            this.handleTabsFilterContextMenuCommand(event);
            break;
        }
        break;
      }
    }
  },

  handleTabsFilterContextMenuShown(event) {
    let document = event.target.ownerDocument;
    let focusedElement = document.commandDispatcher.focusedElement;
    if (focusedElement != this.tabsFilter.inputField) {
      this.tabsFilter.focus();
    }
    for (let item of event.target.children) {
      if (!item.hasAttribute("cmd")) {
        continue;
      }
      let command = item.getAttribute("cmd");
      let controller =
        document.commandDispatcher.getControllerForCommand(command);
      if (controller.isCommandEnabled(command)) {
        item.removeAttribute("disabled");
      } else {
        item.setAttribute("disabled", "true");
      }
    }
  },

  handleContentContextMenuCommand(event) {
    let id = event.target.getAttribute("id");
    switch (id) {
      case "syncedTabsOpenSelected":
      case "syncedTabsOpenSelectedInTab":
      case "syncedTabsOpenSelectedInWindow":
      case "syncedTabsOpenSelectedInPrivateWindow":
        this.onOpenSelectedFromContextMenu(event);
        break;
      case "syncedTabsOpenAllInTabs":
        this.onOpenAllInTabs();
        break;
      case "syncedTabsBookmarkSelected":
        this.onBookmarkTab();
        break;
      case "syncedTabsCopySelected":
        this.onCopyTabLocation();
        break;
      case "syncedTabsRefresh":
      case "syncedTabsRefreshFilter":
        this.props.onSyncRefresh();
        break;
    }
  },

  handleTabsFilterContextMenuCommand(event) {
    let command = event.target.getAttribute("cmd");
    let dispatcher = getChromeWindow(this._window).document.commandDispatcher;
    let controller =
      dispatcher.focusedElement.controllers.getControllerForCommand(command);
    controller.doCommand(command);
  },

  handleContextMenu(event) {
    let menu;

    if (event.target == this.tabsFilter) {
      menu = getTabsFilterContextMenu(this._window);
    } else {
      let itemNode = this._findParentItemNode(event.target);
      if (itemNode) {
        let position = this._getSelectionPosition(itemNode);
        this.props.onSelectRow(position);
      }
      menu = getContextMenu(this._window);
      this.adjustContextMenu(menu);
    }

    menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
  },

  adjustContextMenu(menu) {
    let item = this.container.querySelector(".item.selected");
    let showTabOptions = this._isTab(item);

    let el = menu.firstElementChild;

    while (el) {
      let show = false;
      if (showTabOptions) {
        if (el.getAttribute("id") == "syncedTabsOpenSelectedInPrivateWindow") {
          show = lazy.PrivateBrowsingUtils.enabled;
        } else if (
          el.getAttribute("id") === "syncedTabsOpenSelectedInContainerTab"
        ) {
          show =
            Services.prefs.getBoolPref("privacy.userContext.enabled", false) &&
            !lazy.PrivateBrowsingUtils.isWindowPrivate(
              getChromeWindow(this._window)
            );
        } else if (
          el.getAttribute("id") != "syncedTabsOpenAllInTabs" &&
          el.getAttribute("id") != "syncedTabsManageDevices"
        ) {
          show = true;
        }
      } else if (el.getAttribute("id") == "syncedTabsOpenAllInTabs") {
        const tabs = item.querySelectorAll(".item-tabs-list > .item.tab");
        show = !!tabs.length;
      } else if (el.getAttribute("id") == "syncedTabsRefresh") {
        show = true;
      } else if (el.getAttribute("id") == "syncedTabsManageDevices") {
        show = true;
      }
      el.hidden = !show;

      el = el.nextElementSibling;
    }
  },

  /**
   * Find the parent item element, from a given child element.
   *
   * @param {Element} node - Child element.
   * @returns {Element} Element for the item, or null if not found.
   */
  _findParentItemNode(node) {
    while (
      node &&
      node !== this.list &&
      node !== this._doc.documentElement &&
      !node.classList.contains("item")
    ) {
      node = node.parentNode;
    }

    if (node !== this.list && node !== this._doc.documentElement) {
      return node;
    }

    return null;
  },

  _findParentBranchNode(node) {
    while (
      node &&
      !node.classList.contains("list") &&
      node !== this._doc.documentElement &&
      !node.parentNode.classList.contains("list")
    ) {
      node = node.parentNode;
    }

    if (node !== this.list && node !== this._doc.documentElement) {
      return node;
    }

    return null;
  },

  _getSelectionPosition(itemNode) {
    let parent = this._findParentBranchNode(itemNode);
    let parentPosition = this._indexOfNode(parent.parentNode, parent);
    let childPosition = -1;
    // if the node is not a client, find its position within the parent
    if (parent !== itemNode) {
      childPosition = this._indexOfNode(itemNode.parentNode, itemNode);
    }
    return [parentPosition, childPosition];
  },

  _indexOfNode(parent, child) {
    return Array.prototype.indexOf.call(parent.children, child);
  },

  _isTab(item) {
    return item && item.classList.contains("tab");
  },

  _isClient(item) {
    return item && item.classList.contains("client");
  },

  _openAllClientTabs(clientNode, where) {
    const tabs = clientNode.querySelector(".item-tabs-list").children;
    const urls = [...tabs].map(tab => tab.dataset.url);
    this.props.onOpenTabs(urls, where);
  },
};
