/* 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 ToolkitModules = {};

ChromeUtils.defineESModuleGetters(ToolkitModules, {
  EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
});

const AlertNotification = Components.Constructor(
  "@mozilla.org/alert-notification;1",
  "nsIAlertNotification",
  "initWithObject"
);

var { ignoreEvent } = ExtensionCommon;

// Manages a notification popup (notifications API) created by the extension.
function Notification(context, notificationsMap, id, options) {
  this.notificationsMap = notificationsMap;
  this.id = id;
  this.options = options;

  // Set before calling into nsIAlertsService, because the notification may be
  // closed during the call.
  notificationsMap.set(id, this);

  this.showAlert(context);
}

Notification.prototype = {
  clear() {
    try {
      let svc = Cc["@mozilla.org/alerts-service;1"].getService(
        Ci.nsIAlertsService
      );
      svc.closeAlert(this.id);
    } catch (e) {
      // This will fail if the OS doesn't support this function.
    }
    this.notificationsMap.delete(this.id);
  },

  async showAlert(context) {
    let imageURL;
    if (this.options.iconUrl) {
      imageURL = context.extension.baseURI.resolve(this.options.iconUrl);
    }

    let image;
    try {
      if (imageURL) {
        let uri = ToolkitModules.NetUtil.newURI(imageURL);
        let channel = ToolkitModules.NetUtil.newChannel({
          uri,
          securityFlags:
            Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
          contentPolicyType: Ci.nsIContentPolicy.TYPE_IMAGE,
          loadingPrincipal: context.principal,
        });
        image = await ChromeUtils.fetchDecodedImage(uri, channel);
      }
    } catch (e) {
      console.error(`Loading image ${imageURL} for Notification failed: ${e}`);
      imageURL = undefined;
    }

    // Return if the notification was explicitly closed while waiting for the image.
    if (!this.notificationsMap.has(this.id)) {
      this.observe(null, "alertfinished");
      return;
    }

    try {
      let svc = Cc["@mozilla.org/alerts-service;1"].getService(
        Ci.nsIAlertsService
      );
      // Principal is not set because doing so reveals buttons to control
      // notification preferences, which are currently not implemented for
      // notifications triggered via this extension API (bug 1589693).
      let alert = new AlertNotification({
        // The imageURL and image should be in sync. Only Android still needs
        // the imageURL instead of image.
        imageURL,
        image,
        title: this.options.title,
        text: this.options.message,
        textClickable: true,
        name: this.id,
        inPrivateBrowsing: context.incognito,
      });
      svc.showAlert(alert, this);
    } catch (e) {
      // This will fail if alerts aren't available on the system.
      this.observe(null, "alertfinished");
    }
  },

  observe(subject, topic) {
    switch (topic) {
      case "alertclickcallback":
        this.notificationsMap.emit("clicked", this.id);
        break;
      case "alertfinished":
        this.notificationsMap.emit("closed", this.id);
        this.notificationsMap.delete(this.id);
        break;
      case "alertshow":
        this.notificationsMap.emit("shown", this.id);
        break;
    }
  },
};

this.notifications = class extends ExtensionAPIPersistent {
  constructor(extension) {
    super(extension);

    this.nextId = 0;
    this.notificationsMap = new Map();
    ToolkitModules.EventEmitter.decorate(this.notificationsMap);
  }

  onShutdown() {
    for (let notification of this.notificationsMap.values()) {
      notification.clear();
    }
  }

  PERSISTENT_EVENTS = {
    onClosed({ fire }) {
      let listener = (event, notificationId) => {
        // TODO Bug 1413188, Support the byUser argument.
        fire.async(notificationId, true);
      };

      this.notificationsMap.on("closed", listener);
      return {
        unregister: () => {
          this.notificationsMap.off("closed", listener);
        },
        convert(_fire) {
          fire = _fire;
        },
      };
    },
    onClicked({ fire }) {
      let listener = (event, notificationId) => {
        fire.async(notificationId);
      };

      this.notificationsMap.on("clicked", listener);
      return {
        unregister: () => {
          this.notificationsMap.off("clicked", listener);
        },
        convert(_fire) {
          fire = _fire;
        },
      };
    },
    onShown({ fire }) {
      let listener = (event, notificationId) => {
        fire.async(notificationId);
      };

      this.notificationsMap.on("shown", listener);
      return {
        unregister: () => {
          this.notificationsMap.off("shown", listener);
        },
        convert(_fire) {
          fire = _fire;
        },
      };
    },
  };

  getAPI(context) {
    let notificationsMap = this.notificationsMap;

    return {
      notifications: {
        create: (notificationId, options) => {
          if (!notificationId) {
            notificationId = String(this.nextId++);
          }

          if (notificationsMap.has(notificationId)) {
            notificationsMap.get(notificationId).clear();
          }

          new Notification(context, notificationsMap, notificationId, options);

          return Promise.resolve(notificationId);
        },

        clear: function (notificationId) {
          if (notificationsMap.has(notificationId)) {
            notificationsMap.get(notificationId).clear();
            return Promise.resolve(true);
          }
          return Promise.resolve(false);
        },

        getAll: function () {
          let result = {};
          notificationsMap.forEach((value, key) => {
            result[key] = value.options;
          });
          return Promise.resolve(result);
        },

        onClosed: new EventManager({
          context,
          module: "notifications",
          event: "onClosed",
          extensionApi: this,
        }).api(),

        onClicked: new EventManager({
          context,
          module: "notifications",
          event: "onClicked",
          extensionApi: this,
        }).api(),

        onShown: new EventManager({
          context,
          module: "notifications",
          event: "onShown",
          extensionApi: this,
        }).api(),

        // TODO Bug 1190681, implement button support.
        onButtonClicked: ignoreEvent(context, "notifications.onButtonClicked"),
      },
    };
  }
};
