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

this.EXPORTED_SYMBOLS = ["DownloadNotifications"];

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

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

XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
  "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");

var Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.i.bind(null, "DownloadNotifications");

XPCOMUtils.defineLazyGetter(this, "strings",
                            () => Services.strings.createBundle("chrome://browser/locale/browser.properties"));

Object.defineProperty(this, "window",
                      { get: () => Services.wm.getMostRecentWindow("navigator:browser") });

const kButtons = {
  PAUSE: new DownloadNotificationButton("pause",
                                        "drawable://pause",
                                        "alertDownloadsPause"),
  RESUME: new DownloadNotificationButton("resume",
                                         "drawable://play",
                                         "alertDownloadsResume"),
  CANCEL: new DownloadNotificationButton("cancel",
                                         "drawable://close",
                                         "alertDownloadsCancel")
};

var notifications = new Map();

var DownloadNotifications = {
  _notificationKey: "downloads",

  init: function () {
    Downloads.getList(Downloads.ALL)
             .then(list => list.addView(this))
             .then(() => this._viewAdded = true, Cu.reportError);

    // All click, cancel, and button presses will be handled by this handler as part of the Notifications callback API.
    Notifications.registerHandler(this._notificationKey, this);
  },

  onDownloadAdded: function (download) {
    // Don't create notifications for pre-existing succeeded downloads.
    // We still add notifications for canceled downloads in case the
    // user decides to retry the download.
    if (download.succeeded && !this._viewAdded) {
      return;
    }

    if (!ParentalControls.isAllowed(ParentalControls.DOWNLOAD)) {
      download.cancel().catch(Cu.reportError);
      download.removePartialData().catch(Cu.reportError);
      Snackbars.show(strings.GetStringFromName("downloads.disabledInGuest"), Snackbars.LENGTH_LONG);
      return;
    }

    let notification = new DownloadNotification(download);
    notifications.set(download, notification);
    notification.showOrUpdate();

    // If this is a new download, show a snackbar as well.
    if (this._viewAdded) {
      Snackbars.show(strings.GetStringFromName("alertDownloadsToast"), Snackbars.LENGTH_LONG);
    }
  },

  onDownloadChanged: function (download) {
    let notification = notifications.get(download);

    if (download.succeeded) {
      let file = new FileUtils.File(download.target.path);

      Snackbars.show(strings.formatStringFromName("alertDownloadSucceeded", [file.leafName], 1), Snackbars.LENGTH_LONG, {
        action: {
          label: strings.GetStringFromName("helperapps.open"),
          callback: () => {
            UITelemetry.addEvent("launch.1", "toast", null, "downloads");
            try {
              file.launch();
            } catch (ex) {
              this.showInAboutDownloads(download);
            }
            if (notification) {
              notification.hide();
            }
          }
        }});
    }

    if (notification) {
      notification.showOrUpdate();
    }
  },

  onDownloadRemoved: function (download) {
    let notification = notifications.get(download);
    if (!notification) {
      Cu.reportError("Download doesn't have a notification.");
      return;
    }

    notification.hide();
    notifications.delete(download);
  },

  _findDownloadForCookie: function(cookie) {
    return Downloads.getList(Downloads.ALL)
      .then(list => list.getAll())
      .then((downloads) => {
        for (let download of downloads) {
          let cookie2 = getCookieFromDownload(download);
          if (cookie2 === cookie) {
            return download;
          }
        }

        throw "Couldn't find download for " + cookie;
      });
  },

  onCancel: function(cookie) {
    // TODO: I'm not sure what we do here...
  },

  showInAboutDownloads: function (download) {
    let hash = "#" + window.encodeURIComponent(download.target.path);

    // Force using string equality to find a tab
    window.BrowserApp.selectOrAddTab("about:downloads" + hash, null, { startsWith: true });
  },

  onClick: function(cookie) {
    this._findDownloadForCookie(cookie).then((download) => {
      if (download.succeeded) {
        // We don't call Download.launch(), because there's (currently) no way to
        // tell if the file was actually launched or not, and we want to show
        // about:downloads if the launch failed.
        let file = new FileUtils.File(download.target.path);
        try {
          file.launch();
        } catch (ex) {
          this.showInAboutDownloads(download);
        }
      } else {
        ConfirmCancelPrompt.show(download);
      }
    }).catch(Cu.reportError);
  },

  onButtonClick: function(button, cookie) {
    this._findDownloadForCookie(cookie).then((download) => {
      if (button === kButtons.PAUSE.buttonId) {
        download.cancel().catch(Cu.reportError);
      } else if (button === kButtons.RESUME.buttonId) {
        download.start().catch(Cu.reportError);
      } else if (button === kButtons.CANCEL.buttonId) {
        download.cancel().catch(Cu.reportError);
        download.removePartialData().catch(Cu.reportError);
      }
    }).catch(Cu.reportError);
  },
};

function getCookieFromDownload(download) {
  return download.target.path +
         download.source.url +
         download.startTime;
}

function DownloadNotification(download) {
  this.download = download;
  this._fileName = OS.Path.basename(download.target.path);

  this.id = null;
}

DownloadNotification.prototype = {
  _updateFromDownload: function () {
    this._downloading = !this.download.stopped;
    this._paused = this.download.canceled && this.download.hasPartialData;
    this._succeeded = this.download.succeeded;

    this._show = this._downloading || this._paused || this._succeeded;
  },

  get options() {
    if (!this._show) {
      return null;
    }

    let options = {
      icon: "drawable://alert_download",
      cookie: getCookieFromDownload(this.download),
      handlerKey: DownloadNotifications._notificationKey
    };

    if (this._downloading) {
      options.icon = "drawable://alert_download_animation";
      if (this.download.currentBytes == 0) {
        this._updateOptionsForStatic(options, "alertDownloadsStart2");
      } else {
        let buttons = this.download.hasPartialData ? [kButtons.PAUSE, kButtons.CANCEL] :
                                                     [kButtons.CANCEL]
        this._updateOptionsForOngoing(options, buttons);
      }
    } else if (this._paused) {
      this._updateOptionsForOngoing(options, [kButtons.RESUME, kButtons.CANCEL]);
    } else if (this._succeeded) {
      options.persistent = false;
      this._updateOptionsForStatic(options, "alertDownloadsDone2");
    }

    return options;
  },

  _updateOptionsForStatic : function (options, titleName) {
    options.title = strings.GetStringFromName(titleName);
    options.message = this._fileName;
  },

  _updateOptionsForOngoing: function (options, buttons) {
    options.title = this._fileName;
    options.message = this.download.progress + "%";
    options.buttons = buttons;
    options.ongoing = true;
    options.progress = this.download.progress;
    options.persistent = true;
  },

  showOrUpdate: function () {
    this._updateFromDownload();

    if (this._show) {
      if (!this.id) {
        this.id = Notifications.create(this.options);
      } else if (!this.options.ongoing) {
        // We need to explictly cancel ongoing notifications,
        // since updating them to be non-ongoing doesn't seem
        // to work. See bug 1130834.
        Notifications.cancel(this.id);
        this.id = Notifications.create(this.options);
      } else {
        Notifications.update(this.id, this.options);
      }
    } else {
      this.hide();
    }
  },

  hide: function () {
    if (this.id) {
      Notifications.cancel(this.id);
      this.id = null;
    }
  },
};

var ConfirmCancelPrompt = {
  show: function (download) {
    // Open a prompt that offers a choice to cancel the download
    let title = strings.GetStringFromName("downloadCancelPromptTitle1");
    let message = strings.GetStringFromName("downloadCancelPromptMessage1");

    if (Services.prompt.confirm(null, title, message)) {
      download.cancel().catch(Cu.reportError);
      download.removePartialData().catch(Cu.reportError);
    }
  }
};

function DownloadNotificationButton(buttonId, iconUrl, titleStringName, onClicked) {
  this.buttonId = buttonId;
  this.title = strings.GetStringFromName(titleStringName);
  this.icon = iconUrl;
}