/* 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 { PrivateBrowsingUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
);
const { EnableDelayHelper } = ChromeUtils.importESModule(
  "resource://gre/modules/PromptUtils.sys.mjs"
);
const { getMozRemoteImageURL } = ChromeUtils.importESModule(
  "moz-src:///toolkit/modules/FaviconUtils.sys.mjs"
);

class MozHandler extends window.MozElements.MozRichlistitem {
  static get markup() {
    return `
    <vbox pack="center">
      <html:img alt="" height="32" width="32" loading="lazy" />
    </vbox>
    <vbox flex="1">
      <label class="name"/>
      <label class="description"/>
    </vbox>
    `;
  }

  connectedCallback() {
    this.textContent = "";
    this.appendChild(this.constructor.fragment);
    this.initializeAttributeInheritance();
  }

  static get inheritedAttributes() {
    return {
      img: "src=image,disabled",
      ".name": "value=name,disabled",
      ".description": "value=description,disabled",
    };
  }

  get label() {
    return `${this.getAttribute("name")} ${this.getAttribute("description")}`;
  }
}

customElements.define("mozapps-handler", MozHandler, {
  extends: "richlistitem",
});

window.addEventListener("DOMContentLoaded", () => dialog.initialize(), {
  once: true,
});

let dialog = {
  /**
   * This function initializes the content of the dialog.
   */
  initialize() {
    let args = window.arguments[0].wrappedJSObject || window.arguments[0];
    let { handler, outArgs, usePrivateBrowsing, enableButtonDelay } = args;

    this._handlerInfo = handler.QueryInterface(Ci.nsIHandlerInfo);
    this._outArgs = outArgs;

    this.isPrivate =
      usePrivateBrowsing ||
      (window.opener && PrivateBrowsingUtils.isWindowPrivate(window.opener));

    this._dialog = document.querySelector("dialog");
    this._itemChoose = document.getElementById("item-choose");
    this._rememberCheck = document.getElementById("remember");

    let items = document.getElementById("items");
    items.addEventListener("dblclick", () => this.onDblClick());
    items.addEventListener("select", () => this.updateAcceptButton());
    document
      .getElementById("choose-app-btn")
      .addEventListener("command", () => this.chooseApplication());

    // Register event listener for the checkbox hint.
    this._rememberCheck.addEventListener("change", () => this.onCheck());

    document.addEventListener("dialogaccept", () => {
      this.onAccept();
    });

    // UI is ready, lets populate our list
    this.populateList();

    this.initL10n();

    if (enableButtonDelay) {
      this._delayHelper = new EnableDelayHelper({
        disableDialog: () => {
          this._acceptBtnDisabled = true;
          this.updateAcceptButton();
        },
        enableDialog: () => {
          this._acceptBtnDisabled = false;
          this.updateAcceptButton();
        },
        focusTarget: window,
      });
    }
  },

  initL10n() {
    let rememberLabel = document.getElementById("remember-label");
    document.l10n.setAttributes(rememberLabel, "chooser-dialog-remember", {
      scheme: this._handlerInfo.type,
    });

    let description = document.getElementById("description");
    document.l10n.setAttributes(description, "chooser-dialog-description", {
      scheme: this._handlerInfo.type,
    });
  },

  /**
   * Populates the list that a user can choose from.
   */
  populateList: function populateList() {
    var items = document.getElementById("items");
    var possibleHandlers = this._handlerInfo.possibleApplicationHandlers;
    var preferredHandler = this._handlerInfo.preferredApplicationHandler;
    for (let i = possibleHandlers.length - 1; i >= 0; --i) {
      let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp);
      let elm = document.createXULElement("richlistitem", {
        is: "mozapps-handler",
      });
      elm.setAttribute("name", app.name);
      elm.obj = app;

      // We defer loading the favicon so it doesn't delay load. The dialog is
      // opened in a SubDialog which will only show on window load.
      if (app instanceof Ci.nsIGIOHandlerApp) {
        elm.setAttribute("image", "moz-icon://" + app.id + "?size=32");
      } else if (app instanceof Ci.nsILocalHandlerApp) {
        // See if we have an nsILocalHandlerApp and set the icon
        let uri = Services.io.newFileURI(app.executable);
        elm.setAttribute("image", "moz-icon://" + uri.spec + "?size=32");
      } else if (app instanceof Ci.nsIWebHandlerApp) {
        let uri = Services.io.newURI(app.uriTemplate);
        if (/^https?$/.test(uri.scheme)) {
          // Unfortunately we can't use the favicon service to get the favicon,
          // because the service looks for a record with the exact URL we give
          // it, and users won't have such records for URLs they don't visit,
          // and users won't visit the handler's URL template, they'll only
          // visit URLs derived from that template (i.e. with %s in the template
          // replaced by the URL of the content being handled).
          elm.setAttribute(
            "image",
            getMozRemoteImageURL(uri.prePath + "/favicon.ico", { size: 32 })
          );
        }
        elm.setAttribute("description", uri.prePath);

        // Check for extensions needing private browsing access before
        // creating UI elements.
        if (this.isPrivate) {
          let policy = WebExtensionPolicy.getByURI(uri);
          if (policy && !policy.privateBrowsingAllowed) {
            elm.setAttribute("disabled", true);
            this.getPrivateBrowsingDisabledLabel().then(label => {
              elm.setAttribute("description", label);
            });
            if (app == preferredHandler) {
              preferredHandler = null;
            }
          }
        }
      } else if (app instanceof Ci.nsIDBusHandlerApp) {
        elm.setAttribute("description", app.method);
      } else if (!(app instanceof Ci.nsIGIOMimeApp)) {
        // We support GIO application handler, but no action required there
        throw new Error("unknown handler type");
      }

      items.insertBefore(elm, this._itemChoose);
      if (preferredHandler && app == preferredHandler) {
        this.selectedItem = elm;
      }
    }

    if (this._handlerInfo.hasDefaultHandler) {
      let elm = document.createXULElement("richlistitem", {
        is: "mozapps-handler",
      });
      elm.id = "os-default-handler";
      elm.setAttribute("name", this._handlerInfo.defaultDescription);

      items.insertBefore(elm, items.firstChild);
      if (
        this._handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault
      ) {
        this.selectedItem = elm;
      }
    }

    // Add gio handlers
    if (Cc["@mozilla.org/gio-service;1"]) {
      let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService(
        Ci.nsIGIOService
      );
      var gioApps = gIOSvc.getAppsForURIScheme(this._handlerInfo.type);
      for (let handler of gioApps.enumerate(Ci.nsIHandlerApp)) {
        // OS handler share the same name, it's most likely the same app, skipping...
        if (handler.name == this._handlerInfo.defaultDescription) {
          continue;
        }
        // Check if the handler is already in possibleHandlers
        let appAlreadyInHandlers = false;
        for (let i = possibleHandlers.length - 1; i >= 0; --i) {
          let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp);
          // nsGIOMimeApp::Equals is able to compare with nsILocalHandlerApp
          if (handler.equals(app)) {
            appAlreadyInHandlers = true;
            break;
          }
        }
        if (!appAlreadyInHandlers) {
          let elm = document.createXULElement("richlistitem", {
            is: "mozapps-handler",
          });
          elm.setAttribute("name", handler.name);
          elm.obj = handler;
          items.insertBefore(elm, this._itemChoose);
        }
      }
    }

    items.ensureSelectedElementIsVisible();
  },

  /**
   * Brings up a filepicker and allows a user to choose an application.
   */
  async chooseApplication() {
    let title = await this.getChooseAppWindowTitle();

    var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    fp.init(window.browsingContext, title, Ci.nsIFilePicker.modeOpen);
    fp.appendFilters(Ci.nsIFilePicker.filterApps);

    fp.open(rv => {
      if (rv == Ci.nsIFilePicker.returnOK && fp.file) {
        let uri = Services.io.newFileURI(fp.file);

        let handlerApp = Cc[
          "@mozilla.org/uriloader/local-handler-app;1"
        ].createInstance(Ci.nsILocalHandlerApp);
        handlerApp.executable = fp.file;

        // if this application is already in the list, select it and don't add it again
        let parent = document.getElementById("items");
        for (let i = 0; i < parent.childNodes.length; ++i) {
          let elm = parent.childNodes[i];
          if (
            elm.obj instanceof Ci.nsILocalHandlerApp &&
            elm.obj.equals(handlerApp)
          ) {
            parent.selectedItem = elm;
            parent.ensureSelectedElementIsVisible();
            return;
          }
        }

        let elm = document.createXULElement("richlistitem", {
          is: "mozapps-handler",
        });
        elm.setAttribute("name", fp.file.leafName);
        elm.setAttribute("image", "moz-icon://" + uri.spec + "?size=32");
        elm.obj = handlerApp;

        parent.selectedItem = parent.insertBefore(elm, parent.firstChild);
        parent.ensureSelectedElementIsVisible();
      }
    });
  },

  /**
   * Function called when the OK button is pressed.
   */
  onAccept() {
    this.updateHandlerData(this._rememberCheck.checked);
    this._outArgs.setProperty("openHandler", true);
  },

  /**
   * Determines if the accept button should be disabled or not
   */
  updateAcceptButton() {
    this._dialog.toggleAttribute(
      "buttondisabledaccept",
      this._acceptBtnDisabled || this._itemChoose.selected
    );
  },

  /**
   * Update the handler info to reflect the user choice.
   *
   * @param {boolean} skipAsk - Whether we should persist the application
   * choice and skip asking next time.
   */
  updateHandlerData(skipAsk) {
    // We need to make sure that the default is properly set now
    if (this.selectedItem.obj) {
      // default OS handler doesn't have this property
      this._outArgs.setProperty(
        "preferredAction",
        Ci.nsIHandlerInfo.useHelperApp
      );
      this._outArgs.setProperty(
        "preferredApplicationHandler",
        this.selectedItem.obj
      );
    } else {
      this._outArgs.setProperty(
        "preferredAction",
        Ci.nsIHandlerInfo.useSystemDefault
      );
    }
    this._outArgs.setProperty("alwaysAskBeforeHandling", !skipAsk);
  },

  /**
   * Updates the UI based on the checkbox being checked or not.
   */
  onCheck() {
    if (document.getElementById("remember").checked) {
      document.getElementById("remember-text").setAttribute("visible", "true");
    } else {
      document.getElementById("remember-text").removeAttribute("visible");
    }
  },

  /**
   * Function called when the user double clicks on an item of the list
   */
  onDblClick: function onDblClick() {
    if (this.selectedItem == this._itemChoose) {
      this.chooseApplication();
    } else {
      this._dialog.acceptDialog();
    }
  },

  // Getters / Setters

  /**
   * Returns/sets the selected element in the richlistbox
   */
  get selectedItem() {
    return document.getElementById("items").selectedItem;
  },
  set selectedItem(aItem) {
    document.getElementById("items").selectedItem = aItem;
  },

  /**
   *  Lazy l10n getter for the title of the app chooser window
   */
  async getChooseAppWindowTitle() {
    if (!this._chooseAppWindowTitle) {
      this._chooseAppWindowTitle = await document.l10n.formatValues([
        "choose-other-app-window-title",
      ]);
    }
    return this._chooseAppWindowTitle;
  },

  /**
   * Lazy l10n getter for handler menu items which are disabled due to private
   * browsing.
   */
  async getPrivateBrowsingDisabledLabel() {
    if (!this._privateBrowsingDisabledLabel) {
      this._privateBrowsingDisabledLabel = await document.l10n.formatValues([
        "choose-dialog-privatebrowsing-disabled",
      ]);
    }
    return this._privateBrowsingDisabledLabel;
  },
};
