/**
 * CountlyBulk odule providing object to manage the internal queue and send bulk requests to Countly server.
 * @name CountlyBulk
 * @module lib/countly-bulk
 * @example
 * var CountlyBulk = require("countly-sdk-nodejs").Bulk;
 *
 * var server = new CountlyBulk({
 *   app_key: "{YOUR-APP-KEY}",
 *   url: "https://API_HOST/",
 *   debug: true
 * });
 *
 * //start processing queued requests or next ones that will be added
 * server.start()
 *
 * //adding raw request
 * server.add_request({begin_session:1, metrics:{_os:"Linux"}, device_id:"users_device_id", events:[{key:"Test", count:1}]});
 */

var fs = require("fs"),
    path = require("path"),
    http = require("http"),
    https = require("https"),
    cluster = require("cluster"),
    BulkUser = require("./countly-bulk-user");

/**
 * @lends module:lib/countly-bulk
 * Initialize CountlyBulk server object
 * @param {Object} conf - CountlyBulk server object with configuration options
 * @param {string} conf.app_key - app key for your app created in Countly
 * @param {string} conf.url - your Countly server url, you can use your own server URL or IP here
 * @param {boolean} [conf.debug=false] - output debug info into console
 * @param {number} [conf.interval=5000] - set an interval how often to check if there is any data to report and report it in miliseconds
 * @param {number} [conf.bulk_size=50] - maximum amount or requests per one bulk request
 * @param {number} [conf.fail_timeout=60] - set time in seconds to wait after failed connection to server in seconds
 * @param {number} [conf.session_update=60] - how often in seconds should session be extended
 * @param {number} [conf.max_events=10] - maximum amount of events to send in one batch
 * @param {boolean} [conf.persist_queue=false] - persistantly store queue until processed, default is false if you want to keep queue in memory and process all in one process run
 * @param {boolean} [conf.force_post=false] - force using post method for all requests
 * @param {string} [conf.storage_path="../bulk_data/"] - where SDK would store data, including id, queues, etc
 * @returns {module:lib/countly-bulk} instance
 * @example
 * var server = new CountlyBulk({
 *   app_key: "{YOUR-API-KEY}",
 *   url: "https://API_HOST/",
 *   debug: true
 * });
 */
function CountlyBulk(conf){
    "use strict";

    var SDK_VERSION = "19.02";
    var SDK_NAME = "javascript_native_nodejs_bulk";

    var empty_queue_callback = null;

    var initiated = false,
        lastMsTs = 0,
        apiPath = "/i/bulk",
        failTimeout = 0,
        empty_count = 0,
        readyToProcess = true,
        __data = {};

    if(!conf.app_key){
        log("app_key is missing");
        return;
    }

    if(!conf.url){
        log("url is missing");
        return;
    }

    conf.url = stripTrailingSlash(conf.url);

    conf.debug = conf.debug || false;
    conf.interval = conf.interval || 5000;
    conf.bulk_size = conf.bulk_size || 50;
    conf.fail_timeout = conf.fail_timeout || 60;
    conf.session_update = conf.session_update || 60;
    conf.max_events = conf.max_events || 10;
    conf.force_post = conf.force_post || false;
    conf.persist_queue = conf.persist_queue || false;
    conf.storage_path = conf.storage_path || "../bulk_data/";

    var dir = path.resolve(__dirname, conf.storage_path);
    if(conf.persist_queue){
        try{
            if (!fs.existsSync(dir)){
                fs.mkdirSync(dir);
            }
        } catch(ex){
            //problem creating dir
            console.log(ex.stack);
        }
    }

    this.conf = conf;

    /**
    * Add raw request with provided query string parameters
    * @example
    * var server = new CountlyBulk({
    *   app_key: "{YOUR-API-KEY}",
    *   url: "https://API_HOST/",
    *   debug: true
    * });
    * server.add_request({app_key:"somekey", devide_id:"someid", events:"[{'key':'val','count':1}]", begin_session:1});
    * @param {Object} query - object with key/values which will be used as query string parameters
    * @returns {module:lib/countly-bulk} instance
    **/
    this.add_request = function(query){
        if(cluster.isMaster){
            if(!query.device_id){
                log("device_id is missing", query);
                return;
            }

            if(!query.app_key){
                query.app_key = conf.app_key;
            }

            if((query.timestamp + "").length !== 13 && (query.timestamp + "").length !== 10){
                log("incorrect timestamp format", query);
            }
            query.sdk_name = SDK_NAME;
            query.sdk_version = SDK_VERSION;
            query.timestamp = query.timestamp || getMsTimestamp();
            var date = new Date((query.timestamp + "").length === 13 ? query.timestamp : parseInt(query.timestamp)*1000);
            query.hour = query.hour || date.getHours();
            query.dow = query.dow || date.getDay();

            requestQueue.push(query);
            log("adding request", query);
            storeSet("cly_req_queue", requestQueue);
        }
        else{
            process.send({ cly_bulk: {cly_queue: query} });
        }
        return this;
    };

    /**
    * Add multiple raw requests each with provided query string parameters
    * @example
    * var server = new CountlyBulk({
    *   app_key: "{YOUR-API-KEY}",
    *   url: "https://API_HOST/",
    *   debug: true
    * });
    * server.add_bulk_request([{app_key:"somekey", devide_id:"someid", begin_session:1}, {app_key:"somekey", devide_id:"someid", events:"[{'key':'val','count':1}]"}]);
    * @param {Array} bulk - array with multiple request objects that can be provided with {@link CountlyBulk.add_request}
    * @returns {module:lib/countly-bulk} instance
    **/
    this.add_bulk_request = function(requests){
        if(cluster.isMaster){
            var query;
            for(var i in requests){
                query = requests[i];
                if(!query.device_id){
                    log("device_id is missing", query);
                    return;
                }

                if(!query.app_key){
                    query.app_key = conf.app_key;
                }

                if((query.timestamp + "").length !== 13 && (query.timestamp + "").length !== 10){
                    log("incorrect timestamp format", query);
                }

                query.sdk_name = SDK_NAME;
                query.sdk_version = SDK_VERSION;
                query.timestamp = query.timestamp || getMsTimestamp();
                var date = new Date((query.timestamp + "").length === 13 ? query.timestamp : parseInt(query.timestamp)*1000);
                query.hour = query.hour || date.getHours();
                query.dow = query.dow || date.getDay();
                log("adding bulk request", query);
                requestQueue.push(query);
            }
            storeSet("cly_req_queue", requestQueue);
        }
        else{
            process.send({ cly_bulk: {cly_bulk_queue: requests} });
        }
        return this;
    };

    /**
    * Add raw event data for specific user (events are bulked by users)
    * @example
    * var server = new CountlyBulk({
    *   app_key: "{YOUR-API-KEY}",
    *   url: "https://API_HOST/",
    *   debug: true
    * });
    * server.add_event("my_device_id", {'key':'val','count':1});
    * @param {String} device_id - user or device identification
    * @param {Object} event - event object
    * @param {string} event.key - name or id of the event
    * @param {number} [event.count=1] - how many times did event occur
    * @param {number=} event.sum - sum to report with event (if any)
    * @param {number=} event.dur - duration to report with event (if any)
    * @param {Object=} event.segmentation - object with segments key /values
    * @param {number=} event.timestamp - timestamp when event occurred
    * @returns {module:lib/countly-bulk} instance
    **/
    this.add_event = function(device_id, event){
        if(!device_id){
            log("device_id is missing");
            return;
        }

        if(!event.key){
            log("Event must have key property");
            return;
        }
        if(cluster.isMaster){
            if(!event.count){
                event.count = 1;
            }

            var props = ["key", "count", "sum", "dur", "segmentation", "timestamp"];
            var e = getProperties(event, props);
            e.timestamp = e.timestamp || getMsTimestamp();
            var date = new Date((e.timestamp + "").length === 13 ? e.timestamp : parseInt(e.timestamp)*1000);
            e.hour = date.getHours();
            e.dow = date.getDay();
            log("Adding event: ", event);
            if(!eventQueue[device_id]){
                eventQueue[device_id] = [];
            }
            eventQueue[device_id].push(e);
            storeSet("cly_bulk_event", eventQueue);
        }
        else{
            process.send({ cly_bulk: {device_id:device_id, event: event} });
        }
        return this;
    };

    /**
    * Start processing requests
    * @param {function} callback - to call when queue is empty and you can stop server
    * @returns {module:lib/countly-bulk} instance
    * @example
    * var server = new CountlyBulk({
    *   app_key: "{YOUR-API-KEY}",
    *   url: "https://API_HOST/",
    *   debug: true
    * });
    * server.start();
    **/
    this.start = function(callback){
        if (cluster.isMaster) {
            if(!initiated){
                empty_queue_callback = callback;
                initiated = true;
                heartBeat();
            }
        }
        return this;
    };

    /**
    * Stop processing requests
    * @returns {module:lib/countly-bulk} instance
    * @example
    * var server = new CountlyBulk({
    *   app_key: "{YOUR-API-KEY}",
    *   url: "https://API_HOST/",
    *   debug: true
    * });
    * server.stop();
    **/
    this.stop = function(){
        initiated = false;
        return this;
    };

    /**
    * Manually check queue size
    * @returns {number} amount of items in queue
    * @example
    * var server = new CountlyBulk({
    *   app_key: "{YOUR-API-KEY}",
    *   url: "https://API_HOST/",
    *   debug: true
    * });
    * server.add_request({device_id:"id", app_key:"key", begin_session:1});
    * server.queue_size(); //should return 1
    **/
    this.queue_size = function(){
        var eventCount = 0;
        for(var i in eventQueue){
            eventCount += eventQueue[i].length;
        }
        return Math.ceil(eventCount/conf.max_events) + Math.ceil(requestQueue.length/conf.bulk_size) + bulkQueue.length;
    };

    /**
    * Create specific user to easier send information about specific user
    * @param {Object} conf - CountlyBulkUser configuration options
    * @param {string} conf.device_id - identification of the user
    * @param {string=} conf.country_code - country code for your user
    * @param {string=} conf.city - name of the city of your user
    * @param {string=} conf.ip_address - ip address of your user
    * @returns {module:lib/countly-bulk-user} instance
    * @example
    * var server = new CountlyBulk({
    *   app_key: "{YOUR-API-KEY}",
    *   url: "https://API_HOST/",
    *   debug: true
    * });
    * var user = server.add_user({device_id:"my_device_id"});
    **/
    this.add_user = function(conf){
        conf.server = this;
        return new BulkUser(conf);
    };

    //insert request to queue
    function toBulkRequestQueue(bulkRequest){
        bulkQueue.push(bulkRequest);
        storeSet("cly_bulk_queue", bulkQueue);
    }
    var self = this;
    //heart beat
    function heartBeat(){
        var isEmpty = true;
        //process event queue
        var eventChanges = false;
        for(var device_id in eventQueue){
            if(eventQueue[device_id].length > 0){
                eventChanges = true;
                if(eventQueue[device_id].length <= conf.max_events){
                    self.add_request({device_id:device_id, events: eventQueue[device_id]});
                    eventQueue[device_id] = [];
                }
                else{
                    var events = eventQueue[device_id].splice(0, conf.max_events);
                    self.add_request({device_id:device_id, events: events});
                }
            }
        }
        if(eventChanges){
            isEmpty = false;
            storeSet("cly_bulk_event", eventQueue);
        }

        //process request queue into bulk requests
        if(requestQueue.length > 0){
            isEmpty = false;
            if(requestQueue.length <= conf.bulk_size){
                toBulkRequestQueue({app_key:conf.app_key, requests: JSON.stringify(requestQueue)});
                requestQueue = [];
            }
            else{
                var requests = requestQueue.splice(0, conf.bulk_size);
                toBulkRequestQueue({app_key:conf.app_key, requests: JSON.stringify(requests)});
            }
            storeSet("cly_req_queue", requestQueue);
        }

        //process bulk request queue
        if(bulkQueue.length > 0 && readyToProcess && getTimestamp() > failTimeout){
            isEmpty = false;
            readyToProcess = false;
            var params = bulkQueue.shift();
            log("Processing request", params);
            makeRequest(params, function(err, params){
                log("Request Finished", params, err);
                if(err){
                    bulkQueue.unshift(params);
                    failTimeout = getTimestamp() + conf.fail_timeout;
                }
                storeSet("cly_bulk_queue", bulkQueue);
                readyToProcess = true;
            });
        }

        if(isEmpty){
            empty_count++;
            if(empty_count === 3){
                empty_count = 0;
                if(empty_queue_callback){
                    empty_queue_callback();
                }
            }
        }

        if(initiated){
            setTimeout(heartBeat, conf.interval);
        }
    }

    //log stuff
    function log(){
        if(conf.debug && typeof console !== "undefined"){
            if(arguments[1] && typeof arguments[1] === "object"){
                arguments[1] = JSON.stringify(arguments[1]);
            }
            console.log( Array.prototype.slice.call(arguments).join("\n") );
        }
    }

    //get unique timestamp in miliseconds
    function getMsTimestamp(){
        var ts = new Date().getTime();
        if(lastMsTs >= ts){
            lastMsTs++;
        }
        else{
            lastMsTs = ts;
        }
        return lastMsTs;
    }

    //parsing host and port information from url
    function parseUrl(url) {
        var serverOptions = {
            host: "localhost",
            port: 80
        };
        if(url.indexOf("https") === 0){
            serverOptions.port = 443;
        }
        var host = url.split("://").pop();
        serverOptions.host = host;
        var lastPos = host.indexOf(":");
        if (lastPos > -1) {
            serverOptions.host = host.slice(0,lastPos);
            serverOptions.port = Number(host.slice(lastPos+1,host.length));
        }
        return serverOptions;
    }

    //convert JSON object to query params
    function prepareParams(params){
        var str = [];
        for(var i in params){
            str.push(i+"="+encodeURIComponent(params[i]));
        }
        return str.join("&");
    }

    //removing trailing slashes
    function stripTrailingSlash(str) {
        if(str.substr(str.length - 1) === "/") {
            return str.substr(0, str.length - 1);
        }
        return str;
    }

    //sending HTTP request
    function makeRequest(params, callback) {
        try {
            log("Sending HTTP request");
            var serverOptions = parseUrl(conf.url);
            var data = prepareParams(params);
            var method = "GET";
            var options = {
                host: serverOptions.host,
                port: serverOptions.port,
                path: apiPath+"?"+data,
                method: "GET"
            };

            if(data.length >= 2000){
                method = "POST";
            }
            else if(conf.force_post){
                method = "POST";
            }

            if(method === "POST"){
                options.method = "POST";
                options.path = apiPath;
                options.headers = {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Content-Length": Buffer.byteLength(data)
                };
            }
            var protocol = http;
            if(conf.url.indexOf("https") === 0){
                protocol = https;
            }
            var req = protocol.request(options, function(res) {
                var str = "";
                res.on("data", function (chunk) {
                    str += chunk;
                });

                res.on("end", function () {
                    try{
                        str = JSON.parse(str);
                    }
                    catch(ex){
                        str = {};
                    }
                    if(res.statusCode >= 200 && res.statusCode < 300 && str.result === "Success"){
                        callback(false, params);
                    }
                    else{
                        callback(true, params);
                    }
                });
            });
            if(method === "POST"){
                // write data to request body
                req.write(data);
            }

            req.on("error", function(err){
                log("Connection failed.", err);
                if (typeof callback === "function") {
                    callback(true, params);
                }
            });

            req.end();
        } catch (e) {
            // fallback
            log("Failed HTTP request", e);
            if (typeof callback === "function") { callback(true, params); }
        }
    }

    //get current timestamp
    function getTimestamp(){
        return Math.floor(new Date().getTime() / 1000);
    }

    //retrieve only specific properties from object
    function getProperties(orig, props){
        var ob = {};
        var prop;
        for(var i = 0; i < props.length; i++){
            prop = props[i];
            if(typeof orig[prop] !== "undefined"){
                ob[prop] = orig[prop];
            }
        }
        return ob;
    }

    function handleWorkerMessage(msg){
        if(msg.cly_bulk){
            if(msg.cly_bulk.cly_queue){
                self.add_request(msg.cly_bulk.cly_queue);
            }
            else if(msg.cly_bulk.cly_bulk_queue){
                self.add_bulk_request(msg.cly_bulk.cly_bulk_queue);
            }
            else if(msg.cly_bulk.event && msg.cly_bulk.device_id){
                self.add_event(msg.cly_bulk.device_id, msg.cly_bulk.event);
            }
        }
    }

    var readFile = function(key){
        var data;
        if(conf.persist_queue){
            var dir = path.resolve(__dirname, conf.storage_path+"__"+key+".json");

            //try reading data file
            try{
                data = fs.readFileSync(dir);
            } catch (ex) {
                //ther was no file, probably new init
                data = null;
            }

            try{
                //trying to parse json string
                data = JSON.parse(data);
            } catch (ex) {
                //problem parsing, corrupted file?
                console.log(ex.stack);
                //backup corrupted file data
                fs.writeFile(path.resolve(__dirname, conf.storage_path+"__"+key+"."+getTimestamp()+Math.random()+".json"), data, function(){});
                //start with new clean object
                data = null;
            }
        }
        return data;
    };

    var storeSet = function(key, value) {
        if(conf.persist_queue){
            __data[key] = value;
            var dir = path.resolve(__dirname, conf.storage_path+"__"+key+".json");
            var ob = {};
            ob[key] = value;
            try{
                fs.writeFileSync(dir, JSON.stringify(ob));
            }
            catch(ex){
                //problem saving file
                console.log(ex.stack);
            }
        }
    };

    var storeGet = function(key, def) {
        if(typeof __data[key] === "undefined"){
            var ob = readFile(key);
            if(!ob){
                __data[key] = def;
            }
            else{
                __data[key] = ob[key];
            }
        }
        return __data[key];
    };

    //listen to current workers
    if(cluster.workers){
        for (var id in cluster.workers) {
            cluster.workers[id].on("message", handleWorkerMessage);
        }
    }

    //handle future workers
    cluster.on("fork", function(worker) {
        worker.on("message", handleWorkerMessage);
    });

    var requestQueue = storeGet("cly_req_queue", []),
        eventQueue = storeGet("cly_bulk_event", {}),
        bulkQueue = storeGet("cly_bulk_queue", []);
}

module.exports = CountlyBulk;


/************
* Countly NodeJS SDK
* https://github.com/Countly/countly-sdk-nodejs
************/

/**
 * Countly object to manage the internal queue and send requests to Countly server. More information on {@link http://resources.count.ly/docs/countly-sdk-for-nodejs}
 * @name Countly
 * @global
 * @namespace Countly
 * @example
SDK integration

 * var Countly = require("countly-sdk-nodejs");
 *
 * Countly.init({
 *   app_key: "{YOUR-APP-KEY}",
 *   url: "https://API_HOST/",
 *   debug: true
 * });
 *
 * Countly.begin_session();
 */

var fs = require("fs"),
    os = require("os"),
    path = require("path"),
    http = require("http"),
    https = require("https"),
    cluster = require("cluster"),
    Bulk = require("./countly-bulk");

var Countly = {};

Countly.Bulk = Bulk;
(function (Countly) {
    "use strict";

    var SDK_VERSION = "19.02";
    var SDK_NAME = "javascript_native_nodejs";

    var inited = false,
        sessionStarted = false,
        platform,
        apiPath = "/i",
        readPath = "/o/sdk",
        beatInterval = 500,
        queueSize = 1000,
        requestQueue = [],
        eventQueue = [],
        remoteConfigs = {},
        crashLogs = [],
        maxCrashLogs = 100,
        timedEvents = {},
        crashSegments = null,
        autoExtend = true,
        lastBeat,
        storedDuration = 0,
        lastView = null,
        lastViewTime = 0,
        lastMsTs = 0,
        lastViewStoredDuration = 0,
        failTimeout = 0,
        failTimeoutAmount = 60,
        sessionUpdate = 60,
        maxEventBatch = 10,
        readyToProcess = true,
        trackTime = true,
        metrics = {},
        syncConsents = {},
        lastParams = {},
        startTime,
        __data = {};

    /**
    * Array with list of available features that you can require consent for
    */
    Countly.features = ["sessions","events","views","crashes","attribution","users","location"];

    //create object to store consents
    var consents = {};
    for(var i = 0; i < Countly.features.length; i++){
        consents[Countly.features[i]] = {};
    }

    /**
 * Initialize Countly object
 * @param {Object} conf - Countly initialization {@link Init} object with configuration options
 * @param {string} conf.app_key - app key for your app created in Countly
 * @param {string} conf.device_id - to identify a visitor, will be auto generated if not provided
 * @param {string} conf.url - your Countly server url, you can use your own server URL or IP here
 * @param {string} [conf.app_version=0.0] - the version of your app or website
 * @param {string=} conf.country_code - country code for your visitor
 * @param {string=} conf.city - name of the city of your visitor
 * @param {string=} conf.ip_address - ip address of your visitor
 * @param {boolean} [conf.debug=false] - output debug info into console
 * @param {number} [conf.interval=500] - set an interval how often to check if there is any data to report and report it in miliseconds
 * @param {number} [conf.queue_size=1000] - maximum amount of queued requests to store
 * @param {number} [conf.fail_timeout=60] - set time in seconds to wait after failed connection to server in seconds
 * @param {number} [conf.session_update=60] - how often in seconds should session be extended
 * @param {number} [conf.max_events=10] - maximum amount of events to send in one batch
 * @param {boolean} [conf.force_post=false] - force using post method for all requests
 * @param {string} [conf.storage_path="../data/"] - where SDK would store data, including id, queues, etc
 * @param {boolean} [conf.require_consent=false] - Pass true if you are implementing GDPR compatible consent management. It would prevent running any functionality without proper consent
 * @param {boolean|function} [conf.remote_config=false] - Enable automatic remote config fetching, provide callback function to be notified when fetching done
 * @param {number} [conf.max_logs=100] - maximum amount of breadcrumbs to store for crash logs
 * @param {Object} conf.metrics - provide {@link Metrics} for this user/device, or else will try to collect what's possible
 * @param {string} conf.metrics._os - name of platform/operating system
 * @param {string} conf.metrics._os_version - version of platform/operating system
 * @param {string} conf.metrics._device - device name
 * @param {string} conf.metrics._resolution - screen resolution of the device
 * @param {string} conf.metrics._carrier - carrier or operator used for connection
 * @param {string} conf.metrics._density - screen density of the device
 * @param {string} conf.metrics._locale - locale or language of the device in ISO format
 * @param {string} conf.metrics._store - source from where the user/device/installation came from
 * @example
 * Countly.init({
 *   app_key: "{YOUR-APP-KEY}",
 *   url: "https://API_HOST/",
 *   debug: true,
 *   app_version: "1.0",
 *   metrics:{
 *      _os: "Ubuntu",
 *      _os_version: "16.04",
 *      _device: "aws-server"
 *   }
 * });
 */
    Countly.init = function(ob){
        if(!inited){
            startTime = getTimestamp();
            inited = true;
            ob = ob || {};
            timedEvents = {};
            beatInterval = ob.interval || Countly.interval || beatInterval;
            queueSize = ob.queue_size || Countly.queue_size || queueSize;
            failTimeoutAmount = ob.fail_timeout || Countly.fail_timeout || failTimeoutAmount;
            sessionUpdate = ob.session_update || Countly.session_update || sessionUpdate;
            maxEventBatch = ob.max_events || Countly.max_events || maxEventBatch;
            metrics = ob.metrics || Countly.metrics || {};
            maxCrashLogs = ob.max_logs || Countly.max_logs || maxCrashLogs;
            Countly.debug = ob.debug || Countly.debug || false;
            Countly.app_key = ob.app_key || Countly.app_key || null;
            Countly.url = stripTrailingSlash(ob.url || Countly.url || "");
            Countly.app_version = ob.app_version || Countly.app_version || "0.0";
            Countly.country_code = ob.country_code || Countly.country_code || null;
            Countly.city = ob.city || Countly.city || null;
            Countly.ip_address = ob.ip_address || Countly.ip_address || null;
            Countly.force_post = ob.force_post || Countly.force_post || false;
            Countly.storage_path = ob.storage_path || Countly.storage_path || "../data/";
            Countly.require_consent = ob.require_consent || Countly.require_consent || false;
            Countly.remote_config = ob.rremote_config || Countly.remote_config || false;

            var dir = path.resolve(__dirname, Countly.storage_path);
            try{
                if (!fs.existsSync(dir)){
                    fs.mkdirSync(dir);
                }
            } catch(ex){
                //problem creating dir
                console.log(ex.stack);
            }

            if(Countly.url === ""){
                log("Please provide server URL");
            }
            else{
                log("Countly initialized");
                if (cluster.isMaster) {
                    Countly.device_id = ob.device_id || Countly.device_id || getId();
                    storeSet("cly_id", Countly.device_id);
                    requestQueue = storeGet("cly_queue", []);
                    eventQueue = storeGet("cly_event", []);
                    remoteConfigs = storeGet("cly_remote_configs", {});
                    heartBeat();
                    //listen to current workers
                    if(cluster.workers){
                        for (var id in cluster.workers) {
                            cluster.workers[id].on("message", handleWorkerMessage);
                        }
                    }
                    //handle future workers
                    cluster.on("fork", function(worker) {
                        worker.on("message", handleWorkerMessage);
                    });
                    if (Countly.remote_config) {
                        Countly.fetch_remote_config(Countly.remote_config);
                    }
                }
            }
        }
    };

    /**
    * Modify feature groups for consent management. Allows you to group multiple features under one feature group
    * @param {object} features - object to define feature name as key and core features as value
    * @example
Adding all features under one group

    * Countly.group_features({all:["sessions","events","views","crashes","attribution","users"]});
    * //After this call Countly.add_consent("all") to allow all features
    @example
Grouping features

    * Countly.group_features({
    *    activity:["sessions","events","views"],
    *    info:["attribution","users"]
    * });
    * //After this call Countly.add_consent("activity") to allow "sessions","events","views"
    * //or call Countly.add_consent("info") to allow "attribution","users"
    * //or call Countly.add_consent("crashes") to allow some separate feature
    */
    Countly.group_features = function(features){
        if(features){
            for(var i in features){
                if(!consents[i]){
                    if(typeof features[i] === "string"){
                        consents[i] = {features:[features[i]]};
                    }
                    else if(features[i] && features[i].constructor === Array && features[i].length){
                        consents[i] = {features:features[i]};
                    }
                    else{
                        log("Incorrect feature list for "+i+" value: "+features[i]);
                    }
                }
                else{
                    log("Feature name "+i+" is already reserved");
                }
            }
        }
        else{
            log("Incorrect features: "+features);
        }
    };

    /**
    * Check if consent is given for specific feature (either core feature of from custom feature group)
    * @param {string} feature - name of the feature, possible values, "sessions","events","views","crashes","attribution","users" or customly provided through {@link Countly.group_features}
    */
    Countly.check_consent = function(feature){
        if(!Countly.require_consent){
            //we don't need to have specific consents
            return true;
        }
        if(consents[feature]){
            return (consents[feature] && consents[feature].optin) ? true : false;
        }
        else{
            log("No feature available for "+feature);
        }
        return false;
    };

    /**
    * Check if any consent is given, for some cases, when crucial parts are like device_id are needed for any request
    */
    Countly.check_any_consent = function(){
        if(!Countly.require_consent){
            //we don't need to have consents
            return true;
        }
        for(var i in consents){
            if(consents[i] && consents[i].optin){
                return true;
            }
        }
        return false;
    };

    /**
    * Add consent for specific feature, meaning, user allowed to track that data (either core feature of from custom feature group)
    * @param {string|array} feature - name of the feature, possible values, "sessions","events","views","crashes","attribution","users" or customly provided through {@link Countly.group_features}
    */
    Countly.add_consent = function(feature){
        log("Adding consent for "+feature);
        if(feature.constructor === Array){
            for(var i = 0; i < feature.length; i++){
                Countly.add_consent(feature[i]);
            }
        }
        else if(consents[feature]){
            if(consents[feature].features){
                consents[feature].optin = true;
                //this is added group, let's iterate through sub features
                Countly.add_consent(consents[feature].features);
            }
            else{
                //this is core feature
                if(consents[feature].optin !== true){
                    syncConsents[feature] = true;
                    consents[feature].optin = true;
                    updateConsent();
                    setTimeout(function(){
                        if(feature === "sessions" && lastParams.begin_session){
                            Countly.begin_session.apply(Countly, lastParams.begin_session);
                            lastParams.begin_session = null;
                        }
                        else if(feature === "views" && lastParams.track_pageview){
                            lastView = null;
                            Countly.track_pageview.apply(Countly, lastParams.track_pageview);
                            lastParams.track_pageview = null;
                        }
                        if(lastParams.change_id){
                            Countly.change_id.apply(Countly, lastParams.change_id);
                            lastParams.change_id = null;
                        }
                    },1);
                }
            }
        }
        else{
            log("No feature available for "+feature);
        }
    };

    /**
    * Remove consent for specific feature, meaning, user opted out to track that data (either core feature of from custom feature group)
    * @param {string|array} feature - name of the feature, possible values, "sessions","events","views","crashes","attribution","users" or customly provided through {@link Countly.group_features}
    */
    Countly.remove_consent = function(feature){
        log("Removing consent for "+feature);
        if(feature.constructor === Array){
            for(var i = 0; i < feature.length; i++){
                Countly.remove_consent(feature[i]);
            }
        }
        else if(consents[feature]){
            if(consents[feature].features){
                //this is added group, let's iterate through sub features
                Countly.remove_consent(consents[feature].features);
            }
            else{
                //this is core feature
                if(consents[feature].optin !== false){
                    syncConsents[feature] = false;
                    updateConsent();
                }
            }
            consents[feature].optin = false;
        }
        else{
            log("No feature available for "+feature);
        }
    };

    var consentTimer;
    var updateConsent = function(){
        if(consentTimer){
            //delay syncing consents
            clearTimeout(consentTimer);
            consentTimer = null;
        }
        consentTimer = setTimeout(function(){
            if(Object.keys(syncConsents).length){
                //we have consents to sync, create request
                toRequestQueue({consent:JSON.stringify(syncConsents)});
                //clear consents that needs syncing
                syncConsents = {};
            }
        }, 1000);
    };

    /**
    * Start session
    * @param {boolean} noHeartBeat - true if you don't want to use internal heartbeat to manage session
    */

    Countly.begin_session = function(noHeartBeat){
        if(Countly.check_consent("sessions")){
            if(!sessionStarted){
                log("Session started");
                lastBeat = getTimestamp();
                sessionStarted = true;
                autoExtend = (noHeartBeat) ? false : true;
                var req = {};
                req.begin_session = 1;
                req.metrics = JSON.stringify(getMetrics());
                toRequestQueue(req);
            }
        }
        else{
            lastParams.begin_session = arguments;
        }
    };

    /**
    * Report session duration
    * @param {int} sec - amount of seconds to report for current session
    */
    Countly.session_duration = function(sec){
        if(Countly.check_consent("sessions")){
            if(sessionStarted){
                log("Session extended", sec);
                toRequestQueue({session_duration:sec});
            }
        }
    };

    /**
    * End current session
    * @param {int} sec - amount of seconds to report for current session, before ending it
    */
    Countly.end_session = function(sec){
        if(Countly.check_consent("sessions")){
            if(sessionStarted){
                sec = sec || getTimestamp()-lastBeat;
                log("Ending session");
                reportViewDuration();
                sessionStarted = false;
                toRequestQueue({end_session:1, session_duration:sec});
            }
        }
    };

    /**
    * Change current user/device id
    * @param {string} newId - new user/device ID to use
    * @param {boolean=} merge - move data from old ID to new ID on server
    **/
    Countly.change_id = function(newId, merge){
        if(cluster.isMaster){
            if(Countly.device_id !== newId){
                if(!merge){
                    //empty event queue
                    if (eventQueue.length > 0) {
                        toRequestQueue({ events: JSON.stringify(eventQueue) });
                        eventQueue = [];
                        storeSet("cly_event", eventQueue);
                    }
                    //end current session
                    Countly.end_session();
                    //clear timed events
                    timedEvents = {};
                }
                var oldId = Countly.device_id;
                Countly.device_id = newId;
                storeSet("cly_id", Countly.device_id);
                log("Changing id");
                if(merge){
                    if(Countly.check_any_consent()){
                        toRequestQueue({old_device_id:oldId});
                    }
                    else{
                        lastParams.change_id = arguments;
                    }
                }
                else{
                    //start new session for new id
                    Countly.begin_session(!autoExtend);
                }
                if (Countly.remote_config) {
                    remoteConfigs = {};
                    if (cluster.isMaster) {
                        storeSet("cly_remote_configs", remoteConfigs);
                    }
                    Countly.fetch_remote_config(Countly.remote_config);
                }
            }
        }
        else{
            process.send({ cly: {change_id: newId, merge:merge} });
        }
    };

    /**
    * Report custom event
    * @param {Event} event - Countly {@link Event} object
    * @param {string} event.key - name or id of the event
    * @param {number} [event.count=1] - how many times did event occur
    * @param {number=} event.sum - sum to report with event (if any)
    * @param {number=} event.dur - duration to report with event (if any)
    * @param {Object=} event.segmentation - object with segments key /values
    **/
    Countly.add_event = function(event){
        if(Countly.check_consent("events")){
            add_cly_events(event);
        }
    };

    //internal method for events, in case there is no consent for custom events, but internal events has consents
    function add_cly_events(event){
        if(!event.key){
            log("Event must have key property");
            return;
        }
        if(cluster.isMaster){
            if(!event.count){
                event.count = 1;
            }

            var props = ["key", "count", "sum", "dur", "segmentation"];
            var e = getProperties(event, props);
            e.timestamp = getMsTimestamp();
            var date = new Date();
            e.hour = date.getHours();
            e.dow = date.getDay();
            log("Adding event: ", event);
            eventQueue.push(e);
            storeSet("cly_event", eventQueue);
        }
        else{
            process.send({ cly: {event: event} });
        }
    }

    /**
    * Start timed event, which will fill in duration property upon ending automatically
    * @param {string} key - event name that will be used as key property
    **/
    Countly.start_event = function(key){
        if(timedEvents[key]){
            log("Timed event with key " + key + " already started");
            return;
        }
        timedEvents[key] = getTimestamp();
    };

    /**
    * End timed event
    * @param {string|Object} event - event key if string or Countly event same as passed to {@link Countly.add_event}
    **/
    Countly.end_event = function(event){
        if(typeof event === "string"){
            event = {key:event};
        }
        if(!event.key){
            log("Event must have key property");
            return;
        }
        if(!timedEvents[event.key]){
            log("Timed event with key " + event.key + " was not started");
            return;
        }
        event.dur = getTimestamp() - timedEvents[event.key];
        Countly.add_event(event);
        delete timedEvents[event.key];
    };

    /**
    * Report user data
    * @param {Object} user - Countly {@link UserDetails} object
    * @param {string=} user.name - user's full name
    * @param {string=} user.username - user's username or nickname
    * @param {string=} user.email - user's email address
    * @param {string=} user.organization - user's organization or company
    * @param {string=} user.phone - user's phone number
    * @param {string=} user.picture - url to user's picture
    * @param {string=} user.gender - M value for male and F value for femail
    * @param {number=} user.byear - user's birth year used to calculate current age
    * @param {Object=} user.custom - object with custom key value properties you want to save with user
    **/
    Countly.user_details = function(user){
        if(Countly.check_consent("users")){
            log("Adding userdetails: ", user);
            var props = ["name", "username", "email", "organization", "phone", "picture", "gender", "byear", "custom"];
            toRequestQueue({user_details: JSON.stringify(getProperties(user, props))});
        }
    };

    /**
    * Report user conversion to the server (when user signup or made a purchase, or whatever your conversion is)
    * @param {string} campaign_id - id of campaign, the last part of the countly campaign link
    * @param {string=} campaign_user_id - id of user's clicked on campaign link, if you have one
    **/
    Countly.report_conversion = function(campaign_id, campaign_user_id){
        if(Countly.check_consent("attribution")){
            if(campaign_id && campaign_user_id){
                toRequestQueue({campaign_id: campaign_id, campaign_user: campaign_user_id});
            }
            else if(campaign_id){
                toRequestQueue({campaign_id: campaign_id});
            }
            else{
                log("No campaign data found");
            }
        }
    };

    /**************************
    * Modifying custom property values of user details
    * Possible modification commands
    *  - inc, to increment existing value by provided value
    *  - mul, to multiply existing value by provided value
    *  - max, to select maximum value between existing and provided value
    *  - min, to select minimum value between existing and provided value
    *  - setOnce, to set value only if it was not set before
    *  - push, creates an array property, if property does not exist, and adds value to array
    *  - pull, to remove value from array property
    *  - addToSet, creates an array property, if property does not exist, and adds unique value to array, only if it does not yet exist in array
    **************************/
    var customData = {};
    var change_custom_property = function(key, value, mod){
        if(Countly.check_consent("users")){
            if(!customData[key]){
                customData[key] = {};
            }
            if(mod === "$push" || mod === "$pull" || mod === "$addToSet"){
                if(!customData[key][mod]){
                    customData[key][mod] = [];
                }
                customData[key][mod].push(value);
            }
            else{
                customData[key][mod] = value;
            }
        }
    };

    /**
    * Control user related custom properties. Don't forget to call save after finishing manipulation of custom data
    * @namespace Countly.userData
    * @name Countly.userData
    * @example
    * //set custom key value property
    * Countly.userData.set("twitter", "ar2rsawseen");
    * //create or increase specific number property
    * Countly.userData.increment("login_count");
    * //add new value to array property if it is not already there
    * Countly.userData.push_unique("selected_category", "IT");
    * //send all custom property modified data to server
    * Countly.userData.save();
    */
    Countly.userData = {
        /**
        * Sets user's custom property value
        * @param {string} key - name of the property to attach to user
        * @param {string|number} value - value to store under provided property
        **/
        set: function(key, value){
            customData[key] = value;
        },
        /**
        * Sets user's custom property value only if it was not set before
        * @param {string} key - name of the property to attach to user
        * @param {string|number} value - value to store under provided property
        **/
        set_once: function(key, value){
            change_custom_property(key, value, "$setOnce");
        },
        /**
        * Unset's/delete's user's custom property
        * @param {string} key - name of the property to delete
        **/
        unset: function (key) {
            customData[key] = "";
        },
        /**
        * Increment value under the key of this user's custom properties by one
        * @param {string} key - name of the property to attach to user
        **/
        increment: function(key){
            change_custom_property(key, 1, "$inc");
        },
        /**
        * Increment value under the key of this user's custom properties by provided value
        * @param {string} key - name of the property to attach to user
        * @param {number} value - value by which to increment server value
        **/
        increment_by: function(key, value){
            change_custom_property(key, value, "$inc");
        },
        /**
        * Multiply value under the key of this user's custom properties by provided value
        * @param {string} key - name of the property to attach to user
        * @param {number} value - value by which to multiply server value
        **/
        multiply: function(key, value){
            change_custom_property(key, value, "$mul");
        },
        /**
        * Save maximal value under the key of this user's custom properties
        * @param {string} key - name of the property to attach to user
        * @param {number} value - value which to compare to server's value and store maximal value of both provided
        **/
        max: function(key, value){
            change_custom_property(key, value, "$max");
        },
        /**
        * Save minimal value under the key of this user's custom properties
        * @param {string} key - name of the property to attach to user
        * @param {number} value - value which to compare to server's value and store minimal value of both provided
        **/
        min: function(key, value){
            change_custom_property(key, value, "$min");
        },
        /**
        * Add value to array under the key of this user's custom properties. If property is not an array, it will be converted to array
        * @param {string} key - name of the property to attach to user
        * @param {string|number} value - value which to add to array
        **/
        push: function(key, value){
            change_custom_property(key, value, "$push");
        },
        /**
        * Add value to array under the key of this user's custom properties, storing only unique values. If property is not an array, it will be converted to array
        * @param {string} key - name of the property to attach to user
        * @param {string|number} value - value which to add to array
        **/
        push_unique: function(key, value){
            change_custom_property(key, value, "$addToSet");
        },
        /**
        * Remove value from array under the key of this user's custom properties
        * @param {string} key - name of the property
        * @param {string|number} value - value which to remove from array
        **/
        pull: function(key, value){
            change_custom_property(key, value, "$pull");
        },
        /**
        * Save changes made to user's custom properties object and send them to server
        **/
        save: function(){
            if(Countly.check_consent("users")){
                toRequestQueue({user_details: JSON.stringify({custom:customData})});
            }
            customData = {};
        }
    };

    /**
    * Automatically track javascript errors that happen on the nodejs process
    * @param {string=} segments - additional key value pairs you want to provide with error report, like versions of libraries used, etc.
    **/
    Countly.track_errors = function(segments){
        crashSegments = segments;
        process.on("uncaughtException", function (err) {
            recordError(err, false);
            if(cluster.isMaster){
                forceStore();
            }
            console.error((new Date()).toUTCString() + " uncaughtException:", err.message);
            console.error(err.stack);
            process.exit(1);
        });

        process.on('unhandledRejection', function (reason) {
            var err = new Error('Unhandled rejection (reason: ' + (reason && reason.stack ? reason.stack : reason ) + ').');
            recordError(err, false);
            if(cluster.isMaster){
                forceStore();
            }
            console.error((new Date()).toUTCString() + " unhandledRejection:", err.message);
            console.error(err.stack);
        });
    };

    /**
    * Log an exception that you catched through try and catch block and handled yourself and just want to report it to server
    * @param {Object} err - error exception object provided in catch block
    * @param {string=} segments - additional key value pairs you want to provide with error report, like versions of libraries used, etc.
    **/
    Countly.log_error = function(err, segments){
        recordError(err, true, segments);
    };

    /**
    * Add new line in the log of breadcrumbs of what was done did, will be included together with error report
    * @param {string} record - any text describing an action
    **/
    Countly.add_log = function(record){
        if(Countly.check_consent("crashes")){
            if(crashLogs.length > maxCrashLogs) {
                crashLogs.shift();
            }
            crashLogs.push(record);
        }
    };

    /**
    * Fetch remote config
    * @param {array=} keys - Array of keys to fetch, if not provided will fetch all keys
    * @param {array=} omit_keys - Array of keys to omit, if provided will fetch all keys except provided ones
    * @param {function=} callback - Callback to notify with first param error and second param remote config object
    **/
    Countly.fetch_remote_config = function (keys, omit_keys, callback) {
        if (Countly.check_any_consent()) {
            var request = {
                method: "fetch_remote_config"
            };
            if (Countly.check_consent("sessions")) {
                request.metrics = JSON.stringify(getMetrics());
            }
            if (keys) {
                if (!callback && typeof keys === "function") {
                    callback = keys;
                    keys = null;
                }
                else if (Array.isArray(keys) && keys.length) {
                    request.keys = JSON.stringify(keys);
                }
            }
            if (omit_keys) {
                if (!callback && typeof omit_keys === "function") {
                    callback = omit_keys;
                    omit_keys = null;
                }
                else if (Array.isArray(omit_keys) && omit_keys.length) {
                    request.omit_keys = JSON.stringify(omit_keys);
                }
            }
            prepareRequest(request);
            makeRequest(Countly.url, readPath, request, function(err, params, responseText){
                try {
                    var configs = JSON.parse(responseText);
                    if (request.keys || request.omit_keys) {
                        //we merge config
                        for (var i in configs) {
                            remoteConfigs[i] = configs[i];
                        }
                    }
                    else {
                        //we replace config
                        remoteConfigs = configs;
                    }
                    if (cluster.isMaster) {
                        storeSet("cly_remote_configs", remoteConfigs);
                    }
                }
                catch(ex) {
                    //silent catch
                }
                if (typeof callback === "function") {
                    callback(err, remoteConfigs);
                }
            });
        }
    };

    /**
    * Get Remote config object or specific value for provided key
    * @param {string=} key - if provided, will return value for key, or return whole object
    **/
    Countly.get_remote_config = function (key) {
        if (typeof key !== "undefined") {
            return remoteConfigs[key];
        }
        return remoteConfigs;
    };

    /**
    * Stop tracking duration time for this user/device
    **/
    Countly.stop_time = function(){
        trackTime = false;
        storedDuration = getTimestamp() - lastBeat;
        lastViewStoredDuration = getTimestamp() - lastViewTime;
    };

    /**
    * Start tracking duration time for this user/device, by default it is automatically if you scalled (@link begin_session)
    **/
    Countly.start_time = function(){
        trackTime = true;
        lastBeat = getTimestamp() - storedDuration;
        lastViewTime = getTimestamp() - lastViewStoredDuration;
        lastViewStoredDuration = 0;
    };

    /**
    * Track which parts of application user visits
    * @param {string=} name - optional name of the view
    **/
    Countly.track_view = function(name){
        reportViewDuration();
        if(name){
            lastView = name;
            lastViewTime = getTimestamp();
            if(!platform) {
                getMetrics();
            }
            var segments = {
                "name": name,
                "visit":1,
                "segment":platform
            };

            //track pageview
            if(Countly.check_consent("views")){
                add_cly_events({
                    "key": "[CLY]_view",
                    "segmentation": segments
                });
            }
            else{
                lastParams.track_pageview = arguments;
            }
        }
    };

    /**
    * Track which parts of application user visits. Alias of {@link track_view} method for compatability with Web SDK
    * @param {string=} name - optional name of the view
    **/
    Countly.track_pageview = function(name){
        Countly.track_view(name);
    };

    /**
    * Make raw request with provided parameters
    * @example Countly.request({app_key:"somekey", devide_id:"someid", events:"[{'key':'val','count':1}]", begin_session:1});
    * @param {Object} request - object with key/values which will be used as request parameters
    **/
    Countly.request = function(request){
        if(!request.app_key || !request.device_id){
            log("app_key or device_id is missing");
            return;
        }
        if(cluster.isMaster){
            requestQueue.push(request);
            storeSet("cly_queue", requestQueue);
        }
        else{
            process.send({ cly: {request: request} });
        }
    };

    /**
    *  PRIVATE METHODS
    **/

    function reportViewDuration(){
        if(lastView){
            if(!platform){
                getMetrics();
            }
            var segments = {
                "name": lastView,
                "segment":platform
            };

            //track pageview
            if(Countly.check_consent("views")){
                add_cly_events({
                    "key": "[CLY]_view",
                    "dur": getTimestamp() - lastViewTime,
                    "segmentation": segments
                });
            }
            lastView = null;
        }
    }

    //prepare request by adding basic info to it
    function prepareRequest(request) {
        request.app_key = Countly.app_key;
        request.device_id = Countly.device_id;
        request.sdk_name = SDK_NAME;
        request.sdk_version = SDK_VERSION;
        if(Countly.check_consent("location")){
            if(Countly.country_code){
                request.country_code = Countly.country_code;
            }
            if(Countly.city){
                request.city = Countly.city;
            }
            if(Countly.ip_address !== null){
                request.ip_address = Countly.ip_address;
            }
        }
        else{
            request.location = "";
        }

        request.timestamp = getMsTimestamp();
        var date = new Date();
        request.hour = date.getHours();
        request.dow = date.getDay();
    }

    //insert request to queue
    function toRequestQueue(request){
        if(cluster.isMaster){
            if(!Countly.app_key || !Countly.device_id){
                log("app_key or device_id is missing");
                return;
            }
            prepareRequest(request);

            if(requestQueue.length > queueSize){
                requestQueue.shift();
            }

            requestQueue.push(request);
            storeSet("cly_queue", requestQueue);
        }
        else{
            process.send({ cly: {cly_queue: request} });
        }
    }

    //heart beat
    function heartBeat(){

        //extend session if needed
        if(sessionStarted && autoExtend && trackTime){
            var last = getTimestamp();
            if(last - lastBeat > sessionUpdate){
                Countly.session_duration(last - lastBeat);
                lastBeat = last;
            }
        }

        //process event queue
        if(eventQueue.length > 0){
            if(eventQueue.length <= maxEventBatch){
                toRequestQueue({events: JSON.stringify(eventQueue)});
                eventQueue = [];
            }
            else{
                var events = eventQueue.splice(0, maxEventBatch);
                toRequestQueue({events: JSON.stringify(events)});
            }
            storeSet("cly_event", eventQueue);
        }

        //process request queue with event queue
        if(requestQueue.length > 0 && readyToProcess && getTimestamp() > failTimeout){
            readyToProcess = false;
            var params = requestQueue.shift();
            //check if any consent to sync
            if(Object.keys(syncConsents).length){
                if(consentTimer){
                    clearTimeout(consentTimer);
                    consentTimer = null;
                }
                params.consent = JSON.stringify(syncConsents);
                syncConsents = {};
            }
            log("Processing request", params);
            makeRequest(Countly.url, apiPath, params, function(err, params){
                log("Request Finished", params, err);
                if(err){
                    requestQueue.unshift(params);
                    failTimeout = getTimestamp() + failTimeoutAmount;
                }
                storeSet("cly_queue", requestQueue);
                readyToProcess = true;
            });
        }

        setTimeout(heartBeat, beatInterval);
    }

    //get ID
    function getId(){
        return storeGet("cly_id", null) || generateUUID();
    }

    //generate UUID
    function generateUUID() {
        var d = new Date().getTime();
        var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
            var r = (d + Math.random()*16)%16 | 0;
            d = Math.floor(d/16);
            return (c==="x" ? r : (r&0x3|0x8)).toString(16);
        });
        return uuid;
    }

    //get metrics of the browser
    function getMetrics(){
        var m = JSON.parse(JSON.stringify(metrics));

        //getting app version
        m._app_version = m._app_version || Countly.app_version;

        m._os = m._os || os.type();
        m._os_version = m._os_version || os.release();
        platform = os.type();

        log("Got metrics", m);
        return m;
    }

    //log stuff
    function log(){
        if(Countly.debug && typeof console !== "undefined"){
            if(arguments[1] && typeof arguments[1] === "object"){
                arguments[1] = JSON.stringify(arguments[1]);
            }
            console.log( Array.prototype.slice.call(arguments).join("\n") );
        }
    }

    //get current timestamp
    function getTimestamp(){
        return Math.floor(new Date().getTime() / 1000);
    }

    //get unique timestamp in miliseconds
    function getMsTimestamp(){
        var ts = new Date().getTime();
        if(lastMsTs >= ts){
            lastMsTs++;
        }
        else{
            lastMsTs = ts;
        }
        return lastMsTs;
    }

    function recordError(err, nonfatal, segments){
        if(Countly.check_consent("crashes") && err){
            segments = segments || crashSegments;
            var error = "";
            if(typeof err === "object"){
                if(typeof err.stack !== "undefined"){
                    error = err.stack;
                }
                else{
                    if(typeof err.name !== "undefined"){
                        error += err.name+":";
                    }
                    if(typeof err.message !== "undefined"){
                        error += err.message+"\n";
                    }
                    if(typeof err.fileName !== "undefined"){
                        error += "in "+err.fileName+"\n";
                    }
                    if(typeof err.lineNumber !== "undefined"){
                        error += "on "+err.lineNumber;
                    }
                    if(typeof err.columnNumber !== "undefined"){
                        error += ":"+err.columnNumber;
                    }
                }
            }
            else{
                error = err+"";
            }
            nonfatal = (nonfatal) ? true : false;
            var metrics = getMetrics();
            var ob = {_os:metrics._os, _os_version:metrics._os_version, _error:error, _app_version:metrics._app_version, _run:getTimestamp()-startTime};

            ob._not_os_specific = true;

            if(crashLogs.length > 0){
                ob._logs = crashLogs.join("\n");
            }
            crashLogs = [];
            ob._nonfatal = nonfatal;

            if(typeof segments !== "undefined"){
                ob._custom = segments;
            }

            toRequestQueue({crash: JSON.stringify(ob)});
        }
    }

    //sending HTTP request
    function makeRequest(url, path, params, callback) {
        try {
            log("Sending HTTP request");
            var serverOptions = parseUrl(url);
            var data = prepareParams(params);
            var method = "GET";
            var options = {
                host: serverOptions.host,
                port: serverOptions.port,
                path: path+"?"+data,
                method: "GET"
            };

            if(data.length >= 2000){
                method = "POST";
            }
            else if(Countly.force_post){
                method = "POST";
            }

            if(method === "POST"){
                options.method = "POST";
                options.path = path;
                options.headers = {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Content-Length": Buffer.byteLength(data)
                };
            }
            var protocol = http;
            if(url.indexOf("https") === 0){
                protocol = https;
            }
            var req = protocol.request(options, function(res) {
                var str = "";
                res.on("data", function (chunk) {
                    str += chunk;
                });

                res.on("end", function () {
                    if(res.statusCode >= 200 && res.statusCode < 300){
                        callback(false, params, str);
                    }
                    else{
                        callback(true, params);
                    }
                });
            });
            if(method === "POST"){
                // write data to request body
                req.write(data);
            }

            req.on("error", function(err){
                log("Connection failed.", err);
                if (typeof callback === "function") {
                    callback(true, params);
                }
            });

            req.end();
        } catch (e) {
            // fallback
            log("Failed HTTP request", e);
            if (typeof callback === "function") { callback(true, params); }
        }
    }

    //convert JSON object to query params
    function prepareParams(params){
        var str = [];
        for(var i in params){
            str.push(i+"="+encodeURIComponent(params[i]));
        }
        return str.join("&");
    }

    //removing trailing slashes
    function stripTrailingSlash(str) {
        if(str.substr(str.length - 1) === "/") {
            return str.substr(0, str.length - 1);
        }
        return str;
    }

    //parsing host and port information from url
    function parseUrl(url) {
        var serverOptions = {
            host: "localhost",
            port: 80
        };
        if(Countly.url.indexOf("https") === 0){
            serverOptions.port = 443;
        }
        var host = url.split("://").pop();
        serverOptions.host = host;
        var lastPos = host.indexOf(":");
        if (lastPos > -1) {
            serverOptions.host = host.slice(0,lastPos);
            serverOptions.port = Number(host.slice(lastPos+1,host.length));
        }
        return serverOptions;
    }

    //retrieve only specific properties from object
    function getProperties(orig, props){
        var ob = {};
        var prop;
        for(var i = 0; i < props.length; i++){
            prop = props[i];
            if(typeof orig[prop] !== "undefined"){
                ob[prop] = orig[prop];
            }
        }
        return ob;
    }

    function handleWorkerMessage(msg){
        if(msg.cly){
            if(msg.cly.cly_queue){
                toRequestQueue(msg.cly.cly_queue);
            }
            else if(msg.cly.change_id){
                Countly.change_id(msg.cly.change_id, msg.cly.merge);
            }
            else if(msg.cly.event){
                Countly.add_event(msg.cly.event);
            }
            else if(msg.cly.request){
                Countly.request(msg.cly.request);
            }
        }
    }

    var readFile = function(key){
        var dir = path.resolve(__dirname, Countly.storage_path+"__"+key+".json");

        //try reading data file
        var data;
        try{
            data = fs.readFileSync(dir);
        } catch (ex) {
            //ther was no file, probably new init
            data = null;
        }

        try{
            //trying to parse json string
            data = JSON.parse(data);
        } catch (ex) {
            //problem parsing, corrupted file?
            console.log(ex.stack);
            //backup corrupted file data
            fs.writeFile(path.resolve(__dirname, Countly.storage_path+"__"+key+"."+getTimestamp()+Math.random()+".json"), data, function(){});
            //start with new clean object
            data = null;
        }
        return data;
    };

    var forceStore = function(){
        for(var i in __data){
            var dir = path.resolve(__dirname, Countly.storage_path+"__"+i+".json");
            var ob = {};
            ob[i] = __data[i];
            try{
                fs.writeFileSync(dir, JSON.stringify(ob));
            }
            catch(ex){
                //tried to save whats possible
                console.log(ex.stack);
            }
        }
    };

    var storeSet = function(key, value) {
        __data[key] = value;
        var dir = path.resolve(__dirname, Countly.storage_path+"__"+key+".json");
        var ob = {};
        ob[key] = value;
        try{
            fs.writeFileSync(dir, JSON.stringify(ob));
        }
        catch(ex){
            //problem saving file
            console.log(ex.stack);
        }
    };

    var storeGet = function(key, def) {
        if(typeof __data[key] === "undefined"){
            var ob = readFile(key);
            if(!ob){
                __data[key] = def;
            }
            else{
                __data[key] = ob[key];
            }
        }
        return __data[key];
    };
}(Countly));

module.exports = Countly;


const Promise = require("bluebird");

/**
 *  Creates a promise that is resolved when all input promises have been
 *  settled.  The returned Promise is resolved with an array of
 *  Promise.Inspection objects.
 *
 *  This is the commonly accepted way of implementing allSettled() in Bluebird.
 *  See:  http://bluebirdjs.com/docs/api/reflect.html
 *
 * @param promises - The array of input promises.
 * @returns {Promise<Promise.Inspection[]>} A promise that will be resolved once
 * all input Promises have settled. The returned Promise will be resolved with a
 * corresponding array of Promise.Inspection objects.
 */
function allSettled(promises) {
    "use strict";
    const wrappedPromises = promises.map((curPromise) => curPromise.reflect());
    return Promise.all(wrappedPromises);
}

module.exports = {
    allSettled: allSettled
};


'use strict'

var format = require('format')
var visit = require('unist-util-visit')
var is = require('unist-util-is')
var toString = require('nlcst-to-string')
var toWords = require('number-to-words').toWords
var requiresA = require('./a')
var requiresAn = require('./an')

module.exports = indefiniteArticle

var ruleId = 'retext-indefinite-article:retext-indefinite-article'
var vowel = /[aeiou]/
var digits = /^\d+/
var join = /^(and|or|nor)$/i
var punctuation = /^[“”‘’'"()[\]]$/
var split = /['’ -]/

requiresA = factory(requiresA)
requiresAn = factory(requiresAn)

function indefiniteArticle() {
  return transformer
}

function transformer(tree, file) {
  visit(tree, 'WordNode', visitor)

  function visitor(node, index, parent) {
    var value = toString(node)
    var normal = lower(value)
    var message
    var following
    var suggestion
    var an

    if (normal !== 'a' && normal !== 'an') {
      return
    }

    an = value.length !== 1
    following = after(parent, index)

    if (!following) {
      return
    }

    following = toString(following)

    // Exit if `A` and this isn’t sentence-start: `Station A equals`
    if (normal !== value && !an && !firstWord(parent, index)) {
      return
    }

    // Exit if `a` is used as a letter: `a and b`.
    if (normal === value && !an && join.test(following)) {
      return
    }

    suggestion = classify(following)

    if (!(suggestion === 'an' && !an) && !(suggestion === 'a' && an)) {
      return
    }

    if (normal !== value) {
      suggestion = suggestion.charAt(0).toUpperCase() + suggestion.slice(1)
    }

    message = file.message(
      format('Use `%s` before `%s`, not `%s`', suggestion, following, value),
      node,
      ruleId
    )

    message.actual = value
    message.expected = [suggestion]
  }
}

// Check if there’s no word before `index`.
function firstWord(parent, index) {
  var siblings = parent.children

  while (index--) {
    if (is('WordNode', siblings[index])) {
      return false
    }
  }

  return true
}

// Get the next word.
function after(parent, index) {
  var siblings = parent.children
  var sibling = siblings[++index]
  var other

  if (is('WhiteSpaceNode', sibling)) {
    sibling = siblings[++index]

    if (is('PunctuationNode', sibling) && punctuation.test(toString(sibling))) {
      sibling = siblings[++index]
    }

    if (is('WordNode', sibling)) {
      other = sibling
    }
  }

  return other
}

// Classify a word.
function classify(value) {
  var type = null
  var normal

  value = value.replace(digits, toWords).split(split, 1)[0]
  normal = lower(value)

  if (requiresA(value)) {
    type = 'a'
  }

  if (requiresAn(value)) {
    type = type === 'a' ? 'a-or-an' : 'an'
  }

  if (!type && normal === value) {
    type = vowel.test(normal.charAt(0)) ? 'an' : 'a'
  }

  return type
}

// Create a test based on a list of phrases.
function factory(list) {
  var expressions = []
  var sensitive = []
  var insensitive = []

  construct()

  return test

  function construct() {
    var length = list.length
    var index = -1
    var value
    var normal

    while (++index < length) {
      value = list[index]
      normal = value === lower(value)

      if (value.charAt(value.length - 1) === '*') {
        // Regexes are insensitive now, once we need them this should check for
        // `normal` as well.
        expressions.push(new RegExp('^' + value.slice(0, -1), 'i'))
      } else if (normal) {
        insensitive.push(value)
      } else {
        sensitive.push(value)
      }
    }
  }

  function test(value) {
    var normal = lower(value)
    var length
    var index

    if (sensitive.indexOf(value) !== -1 || insensitive.indexOf(normal) !== -1) {
      return true
    }

    length = expressions.length
    index = -1

    while (++index < length) {
      if (expressions[index].test(value)) {
        return true
      }
    }

    return false
  }
}

// Lower-case `value`.
function lower(value) {
  return value.toLowerCase()
}


#!/usr/bin/env node

var cdb = null; //current DB desc
const maxInspectRows = 10;
const maxInspectDepth = 10;
var useColors = true;
var quiet = false;
var cmd = null;
var pkg = require("../package.json");

//Parse aguments
(function () {
    var args = process.argv;
    for (var i = 2; i < args.length; ++i) {
        var a = args[i];
        if (["--help", "-h"].indexOf(a) !== -1) {
            help();
        } else if (["--no-colors", "-n"].indexOf(a) !== -1) {
            useColors = false;
        } else if (["--quiet", "-q"].indexOf(a) !== -1) {
            quiet = true;
        } else if (["--cmd", "-c"].indexOf(a) !== -1) {
            cmd = a;
        } else if (i === args.length - 1) { //last arg
            cmd = "db.open('" + a + "')";  //todo review
        }
    }
})();

function help() {
    var h = [];
    h.push("EJDB CLI v" + pkg.version);
    h.push("usage: ejdb [options] [dbfile]");
    h.push("options:");
    h.push("\t-h --help\tshow this help tip");
    h.push("\t-n --no-colors\tdo not use colored output");
    h.push("\t-q --quiet\trun in quiet output mode");
    h.push("\t-c --cmd\trun specified javascript command");
    console.error(h.join("\n"));
    process.exit(0);
}

if (!quiet) {
    console.log("Welcome to EJDB CLI v" + pkg.version);
}

var util = require("util");
var path = require("path");
var EJDB = require("../ejdb.js");
var clinspect = require("../clinspect.js");

// help messages (for methods with collection)
var helpGetters = {
    save : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "object|array of object, [opts], [cb]) Save/update specified JSON objects in the collection."
    },
    load : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "oid, [cb]) Loads object identified by OID from the collection"
    },
    remove : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "oid, [cb]) Removes object from the collection"
    },
    find : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "[qobj], [qobjarr], [hints], [cb]) Execute query on collection"
    },
    findOne : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "[qobj], [qobjarr], [hints], [cb]) Retrive one object from the collection"
    },
    update : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "[qobj], [qobjarr], [hints], [cb]) Perform update query on collection"
    },
    count : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "[qobj], [qobjarr], [hints], [cb]) Convenient count(*) operation"
    },
    dropIndexes : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb]) Drop indexes of all types for JSON field path"
    },
    optimizeIndexes : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb]) Optimize indexes of all types for JSON field path"
    },

    ensureStringIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb]) Ensure String index for JSON field path"
    },
    rebuildStringIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb])"
    },
    dropStringIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb])"
    },

    ensureIStringIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb]) Ensure case insensitive String index for JSON field path"
    },
    rebuildIStringIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb])"
    },
    dropIStringIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb])"
    },

    ensureNumberIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb]) Ensure index presence of Number type for JSON field path"
    },
    rebuildNumberIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb])"
    },
    dropNumberIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb])"
    },

    ensureArrayIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb]) Ensure index presence of Array type for JSON field path"
    },
    rebuildArrayIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb])"
    },
    dropArrayIndex : function (selected) {
        return "(" + (!selected ? "cname, " : "") + "path, [cb])"
    }
};

//Init help hints
Object.defineProperty(EJDB.open, "_help_", {value : "(dbFile, [openMode], [cb]) Open database"});
Object.defineProperty(EJDB.prototype.close, "_help_", {value : "([cb]) Close database"});
Object.defineProperty(EJDB.prototype.isOpen, "_help_", {value : "Check if database in opened state"});
Object.defineProperty(EJDB.prototype.ensureCollection, "_help_", {value : "(cname, [copts], [cb]) Creates new collection if it does't exists"});
Object.defineProperty(EJDB.prototype.dropCollection, "_help_", {value : "(cname, [prune], [cb]) Drop collection, " +
                                                                        "if `prune` is true collection db files will be erased from disk."});

Object.defineProperty(EJDB.prototype.getDBMeta, "_help_", {value : "Get description of EJDB database and its collections"});
Object.defineProperty(EJDB.prototype.sync, "_help_", {value : "Synchronize entire EJDB database with disk"});

Object.defineProperty(EJDB.prototype.save, "_help_", {value : helpGetters.save()});
Object.defineProperty(EJDB.prototype.load, "_help_", {value : helpGetters.load()});
Object.defineProperty(EJDB.prototype.remove, "_help_", {value : helpGetters.remove()});
Object.defineProperty(EJDB.prototype.find, "_help_", {value : helpGetters.find()});
Object.defineProperty(EJDB.prototype.findOne, "_help_", {value : helpGetters.findOne()});
Object.defineProperty(EJDB.prototype.update, "_help_", {value : helpGetters.update()});
Object.defineProperty(EJDB.prototype.count, "_help_", {value : helpGetters.count()});
Object.defineProperty(EJDB.prototype.dropIndexes, "_help_", {value : helpGetters.dropIndexes()});
Object.defineProperty(EJDB.prototype.optimizeIndexes, "_help_", {value : helpGetters.optimizeIndexes()});

Object.defineProperty(EJDB.prototype.ensureStringIndex, "_help_", {value : helpGetters.ensureStringIndex()});
Object.defineProperty(EJDB.prototype.rebuildStringIndex, "_help_", {value : helpGetters.rebuildStringIndex()});
Object.defineProperty(EJDB.prototype.dropStringIndex, "_help_", {value : helpGetters.dropStringIndex()});

Object.defineProperty(EJDB.prototype.ensureIStringIndex, "_help_", {value : helpGetters.ensureIStringIndex()});
Object.defineProperty(EJDB.prototype.rebuildIStringIndex, "_help_", {value : helpGetters.rebuildIStringIndex()});
Object.defineProperty(EJDB.prototype.dropIStringIndex, "_help_", {value : helpGetters.dropIStringIndex()});

Object.defineProperty(EJDB.prototype.ensureNumberIndex, "_help_", {value : helpGetters.ensureNumberIndex()});
Object.defineProperty(EJDB.prototype.rebuildNumberIndex, "_help_", {value : helpGetters.rebuildNumberIndex()});
Object.defineProperty(EJDB.prototype.dropNumberIndex, "_help_", {value : helpGetters.dropNumberIndex()});

Object.defineProperty(EJDB.prototype.ensureArrayIndex, "_help_", {value : helpGetters.ensureArrayIndex()});
Object.defineProperty(EJDB.prototype.rebuildArrayIndex, "_help_", {value : helpGetters.rebuildArrayIndex()});
Object.defineProperty(EJDB.prototype.dropArrayIndex, "_help_", {value : helpGetters.dropArrayIndex()});

Object.defineProperty(EJDB.prototype.beginTransaction, "_help_", {value : "Begin collection transaction"});
Object.defineProperty(EJDB.prototype.commitTransaction, "_help_", {value : "Commit collection transaction"});
Object.defineProperty(EJDB.prototype.rollbackTransaction, "_help_", {value : "Rollback collection transaction"});
Object.defineProperty(EJDB.prototype.getTransactionStatus, "_help_", {value : "Get collection transaction status"});

// collection controllers history (for merge)
var cchistory = [];

// bind collections controllers
// dbctrl - db controller
// forcerebind - if true force rebind all collection controllers, otherwise check added/deleted collections
var bindColCtls = function(dbctrl, forcerebind) {
    var octrls = forcerebind ? [] : cchistory;
    var nctrls = [];

    var dbMeta = cdb.jb.getDBMeta();
    if (dbMeta && dbMeta.collections) {
        for (var j = 0; j < dbMeta.collections.length; ++j) {
            var collection = dbMeta.collections[j];
            var ci;
            if ((ci = octrls.indexOf(collection.name)) != -1) {
                nctrls.push(collection.name);
                octrls.splice(ci, 1);
            } else if (!dbctrl[collection.name]){
                nctrls.push(collection.name);
                dbctrl[collection.name] = colctl(dbctrl, collection.name);
            }
        }
    }
    for (var i = 0; i < octrls.length; ++i) {
        delete dbctrl[octrls[i]];
    }

    // save current known collections
    cchistory = nctrls;
};

// collection controller (creation function)
var colctl = function (db, cname) {
    // build arguments function: add <cname> as first argument
    var buildargs = function (args) {
        var result = [cname];
        // args is Object, we need to iterate all fields with numeric key for collecting arguments
        for (var i = 0; args[i]; ++i) {
            result.push(args[i]);
        }

        return result;
    };
    // method names for creating aliases (db.<method>(cname, ...) -> db.cname.<method>(...))
    var mnames = [
        "save", "load", "remove", "find", "findOne", "update", "count",
        "dropCollection", "dropIndexes", "optimizeIndexes",
        "ensureStringIndex", "rebuildStringIndex", "dropStringIndex",
        "ensureIStringIndex", "rebuildIStringIndex", "dropIStringIndex",
        "ensureNumberIndex", "rebuildNumberIndex", "dropNumberIndex",
        "ensureArrayIndex", "rebuildArrayIndex", "dropArrayIndex"
    ];
    // bind method alias
    var mbind = function (mname) {
        return function () {
            return db[mname].apply(db, buildargs(arguments));
        }
    };

    // collection controller impl
    var colctlimpl = {
        inspect : function() {
            return '\u001b[' + 36 + 'm' + "[Collection]" + '\u001b[' + 39 + 'm';
        }
    };

    var mname;
    // wrap methods
    for (var i = 0; i < mnames.length; ++i) {
        mname = mnames[i];
        colctlimpl[mname] = mbind(mname);
        if (helpGetters[mname]) {
            Object.defineProperty(colctlimpl[mname], '_help_', {value : helpGetters[mname](true)});
        }
    }

    return colctlimpl
};

repl = require("repl").start({
    prompt : "ejdb> ",
    input : process.stdin,
    output : process.stdout,
    terminal : true,
    writer : function (obj) {
        return clinspect.inspect(obj, maxInspectDepth, useColors)
    }
});

//console.log("MF=" +  module.filename);

var dbctl = {
    open : function (dbpath) {
        if (dbpath == null) {
            return error("No file path specified");
        }
        if (cdb) {
            return error("Database already opened: " + cdb.dbpath);
        }
        dbpath = path.resolve(dbpath);
        cdb = {
            dbpath : dbpath,
            jb : EJDB.open(dbpath)
        };
        syncdbctx();
        return dbstatus(cdb);
    },

    status : function () {
        syncdbctx();
        return dbstatus(cdb);
    },

    close : function () {
        if (!cdb || !cdb.jb) {
            return error("Database already closed");
        }
        try {
            cdb.jb.close();
        } finally {
            cdb = null;
        }
        syncdbctx();
    }
};
Object.defineProperty(dbctl.open, "_help_", {value : EJDB.open._help_});
Object.defineProperty(dbctl.close, "_help_", {value : EJDB.prototype.close._help_});
Object.defineProperty(dbctl.status, "_help_", {value : "Get current database status"});

repl.on("exit", function () {
    dbctl.close();
    console.log("Bye!");
});

function dbstatus(cdb) {
    if (cdb) {
        return cdb.jb.getDBMeta();
    } else {
        return {};
    }
}

function syncdbctx() {
    var db = {};
    repl.resetContext();
    if (cdb && cdb.jb) {
        db.__proto__ = cdb.jb;
        db.close = dbctl.close;
        db.status = dbctl.status;
        db.find = function () {
            var ret = cdb.jb.find.apply(cdb.jb, arguments);
            if (typeof ret === "object") {
                if (!quiet) {
                    println("Found " + ret.length + " records");
                }
                for (var i = 0; ret.next() && i < maxInspectRows; ++i) {
                    println(repl.writer(ret.object()));
                }
                ret.reset();
                if (ret.length > maxInspectRows) {
                    if (!quiet) {
                        println("Shown only first " + maxInspectRows);
                    }
                }
                if (!quiet) {
                    println("\nReturned cursor:");
                }
            }
            // rebind collection controlles if need
            if (!db[arguments[0]]) {
                bindColCtls(db);
            }
            return ret;
        };
        Object.defineProperty(db.find, "_help_", {value : EJDB.prototype.find._help_});

        // db - db controller
        // mname - method name
        // frc - force rebind collections controllers. if false db meta will be reloaded only if method executes on unknown collection.
        // argl - arguments count (for register callback as last argument)
        var dbbind = function(db, mname, frc) {
            return function() {
                var cname = arguments[0];
                var args = [cname];
                // copy all arguments except first and last
                for(var i = 1; i < arguments.length - 1; ++i) {
                    args.push(arguments[i]);
                }

                // creating callback with collection rebuilding
                var ccb = function(rcb) {
                    return function() {
                        if (frc || !db[cname]) {
                            bindColCtls(db);
                        }
                        if (rcb) {
                            rcb.apply(this, arguments);
                        }
                    }
                };

                if (arguments.length > 1) {
                    if (typeof arguments[arguments.length - 1] === 'function') {
                        // wrap existing callback
                        args.push(ccb(arguments[arguments.length - 1]));
                    } else {
                        // adding registering callback after last argument
                        args.push(arguments[arguments.length - 1]);
                        args.push(ccb());
                    }
                } else {
                    args.push(ccb());
                }

                return cdb.jb[mname].apply(cdb.jb, args);
            }
        };

        var rbmnames, j;
        // reload collections statuses for some methods (rebind collection controllers if need, force)
        rbmnames = ["ensureCollection", "dropCollection"];
        for (j = 0; j < rbmnames.length; ++j) {
            db[rbmnames[j]] = dbbind(db, rbmnames[j], true);
            Object.defineProperty(db[rbmnames[j]], "_help_", {value : EJDB.prototype[rbmnames[j]]._help_});
        }

        // reload collections statuses for some methods (rebind collection controllers if need, non force)
        rbmnames = ["save", "update"];
        for (j = 0; j < rbmnames.length; ++j) {
            db[rbmnames[j]] = dbbind(db, rbmnames[j]);
            Object.defineProperty(db[rbmnames[j]], "_help_", {value : EJDB.prototype[rbmnames[j]]._help_});
        }

        // bind collection controllers for all known collections
        bindColCtls(db, true);
    } else {
        db.__proto__ = dbctl;
    }
    repl.context.db = db;
    repl.context.EJDB = EJDB;
}

function println(msg) {
    repl.outputStream.write(msg + "\n");
}

function error(msg) {
    return "ERROR: " + msg;
}

syncdbctx();

if (cmd) {
    repl.rli.write(cmd + "\n");
}