/* 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 React = require("devtools/client/shared/vendor/react");
const Immutable = require("devtools/client/shared/vendor/immutable");

// Shortcuts
const { PropTypes, createClass, DOM } = React;
const { div, span, button } = DOM;

// Priority Levels
const PriorityLevels = {
  PRIORITY_INFO_LOW: 1,
  PRIORITY_INFO_MEDIUM: 2,
  PRIORITY_INFO_HIGH: 3,
  PRIORITY_WARNING_LOW: 4,
  PRIORITY_WARNING_MEDIUM: 5,
  PRIORITY_WARNING_HIGH: 6,
  PRIORITY_CRITICAL_LOW: 7,
  PRIORITY_CRITICAL_MEDIUM: 8,
  PRIORITY_CRITICAL_HIGH: 9,
  PRIORITY_CRITICAL_BLOCK: 10,
};

/**
 * This component represents Notification Box - HTML alternative for
 * <xul:notifictionbox> binding.
 *
 * See also MDN for more info about <xul:notificationbox>:
 * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/notificationbox
 */
var NotificationBox = createClass({
  displayName: "NotificationBox",

  propTypes: {
    // List of notifications appended into the box.
    notifications: PropTypes.arrayOf(PropTypes.shape({
      // label to appear on the notification.
      label: PropTypes.string.isRequired,

      // Value used to identify the notification
      value: PropTypes.string.isRequired,

      // URL of image to appear on the notification. If "" then an icon
      // appropriate for the priority level is used.
      image: PropTypes.string.isRequired,

      // Notification priority; see Priority Levels.
      priority: PropTypes.number.isRequired,

      // Array of button descriptions to appear on the notification.
      buttons: PropTypes.arrayOf(PropTypes.shape({
        // Function to be called when the button is activated.
        // This function is passed three arguments:
        // 1) the NotificationBox component the button is associated with
        // 2) the button description as passed to appendNotification.
        // 3) the element which was the target of the button press event.
        // If the return value from this function is not True, then the
        // notification is closed. The notification is also not closed
        // if an error is thrown.
        callback: PropTypes.func.isRequired,

        // The label to appear on the button.
        label: PropTypes.string.isRequired,

        // The accesskey attribute set on the <button> element.
        accesskey: PropTypes.string,
      })),

      // A function to call to notify you of interesting things that happen
      // with the notification box.
      eventCallback: PropTypes.func,
    })),
  },

  getInitialState() {
    return {
      notifications: new Immutable.OrderedMap()
    };
  },

  /**
   * Create a new notification and display it. If another notification is
   * already present with a higher priority, the new notification will be
   * added behind it. See `propTypes` for arguments description.
   */
  appendNotification(label, value, image, priority, buttons = [],
    eventCallback) {
    // Priority level must be within expected interval
    // (see priority levels at the top of this file).
    if (priority < PriorityLevels.PRIORITY_INFO_LOW ||
      priority > PriorityLevels.PRIORITY_CRITICAL_BLOCK) {
      throw new Error("Invalid notification priority " + priority);
    }

    // Custom image URL is not supported yet.
    if (image) {
      throw new Error("Custom image URL is not supported yet");
    }

    let type = "warning";
    if (priority >= PriorityLevels.PRIORITY_CRITICAL_LOW) {
      type = "critical";
    } else if (priority <= PriorityLevels.PRIORITY_INFO_HIGH) {
      type = "info";
    }

    let notifications = this.state.notifications.set(value, {
      label: label,
      value: value,
      image: image,
      priority: priority,
      type: type,
      buttons: buttons,
      eventCallback: eventCallback,
    });

    // High priorities must be on top.
    notifications = notifications.sortBy((val, key) => {
      return -val.priority;
    });

    this.setState({
      notifications: notifications
    });
  },

  /**
   * Remove specific notification from the list.
   */
  removeNotification(notification) {
    this.close(this.state.notifications.get(notification.value));
  },

  /**
   * Returns an object that represents a notification. It can be
   * used to close it.
   */
  getNotificationWithValue(value) {
    let notification = this.state.notifications.get(value);
    if (!notification) {
      return null;
    }

    // Return an object that can be used to remove the notification
    // later (using `removeNotification` method) or directly close it.
    return Object.assign({}, notification, {
      close: () => {
        this.close(notification);
      }
    });
  },

  getCurrentNotification() {
    return this.state.notifications.first();
  },

  /**
   * Close specified notification.
   */
  close(notification) {
    if (!notification) {
      return;
    }

    if (notification.eventCallback) {
      notification.eventCallback("removed");
    }

    this.setState({
      notifications: this.state.notifications.remove(notification.value)
    });
  },

  /**
   * Render a button. A notification can have a set of custom buttons.
   * These are used to execute custom callback.
   */
  renderButton(props, notification) {
    let onClick = event => {
      if (props.callback) {
        let result = props.callback(this, props, event.target);
        if (!result) {
          this.close(notification);
        }
        event.stopPropagation();
      }
    };

    return (
      button({
        key: props.label,
        className: "notification-button",
        accesskey: props.accesskey,
        onClick: onClick},
        props.label
      )
    );
  },

  /**
   * Render a notification.
   */
  renderNotification(notification) {
    return (
      div({
        key: notification.value,
        className: "notification",
        "data-type": notification.type},
        div({className: "notificationInner"},
          div({className: "details"},
            div({
              className: "messageImage",
              "data-type": notification.type}),
            span({className: "messageText"},
              notification.label
            ),
            notification.buttons.map(props =>
              this.renderButton(props, notification)
            )
          ),
          div({
            className: "messageCloseButton",
            onClick: this.close.bind(this, notification)}
          )
        )
      )
    );
  },

  /**
   * Render the top (highest priority) notification. Only one
   * notification is rendered at a time.
   */
  render() {
    let notification = this.state.notifications.first();
    let content = notification ?
      this.renderNotification(notification) :
      null;

    return div({className: "notificationbox"},
      content
    );
  },
});

module.exports.NotificationBox = NotificationBox;
module.exports.PriorityLevels = PriorityLevels;
