from typing import List, Dict, Tuple
from enum import Enum

class MemoryProtection(Enum):
    NoAccess = ...
    ReadOnly = ...
    ReadWrite = ...
    ExecuteOnly = ...
    ExecuteRead = ...
    ExecuteReadWrite = ...

class MemoryExceptionCode(Enum):
    Unallocated = ...
    Unmapped = ...
    UnmappedRegisters = ...
    Uninitialized = ...
    ReadViolation = ...
    WriteViolation = ...
    ExecViolation = ...
    ReadWatch = ...
    WriteWatch = ...
    Unaligned = ...
    OutOfMemory = ...
    SelfModifyingCode = ...
    AddressOverflow = ...
    Unknown = ...

class RunStatus(Enum):
    Running = ...
    InstructionLimit = ...
    Breakpoint = ...
    Interrupted = ...
    Halt = ...
    Killed = ...
    Deadlock = ...
    OutOfMemory = ...
    Unimplemented = ...
    UnhandledException = ...

class ExceptionCode(Enum):
    NoException = ...
    InstructionLimit = ...
    Halt = ...
    Sleep = ...
    Syscall = ...
    CpuStateChanged = ...
    DivisionException = ...
    ReadUnmapped = ...
    ReadPerm = ...
    ReadUnaligned = ...
    ReadWatch = ...
    ReadUninitialized = ...
    WriteUnmapped = ...
    WritePerm = ...
    WriteWatch = ...
    WriteUnaligned = ...
    ExecViolation = ...
    SelfModifyingCode = ...
    OutOfMemory = ...
    AddressOverflow = ...
    InvalidInstruction = ...
    UnknownInterrupt = ...
    UnknownCpuID = ...
    InvalidOpSize = ...
    InvalidFloatSize = ...
    CodeNotTranslated = ...
    ShadowStackOverflow = ...
    ShadowStackInvalid = ...
    InvalidTarget = ...
    UnimplementedOp = ...
    ExternalAddr = ...
    Environment = ...
    JitError = ...
    InternalError = ...
    UnmappedRegister = ...
    UnknownError = ...

class Icicle:
    def __init__(self, architecture: str, *,
                 jit = True,
                 jit_mem = True,
                 shadow_stack = False,
                 recompilation = True,
                 track_uninitialized = False,
                 optimize_instructions = True,
                 optimize_block = False,
                 tracing = False,
                 ) -> None: ...

    @property
    def architecture(self) -> str: ...

    @property
    def exception_code(self) -> ExceptionCode: ...

    @property
    def exception_value(self) -> int: ...

    @property
    def exception_size(self) -> int: ...

    @property
    def exception_data(self) -> bytes: ...

    icount: int

    icount_limit: int

    pc: int

    sp: int

    """
    Physical memory capacity in pages (adjust when seeing OutOfMemory)
    The default limit is set so that the maximum corresponds to ~400 MB of host memory.
    """
    mem_capacity: int

    # TODO: API to get memory information?

    def mem_map(self, address: int, size: int, protection: MemoryProtection): ...

    def mem_unmap(self, address: int, size: int): ...

    def mem_protect(self, address: int, size: int, protection: MemoryProtection): ...

    def mem_read(self, address: int, size: int) -> bytes: ...

    def mem_write(self, address: int, data: bytes) -> None: ...

    def reg_list(self) -> Dict[str, Tuple[int, int]]: ...

    def reg_offset(self, name: str) -> int: ...

    def reg_size(self, name: str) -> int: ...

    def reg_read(self, name: str) -> int: ...

    def reg_write(self, name: str, value: int) -> None: ...

    def reset(self): ...

    def run(self) -> RunStatus: ...

    def run_until(self, address: int) -> RunStatus: ...

    def step(self, count: int) -> RunStatus: ...

    def add_breakpoint(self, address: int) -> bool: ...

    def remove_breakpoint(self, address: int) -> bool: ...

def architectures() -> List[str]: ...

class MemoryException(Exception):
    def __init__(self, message: str, code: MemoryExceptionCode, size: int = 0, data: bytes = b""):
        super().__init__(message)
        self.code = code
        self.size = size
        self.data = data

    def __str__(self):
        data_hex = self.data.hex() if self.data else "none"
        return f"{super().__str__()}: {self.code} (size={self.size}, data={data_hex})"

def __ghidra_init():
    import os
    for path in __path__ + [os.getenv("GHIDRA_SRC", ".")]:
        processors_dir = os.path.join(path, "Ghidra", "Processors")
        if os.path.isdir(processors_dir):
            os.putenv("GHIDRA_SRC", path)
            os.environ["GHIDRA_SRC"] = path
            return
    raise FileNotFoundError("Ghidra processor definitions not found")

__ghidra_init()

# NOTE: This overrides the stubs at runtime with the actual implementation
from .icicle import *
