# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
# 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/.

// Removes a doorhanger notification if all of the installs it was notifying
// about have ended in some way.
function removeNotificationOnEnd(notification, installs) {
  let count = installs.length;

  function maybeRemove(install) {
    install.removeListener(this);

    if (--count == 0) {
      // Check that the notification is still showing
      let current = PopupNotifications.getNotification(notification.id, notification.browser);
      if (current === notification)
        notification.remove();
    }
  }

  for (let install of installs) {
    install.addListener({
      onDownloadCancelled: maybeRemove,
      onDownloadFailed: maybeRemove,
      onInstallFailed: maybeRemove,
      onInstallEnded: maybeRemove
    });
  }
}

const gXPInstallObserver = {
  _findChildShell: function (aDocShell, aSoughtShell) {
    if (aDocShell == aSoughtShell) {
      return aDocShell;
    }

    var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem);
    for (var i = 0; i < node.childCount; ++i) {
      var docShell = node.getChildAt(i);
      docShell = this._findChildShell(docShell, aSoughtShell);
      if (docShell == aSoughtShell) {
        return docShell;
      }
    }
    return null;
  },

  _getBrowser: function (aDocShell) {
    for (let browser of gBrowser.browsers) {
      if (this._findChildShell(browser.docShell, aDocShell)) {
        return browser;
      }
    }
    return null;
  },

  observe: function (aSubject, aTopic, aData) {
    var brandBundle = document.getElementById("bundle_brand");
    var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo);
    var browser = installInfo.browser;

    // Make sure the browser is still alive.
    if (!browser || gBrowser.browsers.indexOf(browser) == -1) {
      return;
    }

    const anchorID = "addons-notification-icon";
    var messageString, action;
    var brandShortName = brandBundle.getString("brandShortName");

    var notificationID = aTopic;
    // Make notifications persist a minimum of 30 seconds
    var options = { timeout: Date.now() + 30000 };

    switch (aTopic) {
      case "addon-install-disabled": {
        notificationID = "xpinstall-disabled"

        if (gPrefService.prefIsLocked("xpinstall.enabled")) {
          messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked");
          buttons = [];
        } else {
          messageString = gNavigatorBundle.getString("xpinstallDisabledMessage");

          action = {
            label: gNavigatorBundle.getString("xpinstallDisabledButton"),
            accessKey: gNavigatorBundle.getString("xpinstallDisabledButton.accesskey"),
            callback: function editPrefs() {
              gPrefService.setBoolPref("xpinstall.enabled", true);
            }
          };
        }

        PopupNotifications.show(browser, notificationID, messageString, anchorID,
                                action, null, options);
        break;
      }
      case "addon-install-origin-blocked": {
        messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarningOrigin",
                          [brandShortName]);

        let popup = PopupNotifications.show(browser, notificationID,
                                            messageString, anchorID,
                                            null, null, options);
        removeNotificationOnEnd(popup, installInfo.installs);
        break;
      }
      case "addon-install-blocked": {
        let originatingHost;
        try {
          originatingHost = installInfo.originatingURI.host;
        } catch(ex) {
          // Need to deal with missing originatingURI and with about:/data: URIs more gracefully,
          // see bug 1063418 - but for now, bail:
          return;
        }
        messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarning",
                          [brandShortName, originatingHost]);

        action = {
          label: gNavigatorBundle.getString("xpinstallPromptAllowButton"),
          accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"),
          callback: function() {
            installInfo.install();
          }
        };

        let popup = PopupNotifications.show(browser, notificationID, messageString,
                                            anchorID, action, null, options);
        removeNotificationOnEnd(popup, installInfo.installs);
        break;
      }
      case "addon-install-started": {
        var needsDownload = function needsDownload(aInstall) {
          return aInstall.state != AddonManager.STATE_DOWNLOADED;
        }
        // If all installs have already been downloaded then there is no need to
        // show the download progress
        if (!installInfo.installs.some(needsDownload)) {
          return;
        }
        notificationID = "addon-progress";
        messageString = gNavigatorBundle.getString("addonDownloading");
        messageString = PluralForm.get(installInfo.installs.length, messageString);
        options.installs = installInfo.installs;
        options.contentWindow = browser.contentWindow;
        options.sourceURI = browser.currentURI;
        options.eventCallback = function(aEvent) {
          if (aEvent != "removed") {
            return;
          }
          options.contentWindow = null;
          options.sourceURI = null;
        };
        PopupNotifications.show(browser, notificationID, messageString, anchorID,
                                null, null, options);
        break;
      }
      case "addon-install-failed": {
        // TODO This isn't terribly ideal for the multiple failure case
        for (let install of installInfo.installs) {
          let host = (installInfo.originatingURI instanceof Ci.nsIStandardURL) &&
                     installInfo.originatingURI.host;
          if (!host) {
            host = (install.sourceURI instanceof Ci.nsIStandardURL) &&
                   install.sourceURI.host;
          }

          let error = (host || install.error == 0) ? "addonError" : "addonLocalError";
          if (install.error != 0) {
            error += install.error;
          } else if (install.addon.jetsdk) {
            error += "JetSDK";
          } else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
            error += "Blocklisted";
          } else {
            error += "Incompatible";
          }

          messageString = gNavigatorBundle.getString(error);
          messageString = messageString.replace("#1", install.name);
          if (host) {
            messageString = messageString.replace("#2", host);
          }
          messageString = messageString.replace("#3", brandShortName);
          messageString = messageString.replace("#4", Services.appinfo.version);

          PopupNotifications.show(browser, notificationID, messageString, anchorID,
                                  action, null, options);
        }
        break;
      }
      case "addon-install-complete": {
        var needsRestart = installInfo.installs.some(function(i) {
          return i.addon.pendingOperations != AddonManager.PENDING_NONE;
        });

        if (needsRestart) {
          messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart");
          action = {
            label: gNavigatorBundle.getString("addonInstallRestartButton"),
            accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"),
            callback: function() {
              Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
            }
          };
        } else {
          messageString = gNavigatorBundle.getString("addonsInstalled");
          action = null;
        }

        messageString = PluralForm.get(installInfo.installs.length, messageString);
        messageString = messageString.replace("#1", installInfo.installs[0].name);
        messageString = messageString.replace("#2", installInfo.installs.length);
        messageString = messageString.replace("#3", brandShortName);

        // Remove notificaion on dismissal, since it's possible to cancel the
        // install through the addons manager UI, making the "restart" prompt
        // irrelevant.
        options.removeOnDismissal = true;

        PopupNotifications.show(browser, notificationID, messageString, anchorID,
                                action, null, options);
        break;
      }
    }
  }
};

/*
 * When addons are installed/uninstalled, check and see if the number of items
 * on the add-on bar changed:
 * - If an add-on was installed, incrementing the count, show the bar.
 * - If an add-on was uninstalled, and no more items are left, hide the bar.
 */
var AddonsMgrListener = {
  get addonBar() document.getElementById("addon-bar"),
  get statusBar() document.getElementById("status-bar"),
  getAddonBarItemCount: function() {
    // Take into account the contents of the status bar shim for the count.
    var itemCount = this.statusBar.childNodes.length;

    var defaultOrNoninteractive = this.addonBar.getAttribute("defaultset")
                                      .split(",")
                                      .concat(["separator", "spacer", "spring"]);
    for (let item of this.addonBar.currentSet.split(",")) {
      if (defaultOrNoninteractive.indexOf(item) == -1) {
        itemCount++;
      }
    }

    return itemCount;
  },
  onInstalling: function(aAddon) {
    this.lastAddonBarCount = this.getAddonBarItemCount();
  },
  onInstalled: function(aAddon) {
    if (this.getAddonBarItemCount() > this.lastAddonBarCount) {
      setToolbarVisibility(this.addonBar, true);
    }
  },
  onUninstalling: function(aAddon) {
    this.lastAddonBarCount = this.getAddonBarItemCount();
  },
  onUninstalled: function(aAddon) {
    if (this.getAddonBarItemCount() == 0) {
      setToolbarVisibility(this.addonBar, false);
    }
  },
  onEnabling: function(aAddon) {
    return this.onInstalling();
  },
  onEnabled: function(aAddon) {
    return this.onInstalled();
  },
  onDisabling: function(aAddon) {
    return this.onUninstalling();
  },
  onDisabled: function(aAddon) {
    return this.onUninstalled();
  }
};

#ifdef MOZ_PERSONAS
var LightWeightThemeWebInstaller = {
  handleEvent: function (event) {
    switch (event.type) {
      case "InstallBrowserTheme":
      case "PreviewBrowserTheme":
      case "ResetBrowserThemePreview":
        // ignore requests from background tabs
        if (event.target.ownerDocument.defaultView.top != content) {
          return;
        }
    }
    switch (event.type) {
      case "InstallBrowserTheme":
        this._installRequest(event);
        break;
      case "PreviewBrowserTheme":
        this._preview(event);
        break;
      case "ResetBrowserThemePreview":
        this._resetPreview(event);
        break;
      case "pagehide":
      case "TabSelect":
        this._resetPreview();
        break;
    }
  },

  get _manager () {
    var temp = {};
    Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
    delete this._manager;
    return this._manager = temp.LightweightThemeManager;
  },

  _installRequest: function (event) {
    var node = event.target;
    var data = this._getThemeFromNode(node);
    if (!data) {
      return;
    }

    if (this._isAllowed(node)) {
      this._install(data);
      return;
    }

    var allowButtonText = gNavigatorBundle.getString("lwthemeInstallRequest.allowButton");
    var allowButtonAccesskey = gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey");
    var message = gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message",
                                                      [node.ownerDocument.location.host]);
    var buttons = [{
      label: allowButtonText,
      accessKey: allowButtonAccesskey,
      callback: function () {
        LightWeightThemeWebInstaller._install(data);
      }
    }];

    this._removePreviousNotifications();

    var notificationBox = gBrowser.getNotificationBox();
    var notificationBar =
      notificationBox.appendNotification(message, "lwtheme-install-request", "",
                                         notificationBox.PRIORITY_INFO_MEDIUM,
                                         buttons);
    notificationBar.persistence = 1;
  },

  _install: function (newLWTheme) {
    var previousLWTheme = this._manager.currentTheme;

    var listener = {
      onEnabling: function(aAddon, aRequiresRestart) {
        if (!aRequiresRestart) {
          return;
        }

        let messageString = gNavigatorBundle.getFormattedString("lwthemeNeedsRestart.message",
                                                                [aAddon.name], 1);

        let action = {
          label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"),
          accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"),
          callback: function () {
            Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
          }
        };

        let options = { timeout: Date.now() + 30000 };

        PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change",
                                messageString, "addons-notification-icon",
                                action, null, options);
      },

      onEnabled: function(aAddon) {
        LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme);
      }
    };

    AddonManager.addAddonListener(listener);
    this._manager.currentTheme = newLWTheme;
    AddonManager.removeAddonListener(listener);
  },

  _postInstallNotification: function (newTheme, previousTheme) {
    function text(id) {
      return gNavigatorBundle.getString("lwthemePostInstallNotification." + id);
    }

    var buttons = [{
      label: text("undoButton"),
      accessKey: text("undoButton.accesskey"),
      callback: function () {
        LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id);
        LightWeightThemeWebInstaller._manager.currentTheme = previousTheme;
      }
    }, {
      label: text("manageButton"),
      accessKey: text("manageButton.accesskey"),
      callback: function () {
        BrowserOpenAddonsMgr("addons://list/theme");
      }
    }];

    this._removePreviousNotifications();

    var notificationBox = gBrowser.getNotificationBox();
    var notificationBar =
      notificationBox.appendNotification(text("message"),
                                         "lwtheme-install-notification", "",
                                         notificationBox.PRIORITY_INFO_MEDIUM,
                                         buttons);
    notificationBar.persistence = 1;
    notificationBar.timeout = Date.now() + 20000; // 20 seconds
  },

  _removePreviousNotifications: function () {
    var box = gBrowser.getNotificationBox();

    ["lwtheme-install-request",
     "lwtheme-install-notification"].forEach(function (value) {
        var notification = box.getNotificationWithValue(value);
        if (notification)
          box.removeNotification(notification);
      });
  },

  _previewWindow: null,
  _preview: function (event) {
    if (!this._isAllowed(event.target)) {
      return;
    }

    var data = this._getThemeFromNode(event.target);
    if (!data) {
      return;
    }

    this._resetPreview();

    this._previewWindow = event.target.ownerDocument.defaultView;
    this._previewWindow.addEventListener("pagehide", this, true);
    gBrowser.tabContainer.addEventListener("TabSelect", this, false);

    this._manager.previewTheme(data);
  },

  _resetPreview: function (event) {
    if (!this._previewWindow ||
        (event && !this._isAllowed(event.target))) {
      return;
    }

    this._previewWindow.removeEventListener("pagehide", this, true);
    this._previewWindow = null;
    gBrowser.tabContainer.removeEventListener("TabSelect", this, false);

    this._manager.resetPreview();
  },

  _isAllowed: function (node) {
    var pm = Services.perms;

    var uri = node.ownerDocument.documentURIObject;
    return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
  },

  _getThemeFromNode: function (node) {
    return this._manager.parseTheme(node.getAttribute("data-browsertheme"),
                                    node.baseURI);
  }
}

/*
 * Listen for Lightweight Theme styling changes and update the browser's theme accordingly.
 */
var LightweightThemeListener = {
  _modifiedStyles: [],

  init: function () {
    XPCOMUtils.defineLazyGetter(this, "styleSheet", function() {
      for (let i = document.styleSheets.length - 1; i >= 0; i--) {
        let sheet = document.styleSheets[i];
        if (sheet.href == "chrome://browser/skin/browser-lightweightTheme.css")
          return sheet;
      }
    });

    Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
    Services.obs.addObserver(this, "lightweight-theme-optimized", false);
    if (document.documentElement.hasAttribute("lwtheme")) {
      this.updateStyleSheet(document.documentElement.style.backgroundImage);
    }
  },

  uninit: function () {
    Services.obs.removeObserver(this, "lightweight-theme-styling-update");
    Services.obs.removeObserver(this, "lightweight-theme-optimized");
  },

  /**
   * Append the headerImage to the background-image property of all rulesets in
   * browser-lightweightTheme.css.
   *
   * @param headerImage - a string containing a CSS image for the lightweight theme header.
   */
  updateStyleSheet: function(headerImage) {
    if (!this.styleSheet) {
      return;
    }
    this.substituteRules(this.styleSheet.cssRules, headerImage);
  },

  substituteRules: function(ruleList, headerImage, existingStyleRulesModified = 0) {
    let styleRulesModified = 0;
    for (let i = 0; i < ruleList.length; i++) {
      let rule = ruleList[i];
      if (rule instanceof Ci.nsIDOMCSSGroupingRule) {
        // Add the number of modified sub-rules to the modified count
        styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified);
      } else if (rule instanceof Ci.nsIDOMCSSStyleRule) {
        if (!rule.style.backgroundImage) {
          continue;
        }
        let modifiedIndex = existingStyleRulesModified + styleRulesModified;
        if (!this._modifiedStyles[modifiedIndex]) {
          this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage };
        }

        rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage;
        styleRulesModified++;
      } else {
        Cu.reportError("Unsupported rule encountered");
      }
    }
    return styleRulesModified;
  },

  // nsIObserver
  observe: function (aSubject, aTopic, aData) {
    if ((aTopic != "lightweight-theme-styling-update" && aTopic != "lightweight-theme-optimized") ||
          !this.styleSheet) {
      return;
    }

    if (aTopic == "lightweight-theme-optimized" && aSubject != window) {
      return;
    }

    let themeData = JSON.parse(aData);
    if (!themeData) {
      return;
    }
    this.updateStyleSheet("url(" + themeData.headerURL + ")");
  },
};
#endif
