// MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved
var {TypeParser} = require('./types');
var {sprintf} = require('sprintf-js');
var {TraceUtils} = require('@themost/common');
var moment = require('moment');
var _ = require('lodash');
var Q = require('q');

/**
 * @class
 * @classdesc A utility class which offers a set of methods for calculating the default values of a data model
 * @param {DataContext=} context
 * @param {DataModel=} model
 * @param {*=} target
 * @constructor
*/
function FunctionContext(context, model, target) {
    /**
     * @type {DataContext}
    */
    this.context = context;
     /**
      * @type {DataModel}
      */
    this.model = model;
    if (_.isNil(context) && _.isObject(model)) {
        //get current context from DataModel.context property
        this.context = model.context;
    }
    /**
     * @type {*}
     */
    this.target = target;
}

FunctionContext.prototype.eval = function(expr, callback) {
    callback = callback || function() {};
    if (typeof expr !=='string') {
        callback(null);
        return;
    }
    var re = /(fn:)\s?(.*?)\s?\((.*?)\)/, expr1=expr;
    if (expr.indexOf('fn:')!==0) {
        expr1 = 'fn:' + expr1;
    }
    var match = re.exec(expr1);
    if (match) {
        var expr2eval;
        //check parameters (match[3])
        if (match[3].length===0) {
            expr2eval = expr1.replace(/(fn:)\s?(.*?)\s?\((.*?)\)/, '(function() { return this.$2(); });');
        }
        else {
            expr2eval = expr1.replace(/(fn:)\s?(.*?)\s?\((.*?)\)/, '(function() { return this.$2($3); });');
        }
        //evaluate expression
        try {
            var f = eval(expr2eval);
            var value1 = f.call(this);
            if (typeof value1 !== 'undefined' && value1 !== null && typeof value1.then === 'function') {
                value1.then(function(result) {
                    return callback(null, result);
                }).catch(function(err) {
                    callback(err);
                });
            }
            else {
                return callback(null, value1);
            }
        }
        catch(err) {
            callback(err);
        }
    }
    else {
        console.log(sprintf('Cannot evaluate %s.', expr1));
        callback(new Error('Cannot evaluate expression.'));
    }

};
/**
 * Returns the current date and time
 * @returns {Promise<Date>}
 * @description
 * Use this method for calculating the current date and time in a data field definition.
 *
 * The following data model definition contains a field named [dateModified] which accepts a calculated value through DataField.value attribute.
 * FunctionContext.now() method is used for calculating the current date and time.
 ```
    ...
    "fields": [
        {
            "@id": "https://themost.io/schemas/dateModified",
            "name": "dateModified",
            "title": "dateModified",
            "description": "The date on which this item was most recently modified.",
            "type": "DateTime"
            "value": "return this.now();"
        }
    ]
    ...
 ```
 */
FunctionContext.prototype.now = function() {
    return Q.Promise(function(resolve) {
        return resolve(new Date());
    });
};
/**
 * Returns the current date
 * @returns {Promise<Date>}
 * Use this method for calculating the current date in a data field definition.
 *
 * The following data model definition contains a field named [orderDate] which accepts a calculated value through DataField.value attribute.
 * FunctionContext.today() method is used for calculating the current date.
 ```
     ...
     "fields": [
         {
             "@id": "https://themost.io/schemas/orderDate",
             "name": "orderDate",
             "title": "orderDate",
             "type": "DateTime"
             "value": "return this.today();"
         }
     ]
     ...
 ```
 */
FunctionContext.prototype.today = function() {
    return Q.Promise(function(resolve) {
        return resolve(new Date().getDate());
    });
};
/**
 * @returns {Promise|*}
 */
FunctionContext.prototype.newid = function() {
    var deferred = Q.defer();
    this.model.context.db.selectIdentity(this.model.sourceAdapter, this.model.primaryKey, function(err, result) {
        if (err) {
            return deferred.reject(err);
        }
        deferred.resolve(result);
    });
    return deferred.promise;
};

var UUID_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');

function newGuidInternal() {
    var chars = UUID_CHARS, uuid = [], i;
    // rfc4122, version 4 form
    var r;
    // rfc4122 requires these characters
    uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
    uuid[14] = '4';

    // Fill in random data.  At i==19 set the high bits of clock sequence as
    // per rfc4122, sec. 4.1.5
    for (i = 0; i < 36; i++) {
        if (!uuid[i]) {
            r = 0 | Math.random()*16;
            uuid[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r];
        }
    }
    return uuid.join('');
}
/**
 * @returns {Promise|*}
 */
FunctionContext.prototype.newGuid = function() {
    var deferred = Q.defer();
    process.nextTick(function() {
        try {
            deferred.resolve(newGuidInternal());
        }
        catch(err) {
            deferred.reject(err)
        }
    });
    return deferred.promise;
};

/**
 * Generates a random integer value between the given minimum and maximum value
 * @param {number} min
 * @param {number} max
 * @returns {Promise|*}
 */
FunctionContext.prototype.int = function(min, max) {
    var deferred = Q.defer();
    process.nextTick(function() {
        try {
            return deferred.resolve(_.random(min, max));
        }
        catch (err) {
            deferred.reject(err);
        }
        deferred.resolve((new Date()).getDate());
    });
    return deferred.promise;
};

/**
 * Generates a random sequence of numeric characters
 * @param {number} length - A integer which represents the length of the sequence
 * @returns {Promise|*}
 */
FunctionContext.prototype.numbers = function(length) {
    var deferred = Q.defer();
    process.nextTick(function() {
        try {
            length = length || 8;
            if (length<0) {
                return deferred.reject(new Error('Number sequence length must be greater than zero.'));
            }
            if (length>255) {
                return deferred.reject(new Error('Number sequence length exceeds the maximum of 255 characters.'));
            }
            var times = Math.ceil(length / 10);
            var res = '';
            _.times(times, function() {
                 res += _.random(1000000000, 9000000000)
            });
            return deferred.resolve(res.substr(0,length));
        }
        catch (err) {
            deferred.reject(err);
        }
    });
    return deferred.promise;
};

/**
 * @param {number} length
 * @returns {Promise|*}
 */
FunctionContext.prototype.chars = function(length) {

    var deferred = Q.defer();
    process.nextTick(function() {
        try {
            length = length || 8;
            var chars = 'abcdefghkmnopqursuvwxz2456789ABCDEFHJKLMNPQURSTUVWXYZ';
            var str = '';
            for(var i = 0; i < length; i++) {
                str += chars.substr(_.random(0, chars.length-1),1);
            }
            deferred.resolve(str);
        }
        catch (err) {
            return deferred.reject(err);
        }
    });
    return deferred.promise;
};
/**
 * @param {number} length
 * @returns {Promise|*}
 */
FunctionContext.prototype.password = function(length) {
    var deferred = Q.defer();
    process.nextTick(function() {
        try {
            length = length || 8;
            var chars = 'abcdefghkmnopqursuvwxz2456789ABCDEFHJKLMNPQURSTUVWXYZ',
                str = '';
            for(var i = 0; i < length; i++) {
                str += chars.substr(_.random(0, chars.length-1),1);
            }
            deferred.resolve('{clear}' + str);
        }
        catch (err) {
            return deferred.reject(err);
        }
    });
    return deferred.promise;
};
/**
 * @returns {Promise|*}
 */
FunctionContext.prototype.user = function() {
    var self = this, context = self.model.context, deferred = Q.defer();
    var user = context.interactiveUser || context.user || { };
    process.nextTick(function() {
        if (typeof user.id !== 'undefined') {
            return deferred.resolve(user.id);
        }
        var userModel = context.model('User'), parser, undefinedUser = null;
        userModel.where('name').equal(user.name).silent().select('id').first(function(err, result) {
            if (err) {
                TraceUtils.log(err);
                //try to get undefined user
                parser = TypeParser.hasParser(userModel.field('id').type);
                if (typeof parser === 'function')
                    undefinedUser = parser(null);
                //set id for next calls
                user.id = undefinedUser;
                if (_.isNil(context.user)) {
                    context.user = user;
                }
                return deferred.resolve(undefinedUser);
            }
            else if (_.isNil(result)) {
                //try to get undefined user
                parser = TypeParser.hasParser(userModel.field('id').type);
                if (typeof parser === 'function')
                    undefinedUser = parser(null);
                //set id for next calls
                user.id = undefinedUser;
                if (_.isNil(context.user)) {
                    context.user = user;
                }
                return deferred.resolve(undefinedUser);
            }
            else {
                //set id for next calls
                user.id = result.id;
                return deferred.resolve(result.id);
            }
        });
    });
    return deferred.promise;
};
/**
 * @returns {Promise|*}
 */
FunctionContext.prototype.me = function() {
    return this.user();
};

/**
 * Creates a new instance of FunctionContext class
 * @param {DataContext|*=} context
 * @param {DataModel|*=} model
 * @param {*=} target
 * @returns FunctionContext
 */
function createContext(context, model, target) {
    return new FunctionContext(context, model, target);
}

module.exports = {
    createContext,
    FunctionContext
};
