# ---------------------------------------------------------
#  Simulateur
# ---------------------------------------------------------
# - Bus données : 8 bits
# - 4 registres R0..R3 (8 bits)
# - Bus adresse : 8 bits
# - RAM : 256 octets
# - Instructions : 1 ou 2 octets
# - Cycles : 1 octet -> 1, 2 octets -> 2, LDR/STR -> 3
# - PC démarre à 0
# - Pile descendante, SP=255
# ---------------------------------------------------------

import sys, time
import uasyncio as asyncio


class CPU:
    pc: int = 0
    sp: int = 255
    regs: list = [0, 0, 0, 0]  # R0..R3
    lt: int = 0  # flag LT
    eq: int = 0  # flag EQ
    cycles: int = 0
    running: bool = True
    after_ret: bool = False

    def __post_init__(self):
        if not self.regs:
            self.regs = [0, 0, 0, 0]


class Simulator:
    def __init__(self, program: bytes):
        self.ram = bytearray(256)
        for i, b in enumerate(program[:256]):
            self.ram[i] = b
        self.cpu = CPU()
        self.program_size = len(program)
        self.motorCallback = None
        self.registerCallback = None

    # ---- Getter / Setter pour interfacer le robot

    def setMotorCallback(self, callback):
        self.motorCallback = callback

    def setRegisterCallback(self, callback):
        self.registerCallback = callback


    # ----------------- utilitaires mémoire / pile -----------------

    def fetch_byte(self) -> int:
        b = self.ram[self.cpu.pc]
        self.cpu.pc = (self.cpu.pc + 1) & 0xFF
        return b

    def push(self, value: int):
        if self.cpu.sp < 0:
            raise RuntimeError("STACK OVERFLOW")
        self.ram[self.cpu.sp] = value & 0xFF
        self.cpu.sp -= 1

    def pop(self) -> int:
        if self.cpu.sp >= 255:
            return 0
        self.cpu.sp += 1
        return self.ram[self.cpu.sp]

    # ----------------- exécution d'une instruction -----------------

    def step(self):
        c = self.cpu
        pc_before = c.pc
        b = self.fetch_byte()

        instr = ""
        size = 1  # taille en octets (1 ou 2)
        extra_cycles = 0  # pour LDR/STR/TIM

        # --- instructions 2 octets à opcode fixe ---
        #print(pc_before)
        #print(self.program_size)
        if c.after_ret:
            instr = f"DB 0x{b:02X}"

        elif b == 0x00:  # CALL _label
            addr = self.fetch_byte()
            size = 2
            instr = f"CALL {addr}"
            self.push(c.pc)
            c.pc = addr

        elif b == 0x40:  # JMP _label
            addr = self.fetch_byte()
            size = 2
            instr = f"JMP {addr}"
            c.pc = addr

        elif b == 0xC0:  # JLT _label
            addr = self.fetch_byte()
            size = 2
            instr = f"JLT {addr}"
            if c.lt == 1:
                c.pc = addr

        elif b == 0x20:  # JEQ _label
            addr = self.fetch_byte()
            size = 2
            instr = f"JEQ {addr}"
            if c.eq == 1:
                c.pc = addr

        elif b == 0x80:  # RET
            instr = "RET"
            ret = self.pop()
            if c.sp >= 255 and ret == 0:
                c.after_ret = True
                c.running = False
            else:
                c.pc = ret

        # --- PUSH / POP ---
        elif (b & 0b11111100) == 0b10100000:  # PUSH Rx
            r = b & 0b11
            instr = f"PUSH R{r}"
            self.push(c.regs[r])

        elif (b & 0b11111100) == 0b01100000:  # POP Rx
            r = b & 0b11
            instr = f"POP R{r}"
            c.regs[r] = self.pop()
            if (self.registerCallback != None):
                self.registerCallback(r, c.regs[r])

        # --- MOV Rx valeur / SUB Rx valeur / CMP Rx valeur ---
        elif (b & 0b11111100) == 0b11100000:  # MOV Rx valeur
            r = b & 0b11
            imm = self.fetch_byte()
            size = 2
            instr = f"MOV R{r}, {imm}"
            c.regs[r] = imm
            if (self.registerCallback != None):
                self.registerCallback(r, c.regs[r])

        elif (b & 0b11111100) == 0b00010000:  # SUB Rx valeur
            r = b & 0b11
            imm = self.fetch_byte()
            size = 2
            instr = f"SUB R{r}, {imm}"
            c.regs[r] = (c.regs[r] - imm) & 0xFF
            if (self.registerCallback != None):
                self.registerCallback(r, c.regs[r])

        elif (b & 0b11111100) == 0b10010000:  # CMP Rx valeur
            r = b & 0b11
            imm = self.fetch_byte()
            size = 2
            instr = f"CMP R{r}, {imm}"
            v = c.regs[r]
            c.lt = 1 if v < imm else 0
            c.eq = 1 if v == imm else 0

        # --- MOV / SUB / CMP registre-registre ---
        elif (b & 0b11110000) == 0b01010000:  # MOV Rx Ry
            dst = (b >> 2) & 0b11
            src = b & 0b11
            instr = f"MOV R{dst}, R{src}"
            c.regs[dst] = c.regs[src]
            if (self.registerCallback != None):
                self.registerCallback(dst, c.regs[dst])

        elif (b & 0b11110000) == 0b11010000:  # SUB Rx Ry
            dst = (b >> 2) & 0b11
            src = b & 0b11
            instr = f"SUB R{dst}, R{src}"
            c.regs[dst] = (c.regs[dst] - c.regs[src]) & 0xFF
            if (self.registerCallback != None):
                self.registerCallback(dst, c.regs[dst])

        elif (b & 0b11110000) == 0b00110000:  # CMP Rx Ry
            dst = (b >> 2) & 0b11
            src = b & 0b11
            instr = f"CMP R{dst}, R{src}"
            v1 = c.regs[dst]
            v2 = c.regs[src]
            c.lt = 1 if v1 < v2 else 0
            c.eq = 1 if v1 == v2 else 0

        # --- LDR / STR (2 octets, 3 cycles) ---
        elif (b & 0b11110000) == 0b10110000:  # LDR Rx Ry _label
            dst = (b >> 2) & 0b11
            src = b & 0b11
            addr = self.fetch_byte()
            size = 2
            instr = f"LDR R{dst}, R{src}, {addr}"
            eff = (addr + c.regs[src]) & 0xFF
            c.regs[dst] = self.ram[eff]
            extra_cycles = 1  # 2 octets -> 2 cycles +1 = 3
            if (self.registerCallback != None):
                self.registerCallback(dst, c.regs[dst])

        elif (b & 0b11110000) == 0b01110000:  # STR Rx Ry _label
            dst = (b >> 2) & 0b11
            src = b & 0b11
            addr = self.fetch_byte()
            size = 2
            instr = f"STR R{dst}, R{src}, {addr}"
            eff = (addr + c.regs[src]) & 0xFF
            self.ram[eff] = c.regs[dst] & 0xFF
            extra_cycles = 1

        # --- OUT Rx ---
        elif (b & 0b11111100) == 0b11110000:  # OUT Rx
            r = b & 0b11
            instr = f"OUT R{r}"
            registre = c.regs[r]
            print(f"[OUT] R{r} = {registre}")
            if (self.motorCallback != None):
                motG = (registre >> 4) & 0b1111
                motD = (registre) & 0b1111
                self.motorCallback(motG, motD)

        # --- TIM valeur ---
        elif b == 0xF8:  # TIM
            second = self.fetch_byte()
            size = 2
            m = (second >> 7) & 0x1
            v = second & 0x7F
            instr = f"TIM m={m}, v={v}"
            mult = 1 if m == 0 else 100
            pause_ms = mult * (v + 1)
            c.cycles += pause_ms  # modélisation de la pause
            # print(f"Sleep {pause_ms}ms...")
            time.sleep(pause_ms/1000)
            # print("BIPBIP")

       # if pc_before >= self.program_size:
       #     if 32 <= b <= 126:
       #         instr = f"DB 0x{b:02X} ('{chr(b)}')"
       #     else:
       #         instr = f"DB 0x{b:02X}"
        else:
            instr = f"UNKNOWN 0x{b:02X}"
            c.running = False



        # calcul des cycles
        if (b & 0b11110000) in (0xB0, 0x70):  # LDR / STR
            c.cycles += 3
            cycles_added = 3
        else:
            c.cycles += size
            cycles_added = size

        self.report(pc_before, instr, cycles_added)

    # ----------------- rapport d'exécution -----------------

    def report(self, pc_before: int, instr: str, cycles_added: int):
        c = self.cpu
        regs_str = " ".join(f"R{i}={c.regs[i]:02X}" for i in range(4))
        # print(f"PC={pc_before:02X}  {instr:20s}  +Cycles={cycles_added:3d}  Total={c.cycles}")
        # print(f"   {regs_str}   LT={c.lt} EQ={c.eq}  SP={c.sp}")
        # print("-" * 60)

    # ----------------- boucle principale -----------------

    def run(self, max_steps: int = 100000):
        steps = 0
        while self.cpu.running and steps < max_steps:
            self.step()
            steps += 1

def StartCPU(program, callback, registerCallback):
    sim = Simulator(program)
    sim.setMotorCallback(callback)
    sim.setRegisterCallback(registerCallback)
    while sim.cpu.running:
        sim.run(max_steps = 1)
        #time.sleep(0.1)

import time
# ---------------------------------------------------------
#  LECTURE D'UN FICHIER .bin ET LANCEMENT
# ---------------------------------------------------------
if __name__ == "__main__":
    # Nom du fichier binaire à exécuter
    path =""
    args= sys.argv
    if (len(args) > 1):
        filename = args[1]
        print("filename: " + filename)
        with open(filename, "rb") as f:
            program = f.read()
            StartCPU(program, None, None)
    else:
        print("Needs *.bin as parameter")
