/**
 * @overview JsonRpcRequestProcessor
 * @copyright Dreamlab Onet.pl sp. z. o o 2012
 */

var RequestProcessor = require('../http/RequestProcessor.js').RequestProcessor;
var JsonRpcAbstract = require('../jsonrpc/JsonRpcAbstract.js').JsonRpcAbstract;
var JsonRpcResponse = require('../jsonrpc/JsonRpcResponse.js').JsonRpcResponse;
var JsonRpcRequest = require('../jsonrpc/JsonRpcRequest.js').JsonRpcRequest;
var JsonRpcMethod = require('../jsonrpc/JsonRpcMethod.js').JsonRpcMethod;
var Types = require('../common/Types.js').Types;
var BinaryData = require('../data/BinaryData.js').BinaryData;

/**
 * @class JsonRpcRequestProcessor
 * @classdesc JsonRpcRequestProcessor
 * @extends RequestProcessor
 *
 * @requires RequestProcessor
 * @requires JsonRpcAbstract
 * @requires JsonRpcResponse
 * @requires JsonRpcMethod
 * @requires Types
 * @requires BinaryData
 *
 * @param {Server} server
 * @returns {JsonRpcRequestProcessor}
 */
var JsonRpcRequestProcessor = exports.JsonRpcRequestProcessor = function (server) {
    this._request = null;
    this._response = null;
    this._responseData = [];
    this._isBatchRequest = false;
    this._methodCount = 0;
    RequestProcessor.call(this, server);
};

JsonRpcRequestProcessor.prototype = Object.create(RequestProcessor.prototype);

/**
 * Processes request and generates response
 * @param {Request} request
 * @param {Response} response
 * @returns {undefined}
 * @method
 */
JsonRpcRequestProcessor.prototype.process = function (request, response) {
    RequestProcessor.prototype.process.call(this, request, response);

    this._request = request;
    this._response = response;
    var jsonRpcRequests = this._getRequest();
    var methods = [];
    this._methodCount = jsonRpcRequests.length;

    for (var i = 0, tmpMethodCount = jsonRpcRequests.length; i < tmpMethodCount; i++ ) {
        this._processSingle(jsonRpcRequests[i]);
        try {
            methods.push(jsonRpcRequests[i].getMethod());
        } catch (e) {
        }
    }
    //XXX: dziala jesli jest process.nextTick
    this._requestLine = '"' + this._request.getMethod() + ' ' + this._request.getPath() +
        ' method:' + methods.join(':') + ' ' + this._request.getHttpVersion() + '"';
};


/**
 * Returns Method object for specified request
 *
 * @param {JsonRpcRequest} request
 * @returns {JsonRpcMethod}
 * @method
 * @private
 */
JsonRpcRequestProcessor.prototype._getMethodClass = function (requestedMethod) {
    return this.server.getMethod(requestedMethod, this._request);
};
/**
 * Return reposne to caller
 * @private
 */
JsonRpcRequestProcessor.prototype._prepareResponse = function () {
    var tmp_rep = null;
    var that = this;
    if (--this._methodCount <= 0) {

        this._response.setStatusCode(200);
        this._response.setHeaders({
            'Content-Type': 'application/json; charset=utf8',
            'Access-Control-Allow-Origin': '*'
        }, true);

        var responseData;

        if (this._isBatchRequest === true) {
            responseData = this._responseData;
        } else {
            responseData = this._responseData.pop();
        }

        this._response.setBody(new BinaryData(
            this._getBody(JSON.stringify(responseData)),
            BinaryData.Encoding.TEXT,
            BinaryData.CharacterEncoding.UTF8));

        process.nextTick(function() {
            that._response.send();
        });
    }

};
/**
 * Processes single request and generates response
 * @param {Request} request
 * @param {Response} response
 * @param {Number}  index
 * @returns {undefined}
 * @method
 */

JsonRpcRequestProcessor.prototype._processSingle = function(jsonRpcRequest) {
    var method;
    var methodClass;
    var requestedMethod;
    var requestId = jsonRpcRequest.getId();
    var that = this;

    try {
        requestedMethod = jsonRpcRequest.getMethod();
        methodClass = this._getMethodClass(requestedMethod);
    } catch (ex) {
        console.error('_processSingle error', ex);
        this._throwError(JsonRpcRequestProcessor.Errors.INVALID_METHOD, ex, requestId);
    }

    if (!methodClass) {
        console.error('JsonRpcRequestProcessor/process/methodNotFound', requestedMethod);
        var e = new Error(JsonRpcRequestProcessor.Errors.METHOD_NOT_FOUND);
        this._throwError(JsonRpcRequestProcessor.Errors.METHOD_NOT_FOUND, e, requestId);
        return;
    }

    method = new methodClass(this._request, this._response, this.server);

    if (!Types.isInstanceOf(method, JsonRpcMethod)) {
        var error = new Error(JsonRpcRequestProcessor.Errors.METHOD_NOT_FOUND);
        console.error('JsonRpcRequestProcessor/process/methodClassError/' + requestedMethod);
        this._throwError(JsonRpcRequestProcessor.Errors.METHOD_NOT_FOUND, error, requestId);
        return;
    }


    //FIXME: tutaj trzebaby sie zastanowic jak to zrobic bo raz przekazujemy Exception a raz ErrorEvent
    method.addEventListener(JsonRpcMethod.Event.WRONG_PARAMS, function (e) {
        that._handleError(e, requestId);
    }, this);
    method.addEventListener(JsonRpcMethod.Event.ERROR, function(e) {
        that._handleError(e, requestId);
    }, this);
    method.addEventListener(JsonRpcMethod.Event.OK, function(e) {
        that._success(e, requestId);
    }, this);
    try {
        method.execute(jsonRpcRequest.getParams());
    } catch (e) {
        console.error('JsonRpcRequestProcessor/execute/uncaughtException/method/' +
            requestedMethod, e, e.stack);
        this._throwError(JsonRpcRequestProcessor.Errors.INTERNAL_ERROR, e, requestId);
    }

};
/**
 * Returns jsonRpc response to the caller
 * @param {Event} e
 * @returns {undefined}
 * @method
 * @private
 */
JsonRpcRequestProcessor.prototype._success = function(e, requestId) {
    var data;
    if (e.data) {
        data = e.data;
        if (!Types.isInstanceOf(e.data, JsonRpcResponse)) {
            data = new JsonRpcResponse();
            data.setResult(e.data);
        }
        data.setId(requestId);

        this._responseData.push(data.toJson());

    }
    return this._prepareResponse();
};
/**
 * Handles errors from executed JsonRpc Method
 * @param {Event} e
 * @returns {undefined}
 * @method
 * @private
 */
JsonRpcRequestProcessor.prototype._handleError = function (e, requestId) {
    if (e.type == JsonRpcMethod.Event.WRONG_PARAMS) {
        this._throwError(JsonRpcRequestProcessor.Errors.INVALID_PARAMS, e, requestId);
    } else if (e.type == JsonRpcMethod.Event.ERROR) {
        this._throwError(e.code || -1, e, requestId);
    } else {
        this._throwError(JsonRpcRequestProcessor.Errors.INTERNAL_ERROR, e, requestId);
    }
};
/**
 * Retrieves jsonrpc request from request body
 * @returns [JsonRpcRequest]
 * @method
 * @private
 */
JsonRpcRequestProcessor.prototype._getRequest = function () {
    //tutaj mozna dodac pewnie jakas walidacje czy request jest POSTem
    var raw_requests;
    var requests = [];
    try {
        raw_requests = JSON.parse(this._request.getBody().toBuffer().toString());
    } catch (e) {
        var ex =  new Error(JsonRpcAbstract.Exceptions.PARSE_ERROR);
        console.error('JsonRpcRequestProcessor/process/createJsonRpcRequest', e);
        this._throwError(JsonRpcRequestProcessor.Errors.PARSE_ERROR, ex, 0);
        return requests;
    }

    if (Types.isArray(raw_requests) === true) {
        this._isBatchRequest = true;
    }
    else {
        this._isBatchRequest = false;
        raw_requests = [raw_requests];
    }
    for (var i = 0, raw_lenght = raw_requests.length; i < raw_lenght; i++) {
        try{
            requests.push(new JsonRpcRequest(raw_requests[i]));
        } catch(e) {

                console.error('JsonRpcRequestProcessor/process/createBathJsonRpcRequest', e);
                if (e.message == JsonRpcAbstract.Exceptions.INPROPER_JSON) {
                    this._throwError(JsonRpcRequestProcessor.Errors.PARSE_ERROR, e);
                } else if (e.message == JsonRpcAbstract.Exceptions.INVALID_JSON_RPC) {
                    this._throwError(JsonRpcRequestProcessor.Errors.INVALID_REQUEST, e);
                } else {
                    this._throwError(JsonRpcRequestProcessor.Errors.INTERNAL_ERROR, e);
                }
            }
    }

    return requests;
};


/**
 * Retrieves body from given JsonRpcResponse
 * @method
 * @private
 * @param {JsonRpcResponse} response
 * @return {String}
 */
JsonRpcRequestProcessor.prototype._getBody = function (response) {
    return response.toString();
};

/**
 * Returns jsonRpc error to the caller
 * @param {Number|ErrorEvent} code
 * @param {Exception|ErrorEvent} exception
 * @param {Number} index
 * @returns {undefined}
 * @method
 * @private
 */
JsonRpcRequestProcessor.prototype._throwError = function (code, exception, id) {
    var jsonRpcResponse = new JsonRpcResponse();
    jsonRpcResponse.setId(
            id ?
            id : "undefined");
    //TODO: tutaj if czy kod bledu jest z przedzialu zastrzezonych czy nie
    //jak z zastrzezonych to rzucamy standardowy message jak nie to bierzemy z expcetion

    if (Types.isObject(code)) {
        console.error('JsonRpcRequestProcessor/_throwError/ code: ' + code.code + " msg: " + code.message);
        jsonRpcResponse.setError(parseInt(code.code, 10), code.message || "", exception.data ? exception.data : null);
    } else {
        console.error('JsonRpcRequestProcessor/_throwError/ code: ' + code + " msg: " + exception.message);
        jsonRpcResponse.setError(parseInt(code, 10), exception.message || "", exception.data ? exception.data : null);
    }

    this._responseData.push(jsonRpcResponse.toJson());

    return this._prepareResponse();
};

/**
 * @static
 * @constant
 * @namespace
 */
JsonRpcRequestProcessor.Errors = {};

/**
 * @static
 * @constant
 * @type {String}
 * @default
 */
JsonRpcRequestProcessor.Errors.PARSE_ERROR = {code: -32700, message: 'Parse error'};

/**
 * @static
 * @constant
 * @type {Object}
 * @default
 */
JsonRpcRequestProcessor.Errors.INVALID_REQUEST = {code: -32600, message: 'Invalid Request'};

/**
 * @static
 * @constant
 * @type {Object}
 * @default
 */
JsonRpcRequestProcessor.Errors.METHOD_NOT_FOUND = {code: -32601, message: 'Method not found'};

/**
 * @static
 * @constant
 * @type {Object}
 * @default
 */
JsonRpcRequestProcessor.Errors.INVALID_PARAMS = {code: -32602, message: 'Invalid params'};

/**
 * @static
 * @constant
 * @type {Object}
 * @default
 */
JsonRpcRequestProcessor.Errors.INTERNAL_ERROR = {code: -32603, message: 'Internal error'};

/**
 * @static
 * @constant
 * @type {Object}
 * @default
 */
JsonRpcRequestProcessor.Errors.INVALID_METHOD = {code: -32604, message: 'Invalid method'};
