'use strict';

const uuid = require('uuid').v4;
const generateRequest = require('../../generateRequest');

/**
 * Constructor for a Jayson Browser Client that does not depend any node.js core libraries
 * @class ClientBrowser
 * @param {Function} callServer Method that calls the server, receives the stringified request and a regular node-style callback
 * @param {Object} [options]
 * @param {Function} [options.reviver] Reviver function for JSON
 * @param {Function} [options.replacer] Replacer function for JSON
 * @param {Number} [options.version=2] JSON-RPC version to use (1|2)
 * @param {Function} [options.generator] Function to use for generating request IDs
 *  @param {Boolean} [options.notificationIdNull=false] When true, version 2 requests will set id to null instead of omitting it
 * @return {ClientBrowser}
 */
const ClientBrowser = function(callServer, options) {
  if(!(this instanceof ClientBrowser)) {
    return new ClientBrowser(callServer, options);
  }

  if (!options) {
    options = {};
  }

  this.options = {
    reviver: typeof options.reviver !== 'undefined' ? options.reviver : null,
    replacer: typeof options.replacer !== 'undefined' ? options.replacer : null,
    generator: typeof options.generator !== 'undefined' ? options.generator : function() { return uuid(); },
    version: typeof options.version !== 'undefined' ? options.version : 2,
    notificationIdNull: typeof options.notificationIdNull === 'boolean' ? options.notificationIdNull : false,
  };

  this.callServer = callServer;
};

module.exports = ClientBrowser;

/**
 *  Creates a request and dispatches it if given a callback.
 *  @param {String|Array} method A batch request if passed an Array, or a method name if passed a String
 *  @param {Array|Object} [params] Parameters for the method
 *  @param {String|Number} [id] Optional id. If undefined an id will be generated. If null it creates a notification request
 *  @param {Function} [callback] Request callback. If specified, executes the request rather than only returning it.
 *  @throws {TypeError} Invalid parameters
 *  @return {Object} JSON-RPC 1.0 or 2.0 compatible request
 */
ClientBrowser.prototype.request = function(method, params, id, callback) {
  const self = this;
  let request = null;

  // is this a batch request?
  const isBatch = Array.isArray(method) && typeof params === 'function';

  if (this.options.version === 1 && isBatch) {
    throw new TypeError('JSON-RPC 1.0 does not support batching');
  }

  // is this a raw request?
  const isRaw = !isBatch && method && typeof method === 'object' && typeof params === 'function';

  if(isBatch || isRaw) {
    callback = params;
    request = method;
  } else {
    if(typeof id === 'function') {
      callback = id;
      // specifically undefined because "null" is a notification request
      id = undefined;
    }

    const hasCallback = typeof callback === 'function';

    try {
      request = generateRequest(method, params, id, {
        generator: this.options.generator,
        version: this.options.version,
        notificationIdNull: this.options.notificationIdNull,
      });
    } catch(err) {
      if(hasCallback) {
        callback(err);
        return;
      }
      throw err;
    }

    // no callback means we should just return a raw request
    if(!hasCallback) {
      return request;
    }

  }

  let message;
  try {
    message = JSON.stringify(request, this.options.replacer);
  } catch(err) {
    callback(err);
    return;
  }

  this.callServer(message, function(err, response) {
    self._parseResponse(err, response, callback);
  });

  // always return the raw request
  return request;
};

/**
 * Parses a response from a server
 * @param {Object} err Error to pass on that is unrelated to the actual response
 * @param {String} responseText JSON-RPC 1.0 or 2.0 response
 * @param {Function} callback Callback that will receive different arguments depending on the amount of parameters
 * @private
 */
ClientBrowser.prototype._parseResponse = function(err, responseText, callback) {
  if(err) {
    callback(err);
    return;
  }

  if(!responseText) {
    // empty response text, assume that is correct because it could be a
    // notification which jayson does not give any body for
    callback();
    return;
  }

  let response;
  try {
    response = JSON.parse(responseText, this.options.reviver);
  } catch(err) {
    callback(err);
    return;
  }

  if(callback.length === 3) {
    // if callback length is 3, we split callback arguments on error and response

    // is batch response?
    if(Array.isArray(response)) {

      // necessary to split strictly on validity according to spec here
      const isError = function(res) {
        return typeof res.error !== 'undefined';
      };

      const isNotError = function (res) {
        return !isError(res);
      };

      callback(null, response.filter(isError), response.filter(isNotError));
      return;
    } else {

      // split regardless of validity
      callback(null, response.error, response.result);
      return;
    }
  
  }

  callback(null, response);
};
