# z80/core.py
import ctypes as C
import os
from typing import Callable, Optional

# --- načtení knihovny (Z80.dll) ---
_here = os.path.dirname(__file__)
_candidates = [
    os.path.join(_here, "Z80.dll"),          # v balíčku
    os.path.join(os.getcwd(), "Z80.dll"),    # v aktuálním adresáři
    "Z80.dll",                               # v PATH
]
_dll_path = next((p for p in _candidates if os.path.exists(p)), _candidates[-1])

# cdecl API -> CDLL
z80 = C.CDLL(_dll_path)

# --- typedefs z C API ---
class _Z80(C.Structure):  # struct Z80
    pass

Z80_p = C.POINTER(_Z80)

Z80Read    = C.CFUNCTYPE(C.c_uint8,  C.c_void_p, C.c_uint16)
Z80Write   = C.CFUNCTYPE(None,        C.c_void_p, C.c_uint16, C.c_uint8)
Z80Halt    = C.CFUNCTYPE(None,        C.c_void_p, C.c_uint8)
Z80Notify  = C.CFUNCTYPE(None,        C.c_void_p)
Z80Illegal = C.CFUNCTYPE(C.c_uint8,   Z80_p, C.c_uint8)

# --- pomocné unie/struktury (jen to, co je potřeba z hlavičky) ---
class _U8x2(C.Structure):
    _fields_ = [("at_0", C.c_uint8), ("at_1", C.c_uint8)]

class Z80RegisterPair(C.Union):
    _anonymous_ = ("uint8_values",)
    _fields_ = [
        ("uint16_value", C.c_uint16),
        ("uint8_array",  C.c_uint8 * 2),
        ("uint8_values", _U8x2),
    ]

class Z80InsnData(C.Union):
    _fields_ = [
        ("uint32_value", C.c_uint32),
        ("uint8_array",  C.c_uint8 * 4),
    ]

# --- kompletní layout struct Z80 (pořadí musí sedět s hlavičkou) ---
_Z80._fields_ = [
    ("cycles",       C.c_size_t),
    ("cycle_limit",  C.c_size_t),
    ("context",      C.c_void_p),

    ("fetch_opcode", Z80Read),
    ("fetch",        Z80Read),
    ("read",         Z80Read),
    ("write",        Z80Write),
    ("in_",          Z80Read),    # 'in' je keyword v Pythonu
    ("out",          Z80Write),
    ("halt",         Z80Halt),
    ("nop",          Z80Read),
    ("nmia",         Z80Read),
    ("inta",         Z80Read),
    ("int_fetch",    Z80Read),
    ("ld_i_a",       Z80Notify),
    ("ld_r_a",       Z80Notify),
    ("reti",         Z80Notify),
    ("retn",         Z80Notify),
    ("hook",         Z80Read),
    ("illegal",      Z80Illegal),

    ("data",         Z80InsnData),

    ("ix_iy",        Z80RegisterPair * 2),
    ("pc",           Z80RegisterPair),
    ("sp",           Z80RegisterPair),
    ("xy",           Z80RegisterPair),
    ("memptr",       Z80RegisterPair),
    ("af",           Z80RegisterPair),
    ("af_",          Z80RegisterPair),
    ("bc",           Z80RegisterPair),
    ("bc_",          Z80RegisterPair),
    ("de",           Z80RegisterPair),
    ("de_",          Z80RegisterPair),
    ("hl",           Z80RegisterPair),
    ("hl_",          Z80RegisterPair),

    ("r",            C.c_uint8),
    ("i",            C.c_uint8),
    ("r7",           C.c_uint8),
    ("im",           C.c_uint8),
    ("request",      C.c_uint8),
    ("resume",       C.c_uint8),
    ("iff1",         C.c_uint8),
    ("iff2",         C.c_uint8),
    ("q",            C.c_uint8),
    ("options",      C.c_uint8),
    ("int_line",     C.c_uint8),
    ("halt_line",    C.c_uint8),
]

# --- exportované C funkce ---
z80.z80_power.argtypes         = [Z80_p, C.c_bool]
z80.z80_power.restype          = None
z80.z80_instant_reset.argtypes = [Z80_p]
z80.z80_instant_reset.restype  = None
z80.z80_special_reset.argtypes = [Z80_p]
z80.z80_special_reset.restype  = None
z80.z80_int.argtypes           = [Z80_p, C.c_bool]
z80.z80_int.restype            = None
z80.z80_nmi.argtypes           = [Z80_p]
z80.z80_nmi.restype            = None
z80.z80_execute.argtypes       = [Z80_p, C.c_size_t]
z80.z80_execute.restype        = C.c_size_t
z80.z80_run.argtypes           = [Z80_p, C.c_size_t]
z80.z80_run.restype            = C.c_size_t

# --- constants (pro pohodlí – bity options/modely) ---
Z80_OPTIONS = {
    "OUT_VC_255":             1,
    "LD_A_IR_BUG":            2,
    "HALT_SKIP":              4,
    "XQ":                     8,
    "IM0_RETX_NOTIFICATIONS": 16,
    "YQ":                     32,
}
Z80_MODELS = {
    "ZILOG_NMOS": (Z80_OPTIONS["LD_A_IR_BUG"] | Z80_OPTIONS["XQ"] | Z80_OPTIONS["YQ"]),
    "ZILOG_CMOS": (Z80_OPTIONS["OUT_VC_255"]  | Z80_OPTIONS["XQ"] | Z80_OPTIONS["YQ"]),
    "NEC_NMOS":   (Z80_OPTIONS["LD_A_IR_BUG"]),
    "ST_CMOS":    (Z80_OPTIONS["OUT_VC_255"]  | Z80_OPTIONS["LD_A_IR_BUG"] | Z80_OPTIONS["YQ"]),
}

# --- registr instancí podle void* context ---
_ctx_registry: dict[int, "Z80CPU"] = {}

class Z80CPU:
    """
    OOP wrapper nad Z80.dll s možností připojit vlastní I/O/memory/notify callbacky.
    Použití: from z80 import Z80; cpu = Z80(); cpu.set_io_callbacks(...); cpu.execute(1000)
    """
    def __init__(self):
        self._cpu = _Z80()
        self._cpu_p = C.pointer(self._cpu)

        # RAM a default I/O
        self.ports_read: Callable[[int], int]  = lambda port: 0xFF
        self.ports_write: Callable[[int, int], None] = lambda port, val: None

        # držíme reference na CFUNCTYPE, aby je nesebral GC
        self._cb_refs: dict[str, object] = {}

        # registrace contextu -> instance
        ctx_id = id(self)
        _ctx_registry[ctx_id] = self
        self._cpu.context = C.c_void_p(ctx_id)

        # nastavení callbacků a voleb
        self._install_default_callbacks()
        self.options = Z80_MODELS["ZILOG_NMOS"]

        # zapnout a resetnout
        z80.z80_power(self._cpu_p, True)
        z80.z80_instant_reset(self._cpu_p)

    # --- veřejné API ---
    def execute(self, cycles: int) -> int:
        return int(z80.z80_execute(self._cpu_p, int(cycles)))

    def run(self, cycles: int) -> int:
        return int(z80.z80_run(self._cpu_p, int(cycles)))

    def request_int(self, level: bool):
        z80.z80_int(self._cpu_p, bool(level))

    def nmi(self):
        z80.z80_nmi(self._cpu_p)

    @property
    def options(self) -> int:
        return self._cpu.options.uint8_value

    @options.setter
    def options(self, value: int):
        self._cpu.pc.uint8_value = value & 0xFF

    @property
    def af(self) -> int:
        return self._cpu.af.uint16_value

    @af.setter
    def af(self, value: int):
        self._cpu.af.uint16_value = value & 0xFFFF

    @property
    def pc(self) -> int:
        return self._cpu.pc.uint16_value

    @pc.setter
    def pc(self, value: int):
        self._cpu.pc.uint16_value = value & 0xFFFF

    # --- settery na callbacky ---
    def set_io_callbacks(
        self,
        in_cb: Optional[Callable[[int], int]] = None,
        out_cb: Optional[Callable[[int, int], None]] = None,
    ):
        """Nastav vlastní I/O: in_cb(port)->byte, out_cb(port, byte)->None"""
        if in_cb is not None:
            self.ports_read = in_cb
        if out_cb is not None:
            self.ports_write = out_cb
        self._rebind_io_callbacks()

    def set_memory_callbacks(
        self,
        read_cb: Optional[Callable[[int], int]] = None,
        write_cb: Optional[Callable[[int, int], None]] = None,
        fetch_cb: Optional[Callable[[int], int]] = None,
        fetch_opcode_cb: Optional[Callable[[int], int]] = None,
    ):
        """Volitelné nahrazení RAM handlerů vlastním backendem."""
        if read_cb is not None:
            self._cb_refs["read"] = Z80Read(lambda ctx, a: read_cb(a) & 0xFF)
            self._cpu.read = self._cb_refs["read"]
        if write_cb is not None:
            self._cb_refs["write"] = Z80Write(lambda ctx, a, v: write_cb(a, v & 0xFF))
            self._cpu.write = self._cb_refs["write"]
        if fetch_cb is not None:
            self._cb_refs["fetch"] = Z80Read(lambda ctx, a: fetch_cb(a) & 0xFF)
            self._cpu.fetch = self._cb_refs["fetch"]
        if fetch_opcode_cb is not None:
            self._cb_refs["fetch_opcode"] = Z80Read(lambda ctx, a: fetch_opcode_cb(a) & 0xFF)
            self._cpu.fetch_opcode = self._cb_refs["fetch_opcode"]

    def set_notify_callbacks(self, **kwargs):
        """
        Pojmenované handlery (volitelné):
          ld_i_a, ld_r_a, reti, retn, halt, nmia, inta, int_fetch, hook, illegal
        Příklad:
          cpu.set_notify_callbacks(reti=lambda: print("RETI"))
        """
        mapping = {
            "ld_i_a":    ("ld_i_a",    Z80Notify,  lambda f: lambda ctx: f()),
            "ld_r_a":    ("ld_r_a",    Z80Notify,  lambda f: lambda ctx: f()),
            "reti":      ("reti",      Z80Notify,  lambda f: lambda ctx: f()),
            "retn":      ("retn",      Z80Notify,  lambda f: lambda ctx: f()),
            "halt":      ("halt",      Z80Halt,    lambda f: lambda ctx, st: f(st)),
            "nmia":      ("nmia",      Z80Read,    lambda f: lambda ctx, a: f(a) & 0xFF),
            "inta":      ("inta",      Z80Read,    lambda f: lambda ctx, a: f(a) & 0xFF),
            "int_fetch": ("int_fetch", Z80Read,    lambda f: lambda ctx, a: f(a) & 0xFF),
            "hook":      ("hook",      Z80Read,    lambda f: lambda ctx, a: f(a) & 0xFF),
            "illegal":   ("illegal",   Z80Illegal, lambda f: lambda cpu_p, op: f(self, op) & 0xFF),
        }
        for key, (field, ctype, wrap) in mapping.items():
            fn = kwargs.get(key)
            if fn is None:
                continue
            cb = ctype(wrap(fn))
            self._cb_refs[field] = cb
            setattr(self._cpu, field, cb)

    # --- interní nastavení výchozích callbacků ---
    def _install_default_callbacks(self):
        # RAM-based defaults
        self._cb_refs["fetch_opcode"] = Z80Read(lambda ctx, a: self._read_ram(a))
        self._cb_refs["fetch"]        = Z80Read(lambda ctx, a: self._read_ram(a))
        self._cb_refs["read"]         = Z80Read(lambda ctx, a: self._read_ram(a))
        self._cb_refs["write"]        = Z80Write(lambda ctx, a, v: self._write_ram(a, v))

        # I/O defaults
        self._rebind_io_callbacks()

        # ostatní notifikace/hooky jako stuby
        self._cb_refs["halt"]      = Z80Halt(lambda ctx, st: None)
        self._cb_refs["nmia"]      = Z80Read(lambda ctx, a: 0xFF)
        self._cb_refs["inta"]      = Z80Read(lambda ctx, a: 0xFF)
        self._cb_refs["int_fetch"] = Z80Read(lambda ctx, a: 0xFF)
        self._cb_refs["ld_i_a"]    = Z80Notify(lambda ctx: None)
        self._cb_refs["ld_r_a"]    = Z80Notify(lambda ctx: None)
        self._cb_refs["reti"]      = Z80Notify(lambda ctx: None)
        self._cb_refs["retn"]      = Z80Notify(lambda ctx: None)
        self._cb_refs["hook"]      = Z80Read(lambda ctx, a: 0x00)
        self._cb_refs["illegal"]   = Z80Illegal(lambda cpu_p, op: 0x00)

        # přiřazení do C struktury
        self._cpu.fetch_opcode = self._cb_refs["fetch_opcode"]
        self._cpu.fetch        = self._cb_refs["fetch"]
        self._cpu.read         = self._cb_refs["read"]
        self._cpu.write        = self._cb_refs["write"]
        self._cpu.in_          = self._cb_refs["in_"]
        self._cpu.out          = self._cb_refs["out"]
        self._cpu.halt         = self._cb_refs["halt"]
        self._cpu.nmia         = self._cb_refs["nmia"]
        self._cpu.inta         = self._cb_refs["inta"]
        self._cpu.int_fetch    = self._cb_refs["int_fetch"]
        self._cpu.ld_i_a       = self._cb_refs["ld_i_a"]
        self._cpu.ld_r_a       = self._cb_refs["ld_r_a"]
        self._cpu.reti         = self._cb_refs["reti"]
        self._cpu.retn         = self._cb_refs["retn"]
        self._cpu.hook         = self._cb_refs["hook"]
        self._cpu.illegal      = self._cb_refs["illegal"]

    def _rebind_io_callbacks(self):
        self._cb_refs["in_"] = Z80Read(lambda ctx, port: self._io_read(port))
        self._cb_refs["out"] = Z80Write(lambda ctx, port, val: self._io_write(port, val))
        self._cpu.in_ = self._cb_refs["in_"]
        self._cpu.out = self._cb_refs["out"]

    # --- low-level helpers ---
    def _read_ram(self, addr: int) -> int:
        return 0

    def _write_ram(self, addr: int, val: int):
        pass

    def _io_read(self, port: int) -> int:
        try:
            return int(self.ports_read(int(port) & 0xFF)) & 0xFF
        except Exception:
            return 0xFF

    def _io_write(self, port: int, val: int):
        try:
            self.ports_write(int(port) & 0xFF, int(val) & 0xFF)
        except Exception:
            pass
