/* 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";

// This is loaded into chrome windows with the subscript loader. Wrap in
// a block to prevent accidentally leaking globals onto `window`.
{
  const { AppConstants } = ChromeUtils.importESModule(
    "resource://gre/modules/AppConstants.sys.mjs"
  );

  const XUL_NS =
    "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

  // Note: MozWizard currently supports adding, but not removing MozWizardPage
  //       children.
  class MozWizard extends MozXULElement {
    constructor() {
      super();

      // About this._accessMethod:
      //   There are two possible access methods: "sequential" and "random".
      //   "sequential" causes the MozWizardPage's to be displayed in the order
      //   that they are added to the DOM.
      //   The "random" method name is a bit misleading since the pages aren't
      //   displayed in a random order. Instead, each MozWizardPage must have
      //   a "next" attribute containing the id of the MozWizardPage that should
      //   be loaded next.
      this._accessMethod = null;
      this._currentPage = null;
      this._canAdvance = true;
      this._canRewind = false;
      this._hasLoaded = false;
      this._hasStarted = false; // Whether any MozWizardPage has been shown yet
      this._wizardButtonsReady = false;
      this.pageCount = 0;
      this._pageStack = [];

      this._bundle = Services.strings.createBundle(
        "chrome://global/locale/wizard.properties"
      );

      this.addEventListener(
        "keypress",
        event => {
          if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
            this._hitEnter(event);
          } else if (
            event.keyCode == KeyEvent.DOM_VK_ESCAPE &&
            !event.defaultPrevented
          ) {
            this.cancel();
          }
        },
        { mozSystemGroup: true }
      );

      /*
        XXX(ntim): We import button.css here for the wizard-buttons children
        This won't be needed after bug 1624888.
      */
      this.attachShadow({ mode: "open" }).appendChild(
        MozXULElement.parseXULToFragment(`
        <html:link rel="stylesheet" href="chrome://global/skin/button.css"/>
        <html:link rel="stylesheet" href="chrome://global/skin/wizard.css"/>
        <hbox class="wizard-header"></hbox>
        <html:slot name="wizardpage" class="wizard-page-box" style="display: grid; flex: 1;"/>
        <html:slot/>
        <wizard-buttons class="wizard-buttons"></wizard-buttons>
    `)
      );
      this.initializeAttributeInheritance();

      this._wizardButtons = this.shadowRoot.querySelector(".wizard-buttons");

      this._wizardHeader = this.shadowRoot.querySelector(".wizard-header");
      this._wizardHeader.appendChild(
        MozXULElement.parseXULToFragment(
          AppConstants.platform == "macosx"
            ? `<stack class="wizard-header-stack" flex="1">
           <vbox class="wizard-header-box-1">
             <vbox class="wizard-header-box-text">
               <label id="wizard-header-label" class="wizard-header-label"/>
             </vbox>
           </vbox>
           <hbox class="wizard-header-box-icon">
             <spacer flex="1"/>
             <image class="wizard-header-icon"/>
           </hbox>
         </stack>`
            : `<hbox class="wizard-header-box-1" flex="1">
           <vbox class="wizard-header-box-text" flex="1">
             <label id="wizard-header-label" class="wizard-header-label"/>
             <label class="wizard-header-description"/>
           </vbox>
           <image class="wizard-header-icon"/>
         </hbox>`
        )
      );
    }

    static get inheritedAttributes() {
      return {
        ".wizard-buttons": "pagestep,firstpage,lastpage",
      };
    }

    connectedCallback() {
      if (document.l10n) {
        document.l10n.connectRoot(this.shadowRoot);
      }
      document.documentElement.setAttribute("role", "dialog");
      document.documentElement.classList.add("wizard-window");
      this._maybeStartWizard();

      window.addEventListener("close", event => {
        if (this.cancel()) {
          event.preventDefault();
        }
      });

      // Give focus to the first focusable element in the wizard, do it after
      // onload completes, see bug 103197.
      window.addEventListener("load", () =>
        window.setTimeout(() => {
          this._hasLoaded = true;
          if (!document.commandDispatcher.focusedElement) {
            document.commandDispatcher.advanceFocusIntoSubtree(this);
          }
          try {
            let button = this._wizardButtons.defaultButton;
            if (button) {
              window.notifyDefaultButtonLoaded(button);
            }
          } catch (e) {}
        }, 0)
      );
    }

    set title(val) {
      document.title = val;
    }

    get title() {
      return document.title;
    }

    set canAdvance(val) {
      this.getButton("next").disabled = !val;
      this._canAdvance = val;
    }

    get canAdvance() {
      return this._canAdvance;
    }

    set canRewind(val) {
      this.getButton("back").disabled = !val;
      this._canRewind = val;
    }

    get canRewind() {
      return this._canRewind;
    }

    get pageStep() {
      return this._pageStack.length;
    }

    get wizardPages() {
      return this.getElementsByTagNameNS(XUL_NS, "wizardpage");
    }

    set currentPage(val) {
      if (!val) {
        return;
      }

      this._currentPage?.classList.remove("selected");
      val.classList.add("selected");

      this._currentPage = val;

      // Setting this attribute allows wizard's clients to dynamically
      // change the styles of each page based on purpose of the page.
      this.setAttribute("currentpageid", val.pageid);

      this._initCurrentPage();

      this._advanceFocusToPage(val);

      this._fireEvent(val, "pageshow");
    }

    get currentPage() {
      return this._currentPage;
    }

    set pageIndex(val) {
      if (val < 0 || val >= this.pageCount) {
        return;
      }

      var page = this.wizardPages[val];
      this._pageStack[this._pageStack.length - 1] = page;
      this.currentPage = page;
    }

    get pageIndex() {
      return this._currentPage ? this._currentPage.pageIndex : -1;
    }

    get onFirstPage() {
      return this._pageStack.length == 1;
    }

    get onLastPage() {
      var cp = this.currentPage;
      return (
        cp &&
        ((this._accessMethod == "sequential" &&
          cp.pageIndex == this.pageCount - 1) ||
          (this._accessMethod == "random" && !cp.next))
      );
    }

    getButton(aDlgType) {
      return this._wizardButtons.getButton(aDlgType);
    }

    getPageById(aPageId) {
      var els = this.getElementsByAttribute("pageid", aPageId);
      return els.item(0);
    }

    rewind() {
      if (!this.canRewind) {
        return;
      }

      if (this.currentPage && !this._fireEvent(this.currentPage, "pagehide")) {
        return;
      }

      if (
        this.currentPage &&
        !this._fireEvent(this.currentPage, "pagerewound")
      ) {
        return;
      }

      if (!this._fireEvent(this, "wizardback")) {
        return;
      }

      this._pageStack.pop();
      this.currentPage = this._pageStack[this._pageStack.length - 1];
      this.setAttribute("pagestep", this._pageStack.length);
    }

    advance(aPageId) {
      if (!this.canAdvance) {
        return;
      }

      if (this.currentPage && !this._fireEvent(this.currentPage, "pagehide")) {
        return;
      }

      if (
        this.currentPage &&
        !this._fireEvent(this.currentPage, "pageadvanced")
      ) {
        return;
      }

      if (this.onLastPage && !aPageId) {
        if (this._fireEvent(this, "wizardfinish")) {
          window.setTimeout(function () {
            window.close();
          }, 1);
        }
      } else {
        if (!this._fireEvent(this, "wizardnext")) {
          return;
        }

        let page;
        if (aPageId) {
          page = this.getPageById(aPageId);
        } else if (this.currentPage) {
          if (this._accessMethod == "random") {
            page = this.getPageById(this.currentPage.next);
          } else {
            page = this.wizardPages[this.currentPage.pageIndex + 1];
          }
        } else {
          page = this.wizardPages[0];
        }

        if (page) {
          this._pageStack.push(page);
          this.setAttribute("pagestep", this._pageStack.length);

          this.currentPage = page;
        }
      }
    }

    goTo(aPageId) {
      var page = this.getPageById(aPageId);
      if (page) {
        this._pageStack[this._pageStack.length - 1] = page;
        this.currentPage = page;
      }
    }

    cancel() {
      if (!this._fireEvent(this, "wizardcancel")) {
        return true;
      }

      window.close();
      window.setTimeout(function () {
        window.close();
      }, 1);
      return false;
    }

    _initCurrentPage() {
      this.canRewind = !this.onFirstPage;
      this.setAttribute("firstpage", String(this.onFirstPage));
      if (AppConstants.platform == "linux") {
        this.getButton("back").hidden = this.onFirstPage;
      }

      if (this.onLastPage) {
        this.canAdvance = true;
      }
      this.toggleAttribute("lastpage", this.onLastPage);

      this._adjustWizardHeader();
      this._wizardButtons.onPageChange();
    }

    _advanceFocusToPage() {
      if (!this._hasLoaded) {
        return;
      }

      // XXX: it'd be correct to advance focus into the panel, however we can't do
      // it until bug 1558990 is fixed, so moving the focus into a wizard itsef
      // as a workaround - it's same behavior but less optimal.
      document.commandDispatcher.advanceFocusIntoSubtree(this);

      // if advanceFocusIntoSubtree tries to focus one of our
      // dialog buttons, then remove it and put it on the root
      var focused = document.commandDispatcher.focusedElement;
      if (focused && focused.hasAttribute("dlgtype")) {
        this.focus();
      }
    }

    _registerPage(aPage) {
      aPage.pageIndex = this.pageCount;
      this.pageCount += 1;
      if (!this._accessMethod) {
        this._accessMethod = aPage.next ? "random" : "sequential";
      }
      if (!this._maybeStartWizard() && this._hasStarted) {
        // If the wizard has already started, adding a page might require
        // updating elements to reflect that (ex: changing the Finish button to
        // the Next button).
        this._initCurrentPage();
      }
    }

    _onWizardButtonsReady() {
      this._wizardButtonsReady = true;
      this._maybeStartWizard();
    }

    _maybeStartWizard() {
      if (
        !this._hasStarted &&
        this.isConnected &&
        this._wizardButtonsReady &&
        this.pageCount > 0
      ) {
        this._hasStarted = true;
        this.advance();
        return true;
      }
      return false;
    }

    _adjustWizardHeader() {
      let labelElement = this._wizardHeader.querySelector(
        ".wizard-header-label"
      );
      // First deal with fluent. Ideally, we'd stop supporting anything else,
      // but some comm-central consumers still use DTDs. (bug 1627049).
      // Removing the DTD support is bug 1627051.
      if (this.currentPage.hasAttribute("data-header-label-id")) {
        let id = this.currentPage.getAttribute("data-header-label-id");
        document.l10n.setAttributes(labelElement, id);
      } else {
        // Otherwise, make sure we remove any fluent IDs leftover:
        if (labelElement.hasAttribute("data-l10n-id")) {
          labelElement.removeAttribute("data-l10n-id");
        }
        // And use the label attribute or the default:
        var label = this.currentPage.getAttribute("label") || "";
        if (!label && this.onFirstPage && this._bundle) {
          if (AppConstants.platform == "macosx") {
            label = this._bundle.GetStringFromName("default-first-title-mac");
          } else {
            label = this._bundle.formatStringFromName("default-first-title", [
              this.title,
            ]);
          }
        } else if (!label && this.onLastPage && this._bundle) {
          if (AppConstants.platform == "macosx") {
            label = this._bundle.GetStringFromName("default-last-title-mac");
          } else {
            label = this._bundle.formatStringFromName("default-last-title", [
              this.title,
            ]);
          }
        }
        labelElement.textContent = label;
      }
      let headerDescEl = this._wizardHeader.querySelector(
        ".wizard-header-description"
      );
      if (headerDescEl) {
        headerDescEl.textContent = this.currentPage.getAttribute("description");
      }
    }

    _hitEnter(evt) {
      if (!evt.defaultPrevented) {
        this.advance();
      }
    }

    _fireEvent(aTarget, aType) {
      var event = document.createEvent("Events");
      event.initEvent(aType, true, true);

      // handle dom event handlers
      return aTarget.dispatchEvent(event);
    }
  }

  customElements.define("wizard", MozWizard);

  class MozWizardPage extends MozXULElement {
    constructor() {
      super();
      this.pageIndex = -1;
    }
    connectedCallback() {
      this.setAttribute("slot", "wizardpage");

      let wizard = this.closest("wizard");
      if (wizard) {
        wizard._registerPage(this);
      }
    }
    get pageid() {
      return this.getAttribute("pageid");
    }
    set pageid(val) {
      this.setAttribute("pageid", val);
    }
    get next() {
      return this.getAttribute("next");
    }
    set next(val) {
      this.setAttribute("next", val);
      this.parentNode._accessMethod = "random";
    }
  }

  customElements.define("wizardpage", MozWizardPage);

  class MozWizardButtons extends MozXULElement {
    connectedCallback() {
      this._wizard = this.getRootNode().host;

      this.textContent = "";
      this.appendChild(this.constructor.fragment);

      MozXULElement.insertFTLIfNeeded("toolkit/global/wizard.ftl");

      this._wizardButtonDeck = this.querySelector(".wizard-next-deck");

      this.initializeAttributeInheritance();

      const listeners = [
        ["back", () => this._wizard.rewind()],
        ["next", () => this._wizard.advance()],
        ["finish", () => this._wizard.advance()],
        ["cancel", () => this._wizard.cancel()],
      ];
      for (let [name, listener] of listeners) {
        this.getButton(name)?.addEventListener("command", listener);
      }

      this._wizard._onWizardButtonsReady();
    }

    static get inheritedAttributes() {
      return AppConstants.platform == "macosx"
        ? {
            "[dlgtype='next']": "hidden=lastpage",
          }
        : null;
    }

    static get markup() {
      if (AppConstants.platform == "macosx") {
        return `
        <vbox flex="1">
          <hbox class="wizard-buttons-btm">
            <button data-l10n-id="wizard-macos-button-cancel"
                    class="wizard-button" dlgtype="cancel"/>
            <spacer flex="1"/>
            <button data-l10n-id="wizard-macos-button-back"
                    class="wizard-button wizard-nav-button" dlgtype="back"/>
            <button data-l10n-id="wizard-macos-button-next"
                    class="wizard-button wizard-nav-button" dlgtype="next"
                    default="true" />
            <button data-l10n-id="wizard-macos-button-finish" class="wizard-button"
                    dlgtype="finish" default="true" />
          </hbox>
        </vbox>`;
      }

      let buttons =
        AppConstants.platform == "linux"
          ? `
      <button data-l10n-id="wizard-linux-button-cancel"
              class="wizard-button"
              dlgtype="cancel"/>
      <button data-l10n-id="wizard-linux-button-back"
              class="wizard-button" dlgtype="back"/>
      <deck class="wizard-next-deck">
        <button data-l10n-id="wizard-linux-button-finish"
                class="wizard-button"
                dlgtype="finish" default="true" />
        <button data-l10n-id="wizard-linux-button-next"
                class="wizard-button" dlgtype="next"
                default="true" />
      </deck>`
          : `
      <button data-l10n-id="wizard-win-button-back"
              class="wizard-button" dlgtype="back"/>
      <deck class="wizard-next-deck">
        <button data-l10n-id="wizard-win-button-finish"
                class="wizard-button"
                dlgtype="finish" default="true" />
        <button data-l10n-id="wizard-win-button-next"
                class="wizard-button" dlgtype="next"
                default="true" />
      </deck>
      <button data-l10n-id="wizard-win-button-cancel"
              class="wizard-button"
              dlgtype="cancel"/>`;

      return `
      <vbox class="wizard-buttons-box-1" flex="1">
        <separator class="wizard-buttons-separator groove"/>
        <hbox class="wizard-buttons-box-2">
          <spacer flex="1"/>
          ${buttons}
        </hbox>
      </vbox>`;
    }

    onPageChange() {
      if (AppConstants.platform == "macosx") {
        this.getButton("finish").hidden = !this.hasAttribute("lastpage");
      } else if (this.hasAttribute("lastpage")) {
        this._wizardButtonDeck.selectedIndex = 0;
      } else {
        this._wizardButtonDeck.selectedIndex = 1;
      }
    }

    getButton(type) {
      return this.querySelector(`[dlgtype="${type}"]`);
    }

    get defaultButton() {
      let button = this._wizardButtonDeck?.selectedPanel;
      if (
        button &&
        button.getAttribute("default") == "true" &&
        !button.hidden &&
        !button.disabled
      ) {
        return button;
      }
      return null;
    }
  }

  customElements.define("wizard-buttons", MozWizardButtons);
}
