// define exceptions
function InterruptedException(m) {
    this.message = m;
    this.stack = (new Error()).stack;
};
InterruptedException.prototype = Object.create(Error.prototype);
InterruptedException.prototype.name = 'InterruptedException';
InterruptedException.prototype.constructor = InterruptedException;
function IllegalAccessException(m) {
    this.message = m;
    this.stack = (new Error()).stack;
};
IllegalAccessException.prototype = Object.create(Error.prototype);
IllegalAccessException.prototype.name = 'IllegalAccessException';
IllegalAccessException.prototype.constructor = IllegalAccessException;
class SIG {
    constructor(name, number) {
        this.name = "SIG" + name;
        this.number = number|0;
    }
}
const SIGTERM = new SIG("TERM",15);
const SIGSEGV = new SIG("SEGV",11);

function generateRandomHashStr(len) {
    let cs = 'qwfpgarstdzxcvbjluyhneiokmQWFPGARSTDZXCVBJLUYHNEIOKM';
    let s = '';
    for (let i = 0; i < len; i++) {
        s += cs[(Math.random()*cs.length)|0];
    }
    return s;
}

// define TVDOS
const _TVDOS = {};
_TVDOS.VERSION = "1.0";
_TVDOS.DRIVES = {}; // Object where key-value pair is <drive-letter> : [serial-port, drive-number]
// actually figure out the drive letter association
// Drive A is always the device we're currently on
_TVDOS.DRIVES["A"] = _BIOS.FIRST_BOOTABLE_PORT;
//TODO


_TVDOS.getPath = function() {
    return [''].concat(_TVDOS.variables.PATH.split(';'));
};
// initial values
_TVDOS.variables = {
    DOSDIR: "\\tvdos",
    LANG: "EN",
    KEYBOARD: "us_qwerty",
    PATH: "\\tvdos\\bin;\\tbas;\\home",
    PATHEXT: ".com;.bat;.js",
    HELPPATH: "\\tvdos\\help",
    OS_NAME: "Terrarum Virtual DOS",
    OS_VERSION: _TVDOS.VERSION
};
Object.freeze(_TVDOS);

///////////////////////////////////////////////////////////////////////////////

const filesystem = {};
filesystem._toPorts = (driveLetter) => {
    if (driveLetter.toUpperCase === undefined) {
        throw Error("'"+driveLetter+"' (type: "+typeof driveLetter+") is not a valid drive letter");
    }
    var port = _TVDOS.DRIVES[driveLetter.toUpperCase()];
    if (port === undefined) {
        throw Error("Drive letter '" + driveLetter.toUpperCase() + "' does not exist");
    }
    return port
};
filesystem._close = (portNo) => {
    com.sendMessage(portNo, "CLOSE");
};
filesystem._flush = (portNo) => {
    com.sendMessage(portNo, "FLUSH");
};
// @return true if operation committed successfully, false if:
//             - opening file with R-mode and target file does not exists
//         throws if:
//             - java.lang.NullPointerException if path is null
//             - Error if operation mode is not "R", "W" nor "A"
filesystem.open = (driveLetter, path, operationMode) => {
    var port = filesystem._toPorts(driveLetter);

    filesystem._flush(port[0]); filesystem._close(port[0]);

    var mode = operationMode.toUpperCase();
    if (mode != "R" && mode != "W" && mode != "A") {
        throw Error("Unknown file opening mode: " + mode);
    }

    com.sendMessage(port[0], "OPEN"+mode+'"'+path+'",'+port[1]);
    return com.getStatusCode(port[0]);
};
// @return the entire contents of the file in String
filesystem.readAll = (driveLetter) => {
    var port = filesystem._toPorts(driveLetter);
    com.sendMessage(port[0], "READ");
    var response = com.getStatusCode(port[0]);
    if (135 == response) {
        throw Error("File not opened");
    }
    if (response < 0 || response >= 128) {
        throw Error("Reading a file failed with "+response);
    }
    return com.pullMessage(port[0]);
};
filesystem.write = (driveLetter, string) => {
    var port = filesystem._toPorts(driveLetter);
    com.sendMessage(port[0], "WRITE"+string.length);
    var response = com.getStatusCode(port[0]);
    if (135 == response) {
        throw Error("File not opened");
    }
    if (response < 0 || response >= 128) {
        throw Error("Writing a file failed with "+response);
    }
    com.sendMessage(port[0], string);
    filesystem._flush(port[0]); filesystem._close(port[0]);
};
filesystem.isDirectory = (driveLetter) => {
    var port = filesystem._toPorts(driveLetter);
    com.sendMessage(port[0], "LISTFILES");
    var response = com.getStatusCode(port[0]);
    return (response === 0);
};
filesystem.mkDir = (driveLetter) => {
    var port = filesystem._toPorts(driveLetter);
    com.sendMessage(port[0], "MKDIR");
    var response = com.getStatusCode(port[0]);

    if (response < 0 || response >= 128) {
        var status = com.getDeviceStatus(port[0]);
        throw Error("Creating a directory failed with ("+response+"): "+status.message+"\n");
    }
    return (response === 0); // possible status codes: 0 (success), 1 (fail)
};
filesystem.touch = (driveLetter) => {
    var port = filesystem._toPorts(driveLetter);
    com.sendMessage(port[0], "TOUCH");
    var response = com.getStatusCode(port[0]);
    return (response === 0);
};
filesystem.mkFile = (driveLetter) => {
    var port = filesystem._toPorts(driveLetter);
    com.sendMessage(port[0], "MKFILE");
    var response = com.getStatusCode(port[0]);
    return (response === 0);
};
Object.freeze(filesystem);

///////////////////////////////////////////////////////////////////////////////

const input = {};
const inputwork = {};

inputwork.keymap = [];

input.changeKeyLayout = function(name) {
    let res0 = filesystem.open("A",`tvdos/${name.toLowerCase()}.key`,"R");
    if (res0 != 0 && inputwork.keymap.length == 0) throw new Error(`I/O Error ${res0} - A:\\tvdos\\${name.toLowerCase()}.key`);
    try {
        inputwork.keymap = eval(filesystem.readAll("A"));
    }
    catch (e) {
        printerrln(e);
        return -1;
    }
}

// load initial key layout
input.changeKeyLayout(_TVDOS.variables.KEYBOARD || "us_qwerty");

// states to run the keyboard input
inputwork.stroboTime = 4294967296*0;
inputwork.stroboDelays = [0,250000000,0,25000000,0]; // 250ms, 25ms
inputwork.stroboStatus = 0; // 0: first key, 1: waiting for initial delay, 2: repeating key, 3: waiting for repeat delay
inputwork.oldKeys = [];
inputwork.oldMouse = [];
inputwork.repeatCount = 0;

/**
 * callback: takes one argument of object which
 *  [ <eventname>, args ]
 * where:
 *  "key_down", <key symbol string>, <repeat count>, keycode0, keycode1 .. keycode7
 *  "key_change", <key symbol string (what went up)>, 0, keycode0, keycode1 .. keycode7 (remaining keys that are held down)
 *  "mouse_down", pos-x, pos-y, 1 // yes there's only one mouse button :p
 *  "mouse_up", pos-x, pos-y, 0
 *  "mouse_move", pos-x, pos-y, <button down?>, oldpos-x, oldpos-y
 */
input.withEvent = function(callback) {

    // TODO mouse event
    function arrayEq(a,b) {
        for (let i = 0; i < a.length; ++i) {
            if (a[i] !== b[i]) return false;
        }
        return true;
    }

    function arrayDiff(a,b) {
        return a.filter(x => !b.includes(x));
    }

    function keysToStr(keys) {
        let shiftin = keys.includes(59) || keys.includes(60);
        let headkey = keys.head();
        return (inputwork.keymap[headkey] == undefined) ? undefined : (shiftin) ? (inputwork.keymap[headkey][1] || inputwork.keymap[headkey][0]) : inputwork.keymap[headkey][0];
    }

    sys.poke(-40, 255);
    let keys = [sys.peek(-41),sys.peek(-42),sys.peek(-43),sys.peek(-44),sys.peek(-45),sys.peek(-46),sys.peek(-47),sys.peek(-48)];
    let mouse = [sys.peek(-33) | (sys.peek(-34) << 8), sys.peek(-35) | (sys.peek(-36) << 8), sys.peek(-37)];

    if (inputwork.stroboStatus % 2 == 0 && keys[0] != 0) {
        inputwork.stroboStatus += 1;
        inputwork.stroboTime = sys.nanoTime();
        inputwork.repeatCount += 1;

        callback(["key_down", keysToStr(keys), inputwork.repeatCount].concat(keys));
    }
    else if (!arrayEq(keys, inputwork.oldKeys) || keys[0] == 0) {
        inputwork.stroboStatus = 0;
        inputwork.repeatCount = 0;

        if (inputwork.oldKeys[0] != 0)
            callback(["key_change", keysToStr(arrayDiff(inputwork.oldKeys, keys)), inputwork.repeatCount].concat(keys));
    }
    else if (inputwork.stroboStatus % 2 == 1 && sys.nanoTime() - inputwork.stroboTime < inputwork.stroboDelays[inputwork.stroboStatus]) {
        sys.spin();
    }
    else {
        inputwork.stroboStatus += 1;
        if (inputwork.stroboStatus >= 4) {
            inputwork.stroboStatus = 2;
        }
    }

    inputwork.oldKeys = keys;
    inputwork.oldMouse = mouse;

};

Object.freeze(input);

///////////////////////////////////////////////////////////////////////////////

// install other stuffs
filesystem.open("A", "tvdos/gl.js", "R");
const GL = eval(filesystem.readAll("A"));

let checkTerm = `if (sys.peek(-49)&1) throw new InterruptedException();`;
let injectIntChk = (s, n) => {
    // primitive way of injecting a code; will replace a JS string that matches the regex...
    let k = s
        .replace(/while *\([^\n]+\) *{/, "$& "+n+"();")
        .replace(/for *\([^\n]+\) *{/, "$& "+n+"();")
        .replace(/do *{/, "$& "+n+"();");
    //serial.println(k);
    return k;
}

// @param cmdsrc JS source code
// @param args arguments for the program, must be Array, and args[0] is always the name of the program, e.g.
//        for command line 'echo foo bar', args[0] must be 'echo'
// @return status returned by the program
var execApp = (cmdsrc, args) => {
    var intchkFunName = `tvdosSIGTERM_${generateRandomHashStr(16)}`;
    var execAppPrg = eval(
`var ${intchkFunName} = function(){ ${checkTerm} };` +
`var _appStub=function(exec_args){${injectIntChk(cmdsrc, intchkFunName)}\n};` +
`_appStub`); // making 'exec_args' a app-level global

    return execAppPrg(args);
}

///////////////////////////////////////////////////////////////////////////////

// Boot script
serial.println("TVDOS.SYS initialised, running boot script...");
var _G = {};
filesystem.open("A", "tvdos/bin/command.js", "R");
execApp(filesystem.readAll("A"), ["", "/c", "\\AUTOEXEC.BAT"]);
