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

const { interfaces: Ci, utils: Cu, classes: Cc } = Components;

const nsIDM = Ci.nsIDownloadManager;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/osfile.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
  "resource://gre/modules/NetUtil.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
  "resource://gre/modules/DownloadUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
  "resource://gre/modules/PluralForm.jsm");

let gStr = {};

let DownloadItem = function(aID, aDownload) {
  this.id = aID;
  this._download = aDownload;

  this.file = aDownload.target.path;
  this.target = OS.Path.basename(aDownload.target.path);
  this.uri = aDownload.source.url;
  this.endTime = new Date();

  this.updateState();

  this.updateData();

  this.createItem();
};

DownloadItem.prototype = {
  /**
   * The XUL element corresponding to the associated richlistbox item.
   */
  element: null,

  /**
   * The inner XUL element for the progress bar, or null if not available.
   */
  _progressElement: null,

  _lastEstimatedSecondsLeft: Infinity,

  updateState: function() {
    // Collapse state using the correct priority.
    if (this._download.succeeded) {
      this.state = nsIDM.DOWNLOAD_FINISHED;
    } else if (this._download.error &&
               this._download.error.becauseBlockedByParentalControls) {
      this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
    } else if (this._download.error &&
               this._download.error.becauseBlockedByReputationCheck) {
      this.state = nsIDM.DOWNLOAD_DIRTY;
    } else if (this._download.error) {
      this.state = nsIDM.DOWNLOAD_FAILED;
    } else if (this._download.canceled && this._download.hasPartialData) {
      this.state = nsIDM.DOWNLOAD_PAUSED;
    } else if (this._download.canceled) {
      this.state = nsIDM.DOWNLOAD_CANCELED;
    } else if (this._download.stopped) {
      this.state = nsIDM.DOWNLOAD_NOTSTARTED;
    } else {
      this.state = nsIDM.DOWNLOAD_DOWNLOADING;
    }
  },

  updateData: function() {
    let wasInProgress = this.inProgress;

    this.updateState();

    if (wasInProgress && !this.inProgress) {
      this.endTime = new Date();
    }

    this.referrer = this._download.source.referrer;
    this.startTime = this._download.startTime;
    this.currBytes = this._download.currentBytes;
    this.resumable = this._download.hasPartialData;
    this.speed = this._download.speed;

    if (this._download.succeeded) {
      // If the download succeeded, show the final size if available, otherwise
      // use the last known number of bytes transferred.  The final size on disk
      // will be available when bug 941063 is resolved.
      this.maxBytes = this._download.hasProgress ? this._download.totalBytes
                                                 : this._download.currentBytes;
      this.percentComplete = 100;
    } else if (this._download.hasProgress) {
      // If the final size and progress are known, use them.
      this.maxBytes = this._download.totalBytes;
      this.percentComplete = this._download.progress;
    } else {
      // The download final size and progress percentage is unknown.
      this.maxBytes = -1;
      this.percentComplete = -1;
    }
  },

  /**
   * Indicates whether the download is proceeding normally, and not finished
   * yet. This includes paused downloads.  When this property is true, the
   * "progress" property represents the current progress of the download.
   */
  get inProgress() {
    return [
      nsIDM.DOWNLOAD_NOTSTARTED,
      nsIDM.DOWNLOAD_QUEUED,
      nsIDM.DOWNLOAD_DOWNLOADING,
      nsIDM.DOWNLOAD_PAUSED,
      nsIDM.DOWNLOAD_SCANNING,
    ].indexOf(this.state) != -1;
  },

  /**
   * This is true during the initial phases of a download, before the actual
   * download of data bytes starts.
   */
  get starting() {
    return this.state == nsIDM.DOWNLOAD_NOTSTARTED ||
           this.state == nsIDM.DOWNLOAD_QUEUED;
  },

  /**
   * Indicates whether the download is paused.
   */
  get paused() {
    return this.state == nsIDM.DOWNLOAD_PAUSED;
  },

  /**
   * Indicates whether the download is in a final state, either because it
   * completed successfully or because it was blocked.
   */
  get done() {
    return [
      nsIDM.DOWNLOAD_FINISHED,
      nsIDM.DOWNLOAD_BLOCKED_PARENTAL,
      nsIDM.DOWNLOAD_BLOCKED_POLICY,
      nsIDM.DOWNLOAD_DIRTY,
    ].indexOf(this.state) != -1;
  },

  /**
   * Indicates whether the download can be removed.
   */
  get removable() {
    return [
      nsIDM.DOWNLOAD_FINISHED,
      nsIDM.DOWNLOAD_CANCELED,
      nsIDM.DOWNLOAD_BLOCKED_PARENTAL,
      nsIDM.DOWNLOAD_BLOCKED_POLICY,
      nsIDM.DOWNLOAD_DIRTY,
      nsIDM.DOWNLOAD_FAILED,
    ].indexOf(this.state) != -1;
  },

  /**
   * Indicates whether the download is finished and can be opened.
   */
  get openable() {
    return this.state == nsIDM.DOWNLOAD_FINISHED;
  },

  /**
   * Indicates whether the download stopped because of an error, and can be
   * resumed manually.
   */
  get canRetry() {
    return this.state == nsIDM.DOWNLOAD_CANCELED ||
           this.state == nsIDM.DOWNLOAD_FAILED;
  },

  /**
   * Returns the nsILocalFile for the download target.
   *
   * @throws if the native path is not valid.  This can happen if the same
   *         profile is used on different platforms, for example if a native
   *         Windows path is stored and then the item is accessed on a Mac.
   */
  get localFile() {
    return this._getFile(this.file);
  },

  /**
   * Returns the nsILocalFile for the partially downloaded target.
   *
   * @throws if the native path is not valid.  This can happen if the same
   *         profile is used on different platforms, for example if a native
   *         Windows path is stored and then the item is accessed on a Mac.
   */
  get partFile() {
    return this._getFile(this.file + ".part");
  },

  /**
   * Return a nsILocalFile for aFilename. aFilename might be a file URL or
   * a native path.
   *
   * @param aFilename the filename of the file to retrieve.
   * @return an nsILocalFile for the file.
   * @throws if the native path is not valid.  This can happen if the same
   *         profile is used on different platforms, for example if a native
   *         Windows path is stored and then the item is accessed on a Mac.
   * @note This function makes no guarantees about the file's existence -
   *       callers should check that the returned file exists.
   */
  _getFile: function(aFilename) {
    // The download database may contain targets stored as file URLs or native
    // paths.  This can still be true for previously stored items, even if new
    // items are stored using their file URL.  See also bug 239948 comment 12.
    if (aFilename.startsWith("file:")) {
      // Assume the file URL we obtained from the downloads database or from the
      // "spec" property of the target has the UTF-8 charset.
      let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL);
      return fileUrl.file;
    }

    // The downloads database contains a native path.  Try to create a local
    // file, though this may throw an exception if the path is invalid.
    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
    file.initWithPath(aFilename);
    return file;
  },

  /**
   * Show a downloaded file in the system file manager.
   */
  showLocalFile: function() {
    let file = this.localFile;

    try {
      // Show the directory containing the file and select the file.
      file.reveal();
    } catch (ex) {
      // If reveal fails for some reason (e.g., it's not implemented on unix
      // or the file doesn't exist), try using the parent if we have it.
      let parent = file.parent;
      if (!parent) {
        return;
      }

      try {
        // Open the parent directory to show where the file should be.
        parent.launch();
      } catch (ex) {
        // If launch also fails (probably because it's not implemented), let
        // the OS handler try to open the parent.
        Cc["@mozilla.org/uriloader/external-protocol-service;1"].
        getService(Ci.nsIExternalProtocolService).
        loadUrl(NetUtil.newURI(parent));
      }
    }
  },

  /**
   * Open the target file for this download.
   */
  openLocalFile: function() {
    return this._download.launch();
  },

  /**
   * Pause the download, if it is active.
   */
  pause: function() {
    return this._download.cancel();
  },

  /**
   * Resume the download, if it is paused.
   */
  resume: function() {
    return this._download.start();
  },

  /**
   * Attempt to retry the download.
   */
  retry: function() {
    return this._download.start();
  },

  /**
   * Cancel the download.
   */
  cancel: function() {
    this._download.cancel();
    return this._download.removePartialData();
  },

  /**
   * Remove the download.
   */
  remove: function() {
    return Downloads.getList(Downloads.ALL)
                    .then(list => list.remove(this._download))
                    .then(() => this._download.finalize(true));
  },

  /**
   * Create a download richlistitem with the provided attributes.
   */
  createItem: function() {
    this.element = document.createElement("richlistitem");

    this.element.setAttribute("id", this.id);
    this.element.setAttribute("target", this.target);
    this.element.setAttribute("state", this.state);
    this.element.setAttribute("progress", this.percentComplete);
    this.element.setAttribute("type", "download");
    this.element.setAttribute("image", "moz-icon://" + this.file + "?size=32");
  },

  /**
   * Update the status for a download item depending on its state.
   */
  updateStatusText: function() {
    let status = "";
    let statusTip = "";

    switch (this.state) {
      case nsIDM.DOWNLOAD_PAUSED:
        let transfer = DownloadUtils.getTransferTotal(this.currBytes,
                                                      this.maxBytes);
        status = gStr.paused.replace("#1", transfer);

        break;

      case nsIDM.DOWNLOAD_DOWNLOADING:
        let newLast;
        [status, newLast] =
          DownloadUtils.getDownloadStatus(this.currBytes, this.maxBytes,
                                          this.speed,
                                          this._lastEstimatedSecondsLeft);

        this._lastEstimatedSecondsLeft = newLast;

        break;

      case nsIDM.DOWNLOAD_FINISHED:
      case nsIDM.DOWNLOAD_FAILED:
      case nsIDM.DOWNLOAD_CANCELED:
      case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
      case nsIDM.DOWNLOAD_BLOCKED_POLICY:
      case nsIDM.DOWNLOAD_DIRTY:
        let stateSize = {};
        stateSize[nsIDM.DOWNLOAD_FINISHED] = () => {
          // Display the file size, but show "Unknown" for negative sizes.
          let sizeText = gStr.doneSizeUnknown;
          if (this.maxBytes >= 0) {
            let [size, unit] = DownloadUtils.convertByteUnits(this.maxBytes);
            sizeText = gStr.doneSize.replace("#1", size);
            sizeText = sizeText.replace("#2", unit);
          }
          return sizeText;
        };
        stateSize[nsIDM.DOWNLOAD_FAILED] = () => gStr.stateFailed;
        stateSize[nsIDM.DOWNLOAD_CANCELED] = () => gStr.stateCanceled;
        stateSize[nsIDM.DOWNLOAD_BLOCKED_PARENTAL] = () => gStr.stateBlocked;
        stateSize[nsIDM.DOWNLOAD_BLOCKED_POLICY] = () => gStr.stateBlockedPolicy;
        stateSize[nsIDM.DOWNLOAD_DIRTY] = () => gStr.stateDirty;

        // Insert 1 is the download size or download state.
        status = gStr.doneStatus.replace("#1", stateSize[this.state]());

        let [displayHost, fullHost] =
          DownloadUtils.getURIHost(this.referrer || this.uri);

        // Insert 2 is the eTLD + 1 or other variations of the host.
        status = status.replace("#2", displayHost);
        // Set the tooltip to be the full host.
        statusTip = fullHost;

        break;
    }

    this.element.setAttribute("status", status);
    this.element.setAttribute("statusTip", statusTip || status);
  },

  updateView: function() {
    this.updateData();

    // Update this download's progressmeter.
    if (this.starting) {
      // Before the download starts, the progress meter has its initial value.
      this.element.setAttribute("progressmode", "normal");
      this.element.setAttribute("progress", "0");
    } else if (this.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING ||
               this.percentComplete == -1) {
      // We might not know the progress of a running download, and we don't know
      // the remaining time during the malware scanning phase.
      this.element.setAttribute("progressmode", "undetermined");
    } else {
      // This is a running download of which we know the progress.
      this.element.setAttribute("progressmode", "normal");
      this.element.setAttribute("progress", this.percentComplete);
    }

    // Find the progress element as soon as the download binding is accessible.
    if (!this._progressElement) {
      this._progressElement =
           document.getAnonymousElementByAttribute(this.element, "anonid",
                                                   "progressmeter");
    }

    // Dispatch the ValueChange event for accessibility, if possible.
    if (this._progressElement) {
      let event = document.createEvent("Events");
      event.initEvent("ValueChange", true, true);
      this._progressElement.dispatchEvent(event);
    }

    // Update the rest of the UI (bytes transferred, bytes total, download rate,
    // time remaining).

    // Update the time that gets shown for completed download items
    // Don't bother updating it for things that aren't finished
    if (!this.inProgress) {
      let [dateCompact, dateComplete] = DownloadUtils.getReadableDates(this.endTime);
      this.element.setAttribute("dateTime", dateCompact);
      this.element.setAttribute("dateTimeTip", dateComplete);
    }

    this.element.setAttribute("state", this.state);

    this.updateStatusText();

    // Update the disabled state of the buttons of a download
    let buttons = this.element.buttons;
    for (let i = 0; i < buttons.length; ++i) {
      let cmd = buttons[i].getAttribute("cmd");
      let enabled = this.isCommandEnabled(cmd);
      buttons[i].disabled = !enabled;

      if ("cmd_pause" == cmd && !enabled) {
        // We need to add the tooltip indicating that the download cannot be
        // paused now.
        buttons[i].setAttribute("tooltiptext", gStr.cannotPause);
      }
    }

    if (this.done) {
      // getTypeFromFile fails if it can't find a type for this file.
      try {
        // Refresh the icon, so that executable icons are shown.
        let mimeService = Cc["@mozilla.org/mime;1"].
                          getService(Ci.nsIMIMEService);
        let contentType = mimeService.getTypeFromFile(this.localFile);

        let oldImage = this.element.getAttribute("image");
        // Tacking on contentType bypasses cache
        this.element.setAttribute("image", oldImage + "&contentType=" + contentType);
      } catch (e) {}
    }
  },

  /**
   * Check if the download matches the provided search term based on the texts
   * shown to the user. All search terms are checked to see if each matches any
   * of the displayed texts.
   *
   * @return Boolean true if it matches the search; false otherwise
   */
  matchesSearch: function(aTerms, aAttributes) {
    return aTerms.some(term => aAttributes.some(attr => this.element.getAttribute(attr).contains(term)));
  },

  isCommandEnabled: function(aCommand) {
    switch (aCommand) {
      case "cmd_cancel":
        return this.inProgress;

      case "cmd_open":
        return this.openable && this.localFile.exists();

      case "cmd_show":
        return this.localFile.exists();

      case "cmd_pause":
        return this.inProgress && !this.paused && this.resumable;

      case "cmd_pauseResume":
        return (this.inProgress || this.paused) && this.resumable;

      case "cmd_resume":
        return this.paused && this.resumable;

      case "cmd_openReferrer":
        return !!this.referrer;

      case "cmd_removeFromList":
        return this.removable;

      case "cmd_retry":
        return this.canRetry;

      case "cmd_copyLocation":
        return true;
    }

    return false;
  },

  doCommand: function(aCommand) {
    if (!this.isCommandEnabled(aCommand)) {
      return;
    }

    switch (aCommand) {
      case "cmd_cancel":
        this.cancel().catch(Cu.reportError);
        break;

      case "cmd_open":
        this.openLocalFile().catch(Cu.reportError);
        break;

      case "cmd_show":
        this.showLocalFile();
        break;

      case "cmd_pause":
        this.pause().catch(Cu.reportError);
        break;

      case "cmd_pauseResume":
        if (this.paused) {
          this.resume().catch(Cu.reportError);
        } else {
          this.pause().catch(Cu.reportError);
        }
        break;

      case "cmd_resume":
        this.resume().catch(Cu.reportError);
        break;

      case "cmd_openReferrer":
        let uri = Services.io.newURI(this.referrer || this.uri, null, null);

        // Direct the URL to the default browser.
        Cc["@mozilla.org/uriloader/external-protocol-service;1"].
        getService(Ci.nsIExternalProtocolService).
        getProtocolHandlerInfo(uri.scheme).
        launchWithURI(uri);
        break;

      case "cmd_removeFromList":
        this.remove().catch(Cu.reportError);
        break;

      case "cmd_retry":
        this.retry().catch(Cu.reportError);
        break;

      case "cmd_copyLocation":
        let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
                        getService(Ci.nsIClipboardHelper);
        clipboard.copyString(this.uri, document);
        break;
    }
  },
};

let gDownloadList = {
  downloadItemsMap: new Map(),
  idToDownloadItemMap: new Map(),
  _autoIncrementID: 0,
  downloadView: null,
  searchBox: null,
  searchTerms: [],
  searchAttributes: [ "target", "status", "dateTime", ],
  contextMenus: [
    // DOWNLOAD_DOWNLOADING
    [
      "menuitem_pause",
      "menuitem_cancel",
      "menuseparator",
      "menuitem_show",
      "menuseparator",
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
    ],
    // DOWNLOAD_FINISHED
    [
      "menuitem_open",
      "menuitem_show",
      "menuseparator",
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
      "menuseparator",
      "menuitem_removeFromList"
    ],
    // DOWNLOAD_FAILED
    [
      "menuitem_retry",
      "menuseparator",
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
      "menuseparator",
      "menuitem_removeFromList",
    ],
    // DOWNLOAD_CANCELED
    [
      "menuitem_retry",
      "menuseparator",
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
      "menuseparator",
      "menuitem_removeFromList",
    ],
    // DOWNLOAD_PAUSED
    [
      "menuitem_resume",
      "menuitem_cancel",
      "menuseparator",
      "menuitem_show",
      "menuseparator",
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
    ],
    // DOWNLOAD_QUEUED
    [
      "menuitem_cancel",
      "menuseparator",
      "menuitem_show",
      "menuseparator",
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
    ],
    // DOWNLOAD_BLOCKED_PARENTAL
    [
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
      "menuseparator",
      "menuitem_removeFromList",
    ],
    // DOWNLOAD_SCANNING
    [
      "menuitem_show",
      "menuseparator",
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
    ],
    // DOWNLOAD_DIRTY
    [
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
      "menuseparator",
      "menuitem_removeFromList",
    ],
    // DOWNLOAD_BLOCKED_POLICY
    [
      "menuitem_openReferrer",
      "menuitem_copyLocation",
      "menuseparator",
      "menuitem_selectAll",
      "menuseparator",
      "menuitem_removeFromList",
    ]
  ],

  init: function() {
    this.downloadView = document.getElementById("downloadView");
    this.searchBox = document.getElementById("searchbox");

    this.buildList(true);

    this.downloadView.focus();

    // Clear the search box and move focus to the list on escape from the box.
    this.searchBox.addEventListener("keypress", (e) => {
      this.searchBoxKeyPressHandler(e);
    }, false);

    Downloads.getList(Downloads.ALL)
             .then(list => list.addView(this))
             .catch(Cu.reportError);
  },

  buildList: function(aForceBuild) {
    // Stringify the previous search.
    let prevSearch = this.searchTerms.join(" ");

    // Array of space-separated lower-case search terms.
    this.searchTerms = this.searchBox.value.trim().toLowerCase().split(/\s+/);

    // Unless forced, don't rebuild the download list if the search didn't change.
    if (!aForceBuild && this.searchTerms.join(" ") == prevSearch) {
      return;
    }

    // Clear the list before adding items by replacing with a shallow copy.
    let empty = this.downloadView.cloneNode(false);
    this.downloadView.parentNode.replaceChild(empty, this.downloadView);
    this.downloadView = empty;

    for (let downloadItem of this.idToDownloadItemMap.values()) {
      if (downloadItem.inProgress ||
          downloadItem.matchesSearch(this.searchTerms, this.searchAttributes)) {
        this.downloadView.appendChild(downloadItem.element);

        // Because of the joys of XBL, we can't update the buttons until the
        // download object is in the document.
        downloadItem.updateView();
      }
    }

    this.updateClearListButton();
  },

  /**
   * Remove the finished downloads from the shown download list.
   */
  clearList: Task.async(function*() {
    let searchTerms = this.searchTerms;

    let list = yield Downloads.getList(Downloads.ALL);

    yield list.removeFinished((aDownload) => {
      let downloadItem = this.downloadItemsMap.get(aDownload);
      if (!downloadItem) {
        Cu.reportError("Download doesn't exist.");
        return;
      }

      return downloadItem.matchesSearch(searchTerms, this.searchAttributes);
    });

    // Clear the input as if the user did it and move focus to the list.
    this.searchBox.value = "";
    this.searchBox.doCommand();
    this.downloadView.focus();
  }),

  /**
   * Update the disabled state of the clear list button based on whether or not
   * there are items in the list that can potentially be removed.
   */
  updateClearListButton: function() {
    let button = document.getElementById("clearListButton");

    // The button is enabled if we have items in the list that we can clean up.
    for (let downloadItem of this.idToDownloadItemMap.values()) {
      if (!downloadItem.inProgress &&
          downloadItem.matchesSearch(this.searchTerms, this.searchAttributes)) {
        button.disabled = false;
        return;
      }
    }

    button.disabled = true;
  },

  setSearchboxFocus: function() {
    this.searchBox.focus();
    this.searchBox.select();
  },

  searchBoxKeyPressHandler: function(aEvent) {
    if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
      // Move focus to the list instead of closing the window.
      this.downloadView.focus();
      aEvent.preventDefault();
    }
  },

  buildContextMenu: function(aEvent) {
    if (aEvent.target.id != "downloadContextMenu") {
      return false;
    }

    let popup = document.getElementById("downloadContextMenu");
    while (popup.hasChildNodes()) {
      popup.removeChild(popup.firstChild);
    }

    if (this.downloadView.selectedItem) {
      let downloadItem = this._getSelectedDownloadItem();

      let idx = downloadItem.state;
      if (idx < 0) {
        idx = 0;
      }

      let menus = this.contextMenus[idx];
      for (let i = 0; i < menus.length; ++i) {
        let menuitem = document.getElementById(menus[i]).cloneNode(true);
        let cmd = menuitem.getAttribute("cmd");
        if (cmd) {
          menuitem.disabled = !downloadItem.isCommandEnabled(cmd);
        }

        popup.appendChild(menuitem);
      }

      return true;
    }

    return false;
  },

  /**
   * Perform the default action for the currently selected download item.
   */
  doDefaultForSelected: function() {
    // Make sure we have something selected.
    let download = this._getSelectedDownloadItem();
    if (!download) {
      return;
    }

    // Get the default action (first item in the menu).
    let menuitem = document.getElementById(this.contextMenus[download.state][0]);

    // Try to do the action if the command is enabled.
    download.doCommand(menuitem.getAttribute("cmd"));
  },

  onDownloadDblClick: function(aEvent) {
    // Only do the default action for double primary clicks.
    if (aEvent.button == 0 && aEvent.target.selected) {
      this.doDefaultForSelected();
    }
  },

  /**
   * Helper function to do commands.
   *
   * @param aCmd
   *        The command to be performed.
   * @param aItem
   *        The richlistitem that represents the download that will have the
   *        command performed on it. If this is null, the command is performed on
   *        all downloads. If the item passed in is not a richlistitem that
   *        represents a download, it will walk up the parent nodes until it finds
   *        a DOM node that is.
   */
  performCommand: function(aCmd, aItem) {
    if (!aItem) {
      // Convert the nodelist into an array to keep a copy of the download items.
      let items = Array.from(this.downloadView.selectedItems);

      // Do the command for each download item.
      for (let item of items) {
        this.performCommand(aCmd, item);
      }

      return;
    }

    let elm = aItem;

    while (elm.nodeName != "richlistitem" ||
           elm.getAttribute("type") != "download") {
      elm = elm.parentNode;
    }

    let downloadItem = this._getDownloadItemForElement(elm);
    downloadItem.doCommand(aCmd);
  },

  onDragStart: function(aEvent) {
    let downloadItem = this._getSelectedDownloadItem();
    if (!downloadItem) {
      return;
    }

    let dl = this.downloadView.selectedItem;
    let f = downloadItem.localFile;
    if (!f.exists()) {
      return;
    }

    let dt = aEvent.dataTransfer;
    dt.mozSetDataAt("application/x-moz-file", f, 0);
    let url = Services.io.newFileURI(f).spec;
    dt.setData("text/uri-list", url);
    dt.setData("text/plain", url);
    dt.effectAllowed = "copyMove";
    dt.addElement(dl);
  },

  onDragOver: function(aEvent) {
    let types = aEvent.dataTransfer.types;
    if (types.contains("text/uri-list") ||
        types.contains("text/x-moz-url") ||
        types.contains("text/plain")) {
      aEvent.preventDefault();
    }

    aEvent.stopPropagation();
  },

  onDrop: function(aEvent) {
    let dt = aEvent.dataTransfer;
    // If dragged item is from our source, do not try to
    // redownload already downloaded file.
    if (dt.mozGetDataAt("application/x-moz-file", 0)) {
      return;
    }

    let name;
    let url = dt.getData("URL");

    if (!url) {
      url = dt.getData("text/x-moz-url") || dt.getData("text/plain");
      [url, name] = url.split("\n");
    }

    if (url) {
      let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document;
      saveURL(url, name ? name : url, null, true, true, null, sourceDoc);
    }
  },

  pasteHandler: function() {
    let trans = Cc["@mozilla.org/widget/transferable;1"].
                createInstance(Ci.nsITransferable);
    trans.init(null);
    let flavors = ["text/x-moz-url", "text/unicode"];
    flavors.forEach(trans.addDataFlavor);

    Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);

    // Getting the data or creating the nsIURI might fail
    try {
      let data = {};
      trans.getAnyTransferData({}, data, {});
      let [url, name] = data.value.QueryInterface(Ci.nsISupportsString).data.split("\n");

      if (!url) {
        return;
      }

      let uri = Services.io.newURI(url, null, null);

      saveURL(uri.spec, name || uri.spec, null, true, true, null, document);
    } catch (ex) {}
  },

  selectAll: function() {
    this.downloadView.selectAll();
  },

  onUpdateProgress: function() {
    let numActive = 0;
    let totalSize = 0;
    let totalTransferred = 0;

    for (let downloadItem of this.idToDownloadItemMap.values()) {
      if (!downloadItem.inProgress) {
        continue;
      }

      numActive++;

      // Only add to total values if we actually know the download size.
      if (downloadItem.maxBytes > 0 &&
          downloadItem.state != nsIDM.DOWNLOAD_CANCELED &&
          downloadItem.state != nsIDM.DOWNLOAD_FAILED) {
        totalSize += downloadItem.maxBytes;
        totalTransferred += downloadItem.currBytes;
      }
    }

    // Use the default title and reset "last" values if there are no downloads.
    if (numActive == 0) {
      document.title = document.documentElement.getAttribute("statictitle");
      return;
    }

    // Establish the mean transfer speed and amount downloaded.
    let mean = totalTransferred;

    // Calculate the percent transferred, unless we don't have a total file size.
    let title = gStr.downloadsTitlePercent;
    if (totalSize == 0) {
      title = gStr.downloadsTitleFiles;
    } else {
      mean = Math.floor((totalTransferred / totalSize) * 100);
    }

    // Get the correct plural form and insert number of downloads and percent.
    title = PluralForm.get(numActive, title);
    title = title.replace("#1", numActive);
    title = title.replace("#2", mean);

    // Update title of window.
    document.title = title;
  },

  onDownloadAdded: function(aDownload) {
    let newID = this._autoIncrementID++;

    let downloadItem = new DownloadItem(newID, aDownload);
    this.downloadItemsMap.set(aDownload, downloadItem);
    this.idToDownloadItemMap.set(newID, downloadItem);

    if (downloadItem.inProgress ||
        downloadItem.matchesSearch(this.searchTerms, this.searchAttributes)) {
      this.downloadView.appendChild(downloadItem.element);

      // Because of the joys of XBL, we can't update the buttons until the
      // download object is in the document.
      downloadItem.updateView();

      // We might have added an item to an empty list, so update button.
      this.updateClearListButton();
    }
  },

  onDownloadChanged: function(aDownload) {
    let downloadItem = this.downloadItemsMap.get(aDownload);
    if (!downloadItem) {
      Cu.reportError("Download doesn't exist.");
      return;
    }

    downloadItem.updateView();

    this.onUpdateProgress();

    // The download may have changed state, so update the clear list button.
    this.updateClearListButton();

    if (downloadItem.done) {
      // Move the download below active if it should stay in the list.
      if (downloadItem.matchesSearch(this.searchTerms, this.searchAttributes)) {
        // Iterate down until we find a non-active download.
        let next = this.element.nextSibling;
        while (next && next.inProgress) {
          next = next.nextSibling;
        }

        // Move the item.
        this.downloadView.insertBefore(this.element, next);
      } else {
        this.removeFromView(downloadItem);
      }

      return true;
    }

    return false;
  },

  onDownloadRemoved: function(aDownload) {
    let downloadItem = this.downloadItemsMap.get(aDownload);
    if (!downloadItem) {
      Cu.reportError("Download doesn't exist.");
      return;
    }

    this.downloadItemsMap.delete(aDownload);
    this.idToDownloadItemMap.delete(downloadItem.id);

    this.removeFromView(downloadItem);
  },

  removeFromView: function(aDownloadItem) {
    // Make sure we have an item to remove.
    if (!aDownloadItem.element) {
      return;
    }

    let index = this.downloadView.selectedIndex;
    this.downloadView.removeChild(aDownloadItem.element);
    this.downloadView.selectedIndex = Math.min(index, this.downloadView.itemCount - 1);

    // We might have removed the last item, so update the clear list button.
    this.updateClearListButton();
  },
  _getDownloadItemForElement(element) {
    return this.idToDownloadItemMap.get(element.getAttribute("id"));
  },
  _getSelectedDownloadItem() {
    let dl = this.downloadView.selectedItem;
    return dl ? this._getDownloadItemForElement(dl) : null;
  },
};

function Startup() {
  // Convert strings to those in the string bundle.
  let sb = document.getElementById("downloadStrings");
  let strings = ["paused", "cannotPause", "doneStatus", "doneSize",
                 "doneSizeUnknown", "stateFailed", "stateCanceled",
                 "stateBlocked", "stateBlockedPolicy", "stateDirty",
                 "downloadsTitleFiles", "downloadsTitlePercent",];

  for (let name of strings) {
    gStr[name] = sb.getString(name);
  }

  gDownloadList.init();

  let DownloadTaskbarProgress =
    Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
  DownloadTaskbarProgress.onDownloadWindowLoad(window);
}

function Shutdown() {
  Downloads.getList(Downloads.ALL)
           .then(list => list.removeView(gDownloadList))
           .catch(Cu.reportError);
}
