/**
 * Created by TalhaAkkas on 02/03/15.
 */
/**
 * Created by TalhaAkkas on 27/02/15.
 */
var Worker = require('workerjs');
var os = require('os');
var DEFAULT_QUEUE_KEY = "<DEFAULT_KEY>"
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var ARGUMENT_NAMES = /([^\s,]+)/g;
var MAX_WORKING_THREADS = os.cpus().length * 2 || 8;
var DEFAULT_MAX_WORKING_THREADS_PER_KEY = os.cpus().length / 4 || 2;
var MAX_WAITING_TIME = 1 * 20 * 1000;

var defaultInitPath = "";
var maxWorkingThreadCounts = {};
var workerRecycleCan = {};
var jobQueue = [];
var workers = [];
var functionRegistery = {};
exports.registerFunction = function(modelName, className, func){
    functionRegistery[modelName + "-" + className] = func;
};
exports.getRegisteredFunction = function (modelName, className) {
    if( functionRegistery[modelName + "-" + className] == undefined){
        rekuire(modelName)
    }
    return functionRegistery[modelName + "-" + className];
}
exports.getFunctionRegistery = function () {
    return functionRegistery;
}
exports.getActiveQueue = function(){
    return workers.map(function(w){return w.job});
};
exports.getJobQueue = function(){
    return jobQueue;
};
exports.getMaxWorkingThreadCounts = function(){
    return maxWorkingThreadCounts;
};
exports.getEmptyThreadCount = function(){
    return MAX_WORKING_THREADS - workers.length;
};
exports.getDefaultInitPath = function(){
    return defaultInitPath;
};
exports.setDefaultInitPath = function(path){
    defaultInitPath = path;
};
exports.getMaxWaitingTime = function(){
    return MAX_WAITING_TIME;
};
exports.setMaxWaitingTime = function(max){
    MAX_WAITING_TIME = max;
};
exports.getTotalMaxWorkingThreads = function(){
    return MAX_WORKING_THREADS;
};
exports.setTotalMaxWorkingThreads = function(max){
    MAX_WORKING_THREADS = max;
};
exports.getPerKeyMaxWorkingThreads = function(){
    return DEFAULT_MAX_WORKING_THREADS_PER_KEY;
};
exports.setPerKeyMaxWorkingThreads = function(max){
    DEFAULT_MAX_WORKING_THREADS_PER_KEY = max;
};
exports.getParamNames = function(func) {
    var fnStr = func.toString().replace(STRIP_COMMENTS, '');
    var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
    if(result === null)
        result = [];
    return result;
};

function construct(constructor, args) {
    function F() {
        return constructor.apply(this, args);
    }
    F.prototype = constructor.prototype;
    return new F();
}

exports.activeThreadsPerKey = function(){
    var activeThreadsPerKey = {};
    workers.forEach(function(worker){
        if(worker.job) {
            var key = worker.job.key;
            if (activeThreadsPerKey[key] == undefined) {
                activeThreadsPerKey[key] = 0;
            }
            activeThreadsPerKey[key] += 1;
        }
    });
    return activeThreadsPerKey;
};
// assumes you can run this job;
exports.getJobForRun = function(){
    var activeThreadsPerKey = this.activeThreadsPerKey();
   // EWLog(activeThreadsPerKey);
    for(var i = 0; i < jobQueue.length; i++){
        var job = jobQueue[i];
        var maxThreadCount = maxWorkingThreadCounts[job.key] || DEFAULT_MAX_WORKING_THREADS_PER_KEY;
        var activeThreads = activeThreadsPerKey[job.key] || 0;
        //console.log("getJobForRun", job.key, maxThreadCount, activeThreads);
        if(activeThreads < maxThreadCount){
            // console.log("getJobForRun", job.key);
            return jobQueue.splice(i,1)[0];
        }else{
            // console.log("else")
        }
    }
};

exports.setMaxWorkingThread = function(funcname, value){
    maxWorkingThreadCounts[funcname] = value;
};
exports.getWorkerThread = function(path, usageLimit, initPath){
    var key = path + "+" + initPath;
    var worker = undefined;
    if(! workerRecycleCan[key]){
        workerRecycleCan[key] = [];
    }
    if( workerRecycleCan[key].length > 0){
        worker = workerRecycleCan[key].shift();
    }
    //EWLog(workerRecycleCan[key].length, workers.length)
    if(worker == undefined){
        worker = exports.createWorker(path, usageLimit, initPath);
       // EWLog("new Worker", worker.child.signalCode)
    }else if (worker != undefined && worker.child.signalCode == 'SIGKILL'){
        EWLog("need kill worker", worker.child.signalCode)
        worker.terminate();
        delete worker;
        worker = exports.createWorker(path, usageLimit, initPath);
    }else{
       // EWLog("recycled worker", worker.child.signalCode)

    }
    return worker;
};
exports.recycleWorker = function(worker){
    var key = worker.path + "+" + worker.initPath;
    if(! workerRecycleCan[key]){
        workerRecycleCan[key] = [];
    }
    var index = workers.indexOf(worker);
    workers.splice(index,1);
    workerRecycleCan[key].push(worker);
    //EWLog(workerRecycleCan[key].length, __callerlline)
};
exports.createWorker = function(path, usageLimit, initPath){
    //console.log(path);
    var that = this;
    var worker = new Worker(path, true);
    workers.push(worker);
    worker.path = path;
    worker.initPath = initPath;
    worker.addEventListener('message', function (msg) {
        if(worker.didTerminated){
            EWLog("worker.didTerminated worker.didTerminated worker.didTerminated")
            return;
        }
        //EWLog(msg);
        if(msg.type == "COMPLETE"){
            try{
                var data = JSON.parse(msg.data);
                worker.didCollectedData(data);
            }catch(e){
                EWLog(e.stack);
            }finally {
                worker.parts = undefined;
            }
        }else if (msg.type == "PARTIAL"){
            if(worker.parts == undefined){
                worker.parts = [];
            }  
            worker.parts.push(msg);
            if(worker.parts.length == msg.expected){
                worker.parts.sort(function(a,b){return a.start - b.start;});
                var text = worker.parts.map(function(item){return item.data;}).join("");
                try{
                    data = JSON.parse(text);
                    worker.didCollectedData(data);
                }catch(e){
                    EWLog(worker.parts.length ,  msg.expected);
                    EWLog(e.stack);
                }finally {
                    worker.parts = undefined;
                }
            }
        }else if(msg.type == "TIMEOUT"){
            console.log("TIMEOUT");
            worker.terminate();
            delete worker;
            //EWLog("checkWorkQueue>")
            exports.checkWorkQueue();
        }else{
            EWLog("UNKNOWN MESSAGE");
            EWLog(msg);
        }
        /*//console.log("onMessage threads");*/
    });
    worker.didCollectedData = function(data){
        if(worker.job.callback){
            worker.job.callback.apply(worker.job.that, data) ;
        }
        delete worker.job;
        //EWLog("worker.didTerminated",worker.didTerminated)
        //console.log("addEventListener checkWorkQueue ",jobQueue.length, workers.length);
        if(worker.didTerminated == 0 && !that.checkWorkQueue(this)){
            exports.recycleWorker(this);
            // this.terminate();
        }
    };
    worker.checkTimeOut = function(){
        if(Date.now() - worker.lastMessageTime > MAX_WAITING_TIME && workers.indexOf(this) != -1){
            //console.log("terminate due to waiting");
            //EWLog(workers.length, "Worker Terminated Due to Timeout")
            var error = new Error(408, "Worker Terminated Due to Timeout");
            if(worker.job){
                worker.job.callback.apply(worker.job.that, [error]);
            }
            worker.terminate();
            delete worker;
            //EWLog("checkWorkQueue>")
            exports.checkWorkQueue();
            //EWLog("checkWorkQueue<")
            //EWLog(workers.length)
        }
    };
    worker.__terminate = worker.terminate;
    worker.terminate = function(){
        //console.log("worker.terminate");
        worker.didTerminated = 1;
        worker.__postMessage({type:"TERMINATE"});
        delete worker.job;
        var index = workers.indexOf(this);
        workers.splice(index,1);
        worker.__terminate();
    };
    worker.__postMessage = worker.postMessage;
    worker.postMessage = function(data){
        //console.log("worker.terminate");

        worker.lastMessageTime = Date.now();
        setTimeout(function(){
            worker.checkTimeOut();
        }, MAX_WAITING_TIME + 100);
        worker.__postMessage(data);
    };

    if(usageLimit > 0){
        worker.usageLimit = usageLimit;
        worker.used = 0;
    }else{

        worker.usageLimit = -1;
    }
    worker.__postMessage({type:"INIT", path:initPath});
    return worker;
};
exports.checkWorkQueue = function(worker,  q){
    if(worker && worker.usageLimit > 0 && worker.used > worker.usageLimit){
        //console.log("terminated overly used call");
        worker.terminate();
        delete worker;
        return exports.checkWorkQueue();
    }
    if(q){
        q = q + 1
    }else{
        q = 1
    }
    var result = false;
    //EWLog(workers.length, jobQueue.length)
    if(jobQueue.length > 0){
        //EWLog(workers.length);
        if(worker == undefined && workers.length < MAX_WORKING_THREADS ){
            //EWLog(workers.length);
            var job = exports.getJobForRun();
            if(job) {
                worker = exports.getWorkerThread(job.path, job.usageLimit, job.initPath);
                worker.used = 1;
                worker.job = job;
                worker.postMessage(job.data);
                result = true;
            }else{
                return false;
            }
        }else if(worker != undefined && workers.length <= MAX_WORKING_THREADS){
            var job = exports.getJobForRun();
            if(job && job.path == worker.path) {
                worker.job = job;
                worker.used += 1;
                worker.postMessage(job.data);
                result = true;
            }else if (job){
                exports.recycleWorker(worker);
                worker = exports.getWorkerThread(job.path, job.usageLimit, job.initPath);
                worker.used = 1;
                worker.job = job;
                worker.postMessage(job.data);
                result = true;
            }else{
                return false;
            }
        }
    }

    if(jobQueue.length > 0 && 
        workers.length < MAX_WORKING_THREADS && 
        q < 5){
        exports.checkWorkQueue(worker, q);
        //EWLog(">>>>", workers.length, jobQueue.length)
    }
    //console.log("jobQueue.length", jobQueue.length, workers.length);
    return result;
};

exports.pushJob = function(options, cb){
    var usageLimit = options.usageLimit || -1;
    var data = options.data || {};
    var key = options.key || DEFAULT_QUEUE_KEY;
    var that = options.that || null;
    var path = options.path || __dirname + '/childThread.js';
    var initPath = options.initPath || exports.getDefaultInitPath();
    var jobID = options.jobID ||"<id>" + new Date().getTime() + "<rand>" + Math.random() + "<id>";
    var meta = options.meta || {}

    var job = {_id:jobID, type:"JOB", data:data, that:that, callback:cb, key:key, path:path, meta:meta, usageLimit:usageLimit, initPath:initPath};
    jobQueue.push(job);
    var that = this
    process.nextTick(function(){that.checkWorkQueue()});
};
exports.encapsulateFunction = function(object, funcName, modelName) { // last argument must be callback;
    var func = object[funcName];
    var superName = "__" + funcName;
    var arguments = this.getParamNames(func);

    object[superName] = func;

    var mongooseRunnerPath = __dirname + '/Workers/DBWorker.js';
    var mongooseRunnerInitPath = this.getDefaultInitPath();
    var functionBody = "var arguments = [];";
    arguments.forEach(function(arg){ functionBody += "arguments.push(" + arg + ");" });
    functionBody += "var data = {};";
    functionBody += "data.modelName = '"+ modelName +"';";
    functionBody += "data.functionName = '"+ superName +"';";
    functionBody += "data.arguments = arguments;";
    functionBody += "data.callbackArguments = this.threads.getParamNames(cb);";
    functionBody += "var options = {};";
    functionBody += "options.data = data;";
    functionBody += "options.key = '" + funcName + "';";
    functionBody += "options.that = null;";
    functionBody += "options.path = '" + mongooseRunnerPath + "';";

    functionBody += "this.threads.pushJob(options, cb);";
    arguments.push(functionBody);
    //console.log("this.threads.pushJob(data," + funcName + ",cb);");
    object[funcName] = construct(Function,  arguments);

    object.threads = this;
};
exports.encapsulateControllerFunction = function(object, funcName, modelName){
    var func = object[funcName];
    var superName = "__" + funcName;
    var arguments = this.getParamNames(func);

    object[superName] = func;

    var controllerRunnerPath = __dirname + '/Workers/ControllerWorker.js';
    var controllerRunnerInitPath = this.getDefaultInitPath();
    var functionBody = "var arguments = [];";
    arguments.forEach(function(arg){ functionBody += "arguments.push(" + arg + ");" });
    functionBody += "var data = {};";
    functionBody += "data.modelName = '"+ modelName +"';";
    functionBody += "data.functionName = '"+ superName +"';";
    functionBody += "data.arguments = arguments;";
    functionBody += "data.callbackArguments = this.threads.getParamNames(cb);";
    functionBody += "var options = {};";
    functionBody += "options.data = data;";
    functionBody += "options.key = '" + funcName + "';";
    functionBody += "options.that = null;";
    functionBody += "options.path = '" + controllerRunnerPath + "';";

    functionBody += "this.threads.pushJob(options, cb);";
    arguments.push(functionBody);
    //console.log("this.threads.pushJob(data," + funcName + ",cb);");
    object[funcName] = construct(Function,  arguments);
    exports.registerFunction(modelName, superName, func);
    object.threads = this;
};
exports.encapsulateMongoClientFunction = function(object, funcName, modelName){
    var func = object[funcName];
    var superName = "__" + funcName;
    var arguments = this.getParamNames(func);
    object[superName] = func;

    var controllerRunnerPath = __dirname + '/Workers/MongoClientWorker.js';
    var controllerRunnerInitPath = this.getDefaultInitPath();
    var functionBody = "var arguments = [];";
    arguments.forEach(function(arg){
        if(arg == "db") {
            functionBody += "arguments.push(null);"

        }else{
            functionBody += "arguments.push(" + arg + ");" }
    });
    arguments = arguments.filter(function(arg){return arg != "db"})
    functionBody += "var data = {};";
    functionBody += "data.modelName = '"+ modelName +"';";
    functionBody += "data.functionName = '"+ superName +"';";
    functionBody += "data.arguments = arguments;";
    functionBody += "data.callbackArguments = this.threads.getParamNames(cb);";
    functionBody += "var options = {};";
    functionBody += "options.data = data;";
    functionBody += "options.key = '" + funcName + "';";
    functionBody += "options.that = null;";
    functionBody += "options.path = '" + controllerRunnerPath + "';";
    functionBody += "options.usageLimit = 50;";
    functionBody += "this.threads.pushJob(options, cb);";
    arguments.push(functionBody);
    object[funcName] = construct(Function,  arguments);

    exports.registerFunction(modelName, superName, func);
    object.threads = this;
};