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

type CompiledArgs = [string, t.TSType][];

const TON_CORE_QUALIFIER = t.identifier("G");
const UTIL_QUALIFIER = t.identifier("$");

const main = () => {
    writeFileSync(`${__dirname}/../runtime/constructors.ts`, generate());
};

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

    const importTonCore = t.importDeclaration(
        [t.importNamespaceSpecifier(TON_CORE_QUALIFIER)],
        t.stringLiteral("@ton/core"),
    );

    const stmts = instructionList().flatMap(([name, instruction]) => {
        return generateOpcode(name, instruction);
    });

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

const generateOpcode = (name: string, instruction: $.Opcode): t.Statement[] => {
    const args = generateArgs(instruction.args);

    const locArg: [string, t.TSType] = [
        "loc",
        t.tsTypeReference(
            t.tsQualifiedName(UTIL_QUALIFIER, t.identifier("Loc")),
        ),
    ];
    const argsWithLoc = [...args, locArg];

    const params = argsWithLoc.map(([name, type]) => {
        const nameIdent = t.identifier(name);
        if (name === "loc") {
            nameIdent.optional = true;
        }
        nameIdent.typeAnnotation = t.tsTypeAnnotation(type);
        return nameIdent;
    });

    const fields = [
        t.objectProperty(t.identifier("$"), t.stringLiteral(name)),
        ...argsWithLoc.map(([name]) => {
            return t.objectProperty(
                t.identifier(name),
                t.identifier(name),
                false,
                true,
            );
        }),
    ];

    const lambda = t.arrowFunctionExpression(
        params,
        t.objectExpression(fields),
    );
    lambda.returnType = t.tsTypeAnnotation(
        t.tsTypeReference(t.identifier(name)),
    );

    const constructor = t.exportNamedDeclaration(
        t.variableDeclaration("const", [
            t.variableDeclarator(t.identifier(name), lambda),
        ]),
    );

    const typeAlias = t.exportNamedDeclaration(
        t.tsTypeAliasDeclaration(
            t.identifier(name),
            undefined,
            t.tsTypeLiteral([
                t.tsPropertySignature(
                    t.identifier("$"),
                    t.tsTypeAnnotation(t.tsLiteralType(t.stringLiteral(name))),
                ),
                ...args.map(([name, type]) => {
                    return t.tsPropertySignature(
                        t.identifier(name),
                        t.tsTypeAnnotation(type),
                    );
                }),
                t.tsPropertySignature(
                    t.identifier("loc"),
                    t.tsTypeAnnotation(
                        t.tsUnionType([
                            t.tsTypeReference(
                                t.tsQualifiedName(
                                    UTIL_QUALIFIER,
                                    t.identifier("Loc"),
                                ),
                            ),
                            t.tsUndefinedKeyword(),
                        ]),
                    ),
                ),
            ]),
        ),
    );

    return [constructor, typeAlias];
};

const generateArgs = (args: $.args): CompiledArgs => {
    switch (args.$) {
        case "simpleArgs":
            return generateSimpleArgs(args);
        case "xchgArgs":
            return generateXchgArgs(args);
        case "dictpush":
            return generateDictpush(args);
    }

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

const generateXchgArgs = (_args: $.xchgArgs): CompiledArgs => {
    return [
        ["arg0", t.tsNumberKeyword()],
        ["arg1", t.tsNumberKeyword()],
    ];
};

const generateDictpush = (_args: $.dictpush): CompiledArgs => {
    return [
        // TODO: rename parameters
        ["arg0", t.tsNumberKeyword()],
        [
            "arg1",
            t.tsTypeReference(
                t.tsQualifiedName(UTIL_QUALIFIER, t.identifier("Dict")),
            ),
        ],
    ];
};

const generateSimpleArgs = (args: $.simpleArgs): CompiledArgs => {
    return args.children.map((arg, index) => [`arg${index}`, generateArg(arg)]);
};

const generateArg = (arg: $.arg): t.TSType => {
    switch (arg.$) {
        case "int":
        case "uint":
        case "refs":
        case "stack":
        case "control":
        case "plduzArg":
        case "tinyInt":
        case "runvmArg":
        case "minusOne":
        case "s1":
        case "setcpArg":
            return t.tsNumberKeyword();
        case "delta":
            return generateArg(arg.arg);
        case "largeInt":
            return t.tsBigIntKeyword();
        case "debugstr":
            return t.tsTypeReference(
                t.tsQualifiedName(TON_CORE_QUALIFIER, t.identifier("Slice")),
            );
        case "hash":
            return t.tsTypeReference(
                t.tsQualifiedName(UTIL_QUALIFIER, t.identifier("Hash")),
            );
        case "codeSlice":
            return t.tsTypeReference(
                t.tsQualifiedName(UTIL_QUALIFIER, t.identifier("Code")),
            );
        case "refCodeSlice":
            return t.tsTypeReference(
                t.tsQualifiedName(UTIL_QUALIFIER, t.identifier("Code")),
            );
        case "inlineCodeSlice":
            return t.tsTypeReference(
                t.tsQualifiedName(UTIL_QUALIFIER, t.identifier("Code")),
            );
        case "slice":
            return t.tsTypeReference(
                t.tsQualifiedName(TON_CORE_QUALIFIER, t.identifier("Slice")),
            );
        case "exoticCell":
            return t.tsTypeReference(
                t.tsQualifiedName(UTIL_QUALIFIER, t.identifier("ExoticCell")),
            );
    }

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

main();
