
// TODO: must be a better way to do all this

import { WorkerError, CodeListingMap, SourceLocation } from "../../common/workertypes";
import { Assembler } from "../assembler";
import * as vxmlparser from '../../common/hdl/vxmlparser';
import { EmscriptenModule, emglobal, execMain, getWASMMemory, loadNative, moduleInstFn, print_fn, setupFS } from "../wasmutils";
import { getWorkFileAsString, BuildStep, BuildStepResult, gatherFiles, staleFiles, populateFiles, starttime, endtime, putWorkFile, anyTargetChanged, populateExtraFiles } from "../builder";
import { makeErrorMatcher } from "../listingutils";

function detectModuleName(code: string) {
    var m = /^\s*module\s+(\w+_top)\b/m.exec(code)
        || /^\s*module\s+(top|t)\b/m.exec(code)
        || /^\s*module\s+(\w+)\b/m.exec(code);
    return m ? m[1] : null;
}

function detectTopModuleName(code: string) {
    var topmod = detectModuleName(code) || "top";
    var m = /^\s*module\s+(\w+?_top)/m.exec(code);
    if (m && m[1]) topmod = m[1];
    return topmod;
}

// cached stuff (TODO)
var jsasm_module_top;
var jsasm_module_output;
var jsasm_module_key;

function compileJSASM(asmcode: string, platform, options, is_inline) {
    var asm = new Assembler(null);
    var includes = [];
    asm.loadJSON = (filename: string) => {
        var jsontext = getWorkFileAsString(filename);
        if (!jsontext) throw Error("could not load " + filename);
        return JSON.parse(jsontext);
    };
    asm.loadInclude = (filename) => {
        if (!filename.startsWith('"') || !filename.endsWith('"'))
            return 'Expected filename in "double quotes"';
        filename = filename.substr(1, filename.length - 2);
        includes.push(filename);
    };
    var loaded_module = false;
    asm.loadModule = (top_module: string) => {
        // compile last file in list
        loaded_module = true;
        var key = top_module + '/' + includes;
        if (jsasm_module_key != key) {
            jsasm_module_key = key;
            jsasm_module_output = null;
        }
        jsasm_module_top = top_module;
        var main_filename = includes[includes.length - 1];
        // TODO: take out .asm dependency
        var voutput = compileVerilator({ platform: platform, files: includes, path: main_filename, tool: 'verilator' });
        if (voutput)
            jsasm_module_output = voutput;
        return null; // no error
    }
    var result = asm.assembleFile(asmcode);
    if (loaded_module && jsasm_module_output) {
        // errors? return them
        if (jsasm_module_output.errors && jsasm_module_output.errors.length)
            return jsasm_module_output;
        // return program ROM array
        var asmout = result.output;
        // TODO: unify
        result.output = jsasm_module_output.output;
        // TODO: typecheck this garbage
        (result as any).output.program_rom = asmout;
        // TODO: not cpu_platform__DOT__program_rom anymore, make const
        (result as any).output.program_rom_variable = jsasm_module_top + "$program_rom";
        (result as any).listings = {};
        (result as any).listings[options.path] = { lines: result.lines };
        return result;
    } else {
        return result;
    }
}

export function compileJSASMStep(step: BuildStep): BuildStepResult {
    gatherFiles(step);
    var code = getWorkFileAsString(step.path);
    var platform = step.platform || 'verilog';
    return compileJSASM(code, platform, step, false);
}

function compileInlineASM(code: string, platform, options, errors, asmlines) {
    code = code.replace(/__asm\b([\s\S]+?)\b__endasm\b/g, function (s, asmcode, index) {
        var firstline = code.substr(0, index).match(/\n/g).length;
        var asmout = compileJSASM(asmcode, platform, options, true);
        if (asmout.errors && asmout.errors.length) {
            for (var i = 0; i < asmout.errors.length; i++) {
                asmout.errors[i].line += firstline;
                errors.push(asmout.errors[i]);
            }
            return '`error "inline assembly failed"';
        } else if (asmout.output) {
            let s = "";
            var out = asmout.output;
            for (var i = 0; i < out.length; i++) {
                if (i > 0) {
                    s += ",";
                    if ((i & 0xff) == 0) s += "\n";
                }
                s += 0 | out[i];
            }
            if (asmlines) {
                var al = asmout.lines;
                for (var i = 0; i < al.length; i++) {
                    al[i].line += firstline;
                    asmlines.push(al[i]);
                }
            }
            return s;
        }
    });
    return code;
}

export function compileVerilator(step: BuildStep): BuildStepResult {
    loadNative("verilator_bin");
    var platform = step.platform || 'verilog';
    var errors: WorkerError[] = [];
    gatherFiles(step);
    // compile verilog if files are stale
    if (staleFiles(step, [xmlPath])) {
        // TODO: %Error: Specified --top-module 'ALU' isn't at the top level, it's under another cell 'cpu'
        // TODO: ... Use "/* verilator lint_off BLKSEQ */" and lint_on around source to disable this message.
        var match_fn = makeErrorMatcher(errors, /%(.+?): (.+?):(\d+)?[:]?\s*(.+)/i, 3, 4, step.path, 2);
        var verilator_mod: EmscriptenModule = emglobal.verilator_bin({
            instantiateWasm: moduleInstFn('verilator_bin'),
            noInitialRun: true,
            noExitRuntime: true,
            print: print_fn,
            printErr: match_fn,
            wasmMemory: getWASMMemory(), // reuse memory
            //INITIAL_MEMORY:256*1024*1024,
        });
        var code = getWorkFileAsString(step.path);
        var topmod = detectTopModuleName(code);
        var FS = verilator_mod.FS;
        var listings: CodeListingMap = {};
        // process inline assembly, add listings where found
        populateFiles(step, FS, {
            mainFilePath: step.path,
            processFn: (path, code) => {
                if (typeof code === 'string') {
                    let asmlines = [];
                    code = compileInlineASM(code, platform, step, errors, asmlines);
                    if (asmlines.length) {
                        listings[path] = { lines: asmlines };
                    }
                }
                return code;
            }
        });
        starttime();
        var xmlPath = `obj_dir/V${topmod}.xml`;
        try {
            var args = ["--cc", "-O3",
                "-DEXT_INLINE_ASM", "-DTOPMOD__" + topmod, "-D__8BITWORKSHOP__",
                "-Wall",
                "-Wno-DECLFILENAME", "-Wno-UNUSED", "-Wno-EOFNEWLINE", "-Wno-PROCASSWIRE",
                "--x-assign", "fast", "--noassert", "--pins-sc-biguint",
                "--debug-check", // for XML output
                "--top-module", topmod, step.path]
            execMain(step, verilator_mod, args);
        } catch (e) {
            console.log(e);
            errors.push({ line: 0, msg: "Compiler internal error: " + e });
        }
        endtime("compile");
        // remove boring errors
        errors = errors.filter(function (e) { return !/Exiting due to \d+/.exec(e.msg); }, errors);
        errors = errors.filter(function (e) { return !/Use ["][/][*]/.exec(e.msg); }, errors);
        if (errors.length) {
            return { errors: errors };
        }
        starttime();
        var xmlParser = new vxmlparser.VerilogXMLParser();
        try {
            var xmlContent = FS.readFile(xmlPath, { encoding: 'utf8' });
            var xmlScrubbed = xmlContent.replace(/ fl=".+?" loc=".+?"/g, '');
            // TODO: this squelches the .asm listing
            //listings[step.prefix + '.xml'] = {lines:[],text:xmlContent};
            putWorkFile(xmlPath, xmlScrubbed); // don't detect changes in source position
            if (!anyTargetChanged(step, [xmlPath]))
                return;
            xmlParser.parse(xmlContent);
        } catch (e) {
            console.log(e, e.stack);
            if (e.$loc != null) {
                let $loc = e.$loc as SourceLocation;
                errors.push({ msg: "" + e, path: $loc.path, line: $loc.line });
            } else {
                errors.push({ line: 0, msg: "" + e });
            }
            return { errors: errors, listings: listings };
        } finally {
            endtime("parse");
        }
        return {
            output: xmlParser,
            errors: errors,
            listings: listings,
        };
    }
}

// TODO: test
export function compileYosys(step: BuildStep): BuildStepResult {
    loadNative("yosys");
    var code = step.code;
    var errors = [];
    var match_fn = makeErrorMatcher(errors, /ERROR: (.+?) in line (.+?[.]v):(\d+)[: ]+(.+)/i, 3, 4, step.path);
    starttime();
    var yosys_mod: EmscriptenModule = emglobal.yosys({
        instantiateWasm: moduleInstFn('yosys'),
        noInitialRun: true,
        print: print_fn,
        printErr: match_fn,
    });
    endtime("create module");
    var topmod = detectTopModuleName(code);
    var FS = yosys_mod.FS;
    FS.writeFile(topmod + ".v", code);
    starttime();
    try {
        execMain(step, yosys_mod, ["-q", "-o", topmod + ".json", "-S", topmod + ".v"]);
    } catch (e) {
        console.log(e);
        endtime("compile");
        return { errors: errors };
    }
    endtime("compile");
    //TODO: filename in errors
    if (errors.length) return { errors: errors };
    try {
        var json_file = FS.readFile(topmod + ".json", { encoding: 'utf8' });
        var json = JSON.parse(json_file);
        console.log(json);
        return { output: json, errors: errors }; // TODO
    } catch (e) {
        console.log(e);
        return { errors: errors };
    }
}

export function compileSilice(step: BuildStep): BuildStepResult {
    loadNative("silice");
    var params = step.params;
    gatherFiles(step, { mainFilePath: "main.ice" });
    var destpath = step.prefix + '.v';
    var errors: WorkerError[] = [];
    var errfile: string;
    var errline: number;
    if (staleFiles(step, [destpath])) {
        //[preprocessor] 97]  attempt to concatenate a nil value (global 'addrW')
        var match_fn = (s: string) => {
            s = (s as any).replaceAll(/\x1b\[\d+\w/g, '');
            var mf = /file:\s*(\w+)/.exec(s);
            var ml = /line:\s+(\d+)/.exec(s);
            var preproc = /\[preprocessor\] (\d+)\] (.+)/.exec(s);
            if (mf) errfile = mf[1];
            else if (ml) errline = parseInt(ml[1]);
            else if (preproc) {
                errors.push({ path: step.path, line: parseInt(preproc[1]), msg: preproc[2] });
            }
            else if (errfile && errline && s.length > 1) {
                if (s.length > 2) {
                    errors.push({ path: errfile + ".ice", line: errline, msg: s });
                } else {
                    errfile = null;
                    errline = null;
                }
            }
            else console.log(s);
        }
        var silice: EmscriptenModule = emglobal.silice({
            instantiateWasm: moduleInstFn('silice'),
            noInitialRun: true,
            print: match_fn,
            printErr: match_fn,
        });
        var FS = silice.FS;
        setupFS(FS, 'Silice');
        populateFiles(step, FS);
        populateExtraFiles(step, FS, params.extra_compile_files);
        const FWDIR = '/share/frameworks';
        var args = [
            '-D', 'NTSC=1',
            '--frameworks_dir', FWDIR,
            '-f', `/8bitworkshop.v`,
            '-o', destpath,
            step.path];
        execMain(step, silice, args);
        if (errors.length)
            return { errors: errors };
        var vout = FS.readFile(destpath, { encoding: 'utf8' });
        putWorkFile(destpath, vout);
    }
    return {
        nexttool: "verilator",
        path: destpath,
        args: [destpath],
        files: [destpath],
    };
}

