var jayson = require(__dirname);
var utils = require('./utils');
var _ = require('lodash');

/**
 * @summary Constructor for a Jayson Method
 * @class Method
 * @param {Function} [handler] - Function to set as handler
 * @param {Object} [options] 
 * @param {Function} [options.handler] - Same as separate handler
 * @param {Boolean} [options.collect=true] - Params to the handler are collected in one object
 * @param {Array|Object} [options.params] - Defines params that the handler accepts
 */
var Method = function(handler, options) {

  if(!(this instanceof Method)) {
    return new Method(handler, options);
  }

  // only got passed options
  if(_.isPlainObject(handler)) {
    options = handler;
    handler = null;
  }

  var defaults = {
    collect: true
  };

  options = options || {};

  this.options = utils.merge(defaults, options);
  this.handler = handler || options.handler;
};

module.exports = Method;

/**
 * @summary Returns the handler function associated with this method
 * @return {Function}
 */
Method.prototype.getHandler = function() {
  return this.handler;
};

/**
 * @summary Sets the handler function associated with this method
 * @param {Function} handler
 */
Method.prototype.setHandler = function(handler) {
  this.handler = handler;
};

/**
 * @summary Prepare parameters for the method handler
 * @private
 */
Method.prototype._getHandlerParams = function(params) {
  var options = this.options;
  var handler = this.getHandler();

  var isObjectParams = !_.isArray(params) && _.isObject(params) && params;
  var isArrayParams = _.isArray(params);

  if(options.collect) {
    // collect parameters in one argument to handler

    switch(true) {

      // handler always gets an array
      case options.params === Array:
        return isArrayParams ? params : _.toArray(params);

      // handler always gets an object
      case options.params === Object:
        return isObjectParams ? params : _.toPlainObject(params);

      // handler gets a list of defined properties that should always be set
      case _.isArray(options.params):
        var undefinedParams = _.object(_.zip(options.params, _.range(options.params.length).map(_.constant(undefined))));
        return _.extend(undefinedParams, _.pick(params, _.keys(params)));

      // handler gets a map of defined properties and their default values
      case _.isPlainObject(options.params):
        return _.extend({}, options.params, _.pick(params, _.keys(params)));

      // give params as is
      default:
        return params;
    
    }

  } else {
    // let the arguments pass to the handler as given

    if(isObjectParams) {
      // named parameters passed, take all parameters for handler except last (the callback)
      return _.initial(utils.getParameterNames(handler)).map(function(name) {
        return params[name];
      });
    }

    // regular parameters array passed
    return params;
  }
};

/**
 * @summary Executes this method in the context of a server
 * @param {Server} server
 * @param {Array|Object} requestParams
 * @param {Function} callback
 */
Method.prototype.execute = function(server, requestParams, callback) {
  var options = this.options;
  var handler = this.getHandler();
  var params = this._getHandlerParams(requestParams);

  if(options.collect) {
    return handler.call(server, params, callback);
  }

  // compare without the callback
  if(handler.length !== (params.length + 1)) {
    callback(server.error(jayson.Server.errors.INVALID_PARAMS));
    return;
  }

  return handler.apply(server, _.flatten([params, callback]));
};

@@ -3350,24 +3350,172 @@
 back);%0A  %7D%0A%0A
+  // Params is optional according to the JSON-RPC 2.0 spec so if it doesnt%0A  // exist create an empty array.%0A  if (!params) %7B%0A    params = %5B%5D;%0A  %7D%0A%0A
   // compare

Allow for empty params when collect: false is used.