/* 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/. */

/* global loop:true */

var loop = loop || {};
loop.store = loop.store || {};

(function(mozL10n) {
  "use strict";

  /**
   * Shared actions.
   * @type {Object}
   */
  var sharedActions = loop.shared.actions;

  /**
   * Maximum size given to createRoom; only 2 is supported (and is
   * always passed) because that's what the user-experience is currently
   * designed and tested to handle.
   * @type {Number}
   */
  var MAX_ROOM_CREATION_SIZE = loop.store.MAX_ROOM_CREATION_SIZE = 2;

  /**
   * The number of hours for which the room will exist - default 8 weeks
   * @type {Number}
   */
  var DEFAULT_EXPIRES_IN = loop.store.DEFAULT_EXPIRES_IN = 24 * 7 * 8;

  /**
   * Room validation schema. See validate.js.
   * @type {Object}
   */
  var roomSchema = {
    roomToken:    String,
    roomUrl:      String,
    roomName:     String,
    maxSize:      Number,
    participants: Array,
    ctime:        Number
  };

  /**
   * Room type. Basically acts as a typed object constructor.
   *
   * @param {Object} values Room property values.
   */
  function Room(values) {
    var validatedData = new loop.validate.Validator(roomSchema || {})
                                         .validate(values || {});
    for (var prop in validatedData) {
      this[prop] = validatedData[prop];
    }
  }

  loop.store.Room = Room;

  /**
   * Room store.
   *
   * @param {loop.Dispatcher} dispatcher  The dispatcher for dispatching actions
   *                                      and registering to consume actions.
   * @param {Object} options Options object:
   * - {mozLoop}         mozLoop          The MozLoop API object.
   * - {ActiveRoomStore} activeRoomStore  An optional substore for active room
   *                                      state.
   * - {Notifications}   notifications    An optional notifications item that is
   *                                      required if create actions are to be used
   */
  loop.store.RoomStore = loop.store.createStore({
    /**
     * Maximum size given to createRoom; only 2 is supported (and is
     * always passed) because that's what the user-experience is currently
     * designed and tested to handle.
     * @type {Number}
     */
    maxRoomCreationSize: MAX_ROOM_CREATION_SIZE,

    /**
     * The number of hours for which the room will exist - default 8 weeks
     * @type {Number}
     */
    defaultExpiresIn: DEFAULT_EXPIRES_IN,

    /**
     * Registered actions.
     * @type {Array}
     */
    actions: [
      "createRoom",
      "createdRoom",
      "createRoomError",
      "copyRoomUrl",
      "deleteRoom",
      "deleteRoomError",
      "emailRoomUrl",
      "getAllRooms",
      "getAllRoomsError",
      "openRoom",
      "renameRoom",
      "renameRoomError",
      "updateRoomList"
    ],

    initialize: function(options) {
      if (!options.mozLoop) {
        throw new Error("Missing option mozLoop");
      }
      this._mozLoop = options.mozLoop;
      this._notifications = options.notifications;

      if (options.activeRoomStore) {
        this.activeRoomStore = options.activeRoomStore;
        this.activeRoomStore.on("change",
                                this._onActiveRoomStoreChange.bind(this));
      }
    },

    getInitialStoreState: function() {
      return {
        activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
        error: null,
        pendingCreation: false,
        pendingInitialRetrieval: false,
        rooms: [],
      };
    },

    /**
     * Registers mozLoop.rooms events.
     */
    startListeningToRoomEvents: function() {
      // Rooms event registration
      this._mozLoop.rooms.on("add", this._onRoomAdded.bind(this));
      this._mozLoop.rooms.on("update", this._onRoomUpdated.bind(this));
      this._mozLoop.rooms.on("delete", this._onRoomRemoved.bind(this));
      this._mozLoop.rooms.on("refresh", this._onRoomsRefresh.bind(this));
    },

    /**
     * Updates active room store state.
     */
    _onActiveRoomStoreChange: function() {
      this.setStoreState({activeRoom: this.activeRoomStore.getStoreState()});
    },

    /**
     * Updates current room list when a new room is available.
     *
     * @param {String} eventName     The event name (unused).
     * @param {Object} addedRoomData The added room data.
     */
    _onRoomAdded: function(eventName, addedRoomData) {
      addedRoomData.participants = addedRoomData.participants || [];
      addedRoomData.ctime = addedRoomData.ctime || new Date().getTime();
      this.dispatchAction(new sharedActions.UpdateRoomList({
        // Ensure the room isn't part of the list already, then add it.
        roomList: this._storeState.rooms.filter(function(room) {
          return addedRoomData.roomToken !== room.roomToken;
        }).concat(new Room(addedRoomData))
      }));
    },

    /**
     * Executed when a room is updated.
     *
     * @param {String} eventName       The event name (unused).
     * @param {Object} updatedRoomData The updated room data.
     */
    _onRoomUpdated: function(eventName, updatedRoomData) {
      this.dispatchAction(new sharedActions.UpdateRoomList({
        roomList: this._storeState.rooms.map(function(room) {
          return room.roomToken === updatedRoomData.roomToken ?
                 updatedRoomData : room;
        })
      }));
    },

    /**
     * Executed when a room is deleted.
     *
     * @param {String} eventName       The event name (unused).
     * @param {Object} removedRoomData The removed room data.
     */
    _onRoomRemoved: function(eventName, removedRoomData) {
      this.dispatchAction(new sharedActions.UpdateRoomList({
        roomList: this._storeState.rooms.filter(function(room) {
          return room.roomToken !== removedRoomData.roomToken;
        })
      }));
    },

    /**
     * Executed when the user switches accounts.
     *
     * @param {String} eventName The event name (unused).
     */
    _onRoomsRefresh: function(eventName) {
      this.dispatchAction(new sharedActions.UpdateRoomList({
        roomList: []
      }));
    },

    /**
     * Maps and sorts the raw room list received from the mozLoop API.
     *
     * @param  {Array} rawRoomList Raw room list.
     * @return {Array}
     */
    _processRoomList: function(rawRoomList) {
      if (!rawRoomList) {
        return [];
      }
      return rawRoomList
        .map(function(rawRoom) {
          return new Room(rawRoom);
        })
        .slice()
        .sort(function(a, b) {
          return b.ctime - a.ctime;
        });
    },

    /**
     * Finds the next available room number in the provided room list.
     *
     * @param  {String} nameTemplate The room name template; should contain a
     *                               {{conversationLabel}} placeholder.
     * @return {Number}
     */
    findNextAvailableRoomNumber: function(nameTemplate) {
      var searchTemplate = nameTemplate.replace("{{conversationLabel}}", "");
      var searchRegExp = new RegExp("^" + searchTemplate + "(\\d+)$");

      var roomNumbers = this._storeState.rooms.map(function(room) {
        var match = searchRegExp.exec(room.roomName);
        return match && match[1] ? parseInt(match[1], 10) : 0;
      });

      if (!roomNumbers.length) {
        return 1;
      }

      return Math.max.apply(null, roomNumbers) + 1;
    },

    /**
     * Generates a room names against the passed template string.
     *
     * @param  {String} nameTemplate The room name template.
     * @return {String}
     */
    _generateNewRoomName: function(nameTemplate) {
      var roomLabel = this.findNextAvailableRoomNumber(nameTemplate);
      return nameTemplate.replace("{{conversationLabel}}", roomLabel);
    },

    /**
     * Creates a new room.
     *
     * @param {sharedActions.CreateRoom} actionData The new room information.
     */
    createRoom: function(actionData) {
      this.setStoreState({
        pendingCreation: true,
        error: null,
      });

      var roomCreationData = {
        roomName:  this._generateNewRoomName(actionData.nameTemplate),
        roomOwner: actionData.roomOwner,
        maxSize:   this.maxRoomCreationSize,
        expiresIn: this.defaultExpiresIn
      };

      this._notifications.remove("create-room-error");

      this._mozLoop.rooms.create(roomCreationData, function(err, createdRoom) {
        if (err) {
          this.dispatchAction(new sharedActions.CreateRoomError({error: err}));
          return;
        }

        this.dispatchAction(new sharedActions.CreatedRoom({
          roomToken: createdRoom.roomToken
        }));
      }.bind(this));
    },

    /**
     * Executed when a room has been created
     */
    createdRoom: function(actionData) {
      this.setStoreState({pendingCreation: false});

      // Opens the newly created room
      this.dispatchAction(new sharedActions.OpenRoom({
        roomToken: actionData.roomToken
      }));
    },

    /**
     * Executed when a room creation error occurs.
     *
     * @param {sharedActions.CreateRoomError} actionData The action data.
     */
    createRoomError: function(actionData) {
      this.setStoreState({
        error: actionData.error,
        pendingCreation: false
      });

      // XXX Needs a more descriptive error - bug 1109151.
      this._notifications.set({
        id: "create-room-error",
        level: "error",
        message: mozL10n.get("generic_failure_title")
      });
    },

    /**
     * Copy a room url.
     *
     * @param  {sharedActions.CopyRoomUrl} actionData The action data.
     */
    copyRoomUrl: function(actionData) {
      this._mozLoop.copyString(actionData.roomUrl);
      this._mozLoop.notifyUITour("Loop:RoomURLCopied");
    },

    /**
     * Emails a room url.
     *
     * @param  {sharedActions.EmailRoomUrl} actionData The action data.
     */
    emailRoomUrl: function(actionData) {
      loop.shared.utils.composeCallUrlEmail(actionData.roomUrl);
      this._mozLoop.notifyUITour("Loop:RoomURLEmailed");
    },

    /**
     * Creates a new room.
     *
     * @param {sharedActions.DeleteRoom} actionData The action data.
     */
    deleteRoom: function(actionData) {
      this._mozLoop.rooms.delete(actionData.roomToken, function(err) {
        if (err) {
         this.dispatchAction(new sharedActions.DeleteRoomError({error: err}));
        }
      }.bind(this));
    },

    /**
     * Executed when a room deletion error occurs.
     *
     * @param {sharedActions.DeleteRoomError} actionData The action data.
     */
    deleteRoomError: function(actionData) {
      this.setStoreState({error: actionData.error});
    },

    /**
     * Gather the list of all available rooms from the MozLoop API.
     */
    getAllRooms: function() {
      this.setStoreState({pendingInitialRetrieval: true});
      this._mozLoop.rooms.getAll(null, function(err, rawRoomList) {
        var action;

        this.setStoreState({pendingInitialRetrieval: false});

        if (err) {
          action = new sharedActions.GetAllRoomsError({error: err});
        } else {
          action = new sharedActions.UpdateRoomList({roomList: rawRoomList});
        }

        this.dispatchAction(action);

        // We can only start listening to room events after getAll() has been
        // called executed first.
        this.startListeningToRoomEvents();
      }.bind(this));
    },

    /**
     * Updates current error state in case getAllRooms failed.
     *
     * @param {sharedActions.GetAllRoomsError} actionData The action data.
     */
    getAllRoomsError: function(actionData) {
      this.setStoreState({error: actionData.error});
    },

    /**
     * Updates current room list.
     *
     * @param {sharedActions.UpdateRoomList} actionData The action data.
     */
    updateRoomList: function(actionData) {
      this.setStoreState({
        error: undefined,
        rooms: this._processRoomList(actionData.roomList)
      });
    },

    /**
     * Opens a room
     *
     * @param {sharedActions.OpenRoom} actionData The action data.
     */
    openRoom: function(actionData) {
      this._mozLoop.rooms.open(actionData.roomToken);
    },

    /**
     * Renames a room.
     *
     * @param {sharedActions.RenameRoom} actionData
     */
    renameRoom: function(actionData) {
      var oldRoomName = this.getStoreState("roomName");
      var newRoomName = actionData.newRoomName.trim();

      // Skip update if name is unchanged or empty.
      if (!newRoomName || oldRoomName === newRoomName) {
        return;
      }

      this.setStoreState({error: null});
      this._mozLoop.rooms.rename(actionData.roomToken, newRoomName,
        function(err) {
          if (err) {
            this.dispatchAction(new sharedActions.RenameRoomError({error: err}));
          }
        }.bind(this));
    },

    renameRoomError: function(actionData) {
      this.setStoreState({error: actionData.error});
    }
  });
})(document.mozL10n || navigator.mozL10n);
