package de.unitrier.st.uap.w25.tram;
import java.util.*;

public class AbstractMachine {
    protected ArrayList<Integer> stack;
    protected ArrayList<Instruction> prog;

    protected int FP = 0;
    protected int PP = 0;
    protected int PC = 0;
    protected int TOP = -1;

    // Init machine with program and empty stack
    public AbstractMachine(Instruction[] prog) {
        this.prog = new ArrayList<>(Arrays.asList(prog));
        this.stack = new ArrayList<>();
    }

    // Init machine with program and stack
    public AbstractMachine(Instruction[] prog, Integer[] stack) {
        this(prog);

        this.stack = new ArrayList<>(Arrays.asList(stack));
        this.TOP = stack.length - 1;
    }

    // Executes until halts and returns final configuration
    public void execute() {
        while (!isHalted()) {
            executeStep();
        }
    }


    // Executes a single step and returns configuration
    public void executeStep() {
        try {
            Instruction inst = this.prog.get(this.PC);

            int arg1 = inst.getArg1() != null ? inst.getArg1() : 0;
            int arg2 = inst.getArg2() != null ? inst.getArg2() : 0;
            int arg3 = inst.getArg3() != null ? inst.getArg3() : 0;

            switch (inst.getOpcode()) {
                case Instruction.CONST:
                    // Pg. 9
                    write(this.TOP + 1, arg1);
                    this.TOP += 1;
                    this.PC += 1;
                    break;
                case Instruction.LOAD:
                    // Pg. 25
                    int loadSpp = spp(arg2, this.PP, this.FP);
                    write(this.TOP + 1, read(loadSpp + arg1));
                    this.TOP += 1;
                    this.PC += 1;
                    break;
                case Instruction.STORE:
                    // Pg. 25
                    int storeSpp = spp(arg2, this.PP, this.FP);
                    write(storeSpp + arg1, read(this.TOP));
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                case Instruction.ADD:
                    // Pg. 9
                    int addA = read(this.TOP - 1);
                    int addB = read(this.TOP);
                    write(this.TOP - 1, addA + addB);
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                case Instruction.SUB:
                    // Pg. 9
                    int subA = read(this.TOP - 1);
                    int subB = read(this.TOP);
                    write(this.TOP - 1, subA - subB);
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                case Instruction.MUL:
                    // Pg. 9
                    int mulA = read(this.TOP - 1);
                    int mulB = read(this.TOP);
                    write(this.TOP - 1, mulA * mulB);
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                case Instruction.DIV:
                    // Pg. 9
                    int divA = read(this.TOP - 1);
                    int divB = read(this.TOP);

                    if (divB == 0) {
                        throw new ArithmeticException("Division by zero");
                    }

                    write(this.TOP - 1, Math.floorDiv(divA, divB));
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                case Instruction.LT:
                    // Pg. 12
                    int ltA = read(this.TOP - 1);
                    int ltB = read(this.TOP);
                    write(this.TOP - 1, ltA < ltB ? 1 : 0);
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                case Instruction.GT:
                    // Pg. 12
                    int gtA = read(this.TOP - 1);
                    int gtB = read(this.TOP);
                    write(this.TOP - 1, gtA > gtB ? 1 : 0);
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                case Instruction.EQ:
                    // Pg. 12
                    int eqA = read(this.TOP - 1);
                    int eqB = read(this.TOP);
                    write(this.TOP - 1, eqA == eqB ? 1 : 0);
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                case Instruction.NEQ:
                    // Pg. 12
                    int neqA = read(this.TOP - 1);
                    int neqB = read(this.TOP);
                    write(this.TOP - 1, neqA != neqB ? 1 : 0);
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                case Instruction.IFZERO:
                    // Pg. 13
                    if (read(this.TOP) == 0) {
                        this.PC = arg1;
                    } else {
                        this.PC += 1;
                    }
                    this.TOP -= 1;
                    break;
                case Instruction.GOTO:
                    // Pg. 13
                    this.PC = arg1;
                    break;
                case Instruction.HALT:
                    // Pg. 13
                    this.PC = -1;
                    break;
                case Instruction.NOP:
                    // Pg. 13
                    this.PC += 1;
                    break;
                case Instruction.INVOKE:
                    // Pg. 23
                    write(this.TOP + 1, this.PC + 1);
                    write(this.TOP + 2, this.PP);
                    write(this.TOP + 3, this.FP);
                    write(this.TOP + 4, spp(arg3, this.PP, this.FP));
                    write(this.TOP + 5, sfp(arg3, this.PP, this.FP));
                    this.PP = this.TOP - arg1 + 1;
                    this.FP = this.TOP + 1;
                    this.TOP += 5;
                    this.PC = arg2;
                    break;
                case Instruction.RETURN:
                    // Pg. 24
                    int res = read(this.TOP);
                    this.TOP = this.PP;
                    this.PC = read(this.FP);
                    this.PP = read(this.FP + 1);
                    this.FP = read(this.FP + 2);
                    write(this.TOP, res);
                    break;
                case Instruction.POP:
                    // Not in docs, see MaMa
                    this.TOP -= 1;
                    this.PC += 1;
                    break;
                default:
                    break;
            }
        }
        catch(Exception e) {
            String line = this.prog.get(this.PC).toString();
            String msg = e.getMessage();
            msg = String.format("Error at PC=%d (%s): %s%nCurrent stack=%s", this.PC, line, msg, stackStr());
            throw new RuntimeException(msg, e);
        }
    }

    public boolean isHalted() {
        return this.PC < 0;
    }

    // PP of the predecessor
    protected int spp(int d, int pp, int fp) {
        if(d==0) {
            return pp;
        }
        return spp(d-1, this.stack.get(fp+3), this.stack.get(fp+4));
    }

    // FP of the predecessor
    protected int sfp(int d, int pp, int fp) {
        if(d==0) {
            return fp;
        }
        return sfp(d-1, this.stack.get(fp+3), this.stack.get(fp+4));
    }

    // Returns the machine's current configuration as string
    public String confStr() {
        String format = "TOP=%3d | PC=%3d | PP=%3d | FP=%3d | STACK=%s";
        return String.format(format, this.TOP, this.PC, this.PP, this.FP, stackStr());
    }

    // Returns the machine's current stack as string
    public String stackStr() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        int limit = Math.min(this.stack.size(), this.TOP + 1);
        for (int i = 0; i < limit; i++) {
            sb.append(this.stack.get(i));
            if (i < limit - 1) sb.append(", ");
        }
        sb.append("]");
        return sb.toString();
    }

    public int result() {
        return this.TOP >= 0 ? this.stack.get(this.TOP) : 0;
    }

    public ArrayList<Integer> results() {
        ArrayList<Integer> result = new ArrayList<>();
        for (int i = 0; i <= this.TOP; i++) {
            result.add(this.stack.get(i));
        }
        return result;
    }

    protected void write(int index, int value) {
        while (stack.size() <= index) stack.add(0);
        stack.set(index, value);
    }

    protected int read(int index) {
        return stack.get(index);
    }
}
