/*
 * 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 https://mozilla.org/MPL/2.0/.
 */

"use strict";

/* global GetEnigmailSvc, EnigRevokeKey */

var { MailServices } = ChromeUtils.import(
  "resource:///modules/MailServices.jsm"
);

var { EnigmailCore } = ChromeUtils.import(
  "chrome://openpgp/content/modules/core.jsm"
);
var { EnigmailStreams } = ChromeUtils.import(
  "chrome://openpgp/content/modules/streams.jsm"
);
var { EnigmailFuncs } = ChromeUtils.import(
  "chrome://openpgp/content/modules/funcs.jsm"
);
var { EnigmailWindows } = ChromeUtils.import(
  "chrome://openpgp/content/modules/windows.jsm"
);
var { EnigmailKeyServer } = ChromeUtils.import(
  "chrome://openpgp/content/modules/keyserver.jsm"
);
var { EnigmailWks } = ChromeUtils.import(
  "chrome://openpgp/content/modules/webKey.jsm"
);
var { EnigmailCryptoAPI } = ChromeUtils.import(
  "chrome://openpgp/content/modules/cryptoAPI.jsm"
);
var { KeyLookupHelper } = ChromeUtils.import(
  "chrome://openpgp/content/modules/keyLookupHelper.jsm"
);
var { EnigmailTrust } = ChromeUtils.import(
  "chrome://openpgp/content/modules/trust.jsm"
);
var { PgpSqliteDb2 } = ChromeUtils.import(
  "chrome://openpgp/content/modules/sqliteDb.jsm"
);
var { EnigmailLog } = ChromeUtils.import(
  "chrome://openpgp/content/modules/log.jsm"
);
var { EnigmailKeyRing } = ChromeUtils.import(
  "chrome://openpgp/content/modules/keyRing.jsm"
);
var { EnigmailKey } = ChromeUtils.import(
  "chrome://openpgp/content/modules/key.jsm"
);
var { EnigmailConstants } = ChromeUtils.import(
  "chrome://openpgp/content/modules/constants.jsm"
);
var { EnigmailDialog } = ChromeUtils.import(
  "chrome://openpgp/content/modules/dialog.jsm"
);
var { EnigmailKeyserverURIs } = ChromeUtils.import(
  "chrome://openpgp/content/modules/keyserverUris.jsm"
);

const ENIG_KEY_EXPIRED = "e";
const ENIG_KEY_REVOKED = "r";
const ENIG_KEY_INVALID = "i";
const ENIG_KEY_DISABLED = "d";
const ENIG_KEY_NOT_VALID =
  ENIG_KEY_EXPIRED + ENIG_KEY_REVOKED + ENIG_KEY_INVALID + ENIG_KEY_DISABLED;

var l10n = new Localization(["messenger/openpgp/openpgp.ftl"], true);

const INPUT = 0;
const RESULT = 1;

var gUserList;
var gKeyList;
var gEnigLastSelectedKeys = null;
var gKeySortList = null;
var gSearchInput = null;
var gTreeChildren = null;
var gShowInvalidKeys = null;
var gShowOthersKeys = null;
var gTimeoutId = null;

function enigmailKeyManagerLoad() {
  EnigmailLog.DEBUG("enigmailKeyManager.js: enigmailKeyManagerLoad\n");

  // Close the key manager if GnuPG is not available
  if (!EnigmailCore.getService(window)) {
    window.close();
    return;
  }

  gUserList = document.getElementById("pgpKeyList");
  gSearchInput = document.getElementById("filterKey");
  gShowInvalidKeys = document.getElementById("showInvalidKeys");
  gShowOthersKeys = document.getElementById("showOthersKeys");

  window.addEventListener("reload-keycache", reloadKeys);
  gSearchInput.addEventListener("keydown", event => {
    switch (event.key) {
      case "Escape":
        event.target.value = "";
      // fall through
      case "Enter":
        if (gTimeoutId) {
          clearTimeout(gTimeoutId);
          gTimeoutId = null;
        }
        gKeyListView.applyFilter(0);
        event.preventDefault();
        break;
      default:
        gTimeoutId = setTimeout(() => {
          gKeyListView.applyFilter(0);
        }, 200);
        break;
    }
  });

  gUserList.addEventListener("click", onListClick, true);
  document.getElementById("statusText").value = l10n.formatValueSync(
    "key-man-loading-keys"
  );
  document.getElementById("progressBar").style.visibility = "visible";
  setTimeout(loadkeyList, 100);

  gUserList.view = gKeyListView;
  gSearchInput.focus();

  // Dialog event listeners.
  document.addEventListener("dialogaccept", onDialogAccept);
  document.addEventListener("dialogcancel", onDialogClose);
}

function onDialogAccept() {
  if (window.arguments[0].okCallback) {
    window.arguments[0].okCallback();
  }
  window.close();
}

function onDialogClose() {
  if (window.arguments[0].cancelCallback) {
    window.arguments[0].cancelCallback();
  }
  window.close();
}

function loadkeyList() {
  EnigmailLog.DEBUG("enigmailKeyManager.js: loadkeyList\n");

  sortTree();
  gKeyListView.applyFilter(0);
  document.getElementById("pleaseWait").hidePopup();
  document.getElementById("statusText").value = "";
  document.getElementById("progressBar").style.visibility = "collapse";
}

function clearKeyCache() {
  EnigmailKeyRing.clearCache();
  refreshKeys();
}

function refreshKeys() {
  EnigmailLog.DEBUG("enigmailKeyManager.js: refreshKeys\n");
  var keyList = getSelectedKeys();
  gEnigLastSelectedKeys = [];
  for (var i = 0; i < keyList.length; i++) {
    gEnigLastSelectedKeys[keyList[i]] = 1;
  }

  buildKeyList(true);
}

function reloadKeys() {
  let i = 0;
  let c = Components.stack;

  while (c) {
    if (c.name == "reloadKeys") {
      i++;
    }
    c = c.caller;
  }

  // detect recursion and don't continue if too much recursion
  // this can happen if the key list is empty
  if (i < 4) {
    buildKeyList(true);
  }
}

function buildKeyList(refresh) {
  EnigmailLog.DEBUG("enigmailKeyManager.js: buildKeyList\n");

  var keyListObj = {};

  if (refresh) {
    EnigmailKeyRing.clearCache();
  }

  keyListObj = EnigmailKeyRing.getAllKeys(
    window,
    getSortColumn(),
    getSortDirection()
  );

  if (!keyListObj.keySortList) {
    return;
  }

  gKeyList = keyListObj.keyList;
  gKeySortList = keyListObj.keySortList;

  gKeyListView.keysRefreshed();
}

function getSelectedKeys() {
  let selList = [];
  let rangeCount = gUserList.view.selection.getRangeCount();
  for (let i = 0; i < rangeCount; i++) {
    let start = {};
    let end = {};
    gUserList.view.selection.getRangeAt(i, start, end);
    for (let c = start.value; c <= end.value; c++) {
      try {
        //selList.push(gUserList.view.getItemAtIndex(c).getAttribute("keyNum"));
        selList.push(gKeyListView.getFilteredRow(c).keyNum);
      } catch (ex) {
        return [];
      }
    }
  }
  return selList;
}

function getSelectedKeyIds() {
  let keyList = getSelectedKeys();

  let a = [];
  for (let i in keyList) {
    a.push(gKeyList[keyList[i]].keyId);
  }

  return a;
}

function enigmailKeyMenu() {
  var keyList = getSelectedKeys();

  let haveSecretForAll;
  if (keyList.length == 0) {
    haveSecretForAll = false;
  } else {
    haveSecretForAll = true;
    for (let key of keyList) {
      if (!gKeyList[key].secretAvailable) {
        haveSecretForAll = false;
        break;
      }
    }
  }

  let singleSecretSelected = keyList.length == 1 && haveSecretForAll;

  // Make the selected key count available to translations.
  for (let el of document.querySelectorAll(".enigmail-bulk-key-operation")) {
    el.setAttribute(
      "data-l10n-args",
      JSON.stringify({ count: keyList.length })
    );
  }

  document.getElementById("backupSecretKey").disabled = !haveSecretForAll;
  document.getElementById("uploadToServer").disabled = !singleSecretSelected;

  document.getElementById("revokeKey").disabled =
    keyList.length != 1 || !gKeyList[keyList[0]].secretAvailable;
  document.getElementById("ctxRevokeKey").hidden =
    keyList.length != 1 || !gKeyList[keyList[0]].secretAvailable;

  document.getElementById("importFromClipbrd").disabled =
    !Services.clipboard.hasDataMatchingFlavors(
      ["text/plain"],
      Ci.nsIClipboard.kGlobalClipboard
    );

  for (let item of document.querySelectorAll(
    ".requires-single-key-selection"
  )) {
    item.disabled = keyList.length != 1;
  }

  for (let item of document.querySelectorAll(".requires-key-selection")) {
    item.disabled = keyList.length == 0;
  }

  // Disable the "Generate key" menu item if no mail account is available.
  document
    .getElementById("genKey")
    .setAttribute("disabled", MailServices.accounts.defaultAccount == null);

  // Disable the context menu if no keys are selected.
  return keyList.length > 0;
}

function onListClick(event) {
  if (event.detail > 2) {
    return;
  }

  if (event.type === "click") {
    // Mouse event
    let { col } = gUserList.getCellAt(event.clientX, event.clientY);

    if (!col) {
      // not clicked on a valid column (e.g. scrollbar)
      return;
    }
  }

  if (event.detail != 2) {
    return;
  }

  // do not propagate double clicks
  event.stopPropagation();
  enigmailKeyDetails();
}

function enigmailSelectAllKeys() {
  gUserList.view.selection.selectAll();
}

/**
 * Open the Key Properties subdialog.
 *
 * @param {string|null} keyId - Optional ID of the selected OpenPGP Key.
 */
function enigmailKeyDetails(keyId = null) {
  if (!keyId) {
    let keyList = getSelectedKeys();
    // Interrupt if we don't have a single selected key nor a key was passed.
    if (keyList.length != 1) {
      return;
    }
    keyId = gKeyList[keyList[0]].keyId;
  }

  if (EnigmailWindows.openKeyDetails(window, keyId, false)) {
    refreshKeys();
  }
}

async function enigmailDeleteKey() {
  var keyList = getSelectedKeys();
  var deleteSecret = false;

  if (keyList.length == 1) {
    // one key selected
    var userId = gKeyList[keyList[0]].userId;
    if (gKeyList[keyList[0]].secretAvailable) {
      if (
        !EnigmailDialog.confirmDlg(
          window,
          l10n.formatValueSync("delete-secret-key", {
            userId,
          }),
          l10n.formatValueSync("dlg-button-delete")
        )
      ) {
        return;
      }
      deleteSecret = true;
    } else if (
      !EnigmailDialog.confirmDlg(
        window,
        l10n.formatValueSync("delete-pub-key", {
          userId,
        }),
        l10n.formatValueSync("dlg-button-delete")
      )
    ) {
      return;
    }
  } else {
    // several keys selected
    for (var i = 0; i < keyList.length; i++) {
      if (gKeyList[keyList[i]].secretAvailable) {
        deleteSecret = true;
      }
    }

    if (deleteSecret) {
      if (
        !EnigmailDialog.confirmDlg(
          window,
          l10n.formatValueSync("delete-mix"),
          l10n.formatValueSync("dlg-button-delete")
        )
      ) {
        return;
      }
    } else if (
      !EnigmailDialog.confirmDlg(
        window,
        l10n.formatValueSync("delete-selected-pub-key"),
        l10n.formatValueSync("dlg-button-delete")
      )
    ) {
      return;
    }
  }

  const cApi = EnigmailCryptoAPI();
  for (let j in keyList) {
    let fpr = gKeyList[keyList[j]].fpr;
    await cApi.deleteKey(fpr, deleteSecret);
    await PgpSqliteDb2.deleteAcceptance(fpr);
  }
  clearKeyCache();
  gUserList.view.selection.clearSelection();
}

async function enigCreateKeyMsg() {
  var keyList = getSelectedKeyIds();
  var tmpFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
  tmpFile.append("key.asc");
  tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);

  // save file
  var exitCodeObj = {};
  var errorMsgObj = {};

  var keyIdArray = [];
  for (let id of keyList) {
    keyIdArray.push("0x" + id);
  }

  await EnigmailKeyRing.extractPublicKeys(
    keyIdArray, // full
    null,
    null,
    tmpFile,
    exitCodeObj,
    errorMsgObj
  );
  if (exitCodeObj.value !== 0) {
    EnigmailDialog.alert(window, errorMsgObj.value);
    return;
  }

  // create attachment
  var ioServ = Services.io;
  var tmpFileURI = ioServ.newFileURI(tmpFile);
  var keyAttachment = Cc[
    "@mozilla.org/messengercompose/attachment;1"
  ].createInstance(Ci.nsIMsgAttachment);
  keyAttachment.url = tmpFileURI.spec;
  if (keyList.length == 1) {
    keyAttachment.name = "0x" + keyList[0] + ".asc";
  } else {
    keyAttachment.name = "pgpkeys.asc";
  }
  keyAttachment.temporary = true;
  keyAttachment.contentType = "application/pgp-keys";

  // create Msg
  var msgCompFields = Cc[
    "@mozilla.org/messengercompose/composefields;1"
  ].createInstance(Ci.nsIMsgCompFields);
  msgCompFields.addAttachment(keyAttachment);

  var msgCompSvc = Cc["@mozilla.org/messengercompose;1"].getService(
    Ci.nsIMsgComposeService
  );

  var msgCompParam = Cc[
    "@mozilla.org/messengercompose/composeparams;1"
  ].createInstance(Ci.nsIMsgComposeParams);
  msgCompParam.composeFields = msgCompFields;
  msgCompParam.identity = EnigmailFuncs.getDefaultIdentity();
  msgCompParam.type = Ci.nsIMsgCompType.New;
  msgCompParam.format = Ci.nsIMsgCompFormat.Default;
  msgCompParam.originalMsgURI = "";
  msgCompSvc.OpenComposeWindowWithParams("", msgCompParam);
}

async function enigmailRevokeKey() {
  var keyList = getSelectedKeys();
  let keyInfo = gKeyList[keyList[0]];
  EnigRevokeKey(keyInfo, function (success) {
    if (success) {
      refreshKeys();
    }
  });
}

async function enigmailExportKeys(which) {
  let exportSecretKey = which == "secret";
  var keyList = getSelectedKeys();
  var defaultFileName;

  if (keyList.length == 1) {
    let extension = exportSecretKey ? "secret.asc" : "public.asc";
    defaultFileName = gKeyList[keyList[0]].userId.replace(/[<>]/g, "");
    defaultFileName =
      defaultFileName +
      "-" +
      `(0x${gKeyList[keyList[0]].keyId})` +
      "-" +
      extension;
  } else {
    let id = exportSecretKey
      ? "default-pub-sec-key-filename"
      : "default-pub-key-filename";
    defaultFileName = l10n.formatValueSync(id) + ".asc";
  }

  if (exportSecretKey) {
    var fprArray = [];
    for (let id of keyList) {
      fprArray.push(gKeyList[id].fpr);
    }
    EnigmailKeyRing.backupSecretKeysInteractive(
      window,
      defaultFileName,
      fprArray
    );
  } else {
    let keyList2 = getSelectedKeyIds();
    var keyIdArray = [];
    for (let id of keyList2) {
      keyIdArray.push("0x" + id);
    }
    await EnigmailKeyRing.exportPublicKeysInteractive(
      window,
      defaultFileName,
      keyIdArray
    );
  }
}

async function enigmailImportFromClipbrd() {
  if (
    !EnigmailDialog.confirmDlg(
      window,
      l10n.formatValueSync("import-from-clip"),
      l10n.formatValueSync("key-man-button-import")
    )
  ) {
    return;
  }

  let cBoardContent = await navigator.clipboard.readText();
  var errorMsgObj = {};
  var preview = await EnigmailKey.getKeyListFromKeyBlock(
    cBoardContent,
    errorMsgObj,
    true,
    true,
    false
  );
  // should we allow importing secret keys?
  if (preview && preview.length > 0) {
    let confirmImport = false;
    let outParam = {};
    confirmImport = EnigmailDialog.confirmPubkeyImport(
      window,
      preview,
      outParam
    );
    if (confirmImport) {
      // import
      EnigmailKeyRing.importKey(
        window,
        false,
        cBoardContent,
        false,
        "",
        errorMsgObj,
        null,
        false,
        [],
        true,
        outParam.acceptance
      );
      var keyList = preview.map(function (a) {
        return a.id;
      });
      EnigmailDialog.keyImportDlg(window, keyList);
      refreshKeys();
    }
  } else {
    document.l10n.formatValue("preview-failed").then(value => {
      EnigmailDialog.alert(window, value);
    });
  }
}

/**
 * Places the fingerprint of each selected key onto the keyboard.
 */
async function copyOpenPGPFingerPrints() {
  let fprs = getSelectedKeys()
    .map(idx => gKeyList[idx].fpr)
    .join("\n");
  return navigator.clipboard.writeText(fprs);
}

/**
 * Places the key id of each key selected onto the clipboard.
 */
async function copyOpenPGPKeyIds() {
  let ids = getSelectedKeyIds();
  return navigator.clipboard.writeText(ids.map(id => `0x${id}`).join("\n"));
}

async function enigmailCopyToClipbrd() {
  var keyList = getSelectedKeyIds();
  if (keyList.length === 0) {
    document.l10n.formatValue("no-key-selected").then(value => {
      EnigmailDialog.info(window, value);
    });
    return;
  }
  var exitCodeObj = {};
  var errorMsgObj = {};

  var keyIdArray = [];
  for (let id of keyList) {
    keyIdArray.push("0x" + id);
  }

  let keyData = await EnigmailKeyRing.extractPublicKeys(
    keyIdArray, // full
    null,
    null,
    null,
    exitCodeObj,
    errorMsgObj
  );
  if (exitCodeObj.value !== 0) {
    l10n.formatValue("copy-to-clipbrd-failed").then(value => {
      EnigmailDialog.alert(window, value);
    });
    return;
  }
  navigator.clipboard
    .writeText(keyData)
    .then(() => {
      l10n.formatValue("copy-to-clipbrd-ok").then(value => {
        EnigmailDialog.info(window, value);
      });
    })
    .catch(err => {
      l10n.formatValue("copy-to-clipbrd-failed").then(value => {
        EnigmailDialog.alert(window, value);
      });
    });
}

async function enigmailSearchKey() {
  var result = {
    value: "",
  };
  if (
    !Services.prompt.prompt(
      window,
      l10n.formatValueSync("enig-prompt"),
      l10n.formatValueSync("openpgp-key-man-discover-prompt"),
      result,
      "",
      {}
    )
  ) {
    return;
  }

  result.value = result.value.trim();

  let imported = false;
  if (EnigmailFuncs.stringLooksLikeEmailAddress(result.value)) {
    imported = await KeyLookupHelper.lookupAndImportByEmail(
      "interactive-import",
      window,
      result.value,
      true
    );
  } else {
    imported = await KeyLookupHelper.lookupAndImportByKeyID(
      "interactive-import",
      window,
      result.value,
      true
    );
  }

  if (imported) {
    refreshKeys();
  }
}

async function enigmailUploadKey() {
  // Always upload to the first configured keyserver with a supported protocol.
  let selKeyList = getSelectedKeys();
  if (selKeyList.length != 1) {
    return;
  }

  let keyId = gKeyList[selKeyList[0]].keyId;
  let ks = EnigmailKeyserverURIs.getUploadKeyServer();

  let ok = await EnigmailKeyServer.upload(keyId, ks);
  document.l10n
    .formatValue(ok ? "openpgp-key-publish-ok" : "openpgp-key-publish-fail", {
      keyserver: ks,
    })
    .then(value => {
      EnigmailDialog.alert(window, value);
    });
}

/*
function enigmailUploadToWkd() {
  let selKeyList = getSelectedKeys();
  let keyList = [];
  for (let i = 0; i < selKeyList.length; i++) {
    keyList.push(gKeyList[selKeyList[i]]);
  }

  EnigmailWks.wksUpload(keyList, window)
    .then(result => {
      if (result.length > 0) {
        EnigmailDialog.info(window, "Key(s) sent successfully");
      } else if (keyList.length === 1) {
        EnigmailDialog.alert(
          window,
          "Sending of keys failed" +
            "\n\n" +
            "The key %S does not have a WKS identity.".replace("%S", keyList[0].userId)
        );
      } else {
        EnigmailDialog.alert(
          window,
          "The upload was not successful - your provider does not seem to support WKS."
        );
      }
    })
    .catch(error => {
      EnigmailDialog.alert(
        "Sending of keys failed" + "\n" + error
      );
    });
}
*/

function enigmailImportKeysFromUrl() {
  var result = {
    value: "",
  };
  if (
    !Services.prompt.prompt(
      window,
      l10n.formatValueSync("enig-prompt"),
      l10n.formatValueSync("import-from-url"),
      result,
      "",
      {}
    )
  ) {
    return;
  }
  var p = new Promise(function (resolve, reject) {
    var cbFunc = async function (data) {
      EnigmailLog.DEBUG("enigmailImportKeysFromUrl: _cbFunc()\n");
      var errorMsgObj = {};

      var preview = await EnigmailKey.getKeyListFromKeyBlock(
        data,
        errorMsgObj,
        true,
        true,
        false
      );
      // should we allow importing secret keys?
      if (preview && preview.length > 0) {
        let confirmImport = false;
        let outParam = {};
        confirmImport = EnigmailDialog.confirmPubkeyImport(
          window,
          preview,
          outParam
        );
        if (confirmImport) {
          EnigmailKeyRing.importKey(
            window,
            false,
            data,
            false,
            "",
            errorMsgObj,
            null,
            false,
            [],
            true,
            outParam.acceptance
          );
          errorMsgObj.preview = preview;
          resolve(errorMsgObj);
        }
      } else {
        EnigmailDialog.alert(
          window,
          await document.l10n.formatValue("preview-failed")
        );
      }
    };

    try {
      var bufferListener = EnigmailStreams.newStringStreamListener(cbFunc);
      var msgUri = Services.io.newURI(result.value.trim());

      var channel = EnigmailStreams.createChannel(msgUri);
      channel.asyncOpen(bufferListener, msgUri);
    } catch (ex) {
      var err = {
        value: ex,
      };
      reject(err);
    }
  });

  p.then(function (errorMsgObj) {
    var keyList = errorMsgObj.preview.map(function (a) {
      return a.id;
    });
    EnigmailDialog.keyImportDlg(window, keyList);
    refreshKeys();
  }).catch(async function (reason) {
    EnigmailDialog.alert(
      window,
      await document.l10n.formatValue("general-error", {
        reason: reason.value,
      })
    );
  });
}

function initiateAcKeyTransfer() {
  EnigmailWindows.inititateAcSetupMessage();
}

//
// ----- key filtering functionality  -----
//

function determineHiddenKeys(keyObj, showInvalidKeys, showOthersKeys) {
  var show = true;

  const INVALID_KEYS = "ierdD";

  if (
    !showInvalidKeys &&
    INVALID_KEYS.includes(EnigmailTrust.getTrustCode(keyObj))
  ) {
    show = false;
  }

  if (!showOthersKeys && !keyObj.secretAvailable) {
    show = false;
  }

  return show;
}

function getSortDirection() {
  return gUserList.getAttribute("sortDirection") == "ascending" ? 1 : -1;
}

function sortTree(column) {
  var columnName;
  var order = getSortDirection();

  //if the column is passed and it's already sorted by that column, reverse sort
  if (column) {
    columnName = column.id;
    if (gUserList.getAttribute("sortResource") == columnName) {
      order *= -1;
    } else {
      document
        .getElementById(gUserList.getAttribute("sortResource"))
        .removeAttribute("sortDirection");
      order = 1;
    }
  } else {
    columnName = gUserList.getAttribute("sortResource");
  }
  gUserList.setAttribute(
    "sortDirection",
    order == 1 ? "ascending" : "descending"
  );
  let col = document.getElementById(columnName);
  if (col) {
    col.setAttribute("sortDirection", order == 1 ? "ascending" : "descending");
    gUserList.setAttribute("sortResource", columnName);
  } else {
    gUserList.setAttribute("sortResource", "enigUserNameCol");
  }
  buildKeyList(false);
}

function getSortColumn() {
  switch (gUserList.getAttribute("sortResource")) {
    case "enigUserNameCol":
      return "userid";
    case "keyCol":
      return "keyid";
    case "createdCol":
      return "created";
    case "expCol":
      return "expiry";
    case "fprCol":
      return "fpr";
    default:
      return "?";
  }
}

/**
 * Open the OpenPGP Key Wizard to generate a new key or import secret keys.
 *
 * @param {boolean} isImport - If the keyWizard should automatically switch to
 *   the import or create screen as requested by the user.
 */
function openKeyWizard(isImport = false) {
  let args = {
    gSubDialog: null,
    cancelCallback: clearKeyCache,
    okCallback: clearKeyCache,
    okImportCallback: clearKeyCache,
    okExternalCallback: clearKeyCache,
    keyDetailsDialog: enigmailKeyDetails,
    isCreate: !isImport,
    isImport,
  };

  window.browsingContext.topChromeWindow.openDialog(
    "chrome://openpgp/content/ui/keyWizard.xhtml",
    "enigmail:KeyWizard",
    "dialog,modal,centerscreen,resizable",
    args
  );
}

/***************************** TreeView for user list ***********************************/
/**
 * gKeyListView implements the nsITreeView interface for the displayed list.
 *
 * For speed reasons, we use two lists:
 * - keyViewList:   contains the full list of pointers to all  keys and rows that are
 *                  potentially displayed ordered according to the sort column
 * - keyFilterList: contains the indexes to keyViewList of the keys that are displayed
 *                  according to the current filter criteria.
 */
var gKeyListView = {
  keyViewList: [],
  keyFilterList: [],

  //// nsITreeView implementation

  rowCount: 0,
  selection: null,

  canDrop(index, orientation, dataTransfer) {
    return false;
  },

  cycleCell(row, col) {},
  cycleHeader(col) {},
  drop(row, orientation, dataTransfer) {},

  getCellProperties(row, col) {
    let r = this.getFilteredRow(row);
    if (!r) {
      return "";
    }

    let keyObj = gKeyList[r.keyNum];
    if (!keyObj) {
      return "";
    }

    let keyTrustStyle = "";

    switch (r.rowType) {
      case "key":
      case "uid":
        switch (keyObj.keyTrust) {
          case "q":
            keyTrustStyle = "enigmail_keyValid_unknown";
            break;
          case "r":
            keyTrustStyle = "enigmail_keyValid_revoked";
            break;
          case "e":
            keyTrustStyle = "enigmail_keyValid_expired";
            break;
          case "n":
            keyTrustStyle = "enigmail_keyTrust_untrusted";
            break;
          case "m":
            keyTrustStyle = "enigmail_keyTrust_marginal";
            break;
          case "f":
            keyTrustStyle = "enigmail_keyTrust_full";
            break;
          case "u":
            keyTrustStyle = "enigmail_keyTrust_ultimate";
            break;
          case "-":
            keyTrustStyle = "enigmail_keyTrust_unknown";
            break;
          default:
            keyTrustStyle = "enigmail_keyTrust_unknown";
            break;
        }

        if (
          keyObj.keyTrust.length > 0 &&
          ENIG_KEY_NOT_VALID.includes(keyObj.keyTrust.charAt(0))
        ) {
          keyTrustStyle += " enigKeyInactive";
        }

        if (r.rowType === "key" && keyObj.secretAvailable) {
          keyTrustStyle += " enigmailOwnKey";
        }
        break;
    }

    return keyTrustStyle;
  },

  getCellText(row, col) {
    let r = this.getFilteredRow(row);
    if (!r) {
      return "";
    }
    let keyObj = gKeyList[r.keyNum];
    if (!keyObj) {
      return "???";
    }

    switch (r.rowType) {
      case "key":
        switch (col.id) {
          case "enigUserNameCol":
            return keyObj.userId;
          case "keyCol":
            return `0x${keyObj.keyId}`;
          case "createdCol":
            return keyObj.created;
          case "expCol":
            return keyObj.effectiveExpiry;
          case "fprCol":
            return keyObj.fprFormatted;
        }
        break;
      case "uid":
        switch (col.id) {
          case "enigUserNameCol":
            return keyObj.userIds[r.uidNum].userId;
        }
        break;
    }

    return "";
  },
  getCellValue(row, col) {
    return "";
  },
  getColumnProperties(col) {
    return "";
  },

  getImageSrc(row, col) {
    let r = this.getFilteredRow(row);
    if (!r) {
      return null;
    }
    //let keyObj = gKeyList[r.keyNum];

    return null;
  },

  /**
   * indentation level for rows
   */
  getLevel(row) {
    let r = this.getFilteredRow(row);
    if (!r) {
      return 0;
    }

    switch (r.rowType) {
      case "key":
        return 0;
      case "uid":
        return 1;
    }

    return 0;
  },

  getParentIndex(idx) {
    return -1;
  },
  getProgressMode(row, col) {},

  getRowProperties(row) {
    return "";
  },
  hasNextSibling(rowIndex, afterIndex) {
    return false;
  },
  isContainer(row) {
    let r = this.getFilteredRow(row);
    if (!r) {
      return false;
    }
    switch (r.rowType) {
      case "key":
        return true;
    }

    return false;
  },
  isContainerEmpty(row) {
    let r = this.getFilteredRow(row);
    if (!r) {
      return true;
    }
    switch (r.rowType) {
      case "key":
        return !r.hasSubUID;
    }
    return true;
  },
  isContainerOpen(row) {
    return this.getFilteredRow(row).isOpen;
  },
  isEditable(row, col) {
    return false;
  },
  isSelectable(row, col) {
    return true;
  },
  isSeparator(index) {
    return false;
  },
  isSorted() {
    return false;
  },
  performAction(action) {},
  performActionOnCell(action, row, col) {},
  performActionOnRow(action, row) {},
  selectionChanged() {},
  // void setCellText(in long row, in nsITreeColumn col, in AString value);
  // void setCellValue(in long row, in nsITreeColumn col, in AString value);
  setTree(treebox) {
    this.treebox = treebox;
  },

  toggleOpenState(row) {
    let r = this.getFilteredRow(row);
    if (!r) {
      return;
    }
    let realRow = this.keyFilterList[row];
    switch (r.rowType) {
      case "key":
        if (r.isOpen) {
          let i = 0;
          while (
            this.getFilteredRow(row + 1 + i) &&
            this.getFilteredRow(row + 1 + i).keyNum === r.keyNum
          ) {
            ++i;
          }

          this.keyViewList.splice(realRow + 1, i);
          r.isOpen = false;
          this.applyFilter(row);
        } else {
          this.appendUids("uid", r.keyNum, realRow, this.keyViewList[row]);

          r.isOpen = true;
          this.applyFilter(row);
        }
        break;
    }
  },

  /**
   * add UIDs for a given key to key view
   *
   * @param uidType: String - one of uid (user ID), uat (photo)
   * @param keyNum:  Number - index of key in gKeyList
   * @param realRow: Number - index of row in keyViewList (i.e. without filter)
   *
   * @returns Number: number of UIDs added
   */
  appendUids(uidType, keyNum, realRow, parentRow) {
    let keyObj = gKeyList[keyNum];
    let uidAdded = 0;

    for (let i = 0; i < keyObj.userIds.length; i++) {
      if (keyObj.userIds[i].type === uidType) {
        if (keyObj.userIds[i].userId == keyObj.userId) {
          continue;
        }
        ++uidAdded;
        this.keyViewList.splice(realRow + uidAdded, 0, {
          rowType: uidType,
          keyNum,
          parent: parentRow,
          uidNum: i,
        });
      }
    }

    return uidAdded;
  },

  /**
   * Reload key list entirely
   */
  keysRefreshed() {
    this.keyViewList = [];
    this.keyFilterList = [];
    for (let i = 0; i < gKeySortList.length; i++) {
      this.keyViewList.push({
        row: i,
        rowType: "key",
        fpr: gKeySortList[i].fpr,
        keyNum: gKeySortList[i].keyNum,
        isOpen: false,
        hasSubUID: gKeyList[gKeySortList[i].keyNum].userIds.length > 1,
      });
    }

    this.applyFilter(0);
    let oldRowCount = this.rowCount;
    this.rowCount = this.keyViewList.length;
    gUserList.rowCountChanged(0, this.rowCount - oldRowCount);
  },

  /**
   * If no search term is entered, decide which keys to display
   *
   * @returns array of keyNums (= display some keys) or null (= display ALL keys)
   */
  showOrHideAllKeys() {
    var showInvalidKeys = gShowInvalidKeys.getAttribute("checked") == "true";
    var showOthersKeys = gShowOthersKeys.getAttribute("checked") == "true";

    document.getElementById("nothingFound").hidePopup();

    if (showInvalidKeys && showOthersKeys) {
      return null;
    }

    let keyShowList = [];
    for (let i = 0; i < gKeyList.length; i++) {
      if (determineHiddenKeys(gKeyList[i], showInvalidKeys, showOthersKeys)) {
        keyShowList.push(i);
      }
    }

    return keyShowList;
  },

  /**
   * Search for keys that match filter criteria
   *
   * @returns array of keyNums (= display some keys) or null (= display ALL keys)
   */
  getFilteredKeys() {
    let searchTxt = gSearchInput.value;

    if (!searchTxt || searchTxt.length === 0) {
      return this.showOrHideAllKeys();
    }

    if (!gKeyList) {
      return [];
    }
    let showInvalidKeys = gShowInvalidKeys.getAttribute("checked") == "true";
    let showOthersKeys = gShowOthersKeys.getAttribute("checked") == "true";

    // skip leading 0x in case we search for a key:
    if (searchTxt.length > 2 && searchTxt.substr(0, 2).toLowerCase() == "0x") {
      searchTxt = searchTxt.substr(2);
    }

    searchTxt = searchTxt.toLowerCase();
    searchTxt = searchTxt.replace(/^(\s*)(.*)/, "$2").replace(/\s+$/, ""); // trim spaces

    // check if we search for a full fingerprint (with optional spaces every 4 letters)
    var fpr = null;
    if (searchTxt.length == 49) {
      // possible fingerprint with spaces?
      if (
        searchTxt.search(/^[0-9a-f ]*$/) >= 0 &&
        searchTxt[4] == " " &&
        searchTxt[9] == " " &&
        searchTxt[14] == " " &&
        searchTxt[19] == " " &&
        searchTxt[24] == " " &&
        searchTxt[29] == " " &&
        searchTxt[34] == " " &&
        searchTxt[39] == " " &&
        searchTxt[44] == " "
      ) {
        fpr = searchTxt.replace(/ /g, "");
      }
    } else if (searchTxt.length == 40) {
      // possible fingerprint without spaces
      if (searchTxt.search(/^[0-9a-f ]*$/) >= 0) {
        fpr = searchTxt;
      }
    }

    let keyShowList = [];

    for (let i = 0; i < gKeyList.length; i++) {
      let keyObj = gKeyList[i];
      let uid = keyObj.userId;
      let showKey = false;

      // does a user ID (partially) match?
      for (let idx = 0; idx < keyObj.userIds.length; idx++) {
        uid = keyObj.userIds[idx].userId;
        if (uid.toLowerCase().includes(searchTxt)) {
          showKey = true;
        }
      }

      // does the full fingerprint (without spaces) match?
      // - no partial match check because this is special for the collapsed spaces inside the fingerprint
      if (showKey === false && fpr && keyObj.fpr.toLowerCase() == fpr) {
        showKey = true;
      }
      // does the fingerprint (partially) match?
      if (showKey === false && keyObj.fpr.toLowerCase().includes(searchTxt)) {
        showKey = true;
      }
      // does a sub key of (partially) match?
      if (showKey === false) {
        for (
          let subKeyIdx = 0;
          subKeyIdx < keyObj.subKeys.length;
          subKeyIdx++
        ) {
          let subkey = keyObj.subKeys[subKeyIdx].keyId;
          if (subkey.toLowerCase().includes(searchTxt)) {
            showKey = true;
          }
        }
      }
      // take option to show invalid/untrusted... keys into account
      if (
        showKey &&
        determineHiddenKeys(keyObj, showInvalidKeys, showOthersKeys)
      ) {
        keyShowList.push(i);
      }
    }

    return keyShowList;
  },

  /**
   * Trigger re-displaying the list of keys and apply a filter
   *
   * @param selectedRow: Number - the row that is currently selected or
   *                     clicked on
   */
  applyFilter(selectedRow) {
    let keyDisplayList = this.getFilteredKeys();

    this.keyFilterList = [];
    if (keyDisplayList === null) {
      for (let i = 0; i < this.keyViewList.length; i++) {
        this.keyFilterList.push(i);
      }

      this.adjustRowCount(this.keyViewList.length, selectedRow);
    } else {
      for (let i = 0; i < this.keyViewList.length; i++) {
        if (keyDisplayList.includes(this.keyViewList[i].keyNum)) {
          this.keyFilterList.push(i);
        }
      }

      this.adjustRowCount(this.keyFilterList.length, selectedRow);
    }
  },

  /**
   * Re-calculate the row count and instruct the view to update
   */
  adjustRowCount(newRowCount, selectedRow) {
    if (this.rowCount === newRowCount) {
      gUserList.invalidate();
      return;
    }

    let delta = newRowCount - this.rowCount;
    this.rowCount = newRowCount;
    gUserList.rowCountChanged(selectedRow, delta);
  },

  /**
   * Determine the row object from the a filtered row number
   *
   * @param row: Number - row number of displayed (=filtered) list
   * @returns Object: keyViewList entry of corresponding row
   */
  getFilteredRow(row) {
    let r = this.keyFilterList[row];
    if (r !== undefined) {
      return this.keyViewList[r];
    }
    return null;
  },

  treebox: null,
};
