/**
 * JsonRpcClient
 *
 * A JSON RPC Client that uses WebSockets if available otherwise fallbacks to ajax.
 * Depends on JSON, if browser lacks native support either use JSON3 or jquery.json.
 * Usage example:
 *
 *   var foo = new $.JsonRpcClient({ ajaxUrl: '/backend/jsonrpc' });
 *   foo.call(
 *     'bar', [ 'A parameter', 'B parameter' ],
 *     function(result) { alert('Foo bar answered: ' + result.my_answer); },
 *     function(error)  { console.log('There was an error', error); }
 *   );
 *
 * More examples are available in README.md
 */
(function($) {

  /**
   * @fn new
   * @memberof JsonRpcClient
   *
   * @param {object} options An object stating the backends:
   *                ajaxUrl    A url (relative or absolute) to a http(s) backend.
   *                headers    An object that will be passed along to $.ajax in options.headers
   *                xhrFields  An object that will be passed along to $.ajax in options.xhrFields
   *                socketUrl  A url (relative of absolute) to a ws(s) backend.
   *                onmessage  A socket message handler for other messages (non-responses).
   *                onopen     A socket onopen handler. (Not used for custom getSocket.)
   *                onclose    A socket onclose handler. (Not used for custom getSocket.)
   *                onerror    A socket onerror handler. (Not used for custom getSocket.)
   *                getSocket  A function returning a WebSocket or null.
   *                           It must take an onmessage_cb and bind it to the onmessage event
   *                           (or chain it before/after some other onmessage handler).
   *                           Or, it could return null if no socket is available.
   *                           The returned instance must have readyState <= 1, and if less than 1,
   *                           react to onopen binding.
   *                timeout    (optional) A number of ms to wait before timing out and failing a
   *                           call. If specified a setTimeout will be used to keep track of calls
   *                           made through a websocket.
   */
  var JsonRpcClient = function(options) {
    var self = this;
    var noop = function() {};
    this.options = $.extend({
      ajaxUrl     : null,
      headers     : {},   ///< Optional additional headers to send in $.ajax request.
      socketUrl   : null, ///< WebSocket URL. (Not used if a custom getSocket is supplied.)
      onmessage   : noop, ///< Optional onmessage-handler for WebSocket.
      onopen      : noop, ///< Optional onopen-handler for WebSocket.
      onclose     : noop, ///< Optional onclose-handler for WebSocket.
      onerror     : noop, ///< Optional onerror-handler for WebSocket.
      /// Custom socket supplier for using an already existing socket
      getSocket   : function(onmessageCb) { return self._getSocket(onmessageCb); }
    }, options);

    // Declare an instance version of the onmessage callback to wrap 'this'.
    this.wsOnMessage = function(event) { self._wsOnMessage(event); };

    /// Holding the WebSocket on default getsocket.
    this._wsSocket = null;

    /// Object <id>: { success_cb: cb, error_cb: cb }
    this._wsCallbacks = {};

    /// The next JSON-RPC request id.
    this._currentId = 1;

    //queue for ws request sent *before* ws is open.
    this._wsRequestQueue = [];

    if (!window.JSON && $ && $.toJSON) {
      this.JSON = {
        stringify: $.toJSON,
        parse: $.parseJSON
      };
    } else {
      this.JSON = JSON;
    }

  };

  /**
   * @fn call
   * @memberof JsonRpcClient
   *
   * @param {string}       method     The method to run on JSON-RPC server.
   * @param {object|array} params     The params; an array or object.
   * @param {function}     successCb  A callback for successful request.
   * @param {function}     errorCb    A callback for error.
   *
   * @return {object} Returns the deferred object that $.ajax returns or {null} for websockets
   */
  JsonRpcClient.prototype.call = function(method, params, successCb, errorCb) {
    successCb = typeof successCb === 'function' ? successCb : function() {};
    errorCb   = typeof errorCb   === 'function' ? errorCb   : function() {};

    // Construct the JSON-RPC 2.0 request.
    var request = {
      jsonrpc : '2.0',
      method  : method,
      params  : params,
      id      : this._currentId++  // Increase the id counter to match request/response
    };

    // Try making a WebSocket call.
    var socket = this.options.getSocket(this.wsOnMessage);
    if (socket !== null) {
      this._wsCall(socket, request, successCb, errorCb);
      return null;
    }

    // No WebSocket, and no HTTP backend?  This won't work.
    if (this.options.ajaxUrl === null) {
      throw 'JsonRpcClient.call used with no websocket and no http endpoint.';
    }
	  
	  

    var self = this;

    var deferred = $.ajax({
      type       : 'POST',
      url        : this.options.ajaxUrl,
      contentType: 'application/json',
      data       : this.JSON.stringify(request),
      dataType   : 'json',
      cache      : false,
      headers    : this.options.headers,
      xhrFields  : this.options.xhrFields,
      timeout    : this.options.timeout,

      success    : function(data) {
        if ('error' in data) {
          errorCb(data.error);
        } else {
          successCb(data.result);
        }
      },

      // JSON-RPC Server could return non-200 on error
      error    : function(jqXHR, textStatus, errorThrown) {
        try {
          var response = self.JSON.parse(jqXHR.responseText);
          if ('console' in window) { console.log(response); }

          errorCb(response.error);
        }
        catch (err) {
          // Perhaps the responseText wasn't really a jsonrpc-error.
          errorCb({error: jqXHR.responseText});
        }
      }
    });

    return deferred;
  };

  /**
   * Notify sends a command to the server that won't need a response.  In http, there is probably
   * an empty response - that will be dropped, but in ws there should be no response at all.
   *
   * This is very similar to call, but has no id and no handling of callbacks.
   *
   * @fn notify
   * @memberof JsonRpcClient
   *
   * @param {string} method       The method to run on JSON-RPC server.
   * @param {object|array} params The params; an array or object.
   *
   * @return {object} Returns the deferred object that $.ajax returns or {null} for websockets
   */
  JsonRpcClient.prototype.notify = function(method, params) {
    // Construct the JSON-RPC 2.0 request.
    var request = {
      jsonrpc: '2.0',
      method:  method,
      params:  params
    };

    // Try making a WebSocket call.
    var socket = this.options.getSocket(this.wsOnMessage);
    if (socket !== null) {
      this._wsCall(socket, request);
      return null;
    }

    // No WebSocket, and no HTTP backend?  This won't work.
    if (this.options.ajaxUrl === null) {
      throw 'JsonRpcClient.notify used with no websocket and no http endpoint.';
    }

    var deferred = $.ajax({
      type       : 'POST',
      url        : this.options.ajaxUrl,
      contentType: 'application/json',
      data       : this.JSON.stringify(request),
      dataType   : 'json',
      cache      : false,
      headers    : this.options.headers,
      xhrFields  : this.options.xhrFields
    });

    return deferred;
  };

  /**
   * Make a batch-call by using a callback.
   *
   * The callback will get an object "batch" as only argument.  On batch, you can call the methods
   * "call" and "notify" just as if it was a normal JsonRpcClient object, and all calls will be
   * sent as a batch call then the callback is done.
   *
   * @fn batch
   * @memberof JsonRpcClient
   *
   * @param {function} callback   This function will get a batch handler to run call and notify on.
   * @param {function} allDoneCb  A callback function to call after all results have been handled.
   * @param {function} errorCb    A callback function to call if there is an error from the server.
   *                    Note, that batch calls should always get an overall success, and the
   *                    only error
   */
  JsonRpcClient.prototype.batch = function(callback, allDoneCb, errorCb) {
    var batch = new JsonRpcClient._batchObject(this, allDoneCb, errorCb);
    callback(batch);
    batch._execute();
  };

  /**
   * The default getSocket handler.
   *
   * @param {function} onmessageCb The callback to be bound to onmessage events on the socket.
   *
   * @fn _getSocket
   * @memberof JsonRpcClient
   */
  JsonRpcClient.prototype._getSocket = function(onmessageCb) {
    // If there is no ws url set, we don't have a socket.
    // Likewise, if there is no window.WebSocket.
    if (this.options.socketUrl === null || !('WebSocket' in window)) { return null; }

    if (this._wsSocket === null || this._wsSocket.readyState > 1) {

      try {
        // No socket, or dying socket, let's get a new one.
        this._wsSocket = new WebSocket(this.options.socketUrl);
      } catch (e) {
        // This can happen if the server is down, or malconfigured.
        return null;
      }

      // Set up onmessage handler.
      this._wsSocket.onmessage = onmessageCb;

      var that = this;
      // Set up onclose handler.
      this._wsSocket.onclose = function(ev) { that._wsOnClose(ev); };

      // Set up onerror handler.
      this._wsSocket.onerror = function(ev) { that._wsOnError(ev); };
    }

    return this._wsSocket;
  };

  /**
   * Internal handler to dispatch a JRON-RPC request through a websocket.
   *
   * @fn _wsCall
   * @memberof JsonRpcClient
   */
  JsonRpcClient.prototype._wsCall = function(socket, request, successCb, errorCb) {
    var requestJson = this.JSON.stringify(request);

    // Setup callbacks.  If there is an id, this is a call and not a notify.
    if ('id' in request && typeof successCb !== 'undefined') {
      this._wsCallbacks[request.id] = {successCb: successCb, errorCb: errorCb};
    }

    if (socket.readyState < 1) {

      // Queue request
      this._wsRequestQueue.push(requestJson);

      if (!socket.onopen) {
        // The websocket is not open yet; we have to set sending of the message in onopen.
        var self = this; // In closure below, this is set to the WebSocket.  Use self instead.

        // Set up sending of message for when the socket is open.
        socket.onopen = function(event) {
          // Hook for extra onopen callback
          self.options.onopen(event);

          // Send queued requests.
          var timeout = self.options.timeout;
          var request;
          for (var i = 0; i < self._wsRequestQueue.length; i++) {
            request = self._wsRequestQueue[i];

            // Do we use timeouts, and if so, is it a call?
            if (timeout && self._wsCallbacks[request.id]) {
              self._wsCallbacks[request.id].timeout = self._createTimeout(request.id);
            }
            socket.send(request);
          }
          self._wsRequestQueue = [];
        };
      }
    } else {

      // Do we use timeouts, and if so, is it a call?
      if (this.options.timeout && this._wsCallbacks[request.id]) {
        this._wsCallbacks[request.id].timeout = this._createTimeout(request.id);
      }

      // We have a socket and it should be ready to send on.
      socket.send(requestJson);
    }
  };

  /**
   * Internal handler for the websocket messages.  It determines if the message is a JSON-RPC
   * response, and if so, tries to couple it with a given callback.  Otherwise, it falls back to
   * given external onmessage-handler, if any.
   *
   * @param {event} event The websocket onmessage-event.
   */
  JsonRpcClient.prototype._wsOnMessage = function(event) {

    // Check if this could be a JSON RPC message.
    var response;
    try {
      response = this.JSON.parse(event.data);
    } catch (err) {
      this.options.onmessage(event);
      return;
    }

    /// @todo Make using the jsonrcp 2.0 check optional, to use this on JSON-RPC 1 backends.
    if (typeof response === 'object' && response.jsonrpc === '2.0') {

      /// @todo Handle bad response (without id).

      // If this is an object with result, it is a response.
      if ('result' in response && this._wsCallbacks[response.id]) {
        // Get the success callback.
        var successCb = this._wsCallbacks[response.id].successCb;

        // Clear any timeout
        if (this._wsCallbacks[response.id].timeout) {
          clearTimeout(this._wsCallbacks[response.id].timeout);
        }

        // Delete the callback from the storage.
        delete this._wsCallbacks[response.id];

        // Run callback with result as parameter.
        successCb(response.result);
        return;
      }

      // If this is an object with error, it is an error response.
      else if ('error' in response && this._wsCallbacks[response.id]) {
        // Get the error callback.
        var errorCb = this._wsCallbacks[response.id].errorCb;

        // Delete the callback from the storage.
        delete this._wsCallbacks[response.id];

        // Run callback with the error object as parameter.
        errorCb(response.error);
        return;
      }
    }

    // If we get here it's an invalid JSON-RPC response, pass to fallback message handler.
    this.options.onmessage(event);
  };

  /**
   * Internal WebSocket error handler.
   * Will execute all unresolved calls immideatly.
   **/
  JsonRpcClient.prototype._wsOnError = function(event) {
    this._failAllCalls('Socket errored.');
    this.options.onerror(event);
  };

  /**
   * Internal WebSocket close handler.
   * Will execute all unresolved calls immideatly.
   **/
  JsonRpcClient.prototype._wsOnClose = function(event) {
    this._failAllCalls('Socket closed.');
    this.options.onclose(event);
  };

  /**
   * Execute error handler on all pending calls.
   */
  JsonRpcClient.prototype._failAllCalls = function(error) {
    for (var key in this._wsCallbacks) {
      if (this._wsCallbacks.hasOwnProperty(key)) {
        // Get the error callback.
        var errorCb = this._wsCallbacks[key].errorCb;

        // Run callback with the error object as parameter.
        errorCb(error);
      }
    }

    // Throw 'em away
    this._wsCallbacks = {};
  };

  /**
   * Create a timeout for this request
   */
  JsonRpcClient.prototype._createTimeout = function(id) {
    if (this.options.timeout) {
      var that = this;
      return setTimeout(function() {
        if (that._wsCallbacks[id]) {
          var errorCb = that._wsCallbacks[id].errorCb;
          delete that._wsCallbacks[id];
          errorCb('Call timed out.');
        }
      }, this.options.timeout);
    }
  };

  /************************************************************************************************
   * Batch object with methods
   ************************************************************************************************/

  /**
   * Handling object for batch calls.
   */
  JsonRpcClient._batchObject = function(jsonrpcclient, allDoneCb, errorCb) {
    // Array of objects to hold the call and notify requests.  Each objects will have the request
    // object, and unless it is a notify, successCb and errorCb.
    this._requests   = [];

    this.jsonrpcclient = jsonrpcclient;
    this.allDoneCb = allDoneCb;
    this.errorCb    = typeof errorCb    === 'function' ? errorCb : function() {};
  };

  /**
   * @sa JsonRpcClient.prototype.call
   */
  JsonRpcClient._batchObject.prototype.call = function(method, params, successCb, errorCb) {
    this._requests.push({
      request    : {
        jsonrpc : '2.0',
        method  : method,
        params  : params,
        id      : this.jsonrpcclient._currentId++  // Use the client's id series.
      },
      successCb : successCb,
      errorCb   : errorCb
    });
  };

  /**
   * @sa JsonRpcClient.prototype.notify
   */
  JsonRpcClient._batchObject.prototype.notify = function(method, params) {
    this._requests.push({
      request    : {
        jsonrpc : '2.0',
        method  : method,
        params  : params
      }
    });
  };

  /**
   * Executes the batched up calls.
   *
   * @return {object} Returns the deferred object that $.ajax returns or {null} for websockets
   */
  JsonRpcClient._batchObject.prototype._execute = function() {
    var self = this;
    var deferred = null; // Used to store and return the deffered that $.ajax returns

    if (this._requests.length === 0) { return; } // All done :P

    // Collect all request data and sort handlers by request id.
    var batchRequest = [];

    // If we have a WebSocket, just send the requests individually like normal calls.
    var socket = self.jsonrpcclient.options.getSocket(self.jsonrpcclient.wsOnMessage);

    if (socket !== null) {
      // We need to keep track of results for the all done callback
      var expectedNrOfCb = 0;
      var cbResults = [];

      var wrapCb = function(cb) {
        if (!self.allDoneCb) { // No all done callback? no need to keep track
          return cb;
        }

        return function(data) {
          cb(data);
          cbResults.push(data);
          expectedNrOfCb--;
          if (expectedNrOfCb <= 0) {
            // Change order so that it maps to request order
            var i;
            var resultMap = {};
            for (i = 0; i < cbResults.length; i++) {
              resultMap[cbResults[i].id] = cbResults[i];
            }
            var results = [];
            for (i = 0; i < self._requests.length; i++) {
              if (resultMap[self._requests[i].id]) {
                results.push(resultMap[self._requests[i].id]);
              }
            }
            // Call all done!
            self.allDoneCb(results);
          }
        };
      };

      for (var i = 0; i < this._requests.length; i++) {
        var call = this._requests[i];

        if ('id' in call.request) {
          // We expect an answer
          expectedNrOfCb++;
        }

        self.jsonrpcclient._wsCall(
          socket, call.request, wrapCb(call.successCb), wrapCb(call.errorCb)
        );
      }

      return null;
    } else {
      // No websocket, let's use ajax
      var handlers = {};

      for (var i = 0; i < this._requests.length; i++) {
        var call = this._requests[i];
        batchRequest.push(call.request);

        // If the request has an id, it should handle returns (otherwise it's a notify).
        if ('id' in call.request) {
          handlers[call.request.id] = {
            successCb : call.successCb,
            errorCb   : call.errorCb
          };
        }
      }

      var successCb = function(data) { self._batchCb(data, handlers, self.allDoneCb); };

      // No WebSocket, and no HTTP backend?  This won't work.
      if (self.jsonrpcclient.options.ajaxUrl === null) {
        throw 'JsonRpcClient.batch used with no websocket and no http endpoint.';
      }

      // Send request
      deferred = $.ajax({
        url        : self.jsonrpcclient.options.ajaxUrl,
        contentType: 'application/json',
        data       : this.jsonrpcclient.JSON.stringify(batchRequest),
        dataType   : 'json',
        cache      : false,
        type       : 'POST',
        headers    : self.jsonrpcclient.options.headers,
        xhrFields  : self.jsonrpcclient.options.xhrFields,

        // Batch-requests should always return 200
        error    : function(jqXHR, textStatus, errorThrown) {
          self.errorCb(jqXHR, textStatus, errorThrown);
        },
        success  : successCb
      });

      return deferred;

    }

  };

  /**
   * Internal helper to match the result array from a batch call to their respective callbacks.
   *
   * @fn _batchCb
   * @memberof JsonRpcClient
   */
  JsonRpcClient._batchObject.prototype._batchCb = function(result, handlers, allDoneCb) {
    for (var i = 0; i < result.length; i++) {
      var response = result[i];

      // Handle error
      if ('error' in response) {
        if (response.id === null || !(response.id in handlers)) {
          // An error on a notify?  Just log it to the console.
          if ('console' in window) { console.log(response); }
        } else {
          handlers[response.id].errorCb(response.error);
        }
      } else {
        // Here we should always have a correct id and no error.
        if (!(response.id in handlers) && 'console' in window) {
          console.log(response);
        } else {
          handlers[response.id].successCb(response.result);
        }
      }
    }

    if (typeof allDoneCb === 'function') { allDoneCb(result); }
  };

  $.JsonRpcClient = JsonRpcClient;

})(this.jQuery);
