/* 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.CallConnectionWebSocket = (function() {
  "use strict";

  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;

  // Response timeout is 5 seconds as per API.
  var kResponseTimeout = 5000;

  /**
   * Handles a websocket specifically for a call connection.
   *
   * There should be one of these created for each call connection.
   *
   * options items:
   * - url             The url of the websocket to connect to.
   * - callId          The call id for the call
   * - websocketToken  The authentication token for the websocket
   *
   * @param {Object} options The options for this websocket.
   */
  function CallConnectionWebSocket(options) {
    this.options = options || {};

    if (!this.options.url) {
      throw new Error("No url in options");
    }
    if (!this.options.callId) {
      throw new Error("No callId in options");
    }
    if (!this.options.websocketToken) {
      throw new Error("No websocketToken in options");
    }

    this._lastServerState = "init";

    // Set loop.debug.sdk to true in the browser, or standalone:
    // localStorage.setItem("debug.websocket", true);
    this._debugWebSocket =
      loop.shared.utils.getBoolPreference("debug.websocket");

    _.extend(this, Backbone.Events);
  };

  CallConnectionWebSocket.prototype = {
    /**
     * Start the connection to the websocket.
     *
     * @return {Promise} A promise that resolves when the websocket
     *                   server connection is open and "hello"s have been
     *                   exchanged. It is rejected if there is a failure in
     *                   connection or the initial exchange of "hello"s.
     */
    promiseConnect: function() {
      var promise = new Promise(
        function(resolve, reject) {
          this.socket = new WebSocket(this.options.url);
          this.socket.onopen = this._onopen.bind(this);
          this.socket.onmessage = this._onmessage.bind(this);
          this.socket.onerror = this._onerror.bind(this);
          this.socket.onclose = this._onclose.bind(this);

          var timeout = setTimeout(function() {
            if (this.connectDetails && this.connectDetails.reject) {
              this.connectDetails.reject(WEBSOCKET_REASONS.TIMEOUT);
              this._clearConnectionFlags();
            }
          }.bind(this), kResponseTimeout);
          this.connectDetails = {
            resolve: resolve,
            reject: reject,
            timeout: timeout
          };
        }.bind(this));

      return promise;
    },

    /**
     * Closes the websocket. This shouldn't be the normal action as the server
     * will normally close the socket. Only in bad error cases, or where we need
     * to close the socket just before closing the window (to avoid an error)
     * should we call this.
     */
    close: function() {
      if (this.socket) {
        this.socket.close();
      }
    },

    _clearConnectionFlags: function() {
      clearTimeout(this.connectDetails.timeout);
      delete this.connectDetails;
    },

    /**
     * Internal function called to resolve the connection promise.
     *
     * It will log an error if no promise is found.
     *
     * @param {String} progressState The current state of progress of the
     *                               websocket.
     */
    _completeConnection: function(progressState) {
      if (this.connectDetails && this.connectDetails.resolve) {
        this.connectDetails.resolve(progressState);
        this._clearConnectionFlags();
        return;
      }

      console.error("Failed to complete connection promise - no promise available");
    },

    /**
     * Checks if the websocket is connecting, and rejects the connection
     * promise if appropriate.
     *
     * @param {Object} event The event to reject the promise with if
     *                       appropriate.
     */
    _checkConnectionFailed: function(event) {
      if (this.connectDetails && this.connectDetails.reject) {
        this.connectDetails.reject(event);
        this._clearConnectionFlags();
        return true;
      }

      return false;
    },

    /**
     * Notifies the server that the user has declined the call.
     */
    decline: function() {
      this._send({
        messageType: "action",
        event: "terminate",
        reason: WEBSOCKET_REASONS.REJECT
      });
    },

    /**
     * Notifies the server that the user has accepted the call.
     */
    accept: function() {
      this._send({
        messageType: "action",
        event: "accept"
      });
    },

    /**
     * Notifies the server that the outgoing media is up, and the
     * incoming media is being received.
     */
    mediaUp: function() {
      this._send({
        messageType: "action",
        event: "media-up"
      });
    },

    /**
     * Notifies the server that the outgoing call is cancelled by the
     * user.
     */
    cancel: function() {
      this._send({
        messageType: "action",
        event: "terminate",
        reason: WEBSOCKET_REASONS.CANCEL
      });
    },

    /**
     * Notifies the server that something failed during setup.
     */
    mediaFail: function() {
      this._send({
        messageType: "action",
        event: "terminate",
        reason: WEBSOCKET_REASONS.MEDIA_FAIL
      });
    },

    /**
     * Sends data on the websocket.
     *
     * @param {Object} data The data to send.
     */
    _send: function(data) {
      this._log("WS Sending", data);

      this.socket.send(JSON.stringify(data));
    },

    /**
     * Used to determine if the server state is in a completed state, i.e.
     * the server has determined the connection is terminated or connected.
     *
     * @return True if the last received state is terminated or connected.
     */
    get _stateIsCompleted() {
      return this._lastServerState === "terminated" ||
             this._lastServerState === "connected";
    },

    /**
     * Called when the socket is open. Automatically sends a "hello"
     * message to the server.
     */
    _onopen: function() {
      // Auto-register with the server.
      this._send({
        messageType: "hello",
        callId: this.options.callId,
        auth: this.options.websocketToken
      });
    },

    /**
     * Called when a message is received from the server.
     *
     * @param {Object} event The websocket onmessage event.
     */
    _onmessage: function(event) {
      var msgData;
      try {
        msgData = JSON.parse(event.data);
      } catch (x) {
        console.error("Error parsing received message:", x);
        return;
      }

      this._log("WS Receiving", event.data);

      var previousState = this._lastServerState;
      this._lastServerState = msgData.state;

      switch(msgData.messageType) {
        case "hello":
          this._completeConnection(msgData.state);
          break;
        case "progress":
          this.trigger("progress:" + msgData.state);
          this.trigger("progress", msgData, previousState);
          break;
      }
    },

    /**
     * Called when there is an error on the websocket.
     *
     * @param {Object} event A simple error event.
     */
    _onerror: function(event) {
      this._log("WS Error", event);

      if (!this._stateIsCompleted &&
          !this._checkConnectionFailed(event)) {
        this.trigger("error", event);
      }
    },

    /**
     * Called when the websocket is closed.
     *
     * @param {CloseEvent} event The details of the websocket closing.
     */
    _onclose: function(event) {
      this._log("WS Close", event);

      // If the websocket goes away when we're not in a completed state
      // then its an error. So we either pass it back via the connection
      // promise, or trigger the closed event.
      if (!this._stateIsCompleted &&
          !this._checkConnectionFailed(event)) {
        this.trigger("closed", event);
      }
    },

    /**
     * Logs debug to the console.
     *
     * Parameters: same as console.log
     */
    _log: function() {
      if (this._debugWebSocket) {
        console.log.apply(console, arguments);
      }
    }
  };

  return CallConnectionWebSocket;
})();
