/**
 * rpc.js
 * Copyright (c) 2000 - 2015 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * DongHee Yang <donghee.yang@samsung.com>
 * Sungmin Kim <sm.art.kim@samsung.com>
 * Jiil Hyoun <jiil.hyoun@samsung.com>
 * Jonghwan Park <iwin100.park@samsung.com>
 * Kitae Kim <kt920.kim@samsung.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 * - S-Core Co., Ltd
**/

var dnode = require('dnode');
var net = require('net');
var util = require('util');
var _ = require('underscore');
var uuid = require('node-uuid');
var sizeof = require('object-sizeof');

var DError = require('./exception');
var dibs = require('./dibs');


module.exports = new Rpc();

var rpcLog = {
    info: function (msg) {
        /*console.log("Info: " + msg); */
    },
    warn: function (msg) {
        /*console.log("Warning: " + msg); */
    },
    error: function (msg) {
        /*console.log("Error: " + msg); */
    }
};


function Rpc() {
    self = this;

    this.servers = {};

    this.createLogger = function (logFilePath) {
        rpcLog = dibs.log.open('RPC', {
            filename: logFilePath,
            timestamp: function () {
                var now = new Date();
                return now.format('YYYY-MM-DD hh:mm:ss.SS');
            },
            colorize: false,
            handleExceptions: true
        });
    };


    this.getServers = function () {
        return self.servers;
    };


    // Create a function that call remote function
    // NOTE. THIS MUST BE DEFINED HERE
    var createRpcFunc = function (stype, fname) {
        return function () {
            var s = route(stype);
            if (s === undefined) {
                var error = new DError('RPC006', {
                    stype: stype
                });
                return;
            }
            var args = [];

            for (var key in arguments) {
                args.push(arguments[key]);
            }

            callRemoteMethod(s, fname, args);
        };
    };

    // Return RPC module of specified server type
    this.getRpcModule = function (stype) {
        var modules = {};

        // get base RPC module
        var rpcExts = dibs.plugin.getExtensions('dibs.base.rpc.extension');
        _.each(rpcExts, function (ext) {
            modules = addRpcModule(modules, ext.module);
        });

        // get server dependent module
        var serverExts = dibs.plugin.getExtensions('dibs.base.server');
        var serverTypeExt = _.findWhere(serverExts, {
            serverType: stype
        }); //Find first object
        if (!serverTypeExt) {
            throw new DError('RPC011', {
                serverType: stype
            });
        }
        modules = addRpcModule(modules, serverTypeExt.rpcModule);

        return modules;
    };


    var exts = dibs.plugin.getExtensions('dibs.base.server');
    for (var i = 0; i < exts.length; i++) {
        var ext = exts[i];

        if (!ext.hasAttribute('rpcModule')) continue;

        var stype = ext.serverType;
        var methods = this.getRpcModule(stype);

        this[stype] = {};
        for (var fname in methods) {
            this[stype][fname] = createRpcFunc(stype, fname);
        }
    }



    this.createStream = function (server, header) {

        var stype = server.type;
        var methods = this.getRpcModule(stype);

        if (methods !== null) {
            var args = {};
            for (var fname in methods) {
                args[fname] = defineRpcFunc(header, fname, methods[fname]);
            }
            return dnode(args);
        } else {
            return null;
        }
    };


    function defineRpcFunc(header, fname, func) {
        return function () {
            if (header.remoteAddress && header.remotePort) {
                rpcLog.info('Requested... ' + header.remoteAddress + ':' + header.remotePort + ' - ' + fname);
            } else {
                rpcLog.info('Requested... ' + fname);
            }
            var args = _.initial(arguments); //[1,2,3,4,5] -> [1,2,3,4]
            var lastArg = _.last(arguments); //[1,2,3,4,5] -> 5
            if ((typeof func) === 'function') {
                if ((typeof lastArg) === 'function') { // callback
                    var callback = function () {
                        if (header.remoteAddress && header.remotePort) {
                            rpcLog.info('Responsed... ' + header.remoteAddress + ':' + header.remotePort + ' - ' + fname);
                        } else {
                            rpcLog.info('Responsed... ' + fname);
                        }
                        lastArg.apply(null, arguments);
                    };
                    args.push(callback);
                    func.apply(null, args);
                } else {
                    rpcLog.info('callback not exist... ' + fname);
                    func.apply(null, arguments);
                }
            } else {
                rpcLog.warn(fname + ' is not function!');
            }
        };
    }


    // Create a specified RPC function which call "fname" of its server
    this.createRpcCall = function (fname) {
        return function () {
            var server = this; //called by server

            var args = [];

            for (var key in arguments) {
                args.push(arguments[key]);
            }

            callRemoteMethod(server, fname, args);
        };
    };


    // PRIVATE METHODS

    // select a server in server group
    var route = function (stype) {
        var servers = dibs.getServersByType(stype);

        // get random
        return servers[Math.floor(Math.random() * servers.length)];
    };
}


function connect(server) {
    var sock = new net.Socket();
    sock.connect(server.port, server.host);

    return sock;
}



// Call remote methods of server( host, port )
function callRemoteMethod(server, fname, args) {
    var rpcId = uuid.v1();
    callRemoteMethodInternal(server, fname, args, 5000, 0, rpcId);
}


// Call remote methods of server( host, port )
function callRemoteMethodInternal(server, fname, args, timeOut, retryCnt, rpcId) {
    if (retryCnt > 0) {
        rpcLog.info('[' + rpcId + '+] Retrying... ' + fname + ':' + server.id +
            ' => (' + retryCnt + ')');
    }

    var cbCalled = false;

    // Extract a callback function from argument list
    var remoteArgs = [];
    var cbFunc = null;
    if (args.length === 0) {
        throw new DError('RPC007', {
            arguments: args
        });
    } else if (args.length === 1) {
        remoteArgs = [];
        cbFunc = args[0];
    } else {
        remoteArgs = args.slice(0, args.length - 1);
        cbFunc = args[args.length - 1];
    }

    if (typeof cbFunc !== 'function') {
        throw new DError('RPC007', {
            arguments: args
        });
    }

    if (retryCnt > 3) {
        handleErrorOnRPC(new DError('RPC002', {
            host: server.host,
            port: server.port,
            functionName: fname,
            sec: timeOut / 1000
        }));

        return;
    }

    // Connect to remote server
    var startTime = new Date();
    var conn = connect(server);
    var rpcResponseTimeout = null;
    conn.on('connect', function () {
        var d = dnode(null, {
            weak: false
        });

        // set dnode timeout
        rpcResponseTimeout = setTimeout(function () {
            cbCalled = true; //Ingnore timeout-RPC's callback
            d.end();
            callRemoteMethodInternal(server, fname, args, timeOut + timeOut, retryCnt + 1, rpcId);
        }, timeOut);


        d.on('remote', function (remote) {

            // stop timeout counting
            clearTimeout(rpcResponseTimeout);

            var responseTime = (new Date() - startTime);
            rpcLog.info('[' + rpcId + '+] Connected... ' + server.id + ':' + fname + ' => RESP Time: ' + responseTime);

            // Add "conn.end()" after processing callback result
            remoteArgs.push(function () {
                var size = sizeof(arguments);
                rpcLog.info('[' + rpcId + '+] Callbacked... ' + server.id + ':' + fname + ' => Arguments Size: ' + size);
                cbCalled = true;
                cbFunc.apply(null, arguments);
                d.end();
            });

            // check remote function is supported
            if (remote[fname] === undefined) {
                d.end();
                handleErrorOnRPC(new DError('RPC003', {
                    host: server.host,
                    port: server.port,
                    functionName: fname
                }));
                return;
            }
            // Call remote function
            remote[fname].apply(remote, remoteArgs);
        });

        d.on('error', function (err) {
            // stop timeout counting
            if (rpcResponseTimeout) {
                clearTimeout(rpcResponseTimeout);
            }
            rpcLog.error('[' + rpcId + '+] Error... ' + server.id + ':' + fname + ' => Error: ' + util.inspect(err, { showHidden: true, depth: null }));
            if (err.stack) {
                rpcLog.error('[' + rpcId + '+] Error Stack... ' + err.stack);
            }

            d.end();
            handleErrorOnRPC(new DError('RPC004', {
                host: server.host,
                port: server.port,
                functionName: fname,
                stack: err.stack
            }));
        });

        d.on('fail', function (err) {
            // stop timeout counting
            if (rpcResponseTimeout) {
                clearTimeout(rpcResponseTimeout);
            }
            rpcLog.error('[' + rpcId + '+] Fail... ' + server.id + ':' + fname + ' => Error: ' + util.inspect(err));

            d.end();
            handleErrorOnRPC(new DError('RPC005', {
                host: server.host,
                port: server.port,
                functionName: fname
            }));
        });

        d.on('end', function () {
            // stop timeout counting
            if (rpcResponseTimeout) {
                clearTimeout(rpcResponseTimeout);
            }

            if (!cbCalled) {
                cbCalled = true;
                cbFunc(new DError('RPC008', {
                    host: server.host,
                    port: server.port,
                    functionName: fname
                }));
            }
        });

        // use pipe for passing connect stream
        conn.pipe(d).pipe(conn);
    });

    conn.on('error', function (err) {
        // stop timeout counting
        if (rpcResponseTimeout) {
            clearTimeout(rpcResponseTimeout);
        }

        handleErrorOnRPC(new DError('RPC001', {
            host: server.host,
            port: server.port,
            functionName: fname
        }, err));
    });


    function handleErrorOnRPC(error) {
        // To prevent duplicated-calls of rpc callback
        if (!cbCalled) {
            cbCalled = true;
            cbFunc(error);
        }
    }
}


function addRpcModule(moduleHash, module) {
    if (module !== undefined) {
        if (!moduleHash) {
            moduleHash = {};
        }

        for (var key in module) {
            if (moduleHash[key] === undefined) {
                moduleHash[key] = module[key];
            }
        }
    }

    return moduleHash;
}
