text: // Copyright (c) 2009-2014 Kris Maglione <maglione.k@gmail.com>
  // Copyright (c) 2009-2010 by Doug Kearns <dougkearns@gmail.com>
  //
  // This work is licensed for reuse under an MIT license. Details are
  // given in the LICENSE.txt file included with this file.
  "use strict";
  
  try {
  
  defineModule("addons", {
      exports: ["AddonManager", "Addons", "Addon", "addons"],
      require: ["services", "util"]
  });
  
  this.lazyRequire("completion", ["completion"]);
  lazyRequire("template", ["template"]);
  
  var callResult = function callResult(method, ...args) {
      return function (result) { result[method].apply(result, args); };
  };
  
  var listener = function listener(action, event) {
      return function addonListener(install) {
          this.dactyl[install.error ? "echoerr" : "echomsg"](
              _("addon.error", action, event, (install.name || install.sourceURI.spec) +
                  (install.error ? ": " + addons.errors[install.error] : "")));
      };
  };
  
  var AddonListener = Class("AddonListener", {
      init: function init(modules) {
          this.dactyl = modules.dactyl;
      },
  
      onNewInstall:        function (install) {},
      onExternalInstall:   function (addon, existingAddon, needsRestart) {},
      onDownloadStarted:   listener("download", "started"),
      onDownloadEnded:     listener("download", "complete"),
      onDownloadCancelled: listener("download", "canceled"),
      onDownloadFailed:    listener("download", "failed"),
      onDownloadProgress:  function (install) {},
      onInstallStarted:    function (install) {},
      onInstallEnded:      listener("installation", "complete"),
      onInstallCancelled:  listener("installation", "canceled"),
      onInstallFailed:     listener("installation", "failed")
  });
  
  var updateAddons = Class("UpgradeListener", AddonListener, {
      init: function init(addons, modules) {
          init.supercall(this, modules);
  
          util.assert(!addons.length || addons[0].findUpdates,
                      _("error.unavailable", config.host, services.runtime.version));
  
          this.remaining = addons;
          this.upgrade = [];
          this.dactyl.echomsg(_("addon.check", addons.map(a => a.name).join(", ")));
          for (let addon of addons)
              addon.findUpdates(this, AddonManager.UPDATE_WHEN_USER_REQUESTED, null, null);
  
      },
      onUpdateAvailable: function (addon, install) {
          this.upgrade.push(addon);
          install.addListener(this);
          install.install();
      },
      onUpdateFinished: function (addon, error) {
          this.remaining = this.remaining.filter(a => (a.type != addon.type || a.id != addon.id));
          if (!this.remaining.length)
              this.dactyl.echomsg(
                  this.upgrade.length
                      ? _("addon.installingUpdates", this.upgrade.map(i => i.name).join(", "))
                      : _("addon.noUpdates"));
      }
  });
  
  var actions = {
      delete: {
          name: ["extde[lete]", "extrm"],
          description: "Uninstall an extension",
          action: callResult("uninstall"),
          perm: "uninstall"
      },
      enable: {
          name: "exte[nable]",
          description: "Enable an extension",
          action: function (addon) { addon.userDisabled = false; },
          filter: function (addon) { return addon.userDisabled; },
          perm: "enable"
      },
      disable: {
          name: "extd[isable]",
          description: "Disable an extension",
          action: function (addon) { addon.userDisabled = true; },
          filter: function (addon) { return !addon.userDisabled; },
          perm: "disable"
      },
      options: {
          name: ["exto[ptions]", "extp[references]"],
          description: "Open an extension's preference dialog",
          bang: true,
          action: function (addon, bang) {
              if (bang)
                  this.window.openDialog(addon.optionsURL, "_blank", "chrome");
              else
                  this.dactyl.open(addon.optionsURL, { from: "extoptions" });
          },
          filter: function (addon) {
              return addon.isActive && addon.optionsURL;
          }
      },
      rehash: {
          name: "extr[ehash]",
          description: "Reload an extension",
          action: function (addon) {
              util.flushCache();
              util.timeout(function () {
                  addon.userDisabled = true;
                  addon.userDisabled = false;
              });
          },
          get filter() {
              return addon => (
                  !addon.userDisabled &&
                  !(addon.operationsRequiringRestart & (AddonManager.OP_NEEDS_RESTART_ENABLE
                                                       | AddonManager.OP_NEEDS_RESTART_DISABLE)));
          },
          perm: "disable"
      },
      toggle: {
          name: "extt[oggle]",
          description: "Toggle an extension's enabled status",
          action: function (addon) { addon.userDisabled = !addon.userDisabled; }
      },
      update: {
          name: "extu[pdate]",
          description: "Update an extension",
          actions: updateAddons,
          perm: "upgrade"
      }
  };
  
  var Addon = Class("Addon", {
      init: function init(addon, list) {
          this.addon = addon;
          this.instance = this;
          this.list = list;
  
          this.nodes = {
              commandTarget: this
          };
          DOM.fromJSON(
              ["tr", { highlight: "Addon", key: "row" },
                  ["td", { highlight: "AddonName", key: "name" }],
                  ["td", { highlight: "AddonVersion", key: "version" }],
                  ["td", { highlight: "AddonButtons Buttons" },
                      ["a", { highlight: "Button", href: "javascript:0", key: "enable" }, _("addon.action.On")],
                      ["a", { highlight: "Button", href: "javascript:0", key: "disable" }, _("addon.action.Off")],
                      ["a", { highlight: "Button", href: "javascript:0", key: "delete" }, _("addon.action.Delete")],
                      ["a", { highlight: "Button", href: "javascript:0", key: "update" }, _("addon.action.Update")],
                      ["a", { highlight: "Button", href: "javascript:0", key: "options" }, _("addon.action.Options")]],
                  ["td", { highlight: "AddonStatus", key: "status" }],
                  ["td", { highlight: "AddonDescription", key: "description" }]],
              this.list.document, this.nodes);
  
          this.update();
      },
  
      commandAllowed: function commandAllowed(cmd) {
          util.assert(hasOwnProp(actions, cmd),
                      _("addon.unknownCommand"));
  
          let action = actions[cmd];
          if ("perm" in action && !(this.permissions & AddonManager["PERM_CAN_" + action.perm.toUpperCase()]))
              return false;
          if ("filter" in action && !action.filter(this))
              return false;
          return true;
      },
  
      command: function command(cmd) {
          util.assert(this.commandAllowed(cmd), _("addon.commandNotAllowed"));
  
          let action = actions[cmd];
          if (action.action)
              action.action.call(this.list.modules, this, true);
          else
              action.actions([this], this.list.modules);
      },
  
      compare: function compare(other) {
          return String.localeCompare(this.name, other.name);
      },
  
      get statusInfo() {
          let info = this.isActive ? ["span", { highlight: "Enabled" }, "enabled"]
                                   : ["span", { highlight: "Disabled" }, "disabled"];
  
          let pending;
          if (this.pendingOperations & AddonManager.PENDING_UNINSTALL)
              pending = ["Disabled", "uninstalled"];
          else if (this.pendingOperations & AddonManager.PENDING_DISABLE)
              pending = ["Disabled", "disabled"];
          else if (this.pendingOperations & AddonManager.PENDING_INSTALL)
              pending = ["Enabled", "installed"];
          else if (this.pendingOperations & AddonManager.PENDING_ENABLE)
              pending = ["Enabled", "enabled"];
          else if (this.pendingOperations & AddonManager.PENDING_UPGRADE)
              pending = ["Enabled", "upgraded"];
          if (pending)
              return [info, " (",
                      ["span", { highlight: pending[0] }, pending[1]],
                      " on ",
                      ["a", { href: "#", "dactyl:command": "dactyl.restart" }, "restart"],
                      ")"];
          return info;
      },
  
      update: function callee() {
          let update = (key, xml) => {
              let node = this.nodes[key];
              while (node.firstChild)
                  node.removeChild(node.firstChild);
  
              DOM(node).append(isArray(xml) ? xml : DOM.DOMString(xml));
          };
  
          update("name", template.icon({ icon: this.iconURL }, this.name));
          this.nodes.version.textContent = this.version;
          update("status", this.statusInfo);
          this.nodes.description.textContent = this.description;
          DOM(this.nodes.row).attr("active", this.isActive || null);
  
          for (let node of values(this.nodes))
              if (node.update && node.update !== callee)
                  node.update();
  
          let event = this.list.document.createEvent("Events");
          event.initEvent("dactyl-commandupdate", true, false);
          this.list.document.dispatchEvent(event);
      }
  });
  
  ["cancelUninstall", "findUpdates", "getResourceURI", "hasResource", "isCompatibleWith",
   "uninstall"].forEach(function (prop) {
       Addon.prototype[prop] = function proxy() {
           return apply(this.addon, prop, arguments);
       };
  });
  
  ["aboutURL", "appDisabled", "applyBackgroundUpdates", "blocklistState", "contributors", "creator",
   "description", "developers", "homepageURL", "iconURL", "id", "install", "installDate", "isActive",
   "isCompatible", "isPlatformCompatible", "name", "operationsRequiringRestart", "optionsURL",
   "pendingOperations", "pendingUpgrade", "permissions", "providesUpdatesSecurely", "releaseNotesURI",
   "scope", "screenshots", "size", "sourceURI", "translators", "type", "updateDate", "userDisabled",
   "version"].forEach(function (prop) {
      Object.defineProperty(Addon.prototype, prop, {
          get: function get_proxy() { return this.addon[prop]; },
          set: function set_proxy(val) { this.addon[prop] = val; }
      });
  });
  
  var AddonList = Class("AddonList", {
      init: function init(modules, types, filter) {
          this.modules = modules;
          this.filter = filter && filter.toLowerCase();
          this.nodes = {};
          this.addons = {};
          this.ready = false;
  
          AddonManager.getAddonsByTypes(types, addons => {
              this._addons = addons;
              if (this.document)
                  this._init();
          });
          AddonManager.addAddonListener(this);
      },
      cleanup: function cleanup() {
          AddonManager.removeAddonListener(this);
      },
  
      _init: function _init() {
          this._addons.forEach(this.bound.addAddon);
          this.ready = true;
          this.update();
      },
  
      message: Class.Memoize(function () {
          DOM.fromJSON(["table", { highlight: "Addons", key: "list" },
                          ["tr", { highlight: "AddonHead" },
                              ["td", {}, _("title.Name")],
                              ["td", {}, _("title.Version")],
                              ["td"],
                              ["td", {}, _("title.Status")],
                              ["td", {}, _("title.Description")]]],
                        this.document, this.nodes);
  
          if (this._addons)
              this._init();
  
          return this.nodes.list;
      }),
  
      addAddon: function addAddon(addon) {
          if (addon.id in this.addons)
              this.update(addon);
          else {
              if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1)
                  return;
  
              addon = Addon(addon, this);
              this.addons[addon.id] = addon;
  
              let index = values(this.addons).sort((a, b) => a.compare(b))
                                             .indexOf(addon);
  
              this.nodes.list.insertBefore(addon.nodes.row,
                                           this.nodes.list.childNodes[index + 1]);
              this.update();
          }
      },
      removeAddon: function removeAddon(addon) {
          if (addon.id in this.addons) {
              this.nodes.list.removeChild(this.addons[addon.id].nodes.row);
              delete this.addons[addon.id];
              this.update();
          }
      },
  
      leave: function leave(stack) {
          if (stack.pop)
              this.cleanup();
      },
  
      update: function update(addon) {
          if (addon && addon.id in this.addons)
              this.addons[addon.id].update();
          if (this.ready)
              this.modules.mow.resize(false);
      },
  
      onDisabled:           function (addon) { this.update(addon); },
      onDisabling:          function (addon) { this.update(addon); },
      onEnabled:            function (addon) { this.update(addon); },
      onEnabling:           function (addon) { this.update(addon); },
      onInstalled:          function (addon) { this.addAddon(addon); },
      onInstalling:         function (addon) { this.update(addon); },
      onUninstalled:        function (addon) { this.removeAddon(addon); },
      onUninstalling:       function (addon) { this.update(addon); },
      onOperationCancelled: function (addon) { this.update(addon); },
      onPropertyChanged: function onPropertyChanged(addon, properties) {}
  });
  
  var Addons = Module("addons", {
      errors: Class.Memoize(() =>
              Ary(["ERROR_NETWORK_FAILURE", "ERROR_INCORRECT_HASH",
                   "ERROR_CORRUPT_FILE", "ERROR_FILE_ACCESS"])
                  .map(e => [AddonManager[e], _("AddonManager." + e)])
                  .toObject())
  }, {
  }, {
      commands: function initCommands(dactyl, modules, window) {
          const { CommandOption, commands, completion, io } = modules;
  
          commands.add(["addo[ns]", "ao"],
              "List installed extensions",
              function (args) {
                  let addons = AddonList(modules, args["-types"], args[0]);
                  modules.commandline.echo(addons);
  
                  if (modules.commandline.savingOutput)
                      util.waitFor(() => addons.ready);
              }, {
                  argCount: "?",
                  options: [
                      {
                          names: ["-types", "-type", "-t"],
                          description: "The add-on types to list",
                          default: ["extension"],
                          completer: function (context) {
                              completion.addonType(context);
                          },
                          type: CommandOption.LIST
                      }
                  ]
              });
  
          let addonListener = AddonListener(modules);
  
          commands.add(["exta[dd]"],
              "Install an extension",
              function (args) {
                  let url  = args[0];
                  let file = io.File(url);
                  function install(addonInstall) {
                      addonInstall.addListener(addonListener);
                      addonInstall.install();
                  }
  
                  if (!file.exists())
                      AddonManager.getInstallForURL(url,        install, "application/x-xpinstall");
                  else if (file.isReadable() && file.isFile())
                      AddonManager.getInstallForFile(file.file, install, "application/x-xpinstall");
                  else if (file.isDirectory())
                      dactyl.echoerr(_("addon.cantInstallDir", JSON.stringify(file.path)));
                  else
                      dactyl.echoerr(_("io.notReadable", file.path));
              }, {
                  argCount: "1",
                  completer: function (context) {
                      context.filters.push(({ isdir, text }) => isdir || /\.xpi$/.test(text));
                      completion.file(context);
                  },
                  literal: 0
              });
  
          // TODO: handle extension dependencies
          values(actions).forEach(function (command) {
              let perm = command.perm && AddonManager["PERM_CAN_" + command.perm.toUpperCase()];
              function ok(addon) {
                  return (!perm || addon.permissions & perm) && (!command.filter || command.filter(addon));
              }
  
              commands.add(Array.concat(command.name),
                  command.description,
                  function (args) {
                      let name = args[0];
                      if (args.bang && !command.bang)
                          dactyl.assert(!name, _("error.trailingCharacters"));
                      else
                          dactyl.assert(name, _("error.argumentRequired"));
  
                      AddonManager.getAddonsByTypes(args["-types"], dactyl.wrapCallback(function (list) {
                          if (!args.bang || command.bang) {
                              list = list.filter(addon => (addon.id == name || addon.name == name));
                              dactyl.assert(list.length, _("error.invalidArgument", name));
                              dactyl.assert(list.some(ok), _("error.invalidOperation"));
                              list = list.filter(ok);
                          }
                          dactyl.assert(list.every(ok));
                          if (command.actions)
                              command.actions(list, this.modules);
                          else
                              list.forEach(addon => { command.action.call(this.modules, addon, args.bang); });
                      }));
                  }, {
                      argCount: "?", // FIXME: should be "1"
                      bang: true,
                      completer: function (context, args) {
                          completion.addon(context, args["-types"]);
                          context.filters.push(({ item }) => ok(item));
                      },
                      literal: 0,
                      options: [
                          {
                              names: ["-types", "-type", "-t"],
                              description: "The add-on types to operate on",
                              default: ["extension"],
                              completer: function (context) {
                                  completion.addonType(context);
                              },
                              type: CommandOption.LIST
                          }
                      ]
                  });
          });
      },
      completion: function initCompletion(dactyl, modules, window) {
          completion.addonType = function addonType(context) {
              let base = ["extension", "theme"];
              function update(types) {
                  context.completions = types.map(t => [t, util.capitalize(t)]);
              }
  
              context.generate = function generate() {
                  update(base);
                  if (AddonManager.getAllAddons) {
                      context.incomplete = true;
                      AddonManager.getAllAddons(function (addons) {
                          context.incomplete = false;
                          update(Ary.uniq(base.concat(addons.map(a => a.type)),
                                          true));
                      });
                  }
              };
          };
  
          completion.addon = function addon(context, types) {
              context.title = ["Add-on"];
              context.anchored = false;
              context.keys = {
                  text: addon => [addon.name, addon.id],
                  description: "description",
                  icon: "iconURL"
              };
              context.generate = () => {
                  context.incomplete = true;
                  AddonManager.getAddonsByTypes(types || ["extension"], addons => {
                      context.incomplete = false;
                      context.completions = addons;
                  });
              };
          };
      }
  });
  
  Components.utils.import("resource://gre/modules/AddonManager.jsm", this);
  
  endModule();
  
  } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
  
  // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript:
  