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

// chat/content/imAccountOptionsHelper.js
/* globals accountOptionsHelper */

var { IMServices } = ChromeUtils.importESModule(
  "resource:///modules/IMServices.sys.mjs"
);
var { MailServices } = ChromeUtils.import(
  "resource:///modules/MailServices.jsm"
);
var { ChatIcons } = ChromeUtils.importESModule(
  "resource:///modules/chatIcons.sys.mjs"
);

var PREF_EXTENSIONS_GETMOREPROTOCOLSURL = "extensions.getMoreProtocolsURL";

var accountWizard = {
  onload() {
    document
      .querySelector("wizard")
      .addEventListener("wizardfinish", this.createAccount.bind(this));
    let accountProtocolPage = document.getElementById("accountprotocol");
    accountProtocolPage.addEventListener(
      "pageadvanced",
      this.selectProtocol.bind(this)
    );
    let accountUsernamePage = document.getElementById("accountusername");
    accountUsernamePage.addEventListener(
      "pageshow",
      this.showUsernamePage.bind(this)
    );
    accountUsernamePage.addEventListener(
      "pagehide",
      this.hideUsernamePage.bind(this)
    );
    let accountAdvancedPage = document.getElementById("accountadvanced");
    accountAdvancedPage.addEventListener(
      "pageshow",
      this.showAdvanced.bind(this)
    );
    let accountSummaryPage = document.getElementById("accountsummary");
    accountSummaryPage.addEventListener(
      "pageshow",
      this.showSummary.bind(this)
    );

    // Ensure the im core is initialized before we get a list of protocols.
    IMServices.core.init();

    accountWizard.setGetMoreProtocols();

    var protoList = document.getElementById("protolist");
    var protos = IMServices.core.getProtocols();
    protos.sort((a, b) => {
      if (a.name < b.name) {
        return -1;
      }
      return a.name > b.name ? 1 : 0;
    });
    protos.forEach(function (proto) {
      let image = document.createElement("img");
      image.setAttribute("src", ChatIcons.getProtocolIconURI(proto));
      image.setAttribute("alt", "");
      image.classList.add("protoIcon");

      let label = document.createXULElement("label");
      label.setAttribute("value", proto.name);

      let item = document.createXULElement("richlistitem");
      item.setAttribute("value", proto.id);
      item.appendChild(image);
      item.appendChild(label);
      protoList.appendChild(item);
    });

    // there is a strange selection bug without this timeout
    setTimeout(function () {
      protoList.selectedIndex = 0;
    }, 0);

    Services.obs.addObserver(this, "prpl-quit");
    window.addEventListener("unload", this.unload);
  },
  unload() {
    Services.obs.removeObserver(accountWizard, "prpl-quit");
  },
  observe(aObject, aTopic, aData) {
    if (aTopic == "prpl-quit") {
      // libpurple is being uninitialized. We can't create any new
      // account so keeping this wizard open would be pointless, close it.
      window.close();
    }
  },

  /**
   * Builds the full username from the username boxes.
   *
   * @returns {string} assembled username
   */
  getUsername() {
    let usernameBoxIndex = 0;
    if (this.proto.usernamePrefix) {
      usernameBoxIndex = 1;
    }
    // If the first username input is empty, make sure we return an empty
    // string so that it blocks the 'next' button of the wizard.
    if (!this.userNameBoxes[usernameBoxIndex].value) {
      return "";
    }

    return this.userNameBoxes.reduce((prev, elt) => prev + elt.value, "");
  },

  /**
   * Check that the username fields generate a new username, and if it is valid
   * allow advancing the wizard.
   */
  checkUsername() {
    var wizard = document.querySelector("wizard");
    var name = accountWizard.getUsername();
    var duplicateWarning = document.getElementById("duplicateAccount");
    if (!name) {
      wizard.canAdvance = false;
      duplicateWarning.hidden = true;
      return;
    }

    var exists = accountWizard.proto.accountExists(name);
    wizard.canAdvance = !exists;
    duplicateWarning.hidden = !exists;
  },

  /**
   * Takes the value of the primary username field and splits it if the value
   * matches the split field syntax.
   */
  splitUsername() {
    let usernameBoxIndex = 0;
    if (this.proto.usernamePrefix) {
      usernameBoxIndex = 1;
    }
    let username = this.userNameBoxes[usernameBoxIndex].value;
    let splitValues = this.proto.splitUsername(username);
    if (!splitValues.length) {
      return;
    }
    for (const box of this.userNameBoxes) {
      if (Element.isInstance(box)) {
        box.value = splitValues.shift();
      }
    }
    this.checkUsername();
  },

  selectProtocol() {
    var protoList = document.getElementById("protolist");
    var id = protoList.selectedItem.value;
    this.proto = IMServices.core.getProtocolById(id);
  },

  /**
   * Create a new input field for receiving a username.
   *
   * @param {string} aName - The id for the input.
   * @param {string} aLabel - The text for the username label.
   * @param {Element} grid - A container with a two column grid display to
   *   append the new elements to.
   * @param {string} [aDefaultValue] - The initial value for the username.
   *
   * @returns {HTMLInputElement} - The newly created username input.
   */
  insertUsernameField(aName, aLabel, grid, aDefaultValue) {
    var label = document.createXULElement("label");
    label.setAttribute("value", aLabel);
    label.setAttribute("control", aName);
    label.setAttribute("id", aName + "-label");
    label.classList.add("label-inline");
    grid.appendChild(label);

    var input = document.createElementNS(
      "http://www.w3.org/1999/xhtml",
      "input"
    );
    input.setAttribute("id", aName);
    input.classList.add("input-inline");
    if (aDefaultValue) {
      input.setAttribute("value", aDefaultValue);
    }
    input.addEventListener("input", event => {
      this.checkUsername();
    });
    // Only add the split logic to the first input field
    if (!this.userNameBoxes) {
      input.addEventListener("blur", event => {
        this.splitUsername();
      });
    }
    grid.appendChild(input);

    return input;
  },

  /**
   * Builds the username input boxes from the username split defined by the
   * protocol.
   */
  showUsernamePage() {
    var proto = this.proto.id;
    if ("userNameBoxes" in this && this.userNameProto == proto) {
      this.checkUsername();
      return;
    }

    var bundle = document.getElementById("accountsBundle");
    var usernameInfo;
    var emptyText = this.proto.usernameEmptyText;
    if (emptyText) {
      usernameInfo = bundle.getFormattedString(
        "accountUsernameInfoWithDescription",
        [emptyText, this.proto.name]
      );
    } else {
      usernameInfo = bundle.getFormattedString("accountUsernameInfo", [
        this.proto.name,
      ]);
    }
    document.getElementById("usernameInfo").textContent = usernameInfo;

    var grid = document.getElementById("userNameBox");
    // remove anything that may be there for another protocol
    while (grid.hasChildNodes()) {
      grid.lastChild.remove();
    }
    this.userNameBoxes = undefined;

    var splits = this.proto.getUsernameSplit();

    var label = bundle.getString("accountUsername");
    this.userNameBoxes = [this.insertUsernameField("name", label, grid)];
    this.userNameBoxes[0].emptyText = emptyText;
    let usernameBoxIndex = 0;

    if (this.proto.usernamePrefix) {
      this.userNameBoxes.unshift({ value: this.proto.usernamePrefix });
      usernameBoxIndex = 1;
    }

    for (let i = 0; i < splits.length; ++i) {
      this.userNameBoxes.push({ value: splits[i].separator });
      label = bundle.getFormattedString("accountColon", [splits[i].label]);
      let defaultVal = splits[i].defaultValue;
      this.userNameBoxes.push(
        this.insertUsernameField("username-split-" + i, label, grid, defaultVal)
      );
    }
    this.userNameBoxes[usernameBoxIndex].focus();
    this.userNameProto = proto;
    this.checkUsername();
  },

  hideUsernamePage() {
    document.querySelector("wizard").canAdvance = true;
    var next = "account" + (this.proto.noPassword ? "advanced" : "password");
    document.getElementById("accountusername").next = next;
  },

  showAdvanced() {
    // ensure we don't destroy user data if it's not necessary
    var id = this.proto.id;
    if ("protoSpecOptId" in this && this.protoSpecOptId == id) {
      return;
    }
    this.protoSpecOptId = id;

    this.populateProtoSpecificBox();

    // Make sure the protocol specific options and wizard buttons are visible.
    let wizard = document.querySelector("wizard");
    if (wizard.scrollHeight > window.innerHeight) {
      window.resizeBy(0, wizard.scrollHeight - window.innerHeight);
    }

    let alias = document.getElementById("alias");
    alias.focus();
  },

  populateProtoSpecificBox() {
    let haveOptions = accountOptionsHelper.addOptions(
      this.proto.id + "-",
      this.proto.getOptions()
    );
    document.getElementById("protoSpecificGroupbox").hidden = !haveOptions;
    if (haveOptions) {
      var bundle = document.getElementById("accountsBundle");
      document.getElementById("protoSpecificCaption").textContent =
        bundle.getFormattedString("protoOptions", [this.proto.name]);
    }
  },

  /**
   * Create new summary field and value elements.
   *
   * @param {string} aLabel - The name of the field being summarised.
   * @param {string} aValue - The value of the field being summarised.
   * @param {Element} grid - A container with a two column grid display to
   *   append the new elements to.
   */
  createSummaryRow(aLabel, aValue, grid) {
    var label = document.createXULElement("label");
    label.classList.add("header", "label-inline");
    if (aLabel.length > 20) {
      aLabel = aLabel.substring(0, 20);
      aLabel += "…";
    }

    label.setAttribute("value", aLabel);
    grid.appendChild(label);

    var input = document.createElementNS(
      "http://www.w3.org/1999/xhtml",
      "input"
    );
    input.setAttribute("value", aValue);
    input.classList.add("plain", "input-inline");
    input.setAttribute("readonly", true);
    grid.appendChild(input);
  },

  showSummary() {
    var rows = document.getElementById("summaryRows");
    var bundle = document.getElementById("accountsBundle");
    while (rows.hasChildNodes()) {
      rows.lastChild.remove();
    }

    var label = document.getElementById("protoLabel").value;
    this.createSummaryRow(label, this.proto.name, rows);
    this.username = this.getUsername();
    label = bundle.getString("accountUsername");
    this.createSummaryRow(label, this.username, rows);
    if (!this.proto.noPassword) {
      this.password = this.getValue("password");
      if (this.password) {
        label = document.getElementById("passwordLabel").value;
        var pass = "";
        for (let i = 0; i < this.password.length; ++i) {
          pass += "*";
        }
        this.createSummaryRow(label, pass, rows);
      }
    }
    this.alias = this.getValue("alias");
    if (this.alias) {
      label = document.getElementById("aliasLabel").value;
      this.createSummaryRow(label, this.alias, rows);
    }

    var id = this.proto.id;
    this.prefs = [];
    for (let opt of this.proto.getOptions()) {
      let name = opt.name;
      let eltName = id + "-" + name;
      let val = this.getValue(eltName);
      // The value will be undefined if the proto specific groupbox has never been opened
      if (val === undefined) {
        continue;
      }
      switch (opt.type) {
        case Ci.prplIPref.typeBool:
          if (val != opt.getBool()) {
            this.prefs.push({ opt, name, value: !!val });
          }
          break;
        case Ci.prplIPref.typeInt:
          if (val != opt.getInt()) {
            this.prefs.push({ opt, name, value: val });
          }
          break;
        case Ci.prplIPref.typeString:
          if (val != opt.getString()) {
            this.prefs.push({ opt, name, value: val });
          }
          break;
        case Ci.prplIPref.typeList:
          if (val != opt.getListDefault()) {
            this.prefs.push({ opt, name, value: val });
          }
          break;
        default:
          throw new Error("unknown preference type " + opt.type);
      }
    }

    for (let i = 0; i < this.prefs.length; ++i) {
      let opt = this.prefs[i];
      let label = bundle.getFormattedString("accountColon", [opt.opt.label]);
      this.createSummaryRow(label, opt.value, rows);
    }
  },

  createAccount() {
    var acc = IMServices.accounts.createAccount(this.username, this.proto.id);
    if (!this.proto.noPassword && this.password) {
      acc.password = this.password;
    }
    if (this.alias) {
      acc.alias = this.alias;
    }

    for (let i = 0; i < this.prefs.length; ++i) {
      let option = this.prefs[i];
      let opt = option.opt;
      switch (opt.type) {
        case Ci.prplIPref.typeBool:
          acc.setBool(option.name, option.value);
          break;
        case Ci.prplIPref.typeInt:
          acc.setInt(option.name, option.value);
          break;
        case Ci.prplIPref.typeString:
        case Ci.prplIPref.typeList:
          acc.setString(option.name, option.value);
          break;
        default:
          throw new Error("unknown type");
      }
    }
    var autologin = this.getValue("connectNow");
    acc.autoLogin = autologin;

    acc.save();

    try {
      if (autologin) {
        acc.connect();
      }
    } catch (e) {
      // If the connection fails (for example if we are currently in
      // offline mode), we still want to close the account wizard
    }

    if (window.opener) {
      var am = window.opener.gAccountManager;
      if (am) {
        am.selectAccount(acc.id);
      }
    }

    var inServer = MailServices.accounts.createIncomingServer(
      this.username,
      this.proto.id, // hostname
      "im"
    );
    inServer.wrappedJSObject.imAccount = acc;

    var account = MailServices.accounts.createAccount();
    // Avoid new folder notifications.
    inServer.valid = false;
    account.incomingServer = inServer;
    inServer.valid = true;
    MailServices.accounts.notifyServerLoaded(inServer);

    return true;
  },

  getValue(aId) {
    var elt = document.getElementById(aId);
    if ("selectedItem" in elt) {
      return elt.selectedItem.value;
    }
    // Strangely various input types also have a "checked" property defined,
    // so we check for the expected elements explicitly.
    if (
      ((elt.localName == "input" && elt.getAttribute("type") == "checkbox") ||
        elt.localName == "checkbox") &&
      "checked" in elt
    ) {
      return elt.checked;
    }
    if ("value" in elt) {
      return elt.value;
    }
    // If the groupbox has never been opened, the binding isn't attached
    // so the attributes don't exist. The calling code in showSummary
    // has a special handling of the undefined value for this case.
    return undefined;
  },

  *getIter(aEnumerator) {
    for (let iter of aEnumerator) {
      yield iter;
    }
  },

  /* Check for correctness and set URL for the "Get more protocols..."-link
   *  Stripped down code from preferences/themes.js
   */
  setGetMoreProtocols() {
    let prefURL = PREF_EXTENSIONS_GETMOREPROTOCOLSURL;
    var getMore = document.getElementById("getMoreProtocols");
    var showGetMore = false;
    const nsIPrefBranch = Ci.nsIPrefBranch;

    if (Services.prefs.getPrefType(prefURL) != nsIPrefBranch.PREF_INVALID) {
      try {
        var getMoreURL = Services.urlFormatter.formatURLPref(prefURL);
        getMore.setAttribute("getMoreURL", getMoreURL);
        showGetMore = getMoreURL != "about:blank";
      } catch (e) {}
    }
    getMore.hidden = !showGetMore;
  },

  openURL(aURL) {
    Cc["@mozilla.org/uriloader/external-protocol-service;1"]
      .getService(Ci.nsIExternalProtocolService)
      .loadURI(Services.io.newURI(aURL));
  },
};

window.addEventListener("load", event => {
  accountWizard.onload();
});
