"""Low-level ctypes bindings for libcp210xmanufacturing."""

import ctypes
from ctypes import POINTER, Structure, byref, c_char_p, c_int, c_ubyte, c_uint, c_ushort, c_void_p
from pathlib import Path

# Type aliases matching the C library
CP210x_STATUS = c_int
HANDLE = c_void_p
DWORD = c_uint
WORD = c_ushort
BYTE = c_ubyte
BOOL = c_int

# Return codes
CP210x_SUCCESS = 0x00
CP210x_DEVICE_NOT_FOUND = 0xFF
CP210x_INVALID_HANDLE = 0x01
CP210x_INVALID_PARAMETER = 0x02
CP210x_DEVICE_IO_FAILED = 0x03
CP210x_FUNCTION_NOT_SUPPORTED = 0x04
CP210x_GLOBAL_DATA_ERROR = 0x05
CP210x_FILE_ERROR = 0x06
CP210x_COMMAND_FAILED = 0x08
CP210x_INVALID_ACCESS_TYPE = 0x09

# Part numbers
PART_NUMBERS = {
    0x01: "CP2101",
    0x02: "CP2102",
    0x03: "CP2103",
    0x04: "CP2104",
    0x05: "CP2105",
    0x08: "CP2108",
    0x09: "CP2109",
    0x20: "CP2102N-QFN28",
    0x21: "CP2102N-QFN24",
    0x22: "CP2102N-QFN20",
}

# Part number groups for feature gating
PARTS_CP2102N = {0x20, 0x21, 0x22}
PARTS_WITH_FLUSH_CONFIG = {0x04, 0x05, 0x08}  # CP2104, CP2105, CP2108
PARTS_WITH_DEVICE_MODE = {0x05}  # CP2105 only
PARTS_WITH_INTERFACE_STRING = {0x05, 0x08}  # CP2105, CP2108
PARTS_WITH_PORT_CONFIG = {0x03, 0x04}  # CP2103, CP2104
PARTS_WITH_DUAL_PORT_CONFIG = {0x05}  # CP2105
PARTS_WITH_QUAD_PORT_CONFIG = {0x08}  # CP2108

# Buffer sizes
MAX_DEVICE_STRLEN = 256
MAX_MANUFACTURER_STRLEN = 45
MAX_PRODUCT_STRLEN = 126
MAX_SERIAL_STRLEN = 63
CP2105_MAX_INTERFACE_STRLEN = 32
CP2108_MAX_INTERFACE_STRLEN = 126

# GetProductString flags
RETURN_SERIAL_NUMBER = 0x00
RETURN_DESCRIPTION = 0x01
RETURN_FULL_PATH = 0x02

# Flush buffer config flags (CP2104)
FC_OPEN_TX = 0x01
FC_OPEN_RX = 0x02
FC_CLOSE_TX = 0x04
FC_CLOSE_RX = 0x08
# CP2105 standard port
FC_OPEN_TX_SCI = FC_OPEN_TX
FC_OPEN_RX_SCI = FC_OPEN_RX
FC_CLOSE_TX_SCI = FC_CLOSE_TX
FC_CLOSE_RX_SCI = FC_CLOSE_RX
# CP2105 enhanced port
FC_OPEN_TX_ECI = 0x10
FC_OPEN_RX_ECI = 0x20
FC_CLOSE_TX_ECI = 0x40
FC_CLOSE_RX_ECI = 0x80
# CP2108 per-interface
FC_OPEN_TX_IFC0 = 0x0001
FC_OPEN_RX_IFC0 = 0x0002
FC_CLOSE_TX_IFC0 = 0x0004
FC_CLOSE_RX_IFC0 = 0x0008
FC_OPEN_TX_IFC1 = 0x0010
FC_OPEN_RX_IFC1 = 0x0020
FC_CLOSE_TX_IFC1 = 0x0040
FC_CLOSE_RX_IFC1 = 0x0080
FC_OPEN_TX_IFC2 = 0x0100
FC_OPEN_RX_IFC2 = 0x0200
FC_CLOSE_TX_IFC2 = 0x0400
FC_CLOSE_RX_IFC2 = 0x0800
FC_OPEN_TX_IFC3 = 0x1000
FC_OPEN_RX_IFC3 = 0x2000
FC_CLOSE_TX_IFC3 = 0x4000
FC_CLOSE_RX_IFC3 = 0x8000

# Baud rate config
NUM_BAUD_CONFIGS = 32
BAUD_CONFIG_SIZE = 10

# Port config pin flags (CP2103/CP2104)
PORT_RI_ON = 0x0001
PORT_DCD_ON = 0x0002
PORT_DTR_ON = 0x0004
PORT_DSR_ON = 0x0008
PORT_TXD_ON = 0x0010
PORT_RXD_ON = 0x0020
PORT_RTS_ON = 0x0040
PORT_CTS_ON = 0x0080
PORT_GPIO_0_ON = 0x0100
PORT_GPIO_1_ON = 0x0200
PORT_GPIO_2_ON = 0x0400
PORT_GPIO_3_ON = 0x0800
PORT_SUSPEND_ON = 0x4000
PORT_SUSPEND_BAR_ON = 0x8000

# Enhanced function flags (CP2103/CP2104)
EF_GPIO_0_TXLED = 0x01
EF_GPIO_1_RXLED = 0x02
EF_GPIO_2_RS485 = 0x04
EF_RS485_INVERT = 0x08
EF_WEAKPULLUP = 0x10
EF_RESERVED_1 = 0x20
EF_SERIAL_DYNAMIC_SUSPEND = 0x40
EF_GPIO_DYNAMIC_SUSPEND = 0x80

# Flush buffer flag names for human-readable output
FLUSH_FLAGS_CP2104 = {
    FC_OPEN_TX: "open_tx",
    FC_OPEN_RX: "open_rx",
    FC_CLOSE_TX: "close_tx",
    FC_CLOSE_RX: "close_rx",
}

FLUSH_FLAGS_CP2105 = {
    FC_OPEN_TX_SCI: "open_tx_sci",
    FC_OPEN_RX_SCI: "open_rx_sci",
    FC_CLOSE_TX_SCI: "close_tx_sci",
    FC_CLOSE_RX_SCI: "close_rx_sci",
    FC_OPEN_TX_ECI: "open_tx_eci",
    FC_OPEN_RX_ECI: "open_rx_eci",
    FC_CLOSE_TX_ECI: "close_tx_eci",
    FC_CLOSE_RX_ECI: "close_rx_eci",
}

ENHANCED_FXN_FLAGS = {
    EF_GPIO_0_TXLED: "gpio0_txled",
    EF_GPIO_1_RXLED: "gpio1_rxled",
    EF_GPIO_2_RS485: "gpio2_rs485",
    EF_RS485_INVERT: "rs485_invert",
    EF_WEAKPULLUP: "weak_pullup",
    EF_SERIAL_DYNAMIC_SUSPEND: "serial_dynamic_suspend",
    EF_GPIO_DYNAMIC_SUSPEND: "gpio_dynamic_suspend",
}

PORT_PIN_FLAGS = {
    PORT_RI_ON: "RI",
    PORT_DCD_ON: "DCD",
    PORT_DTR_ON: "DTR",
    PORT_DSR_ON: "DSR",
    PORT_TXD_ON: "TXD",
    PORT_RXD_ON: "RXD",
    PORT_RTS_ON: "RTS",
    PORT_CTS_ON: "CTS",
    PORT_GPIO_0_ON: "GPIO0",
    PORT_GPIO_1_ON: "GPIO1",
    PORT_GPIO_2_ON: "GPIO2",
    PORT_GPIO_3_ON: "GPIO3",
    PORT_SUSPEND_ON: "SUSPEND",
    PORT_SUSPEND_BAR_ON: "SUSPEND_BAR",
}


def decode_bitmask(value: int, flag_map: dict) -> dict[str, bool]:
    """Decode a bitmask into a dict of named flags."""
    return {name: bool(value & bit) for bit, name in flag_map.items()}


def encode_bitmask(flags: dict[str, bool], flag_map: dict) -> int:
    """Encode a dict of named flags back into a bitmask."""
    name_to_bit = {name: bit for bit, name in flag_map.items()}
    value = 0
    for name, enabled in flags.items():
        if name in name_to_bit and enabled:
            value |= name_to_bit[name]
    return value


# --- ctypes Structures ---

class BAUD_CONFIG(Structure):
    _pack_ = 1
    _fields_ = [
        ("BaudGen", WORD),
        ("Timer0Reload", WORD),
        ("Prescaler", BYTE),
        ("_pad", BYTE),
        ("BaudRate", DWORD),
    ]


assert ctypes.sizeof(BAUD_CONFIG) == BAUD_CONFIG_SIZE

BAUD_CONFIG_DATA = BAUD_CONFIG * NUM_BAUD_CONFIGS


class PORT_CONFIG(Structure):
    _fields_ = [
        ("Mode", WORD),
        ("Reset_Latch", WORD),
        ("Suspend_Latch", WORD),
        ("EnhancedFxn", BYTE),
    ]


class DUAL_PORT_CONFIG(Structure):
    _fields_ = [
        ("Mode", WORD),
        ("Reset_Latch", WORD),
        ("Suspend_Latch", WORD),
        ("EnhancedFxn_ECI", BYTE),
        ("EnhancedFxn_SCI", BYTE),
        ("EnhancedFxn_Device", BYTE),
    ]


class QUAD_PORT_STATE(Structure):
    _fields_ = [
        ("Mode_PB0", WORD),
        ("Mode_PB1", WORD),
        ("Mode_PB2", WORD),
        ("Mode_PB3", WORD),
        ("Mode_PB4", WORD),
        ("LowPower_PB0", WORD),
        ("LowPower_PB1", WORD),
        ("LowPower_PB2", WORD),
        ("LowPower_PB3", WORD),
        ("LowPower_PB4", WORD),
        ("Latch_PB0", WORD),
        ("Latch_PB1", WORD),
        ("Latch_PB2", WORD),
        ("Latch_PB3", WORD),
        ("Latch_PB4", WORD),
    ]


class QUAD_PORT_CONFIG(Structure):
    _fields_ = [
        ("Reset_Latch", QUAD_PORT_STATE),
        ("Suspend_Latch", QUAD_PORT_STATE),
        ("IPDelay_IFC0", BYTE),
        ("IPDelay_IFC1", BYTE),
        ("IPDelay_IFC2", BYTE),
        ("IPDelay_IFC3", BYTE),
        ("EnhancedFxn_IFC0", BYTE),
        ("EnhancedFxn_IFC1", BYTE),
        ("EnhancedFxn_IFC2", BYTE),
        ("EnhancedFxn_IFC3", BYTE),
        ("EnhancedFxn_Device", BYTE),
        ("ExtClk0Freq", BYTE),
        ("ExtClk1Freq", BYTE),
        ("ExtClk2Freq", BYTE),
        ("ExtClk3Freq", BYTE),
    ]


class FIRMWARE_T(Structure):
    _fields_ = [
        ("major", BYTE),
        ("minor", BYTE),
        ("build", BYTE),
    ]


class CP210xError(Exception):
    """Exception raised for CP210x library errors."""

    ERROR_MESSAGES = {
        CP210x_DEVICE_NOT_FOUND: "Device not found",
        CP210x_INVALID_HANDLE: "Invalid handle",
        CP210x_INVALID_PARAMETER: "Invalid parameter",
        CP210x_DEVICE_IO_FAILED: "Device I/O failed",
        CP210x_FUNCTION_NOT_SUPPORTED: "Function not supported",
        CP210x_GLOBAL_DATA_ERROR: "Global data error",
        CP210x_FILE_ERROR: "File error",
        CP210x_COMMAND_FAILED: "Command failed",
        CP210x_INVALID_ACCESS_TYPE: "Invalid access type",
    }

    def __init__(self, status: int, context: str = ""):
        self.status = status
        msg = self.ERROR_MESSAGES.get(status, f"Unknown error 0x{status:02X}")
        if context:
            msg = f"{context}: {msg}"
        super().__init__(msg)


class CP210xLibrary:
    """Wrapper for the CP210x manufacturing library."""

    def __init__(self, lib_path: str | None = None):
        if lib_path:
            self._lib = ctypes.CDLL(lib_path)
        else:
            search_paths = [
                "/usr/lib/libcp210xmanufacturing.so",
                "/usr/local/lib/libcp210xmanufacturing.so",
                str(Path(__file__).parent.parent.parent / "AN721SW/Linux/LibrarySourcePackages/cp210xmanufacturing/build/lib/x86_64/libcp210xmanufacturing.so"),
            ]
            for path in search_paths:
                if Path(path).exists():
                    self._lib = ctypes.CDLL(path)
                    break
            else:
                raise FileNotFoundError(
                    "libcp210xmanufacturing.so not found. Install with: "
                    "cd aur/cp210xmanufacturing && makepkg -si"
                )

        self._setup_functions()

    def _setup_functions(self):
        """Set up function prototypes for all library functions."""
        lib = self._lib

        # --- Device enumeration ---
        lib.CP210x_GetNumDevices.argtypes = [POINTER(DWORD)]
        lib.CP210x_GetNumDevices.restype = CP210x_STATUS

        lib.CP210x_GetProductString.argtypes = [DWORD, c_void_p, DWORD]
        lib.CP210x_GetProductString.restype = CP210x_STATUS

        lib.CP210x_Open.argtypes = [DWORD, POINTER(HANDLE)]
        lib.CP210x_Open.restype = CP210x_STATUS

        lib.CP210x_Close.argtypes = [HANDLE]
        lib.CP210x_Close.restype = CP210x_STATUS

        # --- Getters (scalars) ---
        lib.CP210x_GetPartNumber.argtypes = [HANDLE, POINTER(BYTE)]
        lib.CP210x_GetPartNumber.restype = CP210x_STATUS

        lib.CP210x_GetDeviceVid.argtypes = [HANDLE, POINTER(WORD)]
        lib.CP210x_GetDeviceVid.restype = CP210x_STATUS

        lib.CP210x_GetDevicePid.argtypes = [HANDLE, POINTER(WORD)]
        lib.CP210x_GetDevicePid.restype = CP210x_STATUS

        lib.CP210x_GetDeviceManufacturerString.argtypes = [HANDLE, c_void_p, POINTER(BYTE), BOOL]
        lib.CP210x_GetDeviceManufacturerString.restype = CP210x_STATUS

        lib.CP210x_GetDeviceProductString.argtypes = [HANDLE, c_void_p, POINTER(BYTE), BOOL]
        lib.CP210x_GetDeviceProductString.restype = CP210x_STATUS

        lib.CP210x_GetDeviceSerialNumber.argtypes = [HANDLE, c_void_p, POINTER(BYTE), BOOL]
        lib.CP210x_GetDeviceSerialNumber.restype = CP210x_STATUS

        lib.CP210x_GetDeviceInterfaceString.argtypes = [HANDLE, BYTE, c_void_p, POINTER(BYTE), BOOL]
        lib.CP210x_GetDeviceInterfaceString.restype = CP210x_STATUS

        lib.CP210x_GetMaxPower.argtypes = [HANDLE, POINTER(BYTE)]
        lib.CP210x_GetMaxPower.restype = CP210x_STATUS

        lib.CP210x_GetSelfPower.argtypes = [HANDLE, POINTER(BOOL)]
        lib.CP210x_GetSelfPower.restype = CP210x_STATUS

        lib.CP210x_GetDeviceVersion.argtypes = [HANDLE, POINTER(WORD)]
        lib.CP210x_GetDeviceVersion.restype = CP210x_STATUS

        lib.CP210x_GetFlushBufferConfig.argtypes = [HANDLE, POINTER(WORD)]
        lib.CP210x_GetFlushBufferConfig.restype = CP210x_STATUS

        lib.CP210x_GetDeviceMode.argtypes = [HANDLE, POINTER(BYTE), POINTER(BYTE)]
        lib.CP210x_GetDeviceMode.restype = CP210x_STATUS

        lib.CP210x_GetLockValue.argtypes = [HANDLE, POINTER(BYTE)]
        lib.CP210x_GetLockValue.restype = CP210x_STATUS

        lib.CP210x_GetFirmwareVersion.argtypes = [HANDLE, POINTER(FIRMWARE_T)]
        lib.CP210x_GetFirmwareVersion.restype = CP210x_STATUS

        # --- Setters (scalars) ---
        lib.CP210x_SetVid.argtypes = [HANDLE, WORD]
        lib.CP210x_SetVid.restype = CP210x_STATUS

        lib.CP210x_SetPid.argtypes = [HANDLE, WORD]
        lib.CP210x_SetPid.restype = CP210x_STATUS

        lib.CP210x_SetManufacturerString.argtypes = [HANDLE, c_void_p, BYTE, BOOL]
        lib.CP210x_SetManufacturerString.restype = CP210x_STATUS

        lib.CP210x_SetProductString.argtypes = [HANDLE, c_void_p, BYTE, BOOL]
        lib.CP210x_SetProductString.restype = CP210x_STATUS

        lib.CP210x_SetSerialNumber.argtypes = [HANDLE, c_void_p, BYTE, BOOL]
        lib.CP210x_SetSerialNumber.restype = CP210x_STATUS

        lib.CP210x_SetInterfaceString.argtypes = [HANDLE, BYTE, c_void_p, BYTE, BOOL]
        lib.CP210x_SetInterfaceString.restype = CP210x_STATUS

        lib.CP210x_SetMaxPower.argtypes = [HANDLE, BYTE]
        lib.CP210x_SetMaxPower.restype = CP210x_STATUS

        lib.CP210x_SetSelfPower.argtypes = [HANDLE, BOOL]
        lib.CP210x_SetSelfPower.restype = CP210x_STATUS

        lib.CP210x_SetDeviceVersion.argtypes = [HANDLE, WORD]
        lib.CP210x_SetDeviceVersion.restype = CP210x_STATUS

        lib.CP210x_SetFlushBufferConfig.argtypes = [HANDLE, WORD]
        lib.CP210x_SetFlushBufferConfig.restype = CP210x_STATUS

        lib.CP210x_SetDeviceMode.argtypes = [HANDLE, BYTE, BYTE]
        lib.CP210x_SetDeviceMode.restype = CP210x_STATUS

        lib.CP210x_SetLockValue.argtypes = [HANDLE]
        lib.CP210x_SetLockValue.restype = CP210x_STATUS

        # --- Struct-based configs ---
        lib.CP210x_GetBaudRateConfig.argtypes = [HANDLE, POINTER(BAUD_CONFIG)]
        lib.CP210x_GetBaudRateConfig.restype = CP210x_STATUS

        lib.CP210x_SetBaudRateConfig.argtypes = [HANDLE, POINTER(BAUD_CONFIG)]
        lib.CP210x_SetBaudRateConfig.restype = CP210x_STATUS

        lib.CP210x_GetPortConfig.argtypes = [HANDLE, POINTER(PORT_CONFIG)]
        lib.CP210x_GetPortConfig.restype = CP210x_STATUS

        lib.CP210x_SetPortConfig.argtypes = [HANDLE, POINTER(PORT_CONFIG)]
        lib.CP210x_SetPortConfig.restype = CP210x_STATUS

        lib.CP210x_GetDualPortConfig.argtypes = [HANDLE, POINTER(DUAL_PORT_CONFIG)]
        lib.CP210x_GetDualPortConfig.restype = CP210x_STATUS

        lib.CP210x_SetDualPortConfig.argtypes = [HANDLE, POINTER(DUAL_PORT_CONFIG)]
        lib.CP210x_SetDualPortConfig.restype = CP210x_STATUS

        lib.CP210x_GetQuadPortConfig.argtypes = [HANDLE, POINTER(QUAD_PORT_CONFIG)]
        lib.CP210x_GetQuadPortConfig.restype = CP210x_STATUS

        lib.CP210x_SetQuadPortConfig.argtypes = [HANDLE, POINTER(QUAD_PORT_CONFIG)]
        lib.CP210x_SetQuadPortConfig.restype = CP210x_STATUS

        # --- Advanced / raw ---
        lib.CP210x_GetConfig.argtypes = [HANDLE, POINTER(BYTE), WORD]
        lib.CP210x_GetConfig.restype = CP210x_STATUS

        lib.CP210x_SetConfig.argtypes = [HANDLE, POINTER(BYTE), WORD]
        lib.CP210x_SetConfig.restype = CP210x_STATUS

        lib.CP210x_GetGeneric.argtypes = [HANDLE, POINTER(BYTE), WORD]
        lib.CP210x_GetGeneric.restype = CP210x_STATUS

        lib.CP210x_SetGeneric.argtypes = [HANDLE, POINTER(BYTE), WORD]
        lib.CP210x_SetGeneric.restype = CP210x_STATUS

        lib.CP210x_CreateHexFile.argtypes = [HANDLE, c_char_p]
        lib.CP210x_CreateHexFile.restype = CP210x_STATUS

        lib.CP210x_UpdateFirmware.argtypes = [HANDLE]
        lib.CP210x_UpdateFirmware.restype = CP210x_STATUS

        lib.CP210x_Reset.argtypes = [HANDLE]
        lib.CP210x_Reset.restype = CP210x_STATUS

    def _check_status(self, status: int, context: str = ""):
        if status != CP210x_SUCCESS:
            raise CP210xError(status, context)

    # --- Device enumeration ---

    def get_num_devices(self) -> int:
        num = DWORD()
        status = self._lib.CP210x_GetNumDevices(byref(num))
        self._check_status(status, "GetNumDevices")
        return num.value

    def get_product_string(self, device_index: int, flag: int = RETURN_DESCRIPTION) -> str:
        buf = ctypes.create_string_buffer(MAX_DEVICE_STRLEN)
        status = self._lib.CP210x_GetProductString(DWORD(device_index), buf, DWORD(flag))
        self._check_status(status, "GetProductString")
        return buf.value.decode('utf-8', errors='replace')

    def open(self, device_index: int) -> HANDLE:
        handle = HANDLE()
        status = self._lib.CP210x_Open(DWORD(device_index), byref(handle))
        self._check_status(status, f"Open device {device_index}")
        return handle

    def close(self, handle: HANDLE):
        status = self._lib.CP210x_Close(handle)
        self._check_status(status, "Close")

    # --- Scalar getters ---

    def get_part_number(self, handle: HANDLE) -> tuple[int, str]:
        part = BYTE()
        status = self._lib.CP210x_GetPartNumber(handle, byref(part))
        self._check_status(status, "GetPartNumber")
        return part.value, PART_NUMBERS.get(part.value, f"Unknown (0x{part.value:02X})")

    def get_device_vid(self, handle: HANDLE) -> int:
        vid = WORD()
        status = self._lib.CP210x_GetDeviceVid(handle, byref(vid))
        self._check_status(status, "GetDeviceVid")
        return vid.value

    def get_device_pid(self, handle: HANDLE) -> int:
        pid = WORD()
        status = self._lib.CP210x_GetDevicePid(handle, byref(pid))
        self._check_status(status, "GetDevicePid")
        return pid.value

    def get_manufacturer_string(self, handle: HANDLE) -> str:
        buf = ctypes.create_string_buffer(MAX_MANUFACTURER_STRLEN)
        length = BYTE()
        status = self._lib.CP210x_GetDeviceManufacturerString(handle, buf, byref(length), BOOL(1))
        self._check_status(status, "GetDeviceManufacturerString")
        return buf.value.decode('utf-8', errors='replace')

    def get_product_string_device(self, handle: HANDLE) -> str:
        buf = ctypes.create_string_buffer(MAX_PRODUCT_STRLEN)
        length = BYTE()
        status = self._lib.CP210x_GetDeviceProductString(handle, buf, byref(length), BOOL(1))
        self._check_status(status, "GetDeviceProductString")
        return buf.value.decode('utf-8', errors='replace')

    def get_serial_number(self, handle: HANDLE) -> str:
        buf = ctypes.create_string_buffer(MAX_SERIAL_STRLEN)
        length = BYTE()
        status = self._lib.CP210x_GetDeviceSerialNumber(handle, buf, byref(length), BOOL(1))
        self._check_status(status, "GetDeviceSerialNumber")
        return buf.value.decode('utf-8', errors='replace')

    def get_interface_string(self, handle: HANDLE, interface_number: int) -> str:
        buf = ctypes.create_string_buffer(CP2108_MAX_INTERFACE_STRLEN)
        length = BYTE()
        status = self._lib.CP210x_GetDeviceInterfaceString(
            handle, BYTE(interface_number), buf, byref(length), BOOL(1)
        )
        self._check_status(status, f"GetDeviceInterfaceString(ifc={interface_number})")
        return buf.value.decode('utf-8', errors='replace')

    def get_max_power(self, handle: HANDLE) -> int:
        power = BYTE()
        status = self._lib.CP210x_GetMaxPower(handle, byref(power))
        self._check_status(status, "GetMaxPower")
        return power.value

    def get_self_power(self, handle: HANDLE) -> bool:
        self_power = BOOL()
        status = self._lib.CP210x_GetSelfPower(handle, byref(self_power))
        self._check_status(status, "GetSelfPower")
        return bool(self_power.value)

    def get_device_version(self, handle: HANDLE) -> int:
        version = WORD()
        status = self._lib.CP210x_GetDeviceVersion(handle, byref(version))
        self._check_status(status, "GetDeviceVersion")
        return version.value

    def get_flush_buffer_config(self, handle: HANDLE) -> int:
        config = WORD()
        status = self._lib.CP210x_GetFlushBufferConfig(handle, byref(config))
        self._check_status(status, "GetFlushBufferConfig")
        return config.value

    def get_device_mode(self, handle: HANDLE) -> tuple[int, int]:
        eci = BYTE()
        sci = BYTE()
        status = self._lib.CP210x_GetDeviceMode(handle, byref(eci), byref(sci))
        self._check_status(status, "GetDeviceMode")
        return eci.value, sci.value

    def get_lock_value(self, handle: HANDLE) -> int:
        lock = BYTE()
        status = self._lib.CP210x_GetLockValue(handle, byref(lock))
        self._check_status(status, "GetLockValue")
        return lock.value

    def get_firmware_version(self, handle: HANDLE) -> tuple[int, int, int]:
        fw = FIRMWARE_T()
        status = self._lib.CP210x_GetFirmwareVersion(handle, byref(fw))
        self._check_status(status, "GetFirmwareVersion")
        return fw.major, fw.minor, fw.build

    # --- Scalar setters ---

    def set_vid(self, handle: HANDLE, vid: int):
        status = self._lib.CP210x_SetVid(handle, WORD(vid))
        self._check_status(status, "SetVid")

    def set_pid(self, handle: HANDLE, pid: int):
        status = self._lib.CP210x_SetPid(handle, WORD(pid))
        self._check_status(status, "SetPid")

    def set_manufacturer_string(self, handle: HANDLE, manufacturer: str):
        data = manufacturer.encode('utf-8')[:MAX_MANUFACTURER_STRLEN]
        status = self._lib.CP210x_SetManufacturerString(handle, data, BYTE(len(data)), BOOL(1))
        self._check_status(status, "SetManufacturerString")

    def set_product_string(self, handle: HANDLE, product: str):
        data = product.encode('utf-8')[:MAX_PRODUCT_STRLEN]
        status = self._lib.CP210x_SetProductString(handle, data, BYTE(len(data)), BOOL(1))
        self._check_status(status, "SetProductString")

    def set_serial_number(self, handle: HANDLE, serial: str):
        data = serial.encode('utf-8')[:MAX_SERIAL_STRLEN]
        status = self._lib.CP210x_SetSerialNumber(handle, data, BYTE(len(data)), BOOL(1))
        self._check_status(status, "SetSerialNumber")

    def set_interface_string(self, handle: HANDLE, interface_number: int, value: str):
        data = value.encode('utf-8')[:CP2108_MAX_INTERFACE_STRLEN]
        status = self._lib.CP210x_SetInterfaceString(
            handle, BYTE(interface_number), data, BYTE(len(data)), BOOL(1)
        )
        self._check_status(status, f"SetInterfaceString(ifc={interface_number})")

    def set_max_power(self, handle: HANDLE, power_2ma: int):
        if power_2ma > 250:
            raise ValueError("Max power cannot exceed 250 (500mA)")
        status = self._lib.CP210x_SetMaxPower(handle, BYTE(power_2ma))
        self._check_status(status, "SetMaxPower")

    def set_self_power(self, handle: HANDLE, self_powered: bool):
        status = self._lib.CP210x_SetSelfPower(handle, BOOL(1 if self_powered else 0))
        self._check_status(status, "SetSelfPower")

    def set_device_version(self, handle: HANDLE, version: int):
        status = self._lib.CP210x_SetDeviceVersion(handle, WORD(version))
        self._check_status(status, "SetDeviceVersion")

    def set_flush_buffer_config(self, handle: HANDLE, config: int):
        status = self._lib.CP210x_SetFlushBufferConfig(handle, WORD(config))
        self._check_status(status, "SetFlushBufferConfig")

    def set_device_mode(self, handle: HANDLE, eci_mode: int, sci_mode: int):
        status = self._lib.CP210x_SetDeviceMode(handle, BYTE(eci_mode), BYTE(sci_mode))
        self._check_status(status, "SetDeviceMode")

    def lock_device(self, handle: HANDLE):
        status = self._lib.CP210x_SetLockValue(handle)
        self._check_status(status, "SetLockValue")

    # --- Struct-based configs ---

    def get_baud_rate_config(self, handle: HANDLE) -> list[dict]:
        data = BAUD_CONFIG_DATA()
        status = self._lib.CP210x_GetBaudRateConfig(handle, data)
        self._check_status(status, "GetBaudRateConfig")
        return [
            {
                "index": i,
                "baud_gen": data[i].BaudGen,
                "timer0_reload": data[i].Timer0Reload,
                "prescaler": data[i].Prescaler,
                "baud_rate": data[i].BaudRate,
            }
            for i in range(NUM_BAUD_CONFIGS)
        ]

    def set_baud_rate_config(self, handle: HANDLE, configs: list[dict]):
        if len(configs) != NUM_BAUD_CONFIGS:
            raise ValueError(f"Expected {NUM_BAUD_CONFIGS} baud configs, got {len(configs)}")
        data = BAUD_CONFIG_DATA()
        for i, cfg in enumerate(configs):
            data[i].BaudGen = cfg["baud_gen"]
            data[i].Timer0Reload = cfg["timer0_reload"]
            data[i].Prescaler = cfg["prescaler"]
            data[i]._pad = 0
            data[i].BaudRate = cfg["baud_rate"]
        status = self._lib.CP210x_SetBaudRateConfig(handle, data)
        self._check_status(status, "SetBaudRateConfig")

    def get_port_config(self, handle: HANDLE) -> dict:
        cfg = PORT_CONFIG()
        status = self._lib.CP210x_GetPortConfig(handle, byref(cfg))
        self._check_status(status, "GetPortConfig")
        return {
            "mode": cfg.Mode,
            "reset_latch": cfg.Reset_Latch,
            "suspend_latch": cfg.Suspend_Latch,
            "enhanced_fxn": cfg.EnhancedFxn,
        }

    def set_port_config(self, handle: HANDLE, mode: int, reset_latch: int, suspend_latch: int, enhanced_fxn: int):
        cfg = PORT_CONFIG()
        cfg.Mode = mode
        cfg.Reset_Latch = reset_latch
        cfg.Suspend_Latch = suspend_latch
        cfg.EnhancedFxn = enhanced_fxn
        status = self._lib.CP210x_SetPortConfig(handle, byref(cfg))
        self._check_status(status, "SetPortConfig")

    def get_dual_port_config(self, handle: HANDLE) -> dict:
        cfg = DUAL_PORT_CONFIG()
        status = self._lib.CP210x_GetDualPortConfig(handle, byref(cfg))
        self._check_status(status, "GetDualPortConfig")
        return {
            "mode": cfg.Mode,
            "reset_latch": cfg.Reset_Latch,
            "suspend_latch": cfg.Suspend_Latch,
            "enhanced_fxn_eci": cfg.EnhancedFxn_ECI,
            "enhanced_fxn_sci": cfg.EnhancedFxn_SCI,
            "enhanced_fxn_device": cfg.EnhancedFxn_Device,
        }

    def set_dual_port_config(self, handle: HANDLE, mode: int, reset_latch: int,
                             suspend_latch: int, enhanced_fxn_eci: int,
                             enhanced_fxn_sci: int, enhanced_fxn_device: int):
        cfg = DUAL_PORT_CONFIG()
        cfg.Mode = mode
        cfg.Reset_Latch = reset_latch
        cfg.Suspend_Latch = suspend_latch
        cfg.EnhancedFxn_ECI = enhanced_fxn_eci
        cfg.EnhancedFxn_SCI = enhanced_fxn_sci
        cfg.EnhancedFxn_Device = enhanced_fxn_device
        status = self._lib.CP210x_SetDualPortConfig(handle, byref(cfg))
        self._check_status(status, "SetDualPortConfig")

    def _quad_port_state_to_dict(self, qps: QUAD_PORT_STATE) -> dict:
        return {
            f"pb{i}": {"mode": getattr(qps, f"Mode_PB{i}"),
                        "low_power": getattr(qps, f"LowPower_PB{i}"),
                        "latch": getattr(qps, f"Latch_PB{i}")}
            for i in range(5)
        }

    def get_quad_port_config(self, handle: HANDLE) -> dict:
        cfg = QUAD_PORT_CONFIG()
        status = self._lib.CP210x_GetQuadPortConfig(handle, byref(cfg))
        self._check_status(status, "GetQuadPortConfig")
        return {
            "reset_latch": self._quad_port_state_to_dict(cfg.Reset_Latch),
            "suspend_latch": self._quad_port_state_to_dict(cfg.Suspend_Latch),
            "ip_delay": [cfg.IPDelay_IFC0, cfg.IPDelay_IFC1, cfg.IPDelay_IFC2, cfg.IPDelay_IFC3],
            "enhanced_fxn": [cfg.EnhancedFxn_IFC0, cfg.EnhancedFxn_IFC1,
                             cfg.EnhancedFxn_IFC2, cfg.EnhancedFxn_IFC3],
            "enhanced_fxn_device": cfg.EnhancedFxn_Device,
            "ext_clk_freq": [cfg.ExtClk0Freq, cfg.ExtClk1Freq,
                             cfg.ExtClk2Freq, cfg.ExtClk3Freq],
        }

    def set_quad_port_config(self, handle: HANDLE, config_dict: dict):
        cfg = QUAD_PORT_CONFIG()

        for state_name in ("reset_latch", "suspend_latch"):
            state = getattr(cfg, state_name.title().replace("_l", "_L"))
            src = config_dict[state_name]
            for i in range(5):
                pb = src[f"pb{i}"]
                setattr(state, f"Mode_PB{i}", pb["mode"])
                setattr(state, f"LowPower_PB{i}", pb["low_power"])
                setattr(state, f"Latch_PB{i}", pb["latch"])

        for i in range(4):
            setattr(cfg, f"IPDelay_IFC{i}", config_dict["ip_delay"][i])
            setattr(cfg, f"EnhancedFxn_IFC{i}", config_dict["enhanced_fxn"][i])

        cfg.EnhancedFxn_Device = config_dict["enhanced_fxn_device"]

        for i in range(4):
            setattr(cfg, f"ExtClk{i}Freq", config_dict["ext_clk_freq"][i])

        status = self._lib.CP210x_SetQuadPortConfig(handle, byref(cfg))
        self._check_status(status, "SetQuadPortConfig")

    # --- Advanced / raw ---

    def get_config(self, handle: HANDLE, size: int = 512) -> bytes:
        buf = (BYTE * size)()
        status = self._lib.CP210x_GetConfig(handle, buf, WORD(size))
        self._check_status(status, "GetConfig")
        return bytes(buf)

    def set_config(self, handle: HANDLE, data: bytes):
        buf = (BYTE * len(data))(*data)
        status = self._lib.CP210x_SetConfig(handle, buf, WORD(len(data)))
        self._check_status(status, "SetConfig")

    def get_generic(self, handle: HANDLE, size: int = 512) -> bytes:
        buf = (BYTE * size)()
        status = self._lib.CP210x_GetGeneric(handle, buf, WORD(size))
        self._check_status(status, "GetGeneric")
        return bytes(buf)

    def set_generic(self, handle: HANDLE, data: bytes):
        buf = (BYTE * len(data))(*data)
        status = self._lib.CP210x_SetGeneric(handle, buf, WORD(len(data)))
        self._check_status(status, "SetGeneric")

    def create_hex_file(self, handle: HANDLE, filename: str):
        status = self._lib.CP210x_CreateHexFile(handle, filename.encode('utf-8'))
        self._check_status(status, "CreateHexFile")

    def update_firmware(self, handle: HANDLE):
        status = self._lib.CP210x_UpdateFirmware(handle)
        self._check_status(status, "UpdateFirmware")

    def reset(self, handle: HANDLE):
        status = self._lib.CP210x_Reset(handle)
        self._check_status(status, "Reset")


class CP210xDevice:
    """Context manager for safe device access."""

    def __init__(self, lib: CP210xLibrary, device_index: int):
        self.lib = lib
        self.device_index = device_index
        self.handle = None
        self._part_code: int | None = None

    def __enter__(self):
        self.handle = self.lib.open(self.device_index)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.handle:
            self.lib.close(self.handle)
            self.handle = None
        return False

    def _get_part_code(self) -> int:
        if self._part_code is None:
            self._part_code = self.lib.get_part_number(self.handle)[0]
        return self._part_code

    def _require_part(self, allowed: set[int], feature: str):
        code = self._get_part_code()
        if code not in allowed:
            name = PART_NUMBERS.get(code, f"0x{code:02X}")
            allowed_names = ", ".join(PART_NUMBERS.get(c, f"0x{c:02X}") for c in sorted(allowed))
            raise CP210xError(
                CP210x_FUNCTION_NOT_SUPPORTED,
                f"{feature} not supported on {name} (requires {allowed_names})",
            )

    @property
    def part_number(self) -> tuple[int, str]:
        return self.lib.get_part_number(self.handle)

    @property
    def vid(self) -> int:
        return self.lib.get_device_vid(self.handle)

    @property
    def pid(self) -> int:
        return self.lib.get_device_pid(self.handle)

    @property
    def manufacturer(self) -> str:
        return self.lib.get_manufacturer_string(self.handle)

    @manufacturer.setter
    def manufacturer(self, value: str):
        self.lib.set_manufacturer_string(self.handle, value)

    @property
    def product(self) -> str:
        return self.lib.get_product_string_device(self.handle)

    @product.setter
    def product(self, value: str):
        self.lib.set_product_string(self.handle, value)

    @property
    def serial_number(self) -> str:
        return self.lib.get_serial_number(self.handle)

    @serial_number.setter
    def serial_number(self, value: str):
        self.lib.set_serial_number(self.handle, value)

    @property
    def max_power_ma(self) -> int:
        return self.lib.get_max_power(self.handle) * 2

    @max_power_ma.setter
    def max_power_ma(self, value: int):
        self.lib.set_max_power(self.handle, value // 2)

    @property
    def self_powered(self) -> bool:
        return self.lib.get_self_power(self.handle)

    @self_powered.setter
    def self_powered(self, value: bool):
        self.lib.set_self_power(self.handle, value)

    @property
    def device_version(self) -> str:
        v = self.lib.get_device_version(self.handle)
        return f"{(v >> 8) & 0xFF}.{v & 0xFF:02d}"

    @device_version.setter
    def device_version(self, value: int):
        self.lib.set_device_version(self.handle, value)

    @property
    def is_locked(self) -> bool:
        return self.lib.get_lock_value(self.handle) != 0

    @property
    def firmware_version(self) -> str | None:
        """Firmware version string (CP2102N only). Returns None if not supported."""
        try:
            major, minor, build = self.lib.get_firmware_version(self.handle)
            return f"{major}.{minor}.{build}"
        except CP210xError:
            return None

    @property
    def flush_buffer_config(self) -> int | None:
        """Raw flush buffer config word. Returns None if not supported."""
        try:
            return self.lib.get_flush_buffer_config(self.handle)
        except CP210xError:
            return None

    @property
    def device_mode(self) -> tuple[int, int] | None:
        """(ECI mode, SCI mode) for CP2105. Returns None if not supported."""
        try:
            return self.lib.get_device_mode(self.handle)
        except CP210xError:
            return None

    def get_interface_string(self, interface_number: int) -> str:
        self._require_part(PARTS_WITH_INTERFACE_STRING, "Interface strings")
        return self.lib.get_interface_string(self.handle, interface_number)

    def set_interface_string(self, interface_number: int, value: str):
        self._require_part(PARTS_WITH_INTERFACE_STRING, "Interface strings")
        self.lib.set_interface_string(self.handle, interface_number, value)

    def reset(self):
        self.lib.reset(self.handle)
