import { Worker } from "node:worker_threads";
import { CodeListingMap, WorkerError } from "../../common/workertypes";
import { BuildStep, BuildStepResult, gatherFiles, staleFiles, populateFiles, putWorkFile, populateExtraFiles, anyTargetChanged, getWorkFileAsString } from "../builder";
import { parseListing, parseSourceLines, msvcErrorMatcher } from "../listingutils";
import { EmscriptenModule, emglobal, execMain, loadNative, moduleInstFn, print_fn, setupFS, setupStdin } from "../wasmutils";
import { preprocessMCPP } from "./mcpp";

function hexToArray(s, ofs) {
    var buf = new ArrayBuffer(s.length / 2);
    var arr = new Uint8Array(buf);
    for (var i = 0; i < arr.length; i++) {
        arr[i] = parseInt(s.slice(i * 2 + ofs, i * 2 + ofs + 2), 16);
    }
    return arr;
}

function parseIHX(ihx: string, rom_start: number, rom_size: number, errors: WorkerError[]) {
    var output = new Uint8Array(new ArrayBuffer(rom_size));
    var high_size = 0;
    for (var s of ihx.split("\n")) {
        if (s[0] == ':') {
            var arr = hexToArray(s, 1);
            var count = arr[0];
            var offset = (arr[1] << 8) + arr[2] - rom_start;
            var rectype = arr[3];
            //console.log(rectype,address.toString(16),count,arr);
            if (rectype == 0) {
                if (output[offset] !== 0) {
                    errors.push({line:0,msg:`IHX overlap offset 0x${(offset).toString(16)}`});
                }
                for (var i = 0; i < count; i++) {
                    var b = arr[4 + i];
                    output[i + offset] = b;
                }
                if (i + offset > high_size) high_size = i + offset;
            } else if (rectype == 1) {
                break;
            } else {
                console.log(s); // unknown record type
            }
        }
    }
    // TODO: return ROM anyway?
    if (high_size > rom_size) {
        //errors.push({line:0, msg:"ROM size too large: 0x" + high_size.toString(16) + " > 0x" + rom_size.toString(16)});
    }
    return output;
}

function errorMatcherSDASZ80(path: string, errors: WorkerError[]) {
    //?ASxxxx-Error-<o> in line 1 of main.asm null
    //              <o> .org in REL area or directive / mnemonic error
    // ?ASxxxx-Error-<q> in line 1627 of cosmic.asm
    //    <q> missing or improper operators, terminators, or delimiters
    var match_asm_re1 = / in line (\d+) of (\S+)/; // TODO
    var match_asm_re2 = / <\w> (.+)/; // TODO
    var errline = 0;
    var errpath = path;
    var match_asm_fn = (s: string) => {
        var m = match_asm_re1.exec(s);
        if (m) {
            errline = parseInt(m[1]);
            errpath = m[2];
        } else {
            m = match_asm_re2.exec(s);
            if (m) {
                errors.push({
                    line: errline,
                    path: errpath,
                    msg: m[1]
                });
            }
        }
    }
    return match_asm_fn;
}

export function assembleSDASZ80(step: BuildStep): BuildStepResult {
    loadNative('sdasz80');
    var objout, lstout, symout;
    var errors = [];
    gatherFiles(step, { mainFilePath: "main.asm" });
    var objpath = step.prefix + ".rel";
    var lstpath = step.prefix + ".lst";
    if (staleFiles(step, [objpath, lstpath])) {
        const match_asm_fn = errorMatcherSDASZ80(step.path, errors);
        var ASZ80: EmscriptenModule = emglobal.sdasz80({
            instantiateWasm: moduleInstFn('sdasz80'),
            noInitialRun: true,
            //logReadFiles:true,
            print: match_asm_fn,
            printErr: match_asm_fn,
        });
        var FS = ASZ80.FS;
        populateFiles(step, FS);
        execMain(step, ASZ80, ['-plosgffwy', step.path]);
        if (errors.length) {
            return { errors: errors };
        }
        objout = FS.readFile(objpath, { encoding: 'utf8' });
        lstout = FS.readFile(lstpath, { encoding: 'utf8' });
        putWorkFile(objpath, objout);
        putWorkFile(lstpath, lstout);
    }
    return {
        linktool: "sdldz80",
        files: [objpath, lstpath],
        args: [objpath]
    };
    //symout = FS.readFile("main.sym", {encoding:'utf8'});
}

export async function assembleSDASGB(step: BuildStep): Promise<BuildStepResult> {
    loadNative('sdasgb');
    var objout, lstout, symout;
    var errors = [];
    gatherFiles(step, { mainFilePath: "main.asm" });
    var objpath = step.prefix + ".rel";
    var lstpath = step.prefix + ".lst";
    if (staleFiles(step, [objpath, lstpath])) {
        const match_asm_fn = errorMatcherSDASZ80(step.path, errors);
        var ASZ80: EmscriptenModule = await emglobal.sdasgb({
            instantiateWasm: moduleInstFn('sdasgb'),
            noInitialRun: true,
            //logReadFiles:true,
            print: match_asm_fn,
            printErr: match_asm_fn,
        });
        var FS = ASZ80.FS;
        populateFiles(step, FS);
        execMain(step, ASZ80, ['-plosgffwy', step.path]);
        if (errors.length) {
            return { errors: errors };
        }
        objout = FS.readFile(objpath, { encoding: 'utf8' });
        lstout = FS.readFile(lstpath, { encoding: 'utf8' });
        putWorkFile(objpath, objout);
        putWorkFile(lstpath, lstout);
    }
    return {
        linktool: "sdldz80",
        files: [objpath, lstpath],
        args: [objpath]
    };
    //symout = FS.readFile("main.sym", {encoding:'utf8'});
}

export function linkSDLDZ80(step: BuildStep) {
    loadNative("sdldz80");
    const arch = step.params.arch || 'z80';
    var errors = [];
    gatherFiles(step);
    var binpath = "main.ihx";
    if (staleFiles(step, [binpath])) {
        //?ASlink-Warning-Undefined Global '__divsint' referenced by module 'main'
        var match_aslink_re = /\?ASlink-(\w+)-(.+)/;
        var match_aslink_fn = (s: string) => {
            var matches = match_aslink_re.exec(s);
            if (matches) {
                errors.push({
                    line: 0,
                    msg: matches[2]
                });
            }
        }
        var params = step.params;
        var LDZ80: EmscriptenModule = emglobal.sdldz80({
            instantiateWasm: moduleInstFn('sdldz80'),
            noInitialRun: true,
            //logReadFiles:true,
            print: match_aslink_fn,
            printErr: match_aslink_fn,
        });
        var FS = LDZ80.FS;
        setupFS(FS, 'sdcc');
        populateFiles(step, FS);
        populateExtraFiles(step, FS, params.extra_link_files);
        // TODO: coleco hack so that -u flag works
        if (step.platform.startsWith("coleco")) {
            FS.writeFile('crt0.rel', FS.readFile('/share/lib/coleco/crt0.rel', { encoding: 'utf8' }));
            FS.writeFile('crt0.lst', '\n'); // TODO: needed so -u flag works
        }
        var args = ['-mjwxyu',
            '-i', 'main.ihx',
            '-b', '_CODE=0x' + (params.codeseg_start||params.code_start).toString(16),
            '-b', '_DATA=0x' + params.data_start.toString(16),
            '-k', `/share/lib/z80`, // TODO: $arch for gbz80
            '-l', 'z80'];
        if (params.extra_link_args)
            args.push.apply(args, params.extra_link_args);
        args.push.apply(args, step.args);
        //console.log(args);
        execMain(step, LDZ80, args);
        if (errors.length) {
            return { errors: errors };
        }
        var hexout = FS.readFile("main.ihx", { encoding: 'utf8' });
        var noiout = FS.readFile("main.noi", { encoding: 'utf8' });
        putWorkFile("main.ihx", hexout);
        putWorkFile("main.noi", noiout);
        // return unchanged if no files changed
        if (!anyTargetChanged(step, ["main.ihx", "main.noi"]))
            return;
        // parse binary file
        var binout = parseIHX(hexout, params.rom_start !== undefined ? params.rom_start : params.code_start, params.rom_size, errors);
        if (errors.length) {
            return { errors: errors };
        }
        // parse listings
        var listings: CodeListingMap = {};
        for (var fn of step.files) {
            if (fn.endsWith('.lst')) {
                var rstout = FS.readFile(fn.replace('.lst', '.rst'), { encoding: 'utf8' });
                //   0000 21 02 00      [10]   52 	ld	hl, #2
                var asmlines = parseListing(rstout, /^\s*([0-9A-F]{4,6})\s+([0-9A-F][0-9A-F r]*[0-9A-F])\s+\[([0-9 ]+)\]?\s+(\d+) (.*)/i, 4, 1, 2, 3);
                var srclines = parseSourceLines(rstout, /^\s+\d+ ;<stdin>:(\d+):/i, /^\s*([0-9A-F]{4,6})/i);
                putWorkFile(fn, rstout);
                // TODO: you have to get rid of all source lines to get asm listing
                listings[fn] = {
                    asmlines: srclines.length ? asmlines : null,
                    lines: srclines.length ? srclines : asmlines,
                    text: rstout
                };
            }
        }
        // parse symbol map
        var symbolmap = {};
        for (var s of noiout.split("\n")) {
            var toks = s.split(" ");
            if (toks[0] == 'DEF' && !toks[1].startsWith("A$")) {
                symbolmap[toks[1]] = parseInt(toks[2], 16);
            }
        }
        // build segment map
        var seg_re = /^s__(\w+)$/;
        var segments = [];
        // TODO: use stack params for stack segment
        for (let ident in symbolmap) {
            let m = seg_re.exec(ident);
            if (m) {
                let seg = m[1];
                let segstart = symbolmap[ident]; // s__SEG
                let segsize = symbolmap['l__' + seg]; // l__SEG
                if (segstart >= 0 && segsize > 0) {
                    var type = null;
                    if (['INITIALIZER', 'GSINIT', 'GSFINAL'].includes(seg)) type = 'rom';
                    else if (seg.startsWith('CODE')) type = 'rom';
                    else if (['DATA', 'INITIALIZED'].includes(seg)) type = 'ram';
                    if (type == 'rom' || segstart > 0) // ignore HEADER0, CABS0, etc (TODO?)
                        segments.push({ name: seg, start: segstart, size: segsize, type: type });
                }
            }
        }
        // gameboy: compute checksum
        if (step.params.arch === 'gbz80') {
            var checksum = 0;
            for (var address = 0x0134; address <= 0x014C; address++) {
                checksum = checksum - binout[address] - 1;
            }
            binout[0x14D] = checksum & 0xff;
        }
        return {
            output: binout,
            listings: listings,
            errors: errors,
            symbolmap: symbolmap,
            segments: segments
        };
    }
}

export function compileSDCC(step: BuildStep): BuildStepResult {

    gatherFiles(step, {
        mainFilePath: "main.c" // not used
    });
    var params = step.params;
    var isGBZ80 = step.params.arch === 'gbz80';
    var outpath = step.prefix + ".asm";
    if (staleFiles(step, [outpath])) {
        var errors = [];
        loadNative('sdcc');
        var SDCC: EmscriptenModule = emglobal.sdcc({
            instantiateWasm: moduleInstFn('sdcc'),
            noInitialRun: true,
            noFSInit: true,
            print: print_fn,
            printErr: msvcErrorMatcher(errors),
            //TOTAL_MEMORY:256*1024*1024,
        });
        var FS = SDCC.FS;
        populateFiles(step, FS);
        // load source file and preprocess
        var code = getWorkFileAsString(step.path);
        var preproc = preprocessMCPP(step, 'sdcc');
        if (preproc.errors) {
            return { errors: preproc.errors };
        }
        else code = preproc.code;
        // pipe file to stdin
        setupStdin(FS, code);
        setupFS(FS, 'sdcc');
        const machineFlags = isGBZ80 ? '-mgbz80' : '-mz80';
        var args = ['--vc', '--std-sdcc99', machineFlags, //'-Wall',
            '--c1mode',
            //'--debug',
            //'-S', 'main.c',
            //'--asm=sdasz80',
            //'--reserve-regs-iy',
            '--less-pedantic',
            ///'--fomit-frame-pointer',
            //'--opt-code-speed',
            //'--max-allocs-per-node', '1000',
            //'--cyclomatic',
            //'--nooverlay',
            //'--nogcse',
            //'--nolabelopt',
            //'--noinvariant',
            //'--noinduction',
            //'--nojtbound',
            //'--noloopreverse',
            '-o', outpath];
        // if "#pragma opt_code" found do not disable optimziations
        if (!isGBZ80 && !/^\s*#pragma\s+opt_code/m.exec(code)) {
            args.push.apply(args, [
                '--oldralloc',
                '--no-peep',
                '--nolospre'
            ]);
        }
        if (params.extra_compile_args) {
            args.push.apply(args, params.extra_compile_args);
        }
        execMain(step, SDCC, args);
        // TODO: preprocessor errors w/ correct file
        if (errors.length /* && nwarnings < msvc_errors.length*/) {
            return { errors: errors };
        }
        // massage the asm output
        var asmout = FS.readFile(outpath, { encoding: 'utf8' });
        asmout = " .area _HOME\n .area _CODE\n .area _INITIALIZER\n .area _DATA\n .area _INITIALIZED\n .area _BSEG\n .area _BSS\n .area _HEAP\n" + asmout;
        putWorkFile(outpath, asmout);
    }
    return {
        nexttool: isGBZ80 ? 'sdasgb' : 'sdasz80',
        path: outpath,
        args: [outpath],
        files: [outpath],
    };
}
