// ========================================================================== // Project: SC.WebSocket // Copyright: ©2013 Nicolas BADIA and contributors // License: Licensed under MIT license (see license.js) // ==========================================================================

sc_require('mixins/websocket_delegate');

/**

@class

Implements support for WebSocket.

Example Usage:

  var ws = SC.WebSocket.create({
    server: 'ws://server',
  }).connect();

  ws.notify(this, 'wsReceivedMessage');

  ws.send('message');

@since SproutCore 1.11
@extends SC.Object
@extends SC.DelegateSupport
@author Nicolas BADIA

*/ SC.WebSocket = SC.Object.extend(SC.DelegateSupport, SC.WebSocketDelegate, {

/**
  URL of the WebSocket server.

  @type String
  @default null
*/
server: null,

/**
  Determines if the browser support the WebSocket protocol.

  @type Boolean
  @default null
  @readOnly
*/
isSupported: null,

/**
  Determines if the connection is open or not.

  @type Boolean
  @readOnly
*/
isConnected: false,

/**
  In order to handle authentification, set `isAuth` to NO in the
  `webSocketDidOpen` delegate method just after sending a request to
  authentificate the connection. This way, any futher message will be put in
  queue until the server tels you the connection is authentified. Once it
  did, you should set `isAuth` to YES to resume the queue.

  If you don't need authentification, leave `isAuth` to null.

  @type Boolean
  @default null
*/
isAuth: null,

/**
  Processes the messages as JSON if possible.

  @type Boolean
  @default true
*/
isJSON: true,

/**
  A WebSocket delegate.

  @see SC.WebSocketDelegate
  @type {SC.WebSocketDelegate}
  @default null
*/
delegate: null,

/**
  Determines if we should attempt to try an automatic reconnection
  if the connection is close.

  @type {SC.WebSocketDelegate}
  @default null
*/
autoReconnect: true,

/**
  The interval in milliseconds to waits before trying a reconnection.

  @type {SC.WebSocketDelegate}
  @default null
*/
reconnectInterval: 10000, // 10 secondes

// ..........................................................
// PUBLIC METHODS
//

/**
  Call this method to open a connection.

  @returns {SC.WebSocket}
*/
connect: function() {
  var that = this;

  if (this.isSupported === null) this.set('isSupported', !!window.WebSocket);

  if (!this.isSupported || this.socket) return this;

  try {
    var socket = this.socket = new WebSocket(this.get('server'));

    socket.onopen = function() {
      SC.RunLoop.begin();
      that.onOpen.apply(that, arguments);
      SC.RunLoop.end();
    };

    socket.onmessage = function() {
      SC.RunLoop.begin();
      that.onMessage.apply(that, arguments);
      SC.RunLoop.end();
    };

    socket.onclose = function() {
      SC.RunLoop.begin();
      that.onClose.apply(that, arguments);
      SC.RunLoop.end();
    };

    socket.onerror = function() {
      SC.RunLoop.begin();
      that.onError.apply(that, arguments);
      SC.RunLoop.end();
    };
  } catch(e) {
    SC.error('An error has occurred while connnecting to the websocket server: '+e);
  }

  return this;
},

/**
  Call this method to close a connection.

  @param code {Number} A numeric value indicating the status code explaining why the connection is being closed. If this parameter is not specified, a default value of 1000 (indicating a normal "transaction complete" closure) is assumed.
  @param reason {String} A human-readable string explaining why the connection is closing. This string must be no longer than 123 bytes of UTF-8 text (not characters).
  @returns {SC.WebSocket}
*/
close: function(code, reason) {
  var socket = this.socket;

  if (socket && socket.readyState === SC.WebSocket.OPEN) {
    this.socket.close(code, reason);
  }

  return this;
},

/**
  Configures a callback to execute when an event happen. You must pass
  at least a target and action/method to this and optionally an event name.

  You may also pass additional arguments which will then be passed along to
  your callback.

  Example:

      var websocket = SC.WebSocket.create({ server: 'ws://server' }).connect();

      webSocket.notify('onopen', this, 'wsWasOpen');
      webSocket.notify('onmessage', this, 'wsReceivedMessage'); // You can ommit onmessage here
      webSocket.notify('onclose', this, 'wsWasClose');
      webSocket.notify('onerror', this, 'wsDidError');

  ## Callback Format

  Your notification callback should expect to receive the WebSocket object as
  the first parameter and the event or message; plus any additional parameters that you pass. If your callback handles the notification and to prevent further handling, it
  should return YES.

  @param target {String} String Event name.
  @param target {Object} The target object for the callback action.
  @param action {String|Function} The method name or function to call on the target.
  @returns {SC.WebSocket} The SC.WebSocket object.
*/
notify: function(event, target, action) {
  var args,
    i, len;

  if (SC.typeOf(event) !== SC.T_STRING) {
    // Fast arguments access.
    // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly.
    args = new Array(arguments.length - 2); //  SC.A(arguments).slice(2)
    for (i = 0, len = args.length; i < len; i++) { args[i] = arguments[i + 2]; }

    // Shift the arguments
    action = target;
    target = event;
    event = 'onmessage';
  } else {
    if (arguments.length > 3) {
      // Fast arguments access.
      // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly.
      args = new Array(arguments.length - 3); //  SC.A(arguments).slice(3)
      for (i = 0, len = args.length; i < len; i++) { args[i] = arguments[i + 3]; }
    } else {
      args = [];
    }
  }

  var listeners = this.get('listeners');
  if (!listeners) { this.set('listeners', listeners = {}); }
  if(!listeners[event]) { listeners[event] = []; }

  //@if(debug)
  for (i = listeners[event].length - 1; i >= 0; i--) {
    var listener = listeners[event][i];
    if (listener.event === event && listener.target === target && listener.action === action) {
      SC.warn("Developer Warning: This listener is already defined.");
    }
  }
  //@endif

  // Add another listener for the given event name.
  listeners[event].push({target: target, action: action, args: args});

  return this;
},

/**
  Send the passed message. If the connection is not yet open or anthentified,
  the message will be put in the queue.

  @param message {String|Object} The message to send.
  @returns {SC.WebSocket}
*/
send: function(message) {
  if (this.isConnected === true && this.isAuth !== false) {
    if (this.isJSON) {
      message = JSON.stringify(message);
    }

    this.socket.send(message);
  }
  else {
    this.addToQueue(message);
  }
  return this;
},

// ..........................................................
// PRIVATE METHODS
//

/**
   @private
*/
onOpen: function(event) {
  var del = this.get('objectDelegate');

  this.set('isConnected', true);

  var ret = del.webSocketDidOpen(this, event);
  if (ret !== true) this._notifyListeners('onopen', event);

  this.fireQueue();
},

/**
   @private
*/
onMessage: function(messageEvent) {
  if (messageEvent) {
    var del = this.get('objectDelegate'),
      message,
      data,
      ret;

    message = data = messageEvent.data;
    ret = del.webSocketDidReceiveMessage(this, data);

    if (ret !== true) {
      if (this.isJSON) {
        message = JSON.parse(data);
      }
      this._notifyListeners('onmessage', message);
    }
  }

  // If there is message in the queue, we fire them
  this.fireQueue();
},

/**
   @private
*/
onClose: function(closeEvent) {
  var del = this.get('objectDelegate');

  this.set('isConnected', false);
  this.set('isAuth', null);
  this.socket = null;

  var ret = del.webSocketDidClose(this, closeEvent);

  if (ret !== true) {
    this._notifyListeners('onclose', closeEvent);
    this.tryReconnect();
  }
},

/**
   @private
*/
onError: function(event) {
  var del = this.get('objectDelegate'),
    ret = del.webSocketDidError(this, event);

  if (ret !== true) this._notifyListeners('onerror', event);
},

/**
   @private

   Add the message to the queue
*/
addToQueue: function(message) {
  var queue = this.queue;
  if (!queue) { this.queue = queue = []; }

  queue.push(message);
},

/**
   @private

   Send the messages from the queue.
*/
fireQueue: function() {
  var queue = this.queue;
  if (!queue || queue.length === 0) return;

  queue = SC.A(queue);
  this.queue = null;

  for (var i = 0, len = queue.length; i < len; i++) {
    var message = queue[i];
    this.send(message);
  }
},

/**
  @private
*/
tryReconnect: function() {
  if (!this.get('autoReconnect')) return;

  var that = this;
  setTimeout(function() { that.connect(); }, this.get('reconnectInterval'));
},

/**
  @private

  Will notify each listener. Returns true if any of the listeners handle.
*/
_notifyListeners: function(event, message) {
  var listeners = (this.listeners || {})[event], notifier, target, action, args;
  if (!listeners) { return NO; }

  var handled = NO,
    len = listeners.length;

  for (var i = 0; i < len; i++) {
    notifier = listeners[i];
    args = (notifier.args || []).copy();
    args.unshift(message);
    args.unshift(this);

    target = notifier.target;
    action = notifier.action;
    if (SC.typeOf(action) === SC.T_STRING) { action = target[action]; }

    handled = action.apply(target, args);
    if (handled === true) return handled;
  }

  return handled;
},

/**
  @private
*/
objectDelegate: function () {
  var del = this.get('delegate');
  return this.delegateFor('isWebSocketDelegate', del, this);
}.property('delegate').cacheable(),

// ..........................................................
// PRIVATE PROPERTIES
//

/**
  @private

  @type WebSocket
  @default null
*/
socket: null,

/**
  @private

  @type Object
  @default null
*/
listeners: null,

/**
  @private

  Messages that needs to be send once the connection is open.

  @type Array
  @default null
*/
queue: null,

});

// Class Methods SC.WebSocket.mixin( /** @scope SC.WebSocket */ {

// ..........................................................
// CONSTANTS
//

/**
  The connection is not yet open.

  @static
  @constant
  @type Number
  @default 0
*/
CONNECTING: 0,

/**
  The connection is open and ready to communicate.

  @static
  @constant
  @type Number
  @default 1
*/
OPEN: 1,

/**
  The connection is in the process of closing.

  @static
  @constant
  @type Number
  @default 2
*/
CLOSING: 2,

/**
  The connection is closed or couldn't be opened.

  @static
  @constant
  @type Number
  @default 3
*/
CLOSED: 3,

});