// pipe-ipc.js
// Pipe based IPC implementation for WelsonJS framework
// Namhyeon Go (Catswords Research) <abuse@catswords.net>
// https://github.com/gnh1201/welsonjs
var STD = require("lib/std");

// https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/opentextfile-method
var ForReading = 1;
var ForWriting = 2;
var ForAppending = 8;
var TristateUseDefault = -2;
var TristateTrue = -1;
var TristateFalse = 0;

// https://learn.microsoft.com/en-us/office/client-developer/access/desktop-database-reference/streamtypeenum
var adTypeBinary = 1;
var adTypeText = 2;

// https://learn.microsoft.com/ko-kr/sql/ado/reference/ado-api/saveoptionsenum?view=sql-server-ver16
var adSaveCreateNotExist = 1;
var adSaveCreateOverWrite = 2;

// https://learn.microsoft.com/en-us/sql/ado/reference/ado-api/connectmodeenum?view=sql-server-ver16
var adModeRead = 1;
var adModeReadWrite = 3;
var adModeRecursive = 0x400000;
var adModeShareDenyNone = 16;
var adModeShareDenyRead = 4;
var adModeShareDenyWrite = 8;
var adModeShareExclusive = 12;
var adModeUnknown = 0;
var adModeWrite = 2;

// https://learn.microsoft.com/en-us/previous-versions/exchange-server/exchange-10/ms527267(v=exchg.10)
// https://learn.microsoft.com/en-us/previous-versions/exchange-server/exchange-10/ms526296(v=exchg.10)
var CdoCharset = {};
CdoCharset.CdoBIG5        = "big5";
CdoCharset.CdoEUC_JP      = "euc-jp";
CdoCharset.CdoEUC_KR      = "euc-kr";
CdoCharset.CdoGB2312      = "gb2312";
CdoCharset.CdoISO_2022_JP = "iso-2022-jp";
CdoCharset.CdoISO_2022_KR = "iso-2022-kr";
CdoCharset.CdoISO_8859_1  = "iso-8859-1";
CdoCharset.CdoISO_8859_2  = "iso-8859-2";
CdoCharset.CdoISO_8859_3  = "iso-8859-3";
CdoCharset.CdoISO_8859_4  = "iso-8859-4";
CdoCharset.CdoISO_8859_5  = "iso-8859-5";
CdoCharset.CdoISO_8859_6  = "iso-8859-6";
CdoCharset.CdoISO_8859_7  = "iso-8859-7";
CdoCharset.CdoISO_8859_8  = "iso-8859-8";
CdoCharset.CdoISO_8859_9  = "iso-8859-9";
CdoCharset.cdoKOI8_R      = "koi8-r";
CdoCharset.cdoShift_JIS   = "shift-jis";
CdoCharset.CdoUS_ASCII    = "us-ascii";
CdoCharset.CdoUTF_7       = "utf-7";
CdoCharset.CdoUTF_8       = "utf-8";

var randomize = Math.random;

function UUIDv4() {}
UUIDv4.create = function() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = randomize() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
};

function CRC32() {
    this.value = 0;

    this.fromString = function(s) {
        var crc = 0 ^ (-1);
        for (var i = 0; i < s.length; i++ ) {
            crc = (crc >>> 8) ^ CRC32.table[(crc ^ s.charCodeAt(i)) & 0xFF];
        }
        this.value = ((crc ^ (-1)) >>> 0);
    };

    this.toString = function() {
        return this.value.toString(16).padStart(8, '0');
    };
}
CRC32.table = [];
(function() {
    for (var n = 0; n < 256; n++) {
        c = n;
        for(var k =0; k < 8; k++){
            c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
        }
        CRC32.table[n] = c;
    }
})();

function makeProbabilityBit(p) {
    return !( p > 0.0 ? ( (randomize() / p) > 1.0 ) : true) ? 1 : 0;
}

function makeInterface(i) {
    return CreateObject(["Scripting.FileSystemObject", "ADODB.Stream"][i]);
}

function openTextFile(filename, iomode) {
    return makeInterface(0).OpenTextFile(filename, iomode, true, TristateTrue);
}

function checkFileExists(filename) {
    return makeInterface(0).FileExists(filename);;
}

function deleteFile(filename) {
    if (checkFileExists(filename)) makeInterface(0).DeleteFile(filename);
}

function getFileSize(filename) {
    try {
        return makeInterface(0).GetFile(filename).Size;
    } catch (e) {
        return -1; 
    }
}

function Converter() {
    this.value = null;

    this.from = function(value) {
        this.value = value;
    };

    this.getBinaryFromText = function() {
        var stream = makeInterface(1);
        stream.Type = adTypeText;
        stream.CharSet = CdoCharset.CdoUTF_8;
        stream.Open();
        stream.WriteText(this.value);
        stream.Position = 0;
        stream.Type = adTypeBinary;
        return stream.Read();
    };

    this.getTextFromBinary = function() {
        var stream = makeInterface(1);
        stream.Type = adTypeBinary;
        stream.Open();
        stream.Write(this.value);
        stream.Position = 0;
        stream.Type = adTypeText;
        stream.CharSet = CdoCharset.CdoUTF_8;
        return stream.ReadText();
    };
    
    this.repositionObject = function(stream, position) {
        position = (position !== "number" ? 0 : position);

        var _stream = makeInterface(1);
        _stream.Type = adTypeBinary;
        _stream.Mode = adModeReadWrite;
        _stream.Open();
        stream.Position = position;
        stream.CopyTo(_stream);
        stream.Flush();
        stream.Close();

        return _stream;
    };
}

function PipeIPC() {
    this.path = "data\\.pipe_:pipename";
    this.delimiter = "\r\n";
    this.reader = null;
    this.writer = null;
    this.recorder = null;
    this.savefile = null;
    this.tmpfile = null;
    this.charset = CdoCharset.CdoUTF_8;
    this.lastReadTime = -1;
    this.lastWriteTime = -1;
    this.maxSentences = 0;
    this.recorder_iomode = ForAppending;

    this.getCurrentTime = function() {
        return new Date().getTime();
    };

    this.setCharset = function(charset) {
        charset = charset.toLowerCase();

        if (Object.values(CdoCharset).indexOf(charset) < 0) {
            console.warn(charset.toUpperCase() + " may not be an encoding supported by the system.");
        }
        this.charset = charset;
    };
    
    this.setDelimiter = function(delimiter) {
        this.delimiter = delimiter;
    };

    this.setMaxSentences = function(maxSentences) {
        this.maxSentences = maxSentences;
    };

    this.waitForRetry = function() {
        var t = makeProbabilityBit(0.5);
        if (t > 0) sleep(t);
    };

    this.connect = function(pipename, callback) {
        if (pipename == "volatile") {
            pipename = UUIDv4.create().substring(0, 8);
        }
        this.path = this.path.replace(":pipename", pipename);
        //this.openWriter();
        //this.openReader();
        //console.info("Opened pipe:", pipename);
        if (typeof callback === "function") {
            callback(this, this.reader, this.writer);
        }
        return this;
    };

    this.openWriter = function(iomode) {
        while (this.writer == null) {
            try {
                this.writer = openTextFile(this.path, iomode);
            } catch (e) {}
        }
    };

    this.closeWriter = function() {
        if (this.writer != null) {
            this.writer.Close();
            this.writer = null;
        }
    };

    this.openReader = function() {
        while (this.reader == null) {
            try {
                this.reader = openTextFile(this.path, ForReading);
            } catch (e) {}
        }
    };

    this.closeReader = function() {
        if (this.reader != null) {
            this.reader.Close();
            this.reader = null;
        }
    };

    this.startRecorder = function(filename, iomode) {
        iomode = (iomode !== "undefined" ? ForAppending : iomode);  // default: ForAppending

        this.savefile = filename;
        this.tmpfile = this.savefile + ".tmp";
        this.recorder_iomode = iomode;

        // read a text from save file
        if (this.recorder_iomode == ForAppending) {
            var isExistsSaveFile = checkFileExists(this.savefile);
            var isExistsTmpFile = checkFileExists(this.tmpfile);
            while (isExistsSaveFile && !isExistsTmpFile) {
                try {
                    var fso = openTextFile(this.tmpfile, ForWriting);
                    fso.Write(this.readTextFromFile(this.savefile));
                    fso.Close();
                    isExistsTmpFile = checkFileExists(this.tmpfile);
                } catch (e) {
                    isExistsTmpFile = false;
                    this.waitForRetry();
                }
            }
        }

        // open a recorder
        this.openRecorder(iomode);
    };

    this.stopRecorder = function() {
        this.savefile = null;
        this.tmpfile = null;
    };

    this.openRecorder = function(iomode) {
        while (this.recorder == null) {
            try {
                this.recorder = openTextFile(this.tmpfile, iomode);
            } catch (e) {}
        }
    };

    this.closeRecorder = function() {
        if (this.recorder != null) {
            this.recorder.Close();
            this.recorder = null;
        }
    };

    this._write = function(message) {
        this.writer.Write(message);
    };

    this.write = function(message, iomode) {
        iomode = (iomode !== "undefined" ? ForAppending : iomode);  // default: ForAppending

        var isWritten = false;
        while (!isWritten) {
            try {
                this.flush();
                this.openWriter(iomode);
                this._write(message);
                this.closeWriter();
                isWritten = true;
                this.lastWriteTime = this.getCurrentTime();
            } catch (e) {
                //console.log(e.message);
                this.closeWriter();
                isWritten = false;
                this.waitForRetry();
            }
        }
        
        // record the last message
        if (isWritten && this.savefile != null) {
            this.record(message);
        }

        return isWritten;
    };

    this._record = function(message) {
        if (this.recorder_iomode == ForAppending) {
            this.recorder.Write(message);
        } else if (this.recorder_iomode == ForWriting) {
            this.recorder.Write(message + this.delimiter + this.readTextFromFile(this.savefile));
        }
    };

    this.record = function(message) {
        var isRecorded = false;
        while (!isRecorded) {
            try {
                this.openRecorder(this.recorder_iomode);
                this._record(message);
                this.closeRecorder();
                this.commit(this.savefile);
                isRecorded = true;
                this.closeRecorder();
            } catch (e) {
                //console.log(e.message);
                this.closeRecorder();
                isRecorded = false;
                this.waitForRetry();
            }
        }

        return isRecorded;
    };

    this.commit = function(dst) {
        var src = this.tmpfile;
        var charset = this.charset;
        var isCommited = false;

        while (!isCommited) {
            try {
                // Open a temporary file
                var fso = openTextFile(src, ForReading);
                var text = this._read(fso);
                fso.Close();  // close the file immediately
                var sentences = text.split(this.delimiter);
                var newSentences = [], str = '';

                // if enabled "maxSentences" feature
                if (text.length > 0 && this.maxSentences > 0) {
                    while (sentences.length > 0 && newSentences.length < this.maxSentences) {
                        newSentences.push(sentences.pop());
                    }
                    str = newSentences.reverse().join(this.delimiter);

                    // Write a new temporary file
                    var done = false;
                    while (!done) {
                        try {
                            var _fso = openTextFile(src, ForWriting);
                            _fso.Write(str);
                            _fso.Close();
                            done = true;
                        } catch (e) {
                            done = false;
                            this.waitForRetry();
                        }
                    }
                } else {
                    str = text;
                }
                
                // Convert UTF-16 BOM to a character set
                var stream = makeInterface(1);
                stream.Type = adTypeText;
                stream.Charset = charset;
                stream.Open();
                stream.WriteText(str);
                stream = (new Converter()).repositionObject(stream, 3);
                stream.SaveToFile(dst, adSaveCreateOverWrite);
                stream.Close();

                // Set a result
                isCommited = true;
            } catch (e) {
                //console.log(e.message);
                isCommited = false;
                this.waitForRetry();
            }
        }
        
        return isCommited;
    };


    this.flush = function() {
        var isFlushed = false;

        while (!isFlushed) {
            try {
                this.openWriter(ForWriting);
                this._write('');
                this.closeWriter();
                isFlushed = true;
            } catch (e) {
                this.closeWriter();
                isFlushed = false;
                this.waitForRetry();
            }
        }

        return isFlushed;
    };

    // Fixed bug: broken GUI sample #86, Reported by @eogloblin, in 2023-10-26
    // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
    this._read = function(fh) {
        try {
            return (function(s) {
                return (s.length > 1 && s.charCodeAt(0) === 0xFEFF) ? s.substring(1) : s;
            })(fh.ReadAll());
        } catch (e) {
            //console.log(e.message);
            return '';
        }
    };

    this.read = function() {
        var isRead = false;
        var text = '';

        while (!isRead) {
            try {
                this.openReader();
                text += this._read(this.reader);
                isRead = true;
                this.lastReadTime = this.getCurrentTime();
                this.closeReader();
            } catch (e) {
                this.closeReader();
                this.waitForRetry();
            }
        }

        return text;
    };

    this.close = function() {
        this.closeWriter();
        this.closeReader();
        this.closeRecorder();
    };

    this.destroy = function() {
        this.close();
        deleteFile(this.path);
    };

    this.readTextFromFile = function(filename, charset) {
        charset = (typeof charset !== "undefined" ? charset : this.charset);

        var text = '';

        var isFileExists = checkFileExists(filename);
        if (isFileExists) {
            var isRead = false;
            while (!isRead) {
                try {
                    var ado = makeInterface(1);
                    ado.Charset = charset;
                    ado.Open();
                    ado.LoadFromFile(filename);
                    text += ado.ReadText();
                    ado.Close();
                    isRead = true;
                } catch (e) {
                    //console.log(e.message);
                    isRead = false;
                }
            }
        } else {
            console.warn("File", filename, "not exists");
        }

        return text;
    };

    this.loadFromFile = function(filename, charset) {
        charset = (typeof charset !== "undefined" ? charset : this.charset);

        this.write(this.readTextFromFile(filename, charset), ForWriting);
    };

    this.reload = function(charset) {
        charset = (typeof charset !== "undefined" ? charset : this.charset);

        this.loadFromFile(this.path, charset);
    };
}

exports.create = function() {
    return new PipeIPC();
};

exports.connect = function(path, callback) {
    var pipe = exports.create();
    return pipe.connect(path, callback);
};

exports.Converter = Converter;
exports.UUIDv4 = UUIDv4;
exports.CRC32 = CRC32;

exports.ForReading = ForReading;
exports.ForWriting = ForWriting;
exports.ForAppending = ForAppending;
exports.CdoCharset = CdoCharset;
exports.CdoUTF_8 = CdoCharset.CdoUTF_8;
exports.CdoUS_ASCII = CdoCharset.CdoUS_ASCII;
exports.CdoEUC_KR = CdoCharset.CdoEUC_KR;
exports.CdoEUC_JP = CdoCharset.CdoEUC_JP;
exports.adTypeBinary = adTypeBinary;
exports.adTypeText = adTypeText;
exports.adSaveCreateNotExist = adSaveCreateNotExist;
exports.adSaveCreateOverWrite = adSaveCreateOverWrite;
exports.adModeReadWrite = adModeReadWrite;

exports.VERSIONINFO = "PIPE-based IPC Module (pipe-ipc.js) version 0.1.23";
exports.AUTHOR = "abuse@catswords.net";
exports.global = global;
exports.require = require;