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

import {
  classMap,
  html,
  ifDefined,
  styleMap,
  when,
} from "chrome://global/content/vendor/lit.all.mjs";
import {
  FxviewTabListBase,
  FxviewTabRowBase,
} from "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-button.mjs";

const lazy = {};
let XPCOMUtils;

XPCOMUtils = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
).XPCOMUtils;
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "virtualListEnabledPref",
  "browser.firefox-view.virtual-list.enabled"
);

/**
 * A list of clickable tab items
 *
 * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view
 */

export class OpenTabsTabList extends FxviewTabListBase {
  constructor() {
    super();
    this.pinnedTabsGridView = false;
    this.pinnedTabs = [];
    this.unpinnedTabs = [];
  }

  static properties = {
    pinnedTabsGridView: { type: Boolean },
  };

  static queries = {
    ...FxviewTabListBase.queries,
    rowEls: {
      all: "opentabs-tab-row",
    },
  };

  willUpdate(changes) {
    this.activeIndex = Math.min(
      Math.max(this.activeIndex, 0),
      this.tabItems.length - 1
    );

    if (changes.has("dateTimeFormat") || changes.has("updatesPaused")) {
      this.clearIntervalTimer();
      if (!this.updatesPaused && this.dateTimeFormat == "relative") {
        this.startIntervalTimer();
        this.onIntervalUpdate();
      }
    }

    // Move pinned tabs to the beginning of the list
    if (this.pinnedTabsGridView) {
      // Can set maxTabsLength to -1 to have no max
      this.unpinnedTabs = this.tabItems.filter(
        tab => !tab.indicators.includes("pinned")
      );
      this.pinnedTabs = this.tabItems.filter(tab =>
        tab.indicators.includes("pinned")
      );
      if (this.maxTabsLength > 0) {
        this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength);
      }
      this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs];
    } else if (this.maxTabsLength > 0) {
      this.tabItems = this.tabItems.slice(0, this.maxTabsLength);
    }
  }

  /**
   * Focuses the expected element (either the link or button) within fxview-tab-row
   * The currently focused/active element ID within a row is stored in this.currentActiveElementId
   */
  handleFocusElementInRow(e) {
    let fxviewTabRow = e.target;
    if (e.code == "ArrowUp") {
      // Focus either the link or button of the previous row based on this.currentActiveElementId
      e.preventDefault();
      if (
        (this.pinnedTabsGridView &&
          this.activeIndex >= this.pinnedTabs.length) ||
        !this.pinnedTabsGridView
      ) {
        this.focusPrevRow();
      }
    } else if (e.code == "ArrowDown") {
      // Focus either the link or button of the next row based on this.currentActiveElementId
      e.preventDefault();
      if (
        this.pinnedTabsGridView &&
        this.activeIndex < this.pinnedTabs.length
      ) {
        this.focusIndex(this.pinnedTabs.length);
      } else {
        this.focusNextRow();
      }
    } else if (e.code == "ArrowRight") {
      // Focus either the link or the button in the current row and
      // set this.currentActiveElementId to that element's ID
      e.preventDefault();
      if (document.dir == "rtl") {
        fxviewTabRow.moveFocusLeft();
      } else {
        fxviewTabRow.moveFocusRight();
      }
    } else if (e.code == "ArrowLeft") {
      // Focus either the link or the button in the current row and
      // set this.currentActiveElementId to that element's ID
      e.preventDefault();
      if (document.dir == "rtl") {
        fxviewTabRow.moveFocusRight();
      } else {
        fxviewTabRow.moveFocusLeft();
      }
    }
  }

  async focusIndex(index) {
    // Focus link or button of item
    if (
      ((this.pinnedTabsGridView && index > this.pinnedTabs.length) ||
        !this.pinnedTabsGridView) &&
      lazy.virtualListEnabledPref
    ) {
      let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length);
      if (!row) {
        return;
      }
      let subList = this.rootVirtualListEl.getSubListForItem(
        index - this.pinnedTabs.length
      );
      if (!subList) {
        return;
      }
      this.activeIndex = index;

      // In Bug 1866845, these manual updates to the sublists should be removed
      // and scrollIntoView() should also be iterated on so that we aren't constantly
      // moving the focused item to the center of the viewport
      for (const sublist of Array.from(this.rootVirtualListEl.children)) {
        await sublist.requestUpdate();
        await sublist.updateComplete;
      }
      row.scrollIntoView({ block: "center" });
      row.focus();
    } else if (index >= 0 && index < this.rowEls?.length) {
      this.rowEls[index].focus();
      this.activeIndex = index;
    }
  }

  #getTabListWrapperClasses() {
    let wrapperClasses = ["fxview-tab-list"];
    let tabsToCheck = this.pinnedTabsGridView
      ? this.unpinnedTabs
      : this.tabItems;
    if (tabsToCheck.some(tab => tab.containerObj)) {
      wrapperClasses.push(`hasContainerTab`);
    }
    return wrapperClasses;
  }

  itemTemplate = (tabItem, i) => {
    let time;
    if (tabItem.time || tabItem.closedAt) {
      let stringTime = (tabItem.time || tabItem.closedAt).toString();
      // Different APIs return time in different units, so we use
      // the length to decide if it's milliseconds or nanoseconds.
      if (stringTime.length === 16) {
        time = (tabItem.time || tabItem.closedAt) / 1000;
      } else {
        time = tabItem.time || tabItem.closedAt;
      }
    }

    return html`<opentabs-tab-row
      ?active=${i == this.activeIndex}
      class=${classMap({
        pinned:
          this.pinnedTabsGridView && tabItem.indicators?.includes("pinned"),
      })}
      .currentActiveElementId=${this.currentActiveElementId}
      .favicon=${tabItem.icon}
      .compact=${this.compactRows}
      .containerObj=${ifDefined(tabItem.containerObj)}
      .indicators=${tabItem.indicators}
      .pinnedTabsGridView=${ifDefined(this.pinnedTabsGridView)}
      .primaryL10nId=${tabItem.primaryL10nId}
      .primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
      .secondaryL10nId=${tabItem.secondaryL10nId}
      .secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
      .tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)}
      .tertiaryL10nArgs=${ifDefined(tabItem.tertiaryL10nArgs)}
      .secondaryActionClass=${this.secondaryActionClass}
      .tertiaryActionClass=${ifDefined(this.tertiaryActionClass)}
      .sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
      .sourceWindowId=${ifDefined(tabItem.sourceWindowId)}
      .closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
      role=${tabItem.pinned && this.pinnedTabsGridView ? "tab" : "listitem"}
      .tabElement=${ifDefined(tabItem.tabElement)}
      .time=${ifDefined(time)}
      .title=${tabItem.title}
      .url=${tabItem.url}
      .searchQuery=${ifDefined(this.searchQuery)}
      .timeMsPref=${ifDefined(this.timeMsPref)}
      .hasPopup=${this.hasPopup}
      .dateTimeFormat=${this.dateTimeFormat}
    ></opentabs-tab-row>`;
  };

  render() {
    if (this.searchQuery && this.tabItems.length === 0) {
      return this.emptySearchResultsTemplate();
    }
    return html`
      ${this.stylesheets()}
      <link
        rel="stylesheet"
        href="chrome://browser/content/firefoxview/opentabs-tab-list.css"
      />
      ${when(
        this.pinnedTabsGridView && this.pinnedTabs.length,
        () => html`
          <div
            id="fxview-tab-list"
            class="fxview-tab-list pinned"
            data-l10n-id="firefoxview-pinned-tabs"
            role="tablist"
            @keydown=${this.handleFocusElementInRow}
          >
            ${this.pinnedTabs.map((tabItem, i) =>
              this.customItemTemplate
                ? this.customItemTemplate(tabItem, i)
                : this.itemTemplate(tabItem, i)
            )}
          </div>
        `
      )}
      <div
        id="fxview-tab-list"
        class=${this.#getTabListWrapperClasses().join(" ")}
        data-l10n-id="firefoxview-tabs"
        role="list"
        @keydown=${this.handleFocusElementInRow}
      >
        ${when(
          lazy.virtualListEnabledPref,
          () => html`
            <virtual-list
              .activeIndex=${this.activeIndex}
              .pinnedTabsIndexOffset=${this.pinnedTabsGridView
                ? this.pinnedTabs.length
                : 0}
              .items=${this.pinnedTabsGridView
                ? this.unpinnedTabs
                : this.tabItems}
              .template=${this.itemTemplate}
            ></virtual-list>
          `,
          () =>
            html`${this.tabItems.map((tabItem, i) =>
              this.itemTemplate(tabItem, i)
            )}`
        )}
      </div>
      <slot name="menu"></slot>
    `;
  }
}
customElements.define("opentabs-tab-list", OpenTabsTabList);

/**
 * A tab item that displays favicon, title, url, and time of last access
 *
 * @property {object} containerObj - Info about an open tab's container if within one
 * @property {string} indicators - An array of tab indicators if any are present
 * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view
 */

export class OpenTabsTabRow extends FxviewTabRowBase {
  constructor() {
    super();
    this.indicators = [];
    this.pinnedTabsGridView = false;
  }

  static properties = {
    ...FxviewTabRowBase.properties,
    containerObj: { type: Object },
    indicators: { type: Array },
    pinnedTabsGridView: { type: Boolean },
  };

  static queries = {
    ...FxviewTabRowBase.queries,
    mediaButtonEl: "#fxview-tab-row-media-button",
    pinnedTabButtonEl: "moz-button#fxview-tab-row-main",
  };

  connectedCallback() {
    super.connectedCallback();
    this.addEventListener("keydown", this.handleKeydown);
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.removeEventListener("keydown", this.handleKeydown);
  }

  handleKeydown(e) {
    if (
      this.active &&
      this.pinnedTabsGridView &&
      this.indicators?.includes("pinned") &&
      e.key === "m" &&
      e.ctrlKey
    ) {
      this.muteOrUnmuteTab();
    }
  }

  moveFocusRight() {
    let tabList = this.getRootNode().host;
    if (this.pinnedTabsGridView && this.indicators?.includes("pinned")) {
      tabList.focusNextRow();
    } else if (
      (this.indicators?.includes("soundplaying") ||
        this.indicators?.includes("muted")) &&
      this.currentActiveElementId === "fxview-tab-row-main"
    ) {
      this.focusMediaButton();
    } else if (
      this.currentActiveElementId === "fxview-tab-row-media-button" ||
      this.currentActiveElementId === "fxview-tab-row-main"
    ) {
      this.focusSecondaryButton();
    } else if (
      this.tertiaryButtonEl &&
      this.currentActiveElementId === "fxview-tab-row-secondary-button"
    ) {
      this.focusTertiaryButton();
    }
  }

  moveFocusLeft() {
    let tabList = this.getRootNode().host;
    if (
      this.pinnedTabsGridView &&
      (this.indicators?.includes("pinned") ||
        (tabList.currentActiveElementId === "fxview-tab-row-main" &&
          tabList.activeIndex === tabList.pinnedTabs.length))
    ) {
      tabList.focusPrevRow();
    } else if (
      tabList.currentActiveElementId === "fxview-tab-row-tertiary-button"
    ) {
      this.focusSecondaryButton();
    } else if (
      (this.indicators?.includes("soundplaying") ||
        this.indicators?.includes("muted")) &&
      tabList.currentActiveElementId === "fxview-tab-row-secondary-button"
    ) {
      this.focusMediaButton();
    } else {
      this.focusLink();
    }
  }

  focusMediaButton() {
    let tabList = this.getRootNode().host;
    this.mediaButtonEl.focus();
    tabList.currentActiveElementId = this.mediaButtonEl.id;
  }

  #secondaryActionHandler(event) {
    if (
      (this.pinnedTabsGridView &&
        this.indicators?.includes("pinned") &&
        event.type == "contextmenu") ||
      (event.type == "click" && event.detail && !event.altKey) ||
      // detail=0 is from keyboard
      (event.type == "click" && !event.detail)
    ) {
      event.preventDefault();
      this.dispatchEvent(
        new CustomEvent("fxview-tab-list-secondary-action", {
          bubbles: true,
          composed: true,
          detail: { originalEvent: event, item: this },
        })
      );
    }
  }

  #faviconTemplate() {
    return html`<span
      class=${classMap({
        "fxview-tab-row-favicon-wrapper": true,
        pinned: this.indicators?.includes("pinned"),
        pinnedOnNewTab: this.indicators?.includes("pinnedOnNewTab"),
        attention: this.indicators?.includes("attention"),
        bookmark: this.indicators?.includes("bookmark"),
      })}
    >
      <span
        class="fxview-tab-row-favicon icon"
        id="fxview-tab-row-favicon"
        style=${styleMap({
          backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`,
        })}
      ></span>
      ${when(
        this.pinnedTabsGridView &&
          this.indicators?.includes("pinned") &&
          (this.indicators?.includes("muted") ||
            this.indicators?.includes("soundplaying")),
        () => html`
          <button
            class="fxview-tab-row-pinned-media-button"
            id="fxview-tab-row-media-button"
            tabindex="-1"
            data-l10n-id=${this.indicators?.includes("muted")
              ? "fxviewtabrow-unmute-tab-button-no-context"
              : "fxviewtabrow-mute-tab-button-no-context"}
            muted=${this.indicators?.includes("muted")}
            soundplaying=${this.indicators?.includes("soundplaying") &&
            !this.indicators?.includes("muted")}
            @click=${this.muteOrUnmuteTab}
          ></button>
        `
      )}
    </span>`;
  }

  #getContainerClasses() {
    let containerClasses = ["fxview-tab-row-container-indicator", "icon"];
    if (this.containerObj) {
      let { icon, color } = this.containerObj;
      containerClasses.push(`identity-icon-${icon}`);
      containerClasses.push(`identity-color-${color}`);
    }
    return containerClasses;
  }

  muteOrUnmuteTab(e) {
    e?.preventDefault();
    // If the tab has no sound playing, the mute/unmute button will be removed when toggled.
    // We should move the focus to the right in that case. This does not apply to pinned tabs
    // on the Open Tabs page.
    let shouldMoveFocus =
      (!this.pinnedTabsGridView ||
        (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) &&
      this.mediaButtonEl &&
      !this.indicators.includes("soundplaying") &&
      this.currentActiveElementId === "fxview-tab-row-media-button";

    // detail=0 is from keyboard
    if (e?.type == "click" && !e?.detail && shouldMoveFocus) {
      if (document.dir == "rtl") {
        this.moveFocusLeft();
      } else {
        this.moveFocusRight();
      }
    }
    this.tabElement.toggleMuteAudio();
  }

  #mediaButtonTemplate() {
    return html`${when(
      this.indicators?.includes("soundplaying") ||
        this.indicators?.includes("muted"),
      () =>
        html`<moz-button
          type="icon ghost"
          class="fxview-tab-row-button"
          id="fxview-tab-row-media-button"
          data-l10n-id=${this.indicators?.includes("muted")
            ? "fxviewtabrow-unmute-tab-button-no-context"
            : "fxviewtabrow-mute-tab-button-no-context"}
          muted=${this.indicators?.includes("muted")}
          soundplaying=${this.indicators?.includes("soundplaying") &&
          !this.indicators?.includes("muted")}
          @click=${this.muteOrUnmuteTab}
          tabindex=${this.active &&
          this.currentActiveElementId === "fxview-tab-row-media-button"
            ? "0"
            : "-1"}
        ></moz-button>`,
      () => html`<span></span>`
    )}`;
  }

  #containerIndicatorTemplate() {
    let tabList = this.getRootNode().host;
    let tabsToCheck = tabList.pinnedTabsGridView
      ? tabList.unpinnedTabs
      : tabList.tabItems;
    return html`${when(
      tabsToCheck.some(tab => tab.containerObj),
      () => html`<span class=${this.#getContainerClasses().join(" ")}></span>`
    )}`;
  }

  #pinnedTabItemTemplate() {
    return html`
      <moz-button
        type="icon ghost"
        id="fxview-tab-row-main"
        aria-haspopup=${ifDefined(this.hasPopup)}
        data-l10n-id=${ifDefined(this.primaryL10nId)}
        data-l10n-args=${ifDefined(this.primaryL10nArgs)}
        tabindex=${this.active &&
        this.currentActiveElementId === "fxview-tab-row-main"
          ? "0"
          : "-1"}
        role="tab"
        @click=${this.primaryActionHandler}
        @keydown=${this.primaryActionHandler}
        @contextmenu=${this.#secondaryActionHandler}
      >
        ${this.#faviconTemplate()}
      </moz-button>
    `;
  }

  #unpinnedTabItemTemplate() {
    return html`<a
        href=${ifDefined(this.url)}
        class="fxview-tab-row-main"
        id="fxview-tab-row-main"
        tabindex=${this.active &&
        this.currentActiveElementId === "fxview-tab-row-main"
          ? "0"
          : "-1"}
        data-l10n-id=${ifDefined(this.primaryL10nId)}
        data-l10n-args=${ifDefined(this.primaryL10nArgs)}
        @click=${this.primaryActionHandler}
        @keydown=${this.primaryActionHandler}
        title=${!this.primaryL10nId ? this.url : null}
      >
        ${this.#faviconTemplate()} ${this.titleTemplate()}
        ${when(
          !this.compact,
          () =>
            html`${this.#containerIndicatorTemplate()} ${this.urlTemplate()}
            ${this.dateTemplate()} ${this.timeTemplate()}`
        )}
      </a>
      ${this.#mediaButtonTemplate()} ${this.secondaryButtonTemplate()}
      ${this.tertiaryButtonTemplate()}`;
  }

  render() {
    return html`
      ${this.stylesheets()}
      <link
        rel="stylesheet"
        href="chrome://browser/content/firefoxview/opentabs-tab-row.css"
      />
      ${when(
        this.containerObj,
        () => html`
          <link
            rel="stylesheet"
            href="chrome://browser/content/usercontext/usercontext.css"
          />
        `
      )}
      ${when(
        this.pinnedTabsGridView && this.indicators?.includes("pinned"),
        this.#pinnedTabItemTemplate.bind(this),
        this.#unpinnedTabItemTemplate.bind(this)
      )}
    `;
  }
}
customElements.define("opentabs-tab-row", OpenTabsTabRow);
