import { GROOVEBOX_BUILD } from "./groovebox_build.js";
import { filterFFINames } from "./mixcore-common.js";
import { nanoid } from "nanoid";

let workers = {};
let children = {};

function terminateAll() {
    for (let [vmID, worker] of Object.entries(workers)) {
        worker.terminate();
    };
    workers = {};
    children = {};
}

function terminateVM(vmID) {
    let worker = workers[vmID];
    if (worker) {
        let vmChildren = children[vmID];
        delete workers[vmID];
        delete children[vmID];
        worker.terminate();
        if (vmChildren) {
            for (let subID of vmChildren) {
                terminateVM(subID)
            }
        }
    }
}

if (globalThis.Process) {
    Process.on("exit", () => {
        console.log("terminating machine workers");
        terminateAll()
    })
};

let initialMask = 64 + 1024;   // default for web

globalThis.MixSetMask = (m => {
    initialMask = m;
    for (let [vmID, worker] of Object.entries(workers)) {
        worker.postMessage({ "_rmx_type": "msg_vm_logMask",
                             "mask": m
                           })
    }
});

function StartWASM(hub, baseURL, org, workspace, vmID, user, token, noOutputViaMQTT) {
    return StartWASM2(hub, {
        baseURL: baseURL,
        org: org,
        workspace: workspace,
        token: token,
        noOutputViaMQTT: noOutputViaMQTT,
        machType: "WASM"
    })
}

function mixcoreLocalFFIs(mixcore, names) {
    return Object.fromEntries(
        names.map(name => [name, {
            isRaw: true,
            run: (conn, argsBuf) => mixcore.ffiDispatch(conn.call_id, name, argsBuf),
        }]));
}

// See `mix-rs/mixcore/README.md` for a description of the values of
// config.mixcore.
async function StartWASM2(hub, config) {
    console.log("GROOVEBOX_BUILD (machine-starter)", GROOVEBOX_BUILD);
    let channel = await hub.newChannel();
    // injected by either index.js or node.js
    let worker = globalThis.GetMachineWASMWorker();
    workers[config.vmID] = worker;
    let mixcore = null;
    let localFFIs = config.localFFIs || {};
    if (config.mixcore && config.mixcore.kind == 'mixcore') {
        let enableFFIs, disableFFIs;
        ({mixcore, enableFFIs, disableFFIs} = config.mixcore);
        let ffiNames = filterFFINames(await mixcore.ffiNames(), enableFFIs, disableFFIs);
        console.log("Mixcore (starter)", mixcore, ffiNames, mixcore.registerDebugObject);
        localFFIs = {...localFFIs, ...mixcoreLocalFFIs(mixcore, ffiNames)};
        config.mixcore = {};
        if (mixcore.registerDebugObject) {
            config.mixcore.publishDebugObjects = true;
            subscribeDebugObjects(mixcore, hub, config);
        }
    }
    let localFFIkind = {};
    let localFFIfuns = {};
    for (const [k, v] of Object.entries(localFFIs)) {
        localFFIfuns[k] = {...v};
        if (v.isRaw && (v.canFail || v.useJsonDecoder)) {
            console.error("A raw, local FFI must neither be failing nor use the JSON decoder: " + k);
        } else {
            localFFIkind[k] = {...v};
            delete localFFIkind[k].run;
        }
    };
    let vm_kind = { isRaw:false, canFail:false, useJsonDecoder:false};
    localFFIkind["$vm_start"] = vm_kind;
    localFFIfuns["$vm_start"] = ffi_vm_start;
    localFFIkind["$vm_stop"] = vm_kind;
    localFFIfuns["$vm_stop"] = ffi_vm_stop;
    let config_msg =
        { "_rmx_type": "msg_vm_configure",
          "baseURL": config.baseURL,
          "org": config.org,
          "workspace": config.workspace,
          "vmID": config.vmID,
          "user": config.user,
          "token": config.token,
          "hub": channel.port,
          "outputViaMQTT": !(config.noOutputViaMQTT),
          "machType": config.machType,
          "debugMask": initialMask,
          "localFFIs": localFFIkind,
          "allowInsecureHttp": globalThis.GROOVEBOX_ALLOW_INSECURE_HTTP,
          "interceptFFI": config.interceptFFI,
          "interceptDynloadFFI": config.interceptDynloadFFI,
          "mixcore": config.mixcore,
        };
    worker.postMessage(config_msg, [ channel.port ]);
    setupLocalFFIs(hub, config, localFFIfuns);  // don't await!
    return worker;
}

async function ffi_vm_start(conn, args) {
    let vm_id = "embedded." + nanoid();
    let config = {};
    for (const [k, v] of Object.entries(conn.config)) {
        config[k] = v;
    }
    config["vmID"] = vm_id;
    let vmChildren = children[conn.config.vmID];
    if (!vmChildren) vmChildren = [];
    vmChildren.push(vm_id);
    let arg_config = args[0];
    if (arg_config.interceptFFI !== undefined)
        config["interceptFFI"] = arg_config.interceptFFI;
    if (arg_config.interceptDynloadFFI !== undefined)
        config["interceptDynloadFFI"] = arg_config.interceptDynloadFFI;
    let worker = await StartWASM2(conn.hub, config);
    return vm_id;
}

async function ffi_vm_stop(conn, args) {
    let vm_id = args[0];
    let worker = workers[vm_id];
    if (!worker) throw new Error("no such VM: " + vm_id);
    terminateVM(vm_id);
    return null;
}

class Token {
    constructor(data, enabled, extras) {
        // data: must be an Uint8Array
        // enabled: a number with the bitset of enabled operations
        // extras: a map
        if (!data instanceof Uint8Array)
            throw new Error("Token: data must be an Uint8Array");
        if (typeof(enabled) != "number")
            throw new Error("Token: enabled_ops must be a number");
        this.data = data;
        this.enabled = enabled;
        this.extras = extras;
    }
    encode() {
        return new Map([
            [ "_rmx_type", "token" ],
            [ "data", this.data ],
            [ "enabled", this.enabled ],
            [ "extras", this.extras ]
        ]);
    }
}

function decode_Token(m) {
    return new Token(m.get("data"), m.get("enabled"), m.get("extras"))
}

class Case {
    constructor(name, arg) {
        if (typeof(name) != "string")
            throw new Error("Case: name must be a string");
        this.name = name;
        this.arg = arg;
    }
    encode() {
        return new Map([
            [ "_rmx_type", "case" ],
            [ "name", this.name ],
            [ "arg", this.arg ],
        ]);
    }
}

function decode_Case(m) {
    return new Case(m.get("name"), m.get("arg"))
}

class Opaque {
    constructor(buf) {
        if (!(buf instanceof Uint32Array))
            throw new Error("Opaque: must be a Uint32Array");
        this.data = buf;
    }
    encode() {
        return new Map([
            [ "_rmx_type", "opaque" ],
            [ "data", this.data ],
        ]);
    }
}

function decode_Opaque(m) {
    return new Opaque(m.get("data"))
}

function encode(val) {
    switch (typeof(val)) {
    case "object":
        if (val === null) return val;
        if (Array.isArray(val)) {
            // array: map the array elements
            val = val.map(encode)
        } else if (Object.getPrototypeOf(val) === Object.prototype) {
            // simple object: map the object elements
            val = Object.fromEntries(Object.entries(val).map(([ key, elem ]) => [ key, encode(elem) ]))
        } else if (val.encode instanceof Function) {
            // map after encode(): map the map elements
            val = new Map(Array.from(val.encode().entries()).map(([ key, elem ]) => [ key, encode(elem) ]))
        };
        return val;
    default:
        return val;
    }
}

function decode(val) {
    switch (typeof(val)) {
    case "object":
        if (val === null) return val;
        if (Array.isArray(val)) {
            // array: map the array elements
            val = val.map(decode)
        } else if (Object.getPrototypeOf(val) === Object.prototype) {
            // simple object: map the object elements
            val = Object.fromEntries(Object.entries(val).map(([ key, elem ]) => [ key, decode(elem) ]))
        } else if (val instanceof Map) {
            val = new Map(Array.from(val.entries()).map(([ key, elem ]) => [ key, decode(elem) ]))
            let ty = val.get("_rmx_type");
            switch (ty) {
            case "token":
                return decode_Token(val);
            case "case":
                return decode_Case(val);
            case "opaque":
                return decode_Opaque(val);
            default:
                throw new Error("decode: bad _rmx_type: " + ty);
            }
        };
        return val;
    default:
        return val;
    }
}

async function setupLocalFFIs(hub, config, localFFIs) {
    let vmID = config.vmID;
    let channel = await hub.newChannel();
    let comm = new FFIComm(vmID, hub, channel);
    await channel.setLocalSubTopic("/local/" + vmID);
    await channel.setLocalPubTopic("/local/" + vmID);
    let sub = await channel.subscribe("/local/" + vmID + "/ffi/call");
    while (true) {
        let msg = await sub.next();
        let name = msg.payload.name;
        let call_id = msg.payload.call_id;
        let args = decode(msg.payload.args);
        let fun = localFFIs[name];
        try {
            if (!fun) throw new Error("no such local FFI: " + name);
            let connector =
                { call_id: call_id,
                  hub: hub,
                  channel: channel,
                  config: config,
                  state: config.state,
                };
            let r = fun instanceof Function ? fun(connector, args) : fun.run(connector, args);
            if (r instanceof Promise) {
                // r.catch(_ => null);  // prevent "unhandled rejection"
                await comm.later(call_id);
                let value = await r;
                await comm.returnOrFail(fun, call_id, value);
            } else {
                await comm.returnOrFail(fun, call_id, r);
            }
        } catch (reason) {
            await comm.error(call_id, reason.message, reason.stack);
        }
    }
}

async function subscribeDebugObjects(mixcore, hub, config) {
    let channel = await hub.newChannel();
    let vmID = config.vmID;
    await channel.setLocalSubTopic(`/debug-objects/${vmID}`);
    let sub = await channel.subscribe(`/debug-objects/${vmID}`);
    while (true) {
        let msg = await sub.next();
        let id = msg.payload.id;
        let code = msg.payload.code;
        mixcore.registerDebugObject(id, code);
    }
}

class FFIComm {
    constructor(vmID, hub, channel) {
        this.vmID = vmID;
        this.hub = hub;
        this.channel = channel;
    }
    later(call_id) {
        let r_payload =
            { call_id: call_id,
            };
        let response = this.hub.newLocalMessage("msg_ffi_later", "starter", r_payload);
        return this.channel.publish("/local/" + this.vmID + "/ffi/return", response, false);
    }
    return_(fun, call_id, result) {
        let r_payload =
            { call_id: call_id,
              value: fun.useJsonDecoder ? result : encode(result)
            };
        let response = this.hub.newLocalMessage("msg_ffi_return", "starter", r_payload);
        return this.channel.publish("/local/" + this.vmID + "/ffi/return", response, false);
    }
    error(call_id, message, stack) {
        let r_payload =
            { call_id: call_id,
              message: message,
              stack: stack === undefined ? [] : stack,
            };
        let response = this.hub.newLocalMessage("msg_ffi_error", "starter", r_payload);
        this.channel.publish("/local/" + this.vmID + "/ffi/return", response, false);
    }
    returnOrFail(fun, call_id, result) {
        if (fun.canFail) {
            var name, arg;
            if (result instanceof Case) {
                name = result.name;
                arg = result.arg;
            } else if (fun.useJsonDecoder && result._rmx_type && result._rmx_type.startsWith("{tag}case:")) {
                name = result._rmx_type.substring(10);
                arg = result._rmx_value;
            } else {
                return this.error(call_id, "expected result from local FFI that can fail");
            }
            switch (name) {
            case "ok":
                return this.return_(fun, call_id, arg);
            case "error":
                return this.error(call_id, arg);
            default:
                return this.error(call_id, "expected result from local FFI that can fail, found case value " + name);
            }
        } else {
            return this.return_(fun, call_id, result);
        }
    }
}

// use worker.terminate() to shut a worker down!

export { StartWASM, StartWASM2, terminateAll, terminateVM,
         Token, Case, Opaque
       }
