import { Expr } from "./parsed";

export type Register = "acc" | "op";
export type Value = number;
export type Location = number;

export type Instruction = ({
    type: "push" | "pop",
    register: Register,
} | {
    type: "load",
    register: Register,
    value: Value,
} | {
    type: "add" | "mul" | "sub" | "div",
    left: Register,
    right: Register,
    dest: Register,
} | {
    type: "negate",
    src: Register,
    dest: Register,
} | {
    type: "jump_zero",
    register: Register,
    location: Location,
} | {
    type: "jump",
    location: Location,
}) & ({ jumpedTo: true, label: number } | { jumpedTo?: false });

export class Compiler {
    public result: Instruction[] = [];

    public compileExpr(expr: Expr) {
        switch (expr.exprType) {
            case "int": {
                this.result.push({ type: "load", register: "acc", value: expr.value })
                break;
            }
            case "unary": {
                this.compileExpr(expr.subject);
                switch (expr.unaryType) {
                    case "plus": {
                        break;
                    }
                    case "negate": {
                        this.result.push({ type: "negate", src: "acc", dest: "acc" })
                        break;
                    }
                }
                break;
            }
            case "binary": {
                this.compileExpr(expr.right);
                this.result.push({ type: "push", register: "acc" })
                this.compileExpr(expr.left);
                this.result.push({ type: "pop", register: "op" });
                let binaryType: "add" | "sub" | "mul" | "div";
                switch (expr.binaryType) {
                    case "add": {
                        binaryType = "add";
                        break;
                    }
                    case "subtract": {
                        binaryType = "sub";
                        break;
                    }
                    case "multiply": {
                        binaryType = "mul";
                        break;
                    }
                    case "divide": {
                        binaryType = "div";
                        break;
                    }
                }
                this.result.push({ type: binaryType, left: "acc", right: "op", dest: "acc" });
                break;
            }
            case "if": {
                this.compileExpr(expr.condition);

                const jumpToFalsyIndex = this.result.length;
                this.result.push({ type: "jump_zero", register: "acc", location: 0 });

                this.compileExpr(expr.truthy);

                const skipFalsyIndex = this.result.length;
                this.result.push({ type: "jump", location: 0 });

                let jumpToFalsyRef = this.result[jumpToFalsyIndex];
                if (jumpToFalsyRef.type !== "jump_zero") throw new Error("unreachable");
                jumpToFalsyRef.location = this.result.length;

                this.compileExpr(expr.falsy);

                let skipFalsyRef = this.result[skipFalsyIndex];
                if (skipFalsyRef.type !== "jump") throw new Error("unreachable");
                skipFalsyRef.location = this.result.length;

                break;
            }
            case "block": {
                this.compileExpr(expr.expr);
                break;
            }
            default: {
                const exhaustiveCheck: never = expr;
                throw new Error(`Unhandled color case: ${exhaustiveCheck}`);
            }
        }
    }
}

export function locateAndSetJumpedToInstructions(instructions: Instruction[]) {
    let nextLabel = 0;
    for (const ins of instructions) {
        if (ins.type === "jump_zero" || ins.type === "jump") {
            instructions[ins.location] = {
                ...instructions[ins.location],
                jumpedTo: true,
                label: nextLabel,
            };
            nextLabel += 1;
        }
    }
}

export class X86Generator {
    public generate(instructions: Instruction[]): string {
        return this.asmWithHeaders(instructions.map((ins) => this.generateInstruction(ins)).join(""));
    }

    private generateInstruction(ins: Instruction): string {
        let result = "";
        if (ins.jumpedTo) {

        }
        switch (ins.type) {
            case "load": {
                result += `    ; load`
                result += `    mov ${this.reg64(ins.register)}, ${this.value(ins.value)}\n`;
                break;
            }
            case "push": {
                return `
                    ; push
                    push ${this.reg64(ins.register)}
                `;
            }
            case "pop": {
                return `
                    ; pop
                    pop ${this.reg64(ins.register)}
                `;
            }
            case "negate": {
                return `
                    ; neg
                    mov ${this.reg64(ins.dest)}, ${this.reg64(ins.src)}
                    neg ${this.reg64(ins.dest)}
                `;
            }
            case "add": {
                return `
                    ; add
                    mov ${this.reg64(ins.dest)}, ${this.reg64(ins.left)}
                    add ${this.reg64(ins.dest)}, ${this.reg64(ins.right)}
                `;
            }
            case "sub": {
                return `
                    ; sub
                    mov ${this.reg64(ins.dest)}, ${this.reg64(ins.left)}
                    sub ${this.reg64(ins.dest)}, ${this.reg64(ins.right)}
                `;
            }
            case "mul": {
                return `
                    ; mul
                    mov ${this.reg64(ins.dest)}, ${this.reg64(ins.left)}
                    imul ${this.reg64(ins.dest)}, ${this.reg64(ins.right)}
                `;
            }
            case "div": {
                return `
                    ; div
                    mov rdi, ${this.reg64(ins.right)}
                    mov rax, ${this.reg64(ins.left)}
                    xor rdx, rdx
                    cqo
                    idiv rdi
                    mov ${this.reg64(ins.dest)}, rax
                `;
            }
            case "jump_zero": {
                return `
                    ; jump_zero
                    
                `
            }
        }
    }

    private asmWithHeaders(asm: string) {
        return `
            bits 64
            global _start

            _start:
                ${asm}
            exit:
                mov rdi, rax
                mov rax, 60
                syscall

        `;
    }

    private reg64(reg: Register): string {
        switch (reg) {
            case "acc": return "rax";
            case "op": return "rdx";
        }
    }

    private reg32(reg: Register): string {
        switch (reg) {
            case "acc": return "eax";
            case "op": return "edx";
        }
    }


    private value(value: Value): string {
        return value.toString();
    }

}