package projet

import chisel3._
import chisel3.util.switch
import chisel3.util.is
import projet.op.OpImm
import projet.op.OpR
import projet.op.OpBranch
import projet.op.OpStore
import projet.op.OpLoad
import projet.op.OpImmW

class CUInterface extends Bundle {
    val instruction = Input(UInt(32.W))
    val reg_file_we = Output(Bool())
    val alu_opcode = Output(AluOpCode())

    // TODO: Maybe bundle alu <> cu
    // Alu comparison result
    val alu_comp_result = Input(Bool())
    val alu_word_mode = Output(Bool())

    val optype = Output(OpType())

    // True if the decoded instruction is a jump
    val is_jump = Output(Bool())

    // Stall the pipeline on the current instruction
    val should_stall = Output(Bool())
    val should_stop_stall = Output(Bool())

    // DMem connections
    val memory_size = Output(DMemSize())
    val memory_en = Output(Bool())
    val memory_we = Output(Bool())
    val memory_sign_extend = Output(
        Bool()
    ) // Wether to signe extend the output of the memory for bytes, half loads

    // Muxers
    // The naming convention is as follows : mux_xxx_yyy.
    // If the signal is hot, xxx is selected otherwise yyy is selected

    // Sends alu or immediate to write back
    val mux_alu_imm = Output(Bool())

    // Selects regA or pc for alu a
    val mux_rega_pc = Output(Bool())

    // Selects regB or immediate for alu b
    val mux_regb_imm = Output(Bool())

    // Selects 4 or immediate for pc incrementation
    val mux_incrpc4_imm = Output(Bool())

    // Selects pc or previous pc for pc incrementation
    val mux_regpc_executepc = Output(Bool())

    // Selects pc adder or write back for pc write
    val mux_pcadder_jalrwriteback = Output(Bool())

    // Selects pc or write back for register write
    val mux_writeback_pc = Output(Bool())

    // Selects aluout or memory data out for writeback_line
    val mux_executeout_dout = Output(Bool())
}

object OpType extends ChiselEnum {
    val U, I, J, B, S = Value

    // Type IR is not defined in the RiscV specification
    // but the immediate needs to be handled differently
    // since the 31st bit indicates wether it is an
    // arithmetic or logical shift
    val IR = Value
}

object OpCode extends ChiselEnum {
    // Type U
    val LUI = "b0110111".U
    val AUIPC = "b0010111".U

    // Type I + Type IR

    // Instruction format
    //    xxx rd, rs1, imm
    // Perfomrs some operation with rs1 and immediate
    // and writes back to a register

    // addi, andi, ori, xori, slli, srli, srai,
    // slti, sltiu
    val OpImm = "b0010011".U

    // Instruction format
    //    xxx rd, rs1, rs2
    // Perfomrs some operation with rs1 and rd2
    // and writes back to a register

    // add, and, or, xor, sll, srl, sra, slt, stlu, mul, mulh, mulhsu, mulhu, div, divu, rem, remu
    val OpR = "b0110011".U;

    // Type J
    val JAL = "b1101111".U

    // Type I
    val JALR = "b1100111".U

    // Type B
    val OpBranch = "b1100011".U

    // Type S
    val OpStore = "b0100011".U

    // Type I
    val OpLoad = "b0000011".U

    // ~~ Rv64i ~~
    val OpImmW = "b0011011".U

    // TypeR
    val OpRW = "b0111011".U
}

class ControlUnit() extends Module {
    val io = IO(new CUInterface())

    io.reg_file_we := false.B
    io.alu_opcode := AluOpCode.Add
    io.alu_word_mode := false.B
    io.optype := OpType.U
    io.is_jump := false.B;
    io.should_stall := false.B;
    io.should_stop_stall := false.B;
    io.memory_size := DMemSize.Word
    io.memory_en := false.B
    io.memory_we := false.B
    io.memory_sign_extend := false.B;
    io.mux_alu_imm := true.B;
    io.mux_rega_pc := true.B;
    io.mux_regb_imm := true.B;
    io.mux_incrpc4_imm := true.B;
    io.mux_regpc_executepc := true.B;
    io.mux_writeback_pc := true.B;
    io.mux_pcadder_jalrwriteback := true.B;
    io.mux_executeout_dout := true.B;

    // Decode instruction
    val opcode = io.instruction(6, 0);
    val funct3 = io.instruction(14, 12);
    val funct7 = io.instruction(31, 25);

    switch(opcode) {
        // ~~~ Single instruction OpCodes ~~

        // ~ U instructions ~
        is(OpCode.LUI) {
            io.reg_file_we := true.B // Write to regfile
            io.mux_alu_imm := false.B // Send immy directly to refile
            io.optype := OpType.U
        }
        is(OpCode.AUIPC) {
            io.alu_opcode := AluOpCode.Add
            io.reg_file_we := true.B // Write to regfile
            io.mux_rega_pc := false.B // Send PC to ALU A
            io.mux_regb_imm := false.B; // Send immu to ALU B
            io.optype := OpType.U
        }

        // ~ J instructions ~
        is(OpCode.JAL) {
            io.alu_opcode := AluOpCode.Add // ALU is unused

            io.reg_file_we := true.B // Write to regfile
            io.mux_writeback_pc := false.B;

            // io.mux_rega_pc := false.B // Send PC to ALU A
            // io.mux_regb_imm := false.B; // Send imm to ALU B

            io.is_jump := true.B
            io.mux_incrpc4_imm := false.B; // Send the immediate in PC
            io.mux_regpc_executepc := false.B; // Add the immediate to the previous PC
            io.mux_pcadder_jalrwriteback := true.B;
            io.optype := OpType.J
        }

        is(OpCode.JALR) {
            io.alu_opcode := AluOpCode.Add

            io.reg_file_we := true.B // Write to regfile
            io.mux_writeback_pc := false.B // Write next instruction PC to rd
            io.is_jump := true.B // Insert no-op to discard fetched next instruction

            io.mux_rega_pc := true.B // Send rs1 to ALU A
            io.mux_regb_imm := false.B; // Send imm to ALU B

            io.mux_alu_imm := true.B; // Send alu out to write back line
            io.mux_pcadder_jalrwriteback := false.B; // Set pc to writeback dest
            io.optype := OpType.I
        }

        is(OpCode.OpImm) {
            OpImm.opImm(io)
        }

        is(OpCode.OpR) {
            OpR.opR(io);
        }

        is(OpCode.OpBranch) {
            OpBranch.opBranch(io);
        }

        is(OpCode.OpStore) {
            OpStore.opStore(io);
        }

        is(OpCode.OpLoad) {
            OpLoad.opLoad(io);
        }

        is(OpCode.OpImmW) {
            OpImmW.opImmW(io);
        }

        is(OpCode.OpRW) {
            io.alu_word_mode := true.B
            OpR.opR(io);
        }
    }
}
