from __future__ import annotations

from dataclasses import dataclass, field

from ..decoder import H8536Decoder
from ..formatting import h16
from ..rom import DecodeError
from ..vectors import read_vectors_min
from .constants import (
    FRT_TCR_OCIEA,
    FRT_TCSR_OCFA,
    FRT1_FRC_H,
    FRT1_OCRA_H,
    FRT1_TCR,
    FRT1_TCSR,
    FRT2_FRC_H,
    FRT2_OCRA_H,
    FRT2_TCR,
    FRT2_TCSR,
    IPRA,
    IPRC,
    IPRE,
    SCI_SCR_RE,
    SCI_SCR_RIE,
    SCI_SCR_TIE,
    SCI_SSR_FER,
    SCI_SSR_ORER,
    SCI_SSR_PER,
    SCI_SSR_RDRF,
    SCI_SSR_TDRE,
    VECTOR_FRT1_OCIA,
    VECTOR_FRT2_OCIA,
    VECTOR_INTERVAL_TIMER,
    VECTOR_SCI1_ERI,
    VECTOR_SCI1_RXI,
    VECTOR_SCI1_TXI,
)
from .cpu import CPUState, mask, s8, s16, sign_bit
from .errors import EmulatorError, UnsupportedInstruction
from .fast_paths import P9FastPath, P9FastPathConfig
from .memory import MemoryMap
from .sci import SCI1
from .timers import FrtOciaScheduler, FrtRegisters
from .uart import UartTiming


@dataclass
class RunReport:
    steps: int
    cycles: int
    pc: int
    stopped_reason: str
    tx_bytes: bytes
    tx_frames: list[bytes]
    heartbeat_seen: bool
    clock_hz: int
    frt1_ocia_period_ms: float | None = None
    frt2_ocia_period_ms: float | None = None
    unsupported: str | None = None
    trace: list[str] = field(default_factory=list)

    def summary_lines(self) -> list[str]:
        lines = [
            f"steps={self.steps}",
            f"cycles={self.cycles}",
            f"pc={h16(self.pc)}",
            f"stopped={self.stopped_reason}",
            f"clock_hz={self.clock_hz}",
            "frt1_ocia_period_ms=" + _format_optional_ms(self.frt1_ocia_period_ms),
            "frt2_ocia_period_ms=" + _format_optional_ms(self.frt2_ocia_period_ms),
            "tx_bytes=" + self.tx_bytes.hex(" ").upper(),
            "tx_frames=" + ", ".join(frame.hex(" ").upper() for frame in self.tx_frames),
            f"heartbeat_seen={self.heartbeat_seen}",
        ]
        if self.unsupported:
            lines.append(f"unsupported={self.unsupported}")
            lines.append(
                "next_todo=implement the stopped opcode, then add interrupt scheduling for SCI1 TXI and interval/watchdog timer overflow"
            )
        return lines


class H8536Emulator:
    def __init__(
        self,
        rom_bytes: bytes,
        *,
        interval_steps: int = 2048,
        frt1_ocia_steps: int | None = None,
        frt2_ocia_steps: int | None = None,
        clock_hz: int = 10_000_000,
        p9_fast_path: P9FastPath | None = None,
        p9_fast_path_enabled: bool = False,
        p9_fast_default_input_byte: int = 0xFF,
        p9_fast_default_wrapper_success: bool = False,
        p7_input: int = 0xFF,
        eeprom_seed: str = "blank",
        sci1_tx_timing: UartTiming | None = None,
    ) -> None:
        if not rom_bytes:
            raise ValueError("ROM image is empty")
        if eeprom_seed not in {"blank", "factory"}:
            raise ValueError("eeprom_seed must be 'blank' or 'factory'")
        self.sci1 = SCI1()
        self.memory = MemoryMap(rom_bytes, self.sci1, p7_input=p7_input)
        if eeprom_seed == "factory":
            self.memory.seed_factory_eeprom_and_shadow()
        self.sci1.configure_tx_timing(sci1_tx_timing, clock_hz=clock_hz)
        self.memory.p9_bus.default_wrapper_success = bool(p9_fast_default_wrapper_success)
        self.p9_fast_path = p9_fast_path or P9FastPath(
            P9FastPathConfig(enabled=p9_fast_path_enabled, default_input_byte=p9_fast_default_input_byte)
        )
        self.cpu = CPUState()
        self.vectors = read_vectors_min(self.memory.rom)
        self.interval_steps = max(1, interval_steps)
        self.frt1_ocia_steps = max(1, frt1_ocia_steps) if frt1_ocia_steps is not None else None
        self.frt2_ocia_steps = max(1, frt2_ocia_steps) if frt2_ocia_steps is not None else None
        self.clock_hz = max(1, clock_hz)
        self.frt1_ocia = FrtOciaScheduler(FrtRegisters("FRT1", FRT1_TCR, FRT1_TCSR, FRT1_FRC_H, FRT1_OCRA_H))
        self.frt2_ocia = FrtOciaScheduler(FrtRegisters("FRT2", FRT2_TCR, FRT2_TCSR, FRT2_FRC_H, FRT2_OCRA_H))
        self._interval_counter = 0
        self._frt1_ocia_counter = 0
        self._frt2_ocia_counter = 0
        self.reset()

    def reset(self) -> None:
        self.cpu = CPUState(pc=self.reset_vector())

    def reset_vector(self) -> int:
        if self.memory.rom.contains(0, 2):
            return self.memory.rom.u16(0)
        raise DecodeError("ROM does not contain a reset vector at H'0000")

    def inject_sci1_rx_byte(self, value: int) -> None:
        self.memory.inject_sci1_rx_byte(value)

    def step(self) -> str:
        pc = self.cpu.pc
        cycles_before = self.cpu.cycles
        self.memory.current_pc = pc
        try:
            if self.p9_fast_path.try_handle(self):
                self._tick_peripherals(self.cpu.cycles - cycles_before)
                return f"{h16(pc)}: {'<p9-fast-path>':<17} P9 fast-path"

            decoder = H8536Decoder(self.memory.rom, br=self.cpu.br)
            ins = decoder.decode(pc)
            if not ins.valid:
                raise UnsupportedInstruction(pc, ins.raw, ins.text)

            next_pc = (pc + ins.size) & 0xFFFF
            raw = ins.raw
            text = ins.text

            if raw[0] == 0x00:
                pass
            elif raw[0] == 0x02 and len(raw) == 2:
                self._pop_register_mask(raw[1])
            elif raw[0] == 0x11 and len(raw) >= 2:
                next_pc = self._indirect_jump_call(raw, pc, next_pc)
            elif raw[0] == 0x12 and len(raw) == 2:
                self._push_register_mask(raw[1])
            elif raw[0] in (0x01, 0x06, 0x07) and len(raw) == 3 and 0xB8 <= raw[1] <= 0xBF:
                next_pc = self._scb(raw, pc, next_pc)
            elif raw[0] in (0x04, 0x0C):
                next_pc = self._execute_general(pc, next_pc)
            elif 0x40 <= raw[0] <= 0x47 and len(raw) == 2:
                reg = raw[0] & 0x07
                self._cmp(self._reg_read(reg, 1), raw[1], 1)
            elif 0x48 <= raw[0] <= 0x4F and len(raw) == 3:
                reg = raw[0] & 0x07
                self._cmp(self.cpu.regs[reg], int.from_bytes(raw[1:3], "big"), 2)
            elif 0x50 <= raw[0] <= 0x57 and len(raw) == 2:
                self._reg_write(raw[0] & 0x07, raw[1], 1)
                self._set_logic_flags(raw[1], 1)
            elif 0x58 <= raw[0] <= 0x5F and len(raw) == 3:
                self.cpu.regs[raw[0] & 0x07] = int.from_bytes(raw[1:3], "big")
                self._set_logic_flags(self.cpu.regs[raw[0] & 0x07], 2)
            elif raw[0] in (0x0E, 0x1E, 0x18):
                next_pc = self._direct_call(raw, next_pc)
            elif raw[0] == 0x19:
                next_pc = self._pop16()
            elif raw[0] == 0x0A:
                next_pc = self._return_from_interrupt()
            elif raw[0] in (0x15, 0x1D) and len(raw) >= 4:
                next_pc = self._execute_general(pc, next_pc)
            elif raw[0] in range(0xA0, 0x100):
                next_pc = self._execute_general(pc, next_pc)
            elif raw[0] in range(0x20, 0x30) and len(raw) == 2:
                next_pc = self._branch8(raw, pc, next_pc)
            elif raw[0] in range(0x30, 0x40) and len(raw) == 3:
                next_pc = self._branch16(raw, pc, next_pc)
            else:
                raise UnsupportedInstruction(pc, raw, text)

            self.cpu.pc = next_pc
            self.cpu.steps += 1
            cycle_delta = self._rough_cycles(raw)
            self.cpu.cycles += cycle_delta
            self._tick_peripherals(cycle_delta)
            return f"{h16(pc)}: {' '.join(f'{byte:02X}' for byte in raw):<17} {text}"
        finally:
            self.memory.current_pc = None

    def run(self, max_steps: int, trace: bool = False, stop_on_heartbeat: bool = False) -> RunReport:
        trace_lines: list[str] = []
        stopped_reason = "max_steps"
        unsupported: str | None = None
        for _ in range(max_steps):
            try:
                line = self.step()
            except UnsupportedInstruction as exc:
                stopped_reason = "unsupported_instruction"
                unsupported = str(exc)
                break
            if trace:
                trace_lines.append(line)
            if stop_on_heartbeat and self.sci1.saw_heartbeat():
                stopped_reason = "heartbeat"
                break
        return RunReport(
            steps=self.cpu.steps,
            cycles=self.cpu.cycles,
            pc=self.cpu.pc,
            stopped_reason=stopped_reason,
            tx_bytes=bytes(self.sci1.tx_bytes),
            tx_frames=list(self.sci1.tx_frames),
            heartbeat_seen=self.sci1.saw_heartbeat(),
            clock_hz=self.clock_hz,
            frt1_ocia_period_ms=self.frt1_ocia.period_ms(self.memory, self.clock_hz),
            frt2_ocia_period_ms=self.frt2_ocia.period_ms(self.memory, self.clock_hz),
            unsupported=unsupported,
            trace=trace_lines,
        )

    def _execute_general(self, pc: int, next_pc: int) -> int:
        ea = self._decode_ea(pc)
        op = self.memory.rom.u8(pc + int(ea["length"]))
        size = int(ea["size"])
        raw = self.memory.rom.slice(pc, self._general_length(pc, ea, op))

        if op == 0x00:
            ext = self.memory.rom.u8(pc + int(ea["length"]) + 1)
            ext_base = ext & 0xF8
            reg = ext & 0x07
            if ext_base == 0x80:
                value = self._read_ea(ea, 1)
                self._reg_write(reg, value, 1)
                self._set_logic_flags(value, 1)
            elif ext_base == 0x90:
                self._write_ea(ea, self._reg_read(reg, 1), 1)
            else:
                raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
            return next_pc

        if op in (0x06, 0x07):
            value = raw[-1] if op == 0x06 else int.from_bytes(raw[-2:], "big")
            write_size = size if op == 0x06 else 2
            self._write_ea(ea, value, write_size)
            self._set_logic_flags(value, write_size)
            return next_pc

        base = op & 0xF8
        rd = op & 0x07
        if ea["mode"] == "imm" and base in (0x48, 0x58, 0x68):
            value = self._read_ea(ea, size)
            if base == 0x48:
                self.cpu.sr |= value
            elif base == 0x58:
                self.cpu.sr &= value
            else:
                self.cpu.sr ^= value
        elif base == 0x80:
            value = self._read_ea(ea, size)
            self._reg_write(rd, value, size)
            self._set_logic_flags(value, size)
        elif base == 0x90:
            self._write_ea(ea, self._reg_read(rd, size), size)
        elif base == 0x20:
            lhs = self._reg_read(rd, size)
            rhs = self._read_ea(ea, size)
            result = lhs + rhs
            self._reg_write(rd, result, size)
            self._set_add_flags(lhs, rhs, result, size)
        elif base == 0x30:
            lhs = self._reg_read(rd, size)
            rhs = self._read_ea(ea, size)
            result = lhs - rhs
            self._reg_write(rd, result, size)
            self._set_sub_flags(lhs, rhs, result, size)
        elif base == 0x50:
            result = self._reg_read(rd, size) & self._read_ea(ea, size)
            self._reg_write(rd, result, size)
            self._set_logic_flags(result, size)
        elif base == 0x40:
            result = self._reg_read(rd, size) | self._read_ea(ea, size)
            self._reg_write(rd, result, size)
            self._set_logic_flags(result, size)
        elif base == 0x60:
            result = self._reg_read(rd, size) ^ self._read_ea(ea, size)
            self._reg_write(rd, result, size)
            self._set_logic_flags(result, size)
        elif base == 0x70:
            self._cmp(self._reg_read(rd, size), self._read_ea(ea, size), size)
        elif base == 0xA8:
            if size != 1:
                raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
            result = (self._reg_read(rd, 1) * self._read_ea(ea, 1)) & 0xFFFF
            self.cpu.regs[rd] = result
            self._set_logic_flags(result, 2)
            self.cpu.c = False
        elif op in (0x08, 0x09, 0x0C, 0x0D):
            delta = {0x08: 1, 0x09: 2, 0x0C: -1, 0x0D: -2}[op]
            old = self._read_ea(ea, size)
            result = old + delta
            self._write_ea(ea, result, size)
            if delta >= 0:
                self._set_add_flags(old, delta, result, size)
            else:
                self._set_sub_flags(old, -delta, result, size)
        elif op == 0x13:
            self._write_ea(ea, 0, size)
            self._set_logic_flags(0, size)
        elif op == 0x15:
            result = (~self._read_ea(ea, size)) & mask(size)
            self._write_ea(ea, result, size)
            self._set_logic_flags(result, size)
        elif op == 0x16:
            self._set_logic_flags(self._read_ea(ea, size), size)
        elif op in (0x10, 0x11, 0x12) and ea["mode"] == "reg" and size == 1:
            reg = int(ea["reg"])
            value = self.cpu.regs[reg] & 0xFFFF
            if op == 0x10:
                result = ((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF)
            elif op == 0x11:
                result = s8(value & 0xFF) & 0xFFFF
            else:
                result = value & 0x00FF
            self.cpu.regs[reg] = result
            self._set_logic_flags(result, 2)
        elif op == 0x1A:
            value = self._read_ea(ea, size)
            result = (value << 1) & mask(size)
            self.cpu.c = bool(value & sign_bit(size))
            self._write_ea(ea, result, size)
            self._set_logic_flags(result, size)
        elif op == 0x1B:
            value = self._read_ea(ea, size)
            result = value >> 1
            self.cpu.c = bool(value & 1)
            self._write_ea(ea, result, size)
            self._set_logic_flags(result, size)
        elif 0xC0 <= op <= 0xFF:
            bit = op & 0x0F
            self._bit_operation(ea, size, op & 0xF0, bit)
        elif base in (0x48, 0x58, 0x68, 0x78):
            bit = self._reg_read(rd, 1) & 0x0F
            self._bit_operation(ea, size, base + 0x80, bit)
        elif base == 0x88:
            value = self._read_ea(ea, size)
            if size == 2 and rd == 0:
                self.cpu.sr = value & 0xFFFF
            elif size == 1 and rd == 1:
                self.cpu.br = value & 0xFF
            else:
                raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
        elif base == 0x98:
            if size == 2 and rd == 0:
                self._write_ea(ea, self.cpu.sr, 2)
            elif size == 1 and rd == 1:
                self._write_ea(ea, self.cpu.br, 1)
            else:
                raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
        elif op in (0x04, 0x05):
            compare_size = 1 if op == 0x04 else 2
            immediate = raw[-1] if op == 0x04 else int.from_bytes(raw[-2:], "big")
            self._cmp(self._read_ea(ea, compare_size), immediate, compare_size)
        else:
            raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
        return next_pc

    def _branch8(self, raw: bytes, pc: int, next_pc: int) -> int:
        cond = raw[0] & 0x0F
        disp = s8(raw[1])
        target = (pc + 2 + disp) & 0xFFFF
        if self._branch_condition(cond):
            return target
        return next_pc

    def _branch16(self, raw: bytes, pc: int, next_pc: int) -> int:
        cond = raw[0] & 0x0F
        disp = s16(int.from_bytes(raw[1:3], "big"))
        target = (pc + 3 + disp) & 0xFFFF
        if self._branch_condition(cond):
            return target
        return next_pc

    def _rough_cycles(self, raw: bytes) -> int:
        if raw[0] in (0x15, 0x1D):
            return 9
        if raw[0] in (0x0E, 0x1E, 0x18):
            return 14
        if raw[0] in (0x19, 0x0A):
            return 12
        if raw[0] in range(0x20, 0x40):
            return 8 if (raw[0] & 0x0F) == 0 else 4
        return 3

    def _direct_call(self, raw: bytes, next_pc: int) -> int:
        self._push16(next_pc)
        if raw[0] == 0x0E:
            return (next_pc + s8(raw[1])) & 0xFFFF
        if raw[0] == 0x1E:
            return (next_pc + s16(int.from_bytes(raw[1:3], "big"))) & 0xFFFF
        return int.from_bytes(raw[1:3], "big")

    def _indirect_jump_call(self, raw: bytes, pc: int, next_pc: int) -> int:
        op = raw[1]
        if 0xC0 <= op <= 0xDF:
            target = self.cpu.regs[op & 0x07] & 0xFFFF
        elif 0xE0 <= op <= 0xEF and len(raw) >= 3:
            target = (self.cpu.regs[op & 0x07] + s8(raw[2])) & 0xFFFF
        elif 0xF0 <= op <= 0xFF and len(raw) >= 4:
            target = (self.cpu.regs[op & 0x07] + s16(int.from_bytes(raw[2:4], "big"))) & 0xFFFF
        else:
            raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
        if 0xC8 <= op <= 0xCF or 0xD8 <= op <= 0xDF or 0xE8 <= op <= 0xEF or 0xF8 <= op <= 0xFF:
            self._push16(next_pc)
        return target

    def _scb(self, raw: bytes, pc: int, next_pc: int) -> int:
        reg = raw[1] & 0x07
        condition = {0x01: False, 0x06: not self.cpu.z, 0x07: self.cpu.z}[raw[0]]
        if condition:
            return next_pc
        value = (self.cpu.regs[reg] - 1) & 0xFFFF
        self.cpu.regs[reg] = value
        self.cpu.z = value == 0
        if value != 0:
            return (pc + 3 + s8(raw[2])) & 0xFFFF
        return next_pc

    def _tick_peripherals(self, cycle_delta: int) -> None:
        self.sci1.tick(cycle_delta)
        self._interval_counter += 1
        if self.frt1_ocia_steps is None:
            self.frt1_ocia.tick(self.memory, cycle_delta)
        else:
            self._frt1_ocia_counter += 1
        if self.frt2_ocia_steps is None:
            self.frt2_ocia.tick(self.memory, cycle_delta)
        else:
            self._frt2_ocia_counter += 1
        self._service_pending_interrupt()

    def _service_pending_interrupt(self) -> None:
        if self.cpu.interrupt_depth:
            return
        candidates: list[tuple[int, int, str]] = []
        sci1_rx_interrupts_enabled = bool(self.sci1.scr & SCI_SCR_RIE and self.sci1.scr & SCI_SCR_RE)
        if sci1_rx_interrupts_enabled and self.sci1.ssr & (SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER):
            target = self._vector_target(VECTOR_SCI1_ERI)
            if target is not None:
                candidates.append((self._sci1_priority(), target, "sci1_eri"))
        if sci1_rx_interrupts_enabled and self.sci1.ssr & SCI_SSR_RDRF:
            target = self._vector_target(VECTOR_SCI1_RXI)
            if target is not None:
                candidates.append((self._sci1_priority(), target, "sci1_rxi"))
        if self.sci1.scr & SCI_SCR_TIE and self.sci1.ssr & SCI_SSR_TDRE:
            target = self._vector_target(VECTOR_SCI1_TXI)
            if target is not None:
                candidates.append((self._sci1_priority(), target, "sci1_txi"))
        if self._interval_counter >= self.interval_steps:
            target = self._vector_target(VECTOR_INTERVAL_TIMER)
            if target is not None:
                candidates.append((self._interval_priority(), target, "interval_timer"))
        if self._frt1_ocia_pending():
            target = self._vector_target(VECTOR_FRT1_OCIA)
            if target is not None:
                candidates.append((self._frt1_priority(), target, "frt1_ocia"))
        if self._frt2_ocia_pending():
            target = self._vector_target(VECTOR_FRT2_OCIA)
            if target is not None:
                candidates.append((self._frt2_priority(), target, "frt2_ocia"))

        if not candidates:
            return
        priority, target, source = max(candidates, key=lambda item: item[0])
        if priority <= self._interrupt_mask():
            return
        if source == "interval_timer":
            self._interval_counter = 0
        elif source == "frt1_ocia":
            if self.frt1_ocia_steps is not None:
                self._frt1_ocia_counter = 0
                self.memory.write8(FRT1_TCSR, self.memory.read8(FRT1_TCSR) | FRT_TCSR_OCFA)
        elif source == "frt2_ocia":
            if self.frt2_ocia_steps is not None:
                self._frt2_ocia_counter = 0
                self.memory.write8(FRT2_TCSR, self.memory.read8(FRT2_TCSR) | FRT_TCSR_OCFA)
        self._enter_interrupt(target)

    def _enter_interrupt(self, target: int) -> None:
        self._push16(self.cpu.sr)
        self._push16(self.cpu.pc)
        self.cpu.pc = target & 0xFFFF
        self.cpu.interrupt_depth += 1

    def _return_from_interrupt(self) -> int:
        pc = self._pop16()
        self.cpu.sr = self._pop16()
        if self.cpu.interrupt_depth:
            self.cpu.interrupt_depth -= 1
        return pc

    def _vector_target(self, vector_address: int) -> int | None:
        entry = self.vectors.get(vector_address)
        return entry[1] if entry else None

    def _interrupt_mask(self) -> int:
        return (self.cpu.sr >> 8) & 0x07

    def _sci1_priority(self) -> int:
        return (self.memory.read8(IPRE) >> 4) & 0x07

    def _interval_priority(self) -> int:
        return (self.memory.read8(IPRA) >> 4) & 0x07

    def _frt1_ocia_pending(self) -> bool:
        if self.frt1_ocia_steps is None:
            return self.frt1_ocia.pending(self.memory)
        if self._frt1_ocia_counter < self.frt1_ocia_steps:
            return False
        return bool(self.memory.read8(FRT1_TCR) & FRT_TCR_OCIEA)

    def _frt1_priority(self) -> int:
        return (self.memory.read8(IPRC) >> 4) & 0x07

    def _frt2_ocia_pending(self) -> bool:
        if self.frt2_ocia_steps is None:
            return self.frt2_ocia.pending(self.memory)
        if self._frt2_ocia_counter < self.frt2_ocia_steps:
            return False
        return bool(self.memory.read8(FRT2_TCR) & FRT_TCR_OCIEA)

    def _frt2_priority(self) -> int:
        # H8/536 IPRC assigns bits 6..4 to FRT1 and bits 2..0 to FRT2;
        # the ROM's IPRC=H'66 therefore gives both timers priority 6.
        return self.memory.read8(IPRC) & 0x07

    def _push16(self, value: int) -> None:
        sp = (self.cpu.regs[7] - 2) & 0xFFFF
        self.cpu.regs[7] = sp
        self.memory.write16(sp, value)

    def _pop16(self) -> int:
        sp = self.cpu.regs[7] & 0xFFFF
        value = self.memory.read16(sp)
        self.cpu.regs[7] = (sp + 2) & 0xFFFF
        return value

    def _push_register_mask(self, mask_value: int) -> None:
        for reg in range(8):
            if mask_value & (1 << reg):
                self._push16(self.cpu.regs[reg])

    def _pop_register_mask(self, mask_value: int) -> None:
        for reg in reversed(range(8)):
            if mask_value & (1 << reg):
                self.cpu.regs[reg] = self._pop16()

    def _decode_ea(self, pc: int) -> dict[str, int | str | None]:
        first = self.memory.rom.u8(pc)
        if 0xA0 <= first <= 0xAF:
            return {"mode": "reg", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
        if 0xB0 <= first <= 0xBF:
            return {"mode": "predec", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
        if 0xC0 <= first <= 0xCF:
            return {"mode": "postinc", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
        if 0xD0 <= first <= 0xDF:
            return {"mode": "indirect", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
        if 0xE0 <= first <= 0xEF:
            return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 2, "value": s8(self.memory.rom.u8(pc + 1))}
        if 0xF0 <= first <= 0xFF:
            return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 3, "value": s16(self.memory.rom.u16(pc + 1))}
        if first in (0x04, 0x0C):
            size = 2 if first == 0x0C else 1
            value = self.memory.rom.u16(pc + 1) if size == 2 else self.memory.rom.u8(pc + 1)
            return {"mode": "imm", "reg": None, "size": size, "length": 3 if size == 2 else 2, "value": value}
        if first in (0x15, 0x1D):
            return {"mode": "abs16", "reg": None, "size": 2 if first == 0x1D else 1, "length": 3, "value": self.memory.rom.u16(pc + 1)}
        raise UnsupportedInstruction(pc, self.memory.rom.slice(pc, 1), H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)

    def _general_length(self, pc: int, ea: dict[str, int | str | None], op: int) -> int:
        length = int(ea["length"]) + 1
        if op == 0x00:
            return length + 1
        if op == 0x06:
            return length + 1
        if op in (0x07, 0x05):
            return length + 2
        if op == 0x04:
            return length + 1
        return length

    def _ea_address(self, ea: dict[str, int | str | None], size: int, *, write: bool = False) -> int:
        mode = ea["mode"]
        reg = ea.get("reg")
        if mode == "abs16":
            return int(ea["value"]) & 0xFFFF
        if mode == "indirect":
            return self.cpu.regs[int(reg)] & 0xFFFF
        if mode == "disp":
            return (self.cpu.regs[int(reg)] + int(ea["value"])) & 0xFFFF
        if mode == "predec":
            self.cpu.regs[int(reg)] = (self.cpu.regs[int(reg)] - size) & 0xFFFF
            return self.cpu.regs[int(reg)]
        if mode == "postinc":
            address = self.cpu.regs[int(reg)] & 0xFFFF
            if not write:
                self.cpu.regs[int(reg)] = (self.cpu.regs[int(reg)] + size) & 0xFFFF
            return address
        raise EmulatorError(f"EA mode {mode} does not have an address")

    def _read_ea(self, ea: dict[str, int | str | None], size: int) -> int:
        mode = ea["mode"]
        if mode == "imm":
            return int(ea["value"]) & mask(size)
        if mode == "reg":
            return self._reg_read(int(ea["reg"]), size)
        address = self._ea_address(ea, size)
        return self.memory.read16(address) if size == 2 else self.memory.read8(address)

    def _write_ea(self, ea: dict[str, int | str | None], value: int, size: int) -> None:
        if ea["mode"] == "reg":
            self._reg_write(int(ea["reg"]), value, size)
            return
        address = self._ea_address(ea, size, write=True)
        if size == 2:
            self.memory.write16(address, value)
        else:
            self.memory.write8(address, value)

    def _reg_read(self, reg: int, size: int) -> int:
        value = self.cpu.regs[reg] & 0xFFFF
        return value if size == 2 else value & 0xFF

    def _reg_write(self, reg: int, value: int, size: int) -> None:
        if size == 2:
            self.cpu.regs[reg] = value & 0xFFFF
        else:
            self.cpu.regs[reg] = (self.cpu.regs[reg] & 0xFF00) | (value & 0xFF)

    def _cmp(self, lhs: int, rhs: int, size: int) -> None:
        self._set_sub_flags(lhs, rhs, lhs - rhs, size)

    def _set_logic_flags(self, value: int, size: int) -> None:
        value &= mask(size)
        self.cpu.z = value == 0
        self.cpu.n = bool(value & sign_bit(size))
        self.cpu.v = False

    def _set_add_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
        max_value = mask(size)
        sign = sign_bit(size)
        result &= max_value
        lhs &= max_value
        rhs &= max_value
        self.cpu.z = result == 0
        self.cpu.n = bool(result & sign)
        self.cpu.c = lhs + rhs > max_value
        self.cpu.v = bool((~(lhs ^ rhs) & (lhs ^ result) & sign) != 0)

    def _set_sub_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
        max_value = mask(size)
        sign = sign_bit(size)
        result &= max_value
        lhs &= max_value
        rhs &= max_value
        self.cpu.z = result == 0
        self.cpu.n = bool(result & sign)
        self.cpu.c = lhs < rhs
        self.cpu.v = bool(((lhs ^ rhs) & (lhs ^ result) & sign) != 0)

    def _bit_operation(self, ea: dict[str, int | str | None], size: int, op_base: int, bit: int) -> None:
        value = self._read_ea(ea, size)
        bit &= 15 if size == 2 else 7
        bit_mask = 1 << bit
        self.cpu.z = not bool(value & bit_mask)
        if op_base == 0xC0:
            self._write_ea(ea, value | bit_mask, size)
        elif op_base == 0xD0:
            self._write_ea(ea, value & ~bit_mask, size)
        elif op_base == 0xE0:
            self._write_ea(ea, value ^ bit_mask, size)

    def _branch_condition(self, cond: int) -> bool:
        if cond == 0x0:
            return True
        if cond == 0x1:
            return False
        if cond == 0x2:
            return not self.cpu.c and not self.cpu.z
        if cond == 0x3:
            return self.cpu.c or self.cpu.z
        if cond == 0x4:
            return not self.cpu.c
        if cond == 0x5:
            return self.cpu.c
        if cond == 0x6:
            return not self.cpu.z
        if cond == 0x7:
            return self.cpu.z
        if cond == 0x8:
            return not self.cpu.v
        if cond == 0x9:
            return self.cpu.v
        if cond == 0xA:
            return not self.cpu.n
        if cond == 0xB:
            return self.cpu.n
        if cond == 0xC:
            return self.cpu.n == self.cpu.v
        if cond == 0xD:
            return self.cpu.n != self.cpu.v
        if cond == 0xE:
            return not self.cpu.z and self.cpu.n == self.cpu.v
        return self.cpu.z or self.cpu.n != self.cpu.v


def _format_optional_ms(value: float | None) -> str:
    return "unavailable" if value is None else f"{value:.3f}"
