// ---------------------------------------------------------------------------
//
// SATEL Diagnostic trace module. Almost 100% compatible with SATEL.NET Satel.Diagnostics.Trace
//
// Cfg types: App,Fatal,Errors,Warnings,Info,Debug,Msg,Byte,Gen;
//            C=Console S=shortTS [Q=deltaTS O=file 'trace.log' (file not recommended)]");
//
// Created 2015-03-05
//
// (c)opyright 2015-2022 Satel Oy, Salo, Finland
//
// ---------------------------------------------------------------------------
var fs = require('fs');
var path = require('path');
var dgram = require('dgram'); // new 2015-12-09. Need to be able to send UDP datagrams

var traceConfig = {};
// var time = require('../utility/date_time');
var udpclient = null;
var FilterSyms = ["F",        "E",        "W",  "A",  "I",  "D",  "M",  "_",  "B",  "S"];
var LogSymbols = ["F-ERROR ", "E-ERROR ", "W ", "A ", "I ", "D ", "M ", "_ ", "B ", "S "];// output
var TraceSinks = [];
var shortTimestamp = false;
// record start time into last write time
var TraceLatest = new Date();
var linesSinceLastLocaltimeDump = 0;   // 2019-08-20 RHa
var cfgLinesCountAbsTimePrinted = 199; // 2019-08-20 RHa

// Helper function
function ContainsItem(a, obj) {
    for (var i = 0; i < a.length; i++) {
        if (a[i] === obj) {
            return true;
        }
    }
    return false;
}

// 2022-04-04 New ultra-compact zero-dependency time-formatter (Improved 2022-04-05)
//     opts.fmt: either mixture of fields:  YYYY,MM,DD, HH, mm, SS, MS, WKDI, WKDN,WKD2,WKD3
//               or ONE of quick formats:   year, full, fullms, date, time, timems
function GetTs(opts) { // {dt, utc, inflate, fmt}
    var ts = { }; // REF: if input is utc: new Date(Date.UTC(2020, 11, 20, 3, 23, 16, 738));
    // REF: day of year: https://stackoverflow.com/questions/8619879/javascript-calculate-the-day-of-the-year-1-366
    var tnow = (opts.dt) ? new Date(opts.dt) : new Date();
    const zeroPad = num => ((num < 10) ? '0' + num : num);
    ts.year  = opts.utc? tnow.getUTCFullYear() : tnow.getFullYear();
    ts.month = opts.utc?(tnow.getUTCMonth()+1) :(tnow.getMonth()+1);
    ts.day   = opts.utc? tnow.getUTCDate()     : tnow.getDate(); // day of month
    ts.hour  = opts.utc? tnow.getUTCHours()    : tnow.getHours();
    ts.min   = opts.utc? tnow.getUTCMinutes()  : tnow.getMinutes();
    ts.sec   = opts.utc? tnow.getUTCSeconds()  : tnow.getSeconds();
    ts.msec  = opts.utc? tnow.getUTCMilliseconds(): tnow.getMilliseconds();
    ts.wkdi  = opts.utc? tnow.getUTCDay()      : tnow.getDay(); //0=sun,1mon,6=sat
    ts.dayNr = ts.wkdi; // just compatibility-alias (day of week 0=sun,1mon,6=sat)
    ts.wkdn=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][ts.wkdi];
    ts.wkd2=ts.wkdn.slice(0,2);
    ts.wkd3=ts.wkdn.slice(0,3);
    ts.tz  ='#NA'; // slow query, so it is optional
    if (opts.gettz == true) {
      ts.tz = (Intl? Intl.DateTimeFormat().resolvedOptions().timeZone : '');
    }
    ts.now = tnow;
    if ( opts.inflate || opts.fmt != null) {
        ts.month = zeroPad(ts.month); ts.day = zeroPad(ts.day);
        ts.hour = zeroPad(ts.hour);ts.min = zeroPad(ts.min);ts.sec = zeroPad(ts.sec);
        if (ts.msec < 100) {
            if ( ts.msec < 10) ts.msec = '00'+ts.msec;
            else ts.msec = +'0'+ts.msec;
        }
    }
    if ( opts.fmt =='fullms') { // hard-coded optimization for the most used one is necessary
        return  ts.year +'-'+ts.month+'-'+ts.day+' '+ts.hour+':'+ts.min+':'+ts.sec+'.'+ts.msec;
    }
    if ( opts.fmt != null) { // YYYY MM DD HH mm SS MS  (!) only 'mm' is special, i.e. MM=month, mm=minutes.
        var tmp=opts.fmt;var dt='YYYY-MM-DD';var tm='HH:mm:SS';
        var quicks={year:'YYYY',full: dt+' '+tm,fullms: dt+' '+tm+'.MS',date:dt, time:tm,'time-of-day':tm,timems:tm+'.MS'};
        Object.keys(quicks).forEach((okey)=> { if (tmp==okey) tmp=quicks[okey]; });
        var fieldmap= {YYYY:'year',MM:'month',DD:'day',HH:'hour',mm:'min',SS:'sec',MS:'msec',TZ:'tz',WKDI:'wkdi',WKDN:'wkdn',WKD2:'wkd2',WKD3:'wkd3'};
        Object.keys(fieldmap).forEach((k) => { var loops=0;while ((tmp.indexOf(k)>-1)&&(loops<16)) {loops++;tmp=tmp.replace(k, ts[ fieldmap[k] ]); } });
        return tmp;
    }
    return ts;
}

module.exports = function (settings) {
    //var root_config=settings.GetSetting("root_config","");
    traceConfig.tracefile = settings.GetSetting("log", "logs/trace_#YYYY-#MM-#DD.log");

    var progDataPath = settings.GetDataPath();
    console.log("diagtrace init w/  cwd=" + process.cwd() + " tracefile=" + traceConfig.tracefile + " program_data_path=" + progDataPath);
    // example at 2020-02-20 00:28 in design.windows.installer.release:
    //   cwd              =C:\Program Files (x86)\Satel\NETCO\ds_0\app-logic
    //   tracefile        =logs\trace_#YYYY-#MM-#DD.log
    //   program_data_path=C:\ProgramData\Satel\NETCO\ds
    if ((traceConfig.tracefile[0] != '/') && (traceConfig.tracefile[1] != ':')) {
        console.log("diagtrace init: making a hack fix to trace log file path. You should fix this by moving nodejs service cwd to program_data_path and having just relative path for 'log' (20200220)");
        traceConfig.tracefile = path.join(progDataPath, traceConfig.tracefile);
    }

    traceConfig.enabled = settings.GetBooleanSetting("trace", false);  // --trace         true*|false
    var tracefilter = settings.GetSetting("tracefilter", "WEF");      // --tracefilter   AIMWBEFCOST!....
    if (tracefilter.indexOf("all") >= 0) {
        tracefilter = tracefilter.replace("all", "ABFEWIDMG");
    }
    traceConfig.udptrace_monitors = settings.GetSetting("udptrace_monitors", ""); // Same key as in Satel.NET
    traceConfig.udptrace_port = settings.GetIntSetting("udptrace_port", 0);  // Same key as in Satel.NET
    if (traceConfig.udptrace_port > 0) {
        console.log("udptrace_monitors = " + traceConfig.udptrace_monitors);
        console.log("udptrace_port     = " + traceConfig.udptrace_port);
        if (ContainsItem(TraceSinks, 'U') == false) {
            console.log("Request enabling trace sink UDP");
            TraceSinks.push('U'); // Activate UDP Trace output.....
        }
    }
    traceConfig.tracesrcname = settings.GetSetting("tsn", "");         // --tsn           sourcename
    if (traceConfig.tracesrcname == "") {
        traceConfig.tracesrcname = settings.GetSetting("processname", ""); // Use processname if no specific name
        var tsn = traceConfig.tracesrcname;
        if (tsn.length > 7) {
            traceConfig.tracesrcname = tsn.substring(0, 5) + tsn.substring(tsn.length - 2);
        }
    }
    traceConfig.typeEnabled = [];
    traceConfig.deltaMs = false; // show delta timestamps (adds 6 chars per row i.e. -00000)
    InternalHandleTraceFilter(tracefilter);
    var timeNow = new Date();
    diagtrace.NMIWrite(" - - -"); // 2016-04-12 RHa
    diagtrace.NMIWrite(" - - -  System START  at Local time " + GetTs({ dt: timeNow, utc: false, inflate: true, fmt: 'fullms' }) /*time.GetDateAndTime(timeNow, 1)*/
        + "  UTC: " + GetTs({ dt: timeNow, utc: true, inflate: true, fmt: 'fullms' }) /*time.GetDateAndTimeUTC(timeNow, 1)*/);// Local ts since 2019-09-20 RHa
    diagtrace.NMIWrite(" ");
    diagtrace.NMIWrite("tracefilter: " + tracefilter);
    return diagtrace;
};

function InternalHandleTraceFilter(tracefilter_in) {
    // console.log("InternalHandleFilter " + tracefilter_in );
    traceConfig.tracefilter = tracefilter_in;
    for (var i = 0; i < FilterSyms.length; i++) {
        traceConfig.typeEnabled[i] = false;
        if (traceConfig.tracefilter.indexOf(FilterSyms[i]) >= 0) {
            traceConfig.typeEnabled[i] = true;
        }
    }
    for (var j = 0; j < traceConfig.tracefilter.length; j++) {
        switch (traceConfig.tracefilter[j]) {
            case 'u':
            case 'U':
                if (ContainsItem(TraceSinks, 'U') == false) {
                    TraceSinks.push('U'); // U=UDP trace
                }
                break;
            case 'c':
            case 'C':
                if (ContainsItem(TraceSinks, 'C') == false) {
                    TraceSinks.push('C'); // C=console output
                }
                break;
            case 'o':
            case 'O':
                if (ContainsItem(TraceSinks, 'O') == false) {
                    TraceSinks.push('O'); break;  // O=file output
                }
                break;
            case 's':
            case 'S': shortTimestamp = true; break;  // S=Short timestamp

            case 'T': traceConfig.deltaMs = true; break; // delta timestamps
            case '!': traceConfig.enabled = true; break;  // Another way to enable trace
        }
    }
    if (ContainsItem(TraceSinks, 'U') == false) {
        udpclient = null;
    } else {
        if (traceConfig.udptrace_monitors == "") {
            traceConfig.udptrace_monitors = "localhost";
        }
        udpclient = dgram.createSocket('udp4');
        udpclient.unref(); // NOTE: THe program won't exit unless this is done (https://nodejs.org/api/dgram.html)
        // Listen for messages from (to) client
        udpclient.on('message', function (message) {    // message.toString( )
        });
        // udpclient.bind( specificPort); sender port...
        udpclient.on('error', function (err) { // // 22.02.2017: catch udp errors [mharj]
            if (err.message.search(/getaddrinfo/) !== -1) {
                // ignore hostname resolve issues [mharj]
            } else {
                throw err; // keep throw rest of errors [mharj]
            }
        });
    }
}

// Normal externally accessible generic write function. Use the specific functions instead (normally)
function WriteLineIntType(intTypeIndex, text, opt_ts) {
    var wrote = false;
    //console.log("WriteLineIntType [1] " + intTypeIndex + "  txt=" + text + " enabled=" + traceConfig.enabled );
    if (intTypeIndex == 9) { //NMIWrite
        CoreWriteFormat(LogSymbols[intTypeIndex], text, opt_ts);
        wrote = true;
    } else if (traceConfig.enabled == true) {
        //console.log("WriteLineIntType [2] " + intTypeIndex + "  txt=" + text );
        if (traceConfig.typeEnabled[intTypeIndex] == true) {
            //console.log("WriteLineIntType [3] " + intTypeIndex + "  txt=" + text + " calling CoreWriteFormat" );
            CoreWriteFormat(LogSymbols[intTypeIndex], text, opt_ts);
            wrote = true;
        }
    }

    // Time to time, after each about 200 lines add local timestamp and UTC timestamp
    if (wrote) {
        if (++linesSinceLastLocaltimeDump > cfgLinesCountAbsTimePrinted) {
            linesSinceLastLocaltimeDump = 0;
            var dt = new Date();
            CoreWriteFormat(  /*NMI*/ LogSymbols[9], ">>>>>>>>>>> Timeinfo" +
               "   UTC " +   GetTs({ dt: dt, utc: true,  inflate: true, fmt: 'fullms' }) /*time.GetDateAndTimeUTC( dt, 1)*/
             + "   LOCAL " + GetTs({ dt: dt, utc: false, inflate: true, fmt: 'fullms' })/*time.GetDateAndTime(dt, 1)*/ + " >>>>>>>>>>>");
        }
    }
}
// Write one line of trace log to all enabled trace sinks
function WriteToEnabledSinks(ts, logLine) {
    TraceSinks.forEach(function (entry) {
        switch (entry) {
            // UDP trace sink
            case 'u':
            case 'U':
                if (udpclient) {
                    var tracesrc = "[" + traceConfig.tracesrcname + "] ";
                    var allocsize = logLine.length > 1500 ? 1500 : logLine.length;
                    var udpMsg = Buffer.alloc(tracesrc.length + allocsize);
                    if (logLine.length > 1500) {
                        udpMsg.write(tracesrc + logLine.substr(0, 1497) + "..");
                    } else {
                        udpMsg.write(tracesrc + logLine);
                    }
                    udpclient.send(udpMsg, 0, udpMsg.length,
                        traceConfig.udptrace_port, traceConfig.udptrace_monitors);
                }
                break;
            // console sink
            case 'c':
            case 'C': console.log(logLine); break;

            // file sink
            case 'o':
            case 'O':
                var fileName;
                var current_date = GetTs({ dt: ts, utc: true, inflate: true, fmt: 'date' });// time.GetDateUTC(ts);
                if (traceConfig.this_day != current_date) {
                    traceConfig.this_day = current_date;
                    fileName = traceConfig.tracefile;
                    var t = GetTs({ dt: ts, utc: true, inflate: true }); // time.GetUTCTimeAsStruct( true );
                    fileName = fileName.replace("#YYYY", t.year);
                    fileName = fileName.replace("#yyyy", t.year);
                    fileName = fileName.replace("#MM", t.month);
                    fileName = fileName.replace("#mm", t.month);
                    fileName = fileName.replace("#DD", t.day);
                    fileName = fileName.replace("#dd", t.day);

                    if (fs.existsSync(fileName) && fileName.indexOf(".log") == fileName.length - 4) {
                        var index = 1;
                        var tmpFileName;
                        do {
                            tmpFileName = fileName.substring(0, fileName.length - 4) + "_" + index + ".log";
                            index++;
                        }
                        while (fs.existsSync(tmpFileName) && index < 10);
                        fileName = tmpFileName;
                    }
                    traceConfig.fileName = fileName;
                } else {
                    fileName = traceConfig.fileName;
                }
                // console.log("Output "  + fileName + " : " + logLine );
                fs.appendFile(fileName, logLine + "\r\n", function (err) { });
                break;
            default:
                break;
        }
    });
}

// "tracetype" is LogSymbols[ ] so contains one space between "tracetype" and "text"
// 2019-09-20 RHa Changed to use UTC timestamp...
function CoreWriteFormat(traceType, text, opt_ts) {

    let t = GetTs( {dt: (opt_ts ? opt_ts: null), utc:true, inflate: true });
    var timeNow = t.now;
    var elapsedMs = timeNow - TraceLatest;
    var timestamp = t.hour + ":" + t.min + ":" + t.sec + "." + t.msec;

    if (!shortTimestamp) {
        timestamp = t.year + "-" + t.month + "-" + t.day + " " + timestamp;
    }
    if (traceConfig.deltaMs == true) {
        if (elapsedMs < 10000) {
            timestamp += "-";
            if (elapsedMs < 1000) { timestamp += "0"; }
            if (elapsedMs < 100) { timestamp += "0";  }
            if (elapsedMs < 10) { timestamp += "0"; }
            timestamp += elapsedMs;
        } else {
            timestamp += "     ";
        }
    }
    var logLine = timestamp + " " + traceType + text;

    //var timeNow = new Date();
    //var elapsedMs = timeNow - TraceLatest;
    if (elapsedMs >= 1500) {
        WriteToEnabledSinks(timeNow, "   ::    ");
    }
    if (elapsedMs >= 1100) {
        WriteToEnabledSinks(timeNow, "   ::    " + elapsedMs + " ms");
    }
    // Write to all sinks
    TraceLatest = timeNow;// new Date( );
    WriteToEnabledSinks(timeNow, logLine);
}

//                     0          1          2     3     4     5     6     7     8    9
// var LogSymbols = {"F-ERROR ","E-ERROR ", "W ", "A ", "I ", "D ", "M ", "_ ", "B ", "S/NMIWrite };// output
function WriteLine(charType, text) {
    switch (charType) {
        case 's':
        case 'S': WriteLineIntType(9, text); break; // New 2016-04-12
        case 'b':
        case 'B': WriteLineIntType(8, text); break;
        case '_': WriteLineIntType(7, text); break;
        case 'a':
        case 'A': WriteLineIntType(3, text); break;
        case 'd':
        case 'D': WriteLineIntType(5, text); break;
        case 'm':
        case 'M': WriteLineIntType(6, text); break;
        case 'i':
        case 'I': WriteLineIntType(4, text); break;
        case 'w':
        case 'W': WriteLineIntType(2, text); break;
        case 'f':
        case 'F': WriteLineIntType(0, text); break;
        case 'e':
        case 'E': WriteLineIntType(1, text); break;

        default: WriteLineIntType(5, text); break;
    }
}

var diagtrace = {
    GetTs: function( opts) {
        return GetTs( opts);
    },
    Test: function () {
        return "Here we are! Cwd is " + process.cwd() + " tracefile: " + traceConfig.tracefile;
    },
    Configure: function ( /*filter*/ tracefilter, /*appName*/traceSrcName, /*traceFilename*/ tracefile) {
        traceConfig.tracefile = tracefile;
        traceConfig.tracesrcname = traceSrcName;
        InternalHandleTraceFilter(tracefilter);
    },

    ConfigureFilter: function ( /*filter*/ tracefilter) {
        InternalHandleTraceFilter(tracefilter);
    },
    //                     0          1          2     3     4     5     6     7     8    9,
    // var LogSymbols = {"F-ERROR ","E-ERROR ", "W ", "A ", "I ", "D ", "M ", "_ ", "B ", NMIWrite };// output
    Fatal: function (text) { WriteLineIntType(0, text); },
    Error: function (text) { WriteLineIntType(1, text); },
    Warning: function (text) { WriteLineIntType(2, text); },
    Application: function (text) { WriteLineIntType(3, text); },
    Info: function (text) { WriteLineIntType(4, text); },
    Debug: function (text) { WriteLineIntType(5, text); },
    Message: function (text) { WriteLineIntType(6, text); },
    Bytes: function (text) { WriteLineIntType(8, text); },
    NMIWrite: function (text) { WriteLineIntType(9, text); },
    // Passthrough is used by NETCO DEVICE Electron-variants (2022-04..) by logger.js sink-cb-definition
    Passthrough:  function(intTypeIndex, str, ts_date) { WriteLineIntType(intTypeIndex, str, ts_date) },
    Close: function () {
        if (udpclient != null) {
            udpclient.close();
            udpclient = null;
        }
    }
}
