import { writeFileSync } from "node:fs";
import * as t from "@babel/types";
import type * as $ from "@/asm/generator/instructions";
import type { Opcode } from "@/asm/generator/instructions";
import {
    instructionList,
    pseudoInstructions,
} from "@/asm/generator/instructions";
import generateTs from "@babel/generator";

const RUNTIME_QUALIFIER = t.identifier("c");
const PRINTER_QUALIFIER = t.identifier("$");

const main = () => {
    writeFileSync(`${__dirname}/../text/printer-gen.ts`, generate());
};

const generate = (): string => {
    const importUtil = t.importDeclaration(
        [t.importNamespaceSpecifier(PRINTER_QUALIFIER)],
        t.stringLiteral("./printer"),
    );

    const importConstructors = t.importDeclaration(
        [t.importNamespaceSpecifier(RUNTIME_QUALIFIER)],
        t.stringLiteral("../runtime"),
    );

    const convertorFunc = generatePrintFunction(instructionList());

    const file = t.file(
        t.program([importUtil, importConstructors, convertorFunc]),
    );
    t.addComment(
        file,
        "leading",
        ` AUTOGENERATED, DO NOT EDIT, generated by ../generator/gen-printer.ts`,
        true,
    );
    return generateTs(file).code;
};

function generatePrintFunction(
    instructions: [string, Opcode][],
): t.ExportNamedDeclaration {
    const nameIdent = t.identifier("printInstruction");

    const printerParam = t.identifier("p");
    printerParam.typeAnnotation = t.tsTypeAnnotation(
        t.tsTypeReference(
            t.tsQualifiedName(PRINTER_QUALIFIER, t.identifier("Printer")),
        ),
    );

    const instrParam = t.identifier("instr");
    instrParam.typeAnnotation = t.tsTypeAnnotation(
        t.tsTypeReference(
            t.tsQualifiedName(RUNTIME_QUALIFIER, t.identifier("Instr")),
        ),
    );

    const params = [printerParam, instrParam];

    const cases = instructions.flatMap(([name, opcode]) => {
        if (pseudoInstructions.has(name)) {
            return [];
        }

        const args = opcode.args;
        const countArgs = argsLen(args);

        const statements: t.Statement[] = [];

        if (countArgs !== 0) {
            statements.push(writeAppend(" "));
        }

        statements.push(...generateArgs(args), t.returnStatement());

        return [t.switchCase(t.stringLiteral(name), statements)];
    });

    const body = t.blockStatement([
        t.expressionStatement(
            t.callExpression(
                t.memberExpression(
                    t.identifier("p"),
                    t.identifier("beginLine"),
                ),
                [t.memberExpression(t.identifier("instr"), t.identifier("$"))],
            ),
        ),

        t.switchStatement(
            t.memberExpression(t.identifier("instr"), t.identifier("$")),
            [
                ...cases,
                t.switchCase(undefined, []), // default case
            ],
        ),
    ]);

    return t.exportNamedDeclaration(
        t.variableDeclaration("const", [
            t.variableDeclarator(
                nameIdent,
                t.arrowFunctionExpression(params, body),
            ),
        ]),
    );
}

const argsLen = (args: $.args): number => {
    switch (args.$) {
        case "simpleArgs":
            return args.children.length;
        case "xchgArgs":
            return 2;
        case "dictpush":
            return 2;
    }

    throw new Error("Unexpected arg type");
};

const generateSimpleArgs = (args: $.arg[]): t.Statement[] => {
    return args.flatMap((arg, index) => {
        const stmt = generateArg(`arg${index}`, arg);
        if (index !== args.length - 1) {
            return [...stmt, writeAppend(" ")];
        }
        return stmt;
    });
};

function writeToString(name: string) {
    const argMember = t.memberExpression(
        t.identifier("instr"),
        t.identifier(name),
    );
    const toStringCall = t.callExpression(
        t.memberExpression(argMember, t.identifier("toString")),
        [],
    );
    return t.expressionStatement(
        t.callExpression(
            t.memberExpression(t.identifier("p"), t.identifier("append")),
            [toStringCall],
        ),
    );
}

const generateArg = (name: string, arg: $.arg): t.Statement[] => {
    switch (arg.$) {
        case "int":
        case "uint":
        case "refs":
        case "plduzArg":
        case "tinyInt":
        case "largeInt":
        case "runvmArg":
        case "minusOne":
        case "setcpArg":
        case "hash":
            return [writeToString(name)];

        case "delta":
            return generateArg(name, arg.arg);

        case "stack":
            return [writeAppend("s"), writeToString(name)];
        case "control":
            return [writeAppend("c"), writeToString(name)];
        case "s1":
            return [writeAppend("s1")];

        case "codeSlice":
        case "refCodeSlice":
        case "inlineCodeSlice":
            return [
                t.expressionStatement(
                    t.callExpression(
                        t.memberExpression(
                            PRINTER_QUALIFIER,
                            t.identifier("printCode"),
                        ),
                        [
                            t.identifier("p"),
                            t.memberExpression(
                                t.identifier("instr"),
                                t.identifier(name),
                            ),
                        ],
                    ),
                ),
            ];
        case "exoticCell":
        case "debugstr":
        case "slice":
            return [
                t.expressionStatement(
                    t.callExpression(
                        t.memberExpression(
                            PRINTER_QUALIFIER,
                            t.identifier("printSlice"),
                        ),
                        [
                            t.identifier("p"),
                            t.memberExpression(
                                t.identifier("instr"),
                                t.identifier(name),
                            ),
                        ],
                    ),
                ),
            ];
    }

    throw new Error("Unexpected arg type");
};

const generateDictpush = (): t.Statement[] => {
    return [
        writeToString("arg0"),
        writeAppend(" "),
        t.expressionStatement(
            t.callExpression(
                t.memberExpression(
                    PRINTER_QUALIFIER,
                    t.identifier("printDictionary"),
                ),
                [
                    t.identifier("p"),
                    t.memberExpression(
                        t.identifier("instr"),
                        t.identifier("arg1"),
                    ),
                ],
            ),
        ),
    ];
};

const generateXchgArgs = (): t.Statement[] => {
    return [
        writeAppend("s"),
        writeToString("arg0"),
        writeAppend(" "),
        writeAppend("s"),
        writeToString("arg1"),
    ];
};

const generateArgs = (args: $.args): t.Statement[] => {
    switch (args.$) {
        case "simpleArgs":
            return generateSimpleArgs(args.children);
        case "xchgArgs":
            return generateXchgArgs();
        case "dictpush":
            return generateDictpush();
    }

    throw new Error("Unexpected arg type");
};

function writeAppend(val: string) {
    return t.expressionStatement(
        t.callExpression(
            t.memberExpression(t.identifier("p"), t.identifier("append")),
            [t.stringLiteral(val)],
        ),
    );
}

main();
