
var fs    = require("fs");
var util  = require("util");
var iconv = require('iconv-lite');
var path  = require('path');

var CRLF = "\r\n";
var disableTimeStamp = false;

var logLevel = {
    'L_noLog'   : "-1",
    'L_Low'     : 1,
    'L_Normal'  : 2,
    'L_High'    : 3,
    'L_Extended': 4,
    'L_Full'    : 5,

    /// любая ошибка, которая приводит к закрытию службы или приложения для предотвращения потери данных (или дальнейшей
    /// потери данных). Это - самые тяжёлые ошибки и ситуации, когда гарантировано повреждение или потеря данных
    FATAL : 1,
    /// любая ошибка, которая является фатальной для операции, но не для службы или приложения (не удается открыть
    /// требуемый файл, отсутствуют данные, указан неправильный пароль и т.д.). Эти ошибки требуют вмешательства
    /// администратора или пользователя, но не приводят к фатальным сбоям
    ERROR : 2,
    /// все, что потенциально может вызвать странности приложения, но которые программа автоматически устраняет либо
    /// может продолжать работу дальше без фатальных ошибок (например, переключение с основного на резервный сервер,
    /// повторная попытка операции, отсутствие дополнительных данных и т. д.)
    WARN  : 3,
    /// полезная информация для работы. Информация, которую полезно всегда иметь под рукой, но на которую не обращают
    /// внимания в обычных обстоятельствах. Это стандартный уровень протоколирования
    INFO  : 4,
    /// расширенная диагностика (IT персонал, системные администраторы и т.д.)
    DEBUG : 5,
    /// "трассировка", для подробной глубокой отладки, логов будет очень много
    TRACE : 6

};

var ___logLevel = {
  "-1": 'L_noLog',
    // 1: 'L_Low',
    // 2: 'L_Normal',
    // 3: 'L_High',
    // 4: 'L_Extended',
    // 5: 'L_Full',

    1: "FATAL",
    2: "ERROR",
    3: "WARN" ,
    4: "INFO" ,
    5: "DEBUG",
    6: "TRACE"
};

var CurrentLogLevel = logLevel.ERROR;
var ConsoleFile = '';
var colors      = {
    default: '0',
    cyan   : '36',
    lcyan  : '1;36',
    red    : '31',
    lred   : '1;31',
    yellow : '33',
    bWhite : '1'
};
var sendCMD     = null;
var statLogDir  = null;
var logBuffer   = [];
var logTimeOut  = null;
var logDropTime = 100;

Object.defineProperty(global, '__stack', {
    configurable: true,

    get: function () {
        var orig = Error.prepareStackTrace;

        Error.prepareStackTrace = function (_, stack) {
            return stack;
        };

        var err = new Error;

        if (Error.captureStackTrace) {
            Error.captureStackTrace(err, arguments.callee);
        }

        var stack = err.stack;

        Error.prepareStackTrace = orig;

        return stack;
    }
});

var traceStack = global.__stack;

function formatDate(formatDate, formatString) {
    var mcDate = Object.prototype.toString.call(formatDate) === '[object String]';

    if (mcDate) formatDate = formatDate.split('.');

    var yyyy = formatDate.getFullYear();
    var yy = yyyy.toString().substring(2);
    var m = formatDate.getMonth() + 1;
    var mm = m < 10 ? "0" + m : m;
    var d = formatDate.getDate();
    var dd = d < 10 ? "0" + d : d;

    var h = formatDate.getHours();
    var hh = h < 10 ? "0" + h : h;
    var n = formatDate.getMinutes();
    var nn = n < 10 ? "0" + n : n;
    var s = formatDate.getSeconds();
    var ss = s < 10 ? "0" + s : s;
    var zz = formatDate.getMilliseconds();
    // var zzz = s < 10 ? "0" + s : s;

    formatString = formatString.replace(/yyyy/i, yyyy);
    formatString = formatString.replace(/yy/i, yy);
    formatString = formatString.replace(/mm/i, mm);
    formatString = formatString.replace(/m/i, m);
    formatString = formatString.replace(/dd/i, dd);
    formatString = formatString.replace(/d/i, d);
    formatString = formatString.replace(/hh/i, hh);
    formatString = formatString.replace(/h/i, h);
    formatString = formatString.replace(/nn/i, nn);
    formatString = formatString.replace(/n/i, n);
    formatString = formatString.replace(/ss/i, ss);
    formatString = formatString.replace(/s/i, s);
    formatString = formatString.replace(/zzz/i, zz);

    return formatString;
}

Date.prototype.myFormat = function (format) {
    return formatDate(this, format);
};

function PrintLine(source, title, _this, txt, enableLogWhere, level) {
    var trace  = getTrace(source[1]);
    var string = disableTimeStamp ? "" : '[' + ((title && (typeof title === 'string') && (title.length > 0)) ? (title + ' ') : '');

    if (enableLogWhere == true) {
        var fullTrace = "";

        for (var tt = 1; tt < source.length; tt++) {
            var yy = getTrace(source[tt]);

            fullTrace += util.format("%s:%d\n", yy.file, yy.lineno);
        }

        if (disableTimeStamp){
            string += util.format.apply(_this, [txt]) + fullTrace;
        } else {
            string += util.format("%s] [%s]: %s\n", trace.timestamp, ___logLevel[level], util.format.apply(_this, [txt])) + fullTrace;
        }
    } else
    if ((enableLogWhere == false) || (enableLogWhere == undefined)) {
        if (disableTimeStamp){
            string += util.format.apply(_this, [txt]);
        } else {
            string += util.format("%s] [%s]: %s", trace.timestamp, ___logLevel[level], util.format.apply(_this, [txt]));
        }
    }

    return string;
}

function getTrace(call) {
    var curDate = new Date();

    return {
        file: call.getFileName(),
        lineno: call.getLineNumber(),
        timestamp: curDate.myFormat('yyyy.dd.mm hh:nn:ss')
    };
}

function colourise(colourCode, string, background) {
    var backcolor = (!background) ? "\x1B[0m" : background;

    return "\x1B[" + colourCode + "m" + string + backcolor;
}

function ExtractPath(Path) {
    Path = Path.replace(/\\/g, '/');
    
    for (var i = Path.length - 1; i >= 0; i--) {
        if (Path[i] === '/') {
            return Path.slice(0, i + 1);
        }
    }

    return Path;
}

function ExtractFileName(Path) {
    var count = 0;
    Path = Path.replace(/\\/g, '/');

    for (var i = Path.length - 1; i >= 0; i--) {
        count ++;

        if (Path[i] === '/') {
            return Path.slice(i + 1, i + count);
        }
    }

    return Path;
}

function LogToFile(text, file) {
    function __log() {
        var out        = "";
        var _path      = ExtractPath(file);
        var _file      = ExtractFileName(file);
        var _logBuffer = logBuffer;

        logBuffer = [];

        fs.stat(checkDir(_path), function (err) {
            if (err) {
                process.stdout.write(colourise(colors.red, '  >> [err]: Path not found: ' + _path) + CRLF);
            }

            while (_logBuffer.length){
                out += _logBuffer.shift() + CRLF;
            }

            fs.appendFile(err ? './' + _file : ConsoleFile, out, function () {
                logTimeOut = null;

                out = "";
            });
        });
    }

    if (file) {
        if (text !== '' && ConsoleFile.length) {
            logBuffer.push(text);

            if (logDropTime !== -1){
                if (!logTimeOut) {
                    logTimeOut = setTimeout(__log, logDropTime);
                }
            } else {
                __log();
            }
        }
    }
}

function checkDir(checkPath) {
    var _path  = ExtractPath(checkPath);

    if (statLogDir !== _path){
        try {
            statLogDir = fs.statSync(_path) ? _path : "./";
        } catch (e){
            statLogDir = './';
        }

        try {
            if (statLogDir === './' && _path !== './') {
                var sep = '/';
                var initDir = path.isAbsolute(_path) ? sep : '';

                _path.split(sep).reduce((parentDir, childDir) => {
                    var curDir = path.resolve(parentDir, childDir);

                    if (!fs.existsSync(curDir)) {
                        fs.mkdirSync(curDir);
                    }

                    return curDir;
                }, initDir);

                statLogDir = _path;
            }
        } catch (e){
            process.stdout.write(colourise(colors.red, '  >> [err]: Path not found: ' + _path) + CRLF);

            statLogDir = './';
        }
    }

    return statLogDir;
}

function writeLogToFile(fileName) {
    ConsoleFile = checkDir(fileName) + ExtractFileName(fileName);
}

function writeLog(stack, title, color, arg, immediately) {
    var _logLevel = CurrentLogLevel;
    var _txt = arg[0];
    var _logWhere = false;
    var _logFile = undefined;

    if (Object.prototype.toString.call(arg[1]) == '[object Object]') {
        if (arg[1].logLevel) _logLevel = arg[1].logLevel;
        if (arg[1].logWhere) _logWhere = arg[1].logWhere;
        if (arg[1].logFile)  _logFile  = arg[1].logFile;
    } else {
        if (arg.length > 1) {
            if (___logLevel[arg[arg.length - 1]]) {
                _logLevel = arg[arg.length - 1];
            }
        }

        if (arg[1] && (Object.prototype.toString.call(arg[1]) == '[object Boolean]')) {
            _logWhere = arg[1];
        }
    }

    if (!_logLevel || parseInt(_logLevel) <= parseInt(CurrentLogLevel)) {
        var text = PrintLine(stack, /*title*/ '', this, _txt, _logWhere, _logLevel);

        process.stdout.write(colourise(color, text) + CRLF);

        if (immediately) {
            try {
                fs.appendFileSync(ConsoleFile || './CRASH.log', text + CRLF);
            } catch (e){}
        } else
        if (_logFile) {
            LogToFile(text, _logFile)
        } else {
            LogToFile(text, ConsoleFile);
        }
    }
}

function AddCustomLogFile(param) {} // todo: make this?

module.exports = {
    sendToServer: function (sendFunc){
        sendCMD = sendFunc;
    },
    //    log : function () { writeLog.apply(this, [traceStack, 'log ', colors.default, arguments]) },
    //    info: function () { writeLog.apply(this, [traceStack, 'info', colors.lcyan, arguments]) },
    //    err : function () { writeLog.apply(this, [traceStack, 'err ', colors.lred, arguments]) },
    //    error:function () { writeLog.apply(this, [traceStack, 'err ', colors.lred, arguments]) },
    //    warn: function () { writeLog.apply(this, [traceStack, 'warn', colors.yellow, arguments]) },
    //    important: function () { writeLog.apply(this, [traceStack, '  ! ', colors.bWhite, arguments]) },
    log : function () { writeLog.apply(this, [traceStack, 'log ', colors.default, arguments]);     sendCMD && sendCMD('log ', arguments[0])},
    info: function () { writeLog.apply(this, [traceStack, 'info', colors.lcyan, arguments]);       sendCMD && sendCMD('info', arguments[0])},
    err : function () { writeLog.apply(this, [traceStack, 'err ', colors.lred, arguments]);        sendCMD && sendCMD('err ', arguments[0])},
    error:function () { writeLog.apply(this, [traceStack, 'err ', colors.lred, arguments]);        sendCMD && sendCMD('err ', arguments[0])},
    warn: function () { writeLog.apply(this, [traceStack, 'warn', colors.yellow, arguments]);      sendCMD && sendCMD('warn', arguments[0])},
    important: function () { writeLog.apply(this, [traceStack, '  ! ', colors.bWhite, arguments]); sendCMD && sendCMD('  ! ', arguments[0])},
    wrt: {
        write: function () {
            writeLog.apply(this, [traceStack, 'wrt ', colors.bWhite, arguments]);
        }
    },

    immediately: function () { writeLog.apply(this, [traceStack, 'ERR ', colors.lred, arguments, true]) },

    DublicateToFile: writeLogToFile,

    AddCustomLofFile: AddCustomLogFile,

    disableTimeStamp: function () {
        disableTimeStamp = true;
    },

    logLevel: logLevel,
    SetDropTime: function (time) { logDropTime = time; },
    SetLogLevel: function (ll) {
        if (___logLevel.hasOwnProperty(ll)){
            CurrentLogLevel = ll;
        }
    },
    GetLogLevel: function () { return CurrentLogLevel; }
};

// Colours
//
// \x1B[0m    все атрибуты по умолчанию
// \x1B[1m    жирный шрифт (интенсивный цвет)
// \x1B[2m    полу яркий цвет (тёмно-серый, независимо от цвета)
// \x1B[4m    подчеркивание
// \x1B[5m    мигающий
// \x1B[7m    реверсия (знаки приобретают цвет фона, а фон -- цвет знаков)
//
// \x1B[22m    установить нормальную интенсивность
// \x1B[24m    отменить подчеркивание
// \x1B[25m    отменить мигание
// \x1B[27m    отменить реверсию
//
// \x1B[30    чёрный цвет знаков
// \x1B[31    красный цвет знаков
// \x1B[32    зелёный цвет знаков
// \x1B[33    желтый цвет знаков
// \x1B[34    синий цвет знаков
// \x1B[35    фиолетовый цвет знаков
// \x1B[36    цвет морской волны знаков
// \x1B[37    серый цвет знаков
//
// \x1B[40    чёрный цвет фона
// \x1B[41    красный цвет фона
// \x1B[42    зелёный цвет фона
// \x1B[43    желтый цвет фона
// \x1B[44    синий цвет фона
// \x1B[45    фиолетовый цвет фона
// \x1B[46    цвет морской волны фона
// \x1B[47    серый цвет фона


