/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package tools.mips;

import API.Utils;
import Export.MemoryInitializationFile;
import common.Instruction;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import targets.mips.Descriprion;

/**
 *
 * @author EUGENIO CARVALHO
 */
public class MipsProcessor {

    public String ID = "default";
    public Integer WORD = 4, BYTE = 1;
    public MipsSettings settings;
    public Memory DataMemory;
    public Memory InstructionMemory;
    public Long PC = 0L;
    public RegisterBank RBank;
    public HashMap<String, Instruction> instructions = new HashMap<>();

    public TreeMap<String, Boolean> breakPoint = new TreeMap<>();

    public MipsProcessor(MipsSettings settings) throws Exception {

        this.settings = new MipsSettings();
        this.settings.Set("mode", "debug").Set("step.by.step", "false");

        InstructionMemory = new Memory(
                settings.Get("memory.instruction"),
                settings.GetInt("memory.instruction.size")
        );
        InstructionMemory.SetIO(new MemoryInitializationFile());

        DataMemory = new Memory(
                settings.Get("memory.data"),
                settings.GetInt("memory.data.size")
        );
        DataMemory.SetIO(new MemoryInitializationFile());

        WORD = 4;

        // Limpa todos os dados armazenados 
//        DataMemory.Reset();
//        System.out.println("Init Md:" + DataMemory.R(0L));
        RBank = new RegisterBank(32);

        InitInstructions();

        this.settings.copy(
                "debugmode,"
                + "mode,"
                + "step.by.step,"
                + "breakpoints,"
                + "memory.instruction,"
                + "memory.instruction.size,"
                + "memory.data,"
                + "memory.data.reset,"
                + "memory.data.size",
                settings);

        SetBreak(this.settings.Get("breakpoints"));
    }

    public MipsProcessor SetBreak(String address) {
        for (String addrs : address.split(",")) {
            if (!addrs.equals("")) {
                // Range address
                if (addrs.contains("-")) {
                    String[] parts = addrs.split("-");
                    Long init = Long.parseLong(parts[0], 16), end = Long.parseLong(parts[1], 16);
                    for (; init <= end; init += WORD) {
                        breakPoint.put(Long.toString(init, 16), true);
                    }
                } else {

                    breakPoint.put(addrs, true);
                }
            }
        }
        System.out.println("Breakpoints:\n" + breakPoint);
        return this;
    }

    public MipsProcessor Persist() throws Exception {
        DataMemory.Save();
        return this;
    }

    protected void Log(String msg) {
        if (!settings.eq("mode", "debug")) {
            return;
        }

        if (breakPoint.size() > 0 && !breakPoint.containsKey(Long.toHexString(PC))) {
            return;
        }

        System.out.println(msg);
    }

    public MipsProcessor Run() throws Exception {
        Instruction instruction;
        Integer interation = 0;
        String cAddress;
        BufferedReader br = null;
        Boolean stepByStep = settings.eq("step.by.step", "true");

        if (stepByStep || !breakPoint.isEmpty()) {
            br = new BufferedReader(new InputStreamReader(System.in));
        }
        // Carrega o conteudo das memorias
        InstructionMemory.ReadFile();
        if (!settings.eq("memory.data.reset", "true")) {
            DataMemory.ReadFile();
        }

        // Fetch
//        System.out.println("InstructionMemory:" + InstructionMemory);
        System.out.println("Start simulation ... " + ID);

        while (true) {

            instruction = Decode(InstructionMemory.R(PC, WORD));

            // Quando encontra a instrucao stop para a simulacao
            if (instruction.eq("inst", "stop")) {
                break;
            }

            cAddress = Long.toHexString(PC);

            // Executa a instrucao
            Log("Interation(" + (interation++) + ")[ " + cAddress + " ] " + instruction.Get("inst"));
            Execute(instruction);
            // PC já incrementou
            Log("Registradores(\n" + RBank + ")\n Next PC:" + Long.toHexString(PC));

            // Verifica o controle de execução do simulador
            if ((stepByStep || breakPoint.containsKey(cAddress)) && br != null) {
                br.readLine();
            }
        }
        return this;
    }

    public Instruction Decode(String bin) {
//        int val = (int) instruction;
//        String bin = Utils.Pad(32, Integer.toBinaryString(val));
//        System.out.println("Run:" + bin + ":" + bin.length());
        String codop = bin.substring(0, 6), key, func = "";
        String rs = bin.substring(6, 11),
                rt = bin.substring(11, 16);

        switch (codop) {
            case "000000":
                func = bin.substring(26);
                key = "@" + func;
                break;
            case "000001":
                key = codop + rt;
                break;
            default:
                key = codop;
        }

        if (!instructions.containsKey(key)) {
            System.out.println("Não encontrou a instrução " + key);
            return null;
        }

        Instruction i = instructions.get(key).copy();

        switch (i.Get("type")) {
            case "R":
                i.Set("rs.bin", rs);
                i.Set("rt.bin", rt);
                i.Set("rd.bin", bin.substring(16, 21));
                i.Set("shamt.bin", bin.substring(21, 26));
                i.Set("funct.bin", func);

                i.Set("rs", Long.parseLong(rs, 2));
                i.Set("rt", Long.parseLong(rt, 2));
                i.Set("rd", Long.parseLong(bin.substring(16, 21), 2));
                i.Set("shamt", Long.parseLong(bin.substring(21, 26), 2));
                i.Set("funct", func);
                break;
            case "I":
                i.Set("rs.bin", rs);
                i.Set("rt.bin", rt);
                i.Set("imm.bin", bin.substring(16));

                i.Set("rs", Long.parseLong(rs, 2));
                i.Set("rt", Long.parseLong(rt, 2));
                i.Set("imm", Utils.bin32ToDec(bin.substring(16)));

                break;
            case "J":
                i.Set("addr.bin", bin.substring(6));
                i.Set("imm", Utils.bin32ToDec(bin.substring(6)));
        }
        return i;
    }

//    public Integer IntToHex(Integer n) {
//        return Integer.valueOf(String.valueOf(n), 16);
//    }
    protected void InitInstructions() throws Exception {
        String key;
        for (Map.Entry<String, Instruction> entry : Descriprion.Codops.entrySet()) {
//            String string = entry.getKey();
            Instruction instruction = entry.getValue();

            key = instruction.Get("codop");

            switch (key) {
                case "000000":
                    key = "@" + instruction.Get("func");
                    break;
                case "000001":
                    key += instruction.Get("rt");
                    break;
            }

            instructions.put(key, instruction);
        };
    }

    private void Execute(Instruction inst) throws Exception {

        long rs = 0,
                rt = 0,
                imm = 0,
                rd = 0,
                shamt = 0;
        boolean hasImm = true;

        switch (inst.Get("type")) {
            case "R":
                rd = inst.GetInt("rd");
                shamt = inst.GetInt("shamt");
                hasImm = false;
            case "I":
                rs = inst.GetInt("rs");
                rt = inst.GetInt("rt");
            case "J":
                if (hasImm) {
//                    System.out.println("HAS_IMM:" + inst.Get("imm"));
                    imm = inst.getLong("imm");
                }
        }

        Log(inst.Get("inst")
                + " [rd:" + RBank.names.get((int) rd)
                + "] [rs:" + RBank.names.get((int) rs)
                + "] [rt:" + RBank.names.get((int) rt)
                + "] [shamt:" + shamt
                + "] [imm:" + imm + "]");

        switch (inst.Get("inst")) {
            // R
            case "add":
            case "addu":
                RBank.W(rd, RBank.R(rs) + RBank.R(rt));
                break;
            case "addi":
            case "addiu":
//                System.out.println("RBank.R(rs) + imm:" + RBank.R(rs) + ":" + imm);
                RBank.W(rt, RBank.R(rs) + imm);
                break;
            case "stop":
                break;
            case "and":
            case "andi":
                RBank.W(rd, RBank.R(rs) & RBank.R(rt));
                break;
            case "div":
            case "divu":
                RBank.W(RBank.REG_LO, RBank.R(rs) / RBank.R(rt));
                RBank.W(RBank.REG_HI, RBank.R(rs) % RBank.R(rt));
                break;
            case "jr":
                PC = RBank.R(rs);
                break;
            case "mfhi":
                RBank.W(rd, RBank.R(RBank.REG_HI));
                break;
            case "mflo":
                RBank.W(rd, RBank.R(RBank.REG_LO));
                break;
            case "mthi":
                RBank.W(RBank.R(RBank.REG_HI), RBank.R(rs));
                break;
            case "mtlo":
                RBank.W(RBank.R(RBank.REG_LO), RBank.R(rs));
                break;
            case "mult":
            case "multu":
                // verificar a questao do hi e lo

//                System.out.println("Mult:"
//                        + rs + "(" + RBank.names.Get((int) rs) + "):"
//                        + rt + "(" + RBank.names.Get((int) rt) + ")");
                RBank.W(RBank.REG_LO, RBank.R(rs) * RBank.R(rt));
                break;
//            case "nor":
//                RBank.W(rd, rs * rt);
//                break;
            case "or":
                RBank.W(rd, RBank.R(rs) | RBank.R(rt));
                break;
            case "sll":
                RBank.W(rd, RBank.R(rt) << shamt);
                break;
            case "slt":
                RBank.W(rd, (RBank.R(rs) < RBank.R(rt)) ? 1 : 0);
                break;
            case "sra":
                RBank.W(rd, RBank.R(rt) >> shamt);
                break;
            case "srl":
                RBank.W(rd, RBank.R(rt) >>> shamt);
                break;
            case "sub":
            case "subu":
                RBank.W(rd, RBank.R(rs) - RBank.R(rt));
                break;
            case "xor":
                RBank.W(rd, RBank.R(rs) ^ RBank.R(rt));
                break;
//            // I
            case "beq":
                if (RBank.R(rs) == RBank.R(rt)) {
                    PC += ((imm + 1) << 2);
                    return;
                }
                break;
            case "bgez":
                if (RBank.R(rs) >= 0) {
                    PC += ((imm + 1) << 2);
                    return;
                }
                break;
            case "bgtz":
                if (RBank.R(rs) > 0) {
                    PC += ((imm + 1) << 2);
                    return;
                }
                break;

            case "blez":
                if (RBank.R(rs) <= 0) {
                    PC += ((imm + 1) << 2);
                    return;
                }
                break;
            case "bltz":
//                System.out.println("BLTZ:" + rs);
                if (RBank.R(rs) < 0) {
                    PC += ((imm + 1) << 2);
                    return;
                }
                break;
            case "bne":
                if (RBank.R(rs) != RBank.R(rt)) {
                    PC += ((imm + 1) << 2);
                    return;
                }
                break;

//            case "lb":
//                if (RBank.R(rs) != RBank.R(rt)) {
//                    PC += imm << 2;
//                    return;
//                }
//            case "lbu":
//            case "lh":
//            case "lhu":
            case "lui":
                RBank.W(rt, imm);
                break;

            case "lb":
                RBank.W(
                        rt,
                        DataMemory.ReadLong(
                                RBank.R(rs) + imm,
                                BYTE // Apenas 1 byte
                        ));

                Log(String.format(
                        "Load endereco '%d' <- '%d'\n",
                        RBank.R(rs) + imm,
                        DataMemory.ReadLong(RBank.R(rs) + imm, BYTE)
                ));
                break;
            case "lw":
                // Escreve no banco de registradores no registrador enderecado por rt
                RBank.W(
                        rt,
                        // Lê da memoria de dados uma palavra de 4 bytes
                        DataMemory.ReadLong(
                                // Endereco de leitura equivale ao conteudo do registrador rs + imediato
                                RBank.R(rs) + imm,
                                WORD
                        ));

                Log(String.format(
                        "Load endereco '%d' <- '%d'\n",
                        RBank.R(rs) + imm,
                        DataMemory.ReadLong(RBank.R(rs) + imm, WORD)
                ));

                break;
            case "ori":
                RBank.W(rt, RBank.R(rs) | imm);
                break;
            case "slti":
//            case "sltiu":
            case "sw":
                DataMemory.W(
                        RBank.R(rs) + imm,
                        RBank.R(rt),
                        WORD // uma palavra inteira
                );

                Log(String.format("Store '%d' no endereco '%d'\n", RBank.R(rt), RBank.R(rs) + imm));
                break;
//            case "xori":
            // J
            case "jal":
                RBank.W(31, PC);
            case "j":
                PC = imm << 2;
                return;
            default:
                throw new Exception("Instrução " + inst.Get("inst") + " não definida!");
        }
        PC += WORD;
    }
}
