try:
    import hid
except ModuleNotFoundError:
    import sys
    print("\nError: 'hidapi' package not found.", file=sys.stderr)
    print("iowp requires 'hidapi' to interact with IO-Warrior devices.", file=sys.stderr)
    print("\nWe recommend using a virtual environment (venv):", file=sys.stderr)
    print("\n1. Create a venv:", file=sys.stderr)
    print("   python3 -m venv .venv", file=sys.stderr)
    print("\n2. Activate the venv:", file=sys.stderr)
    print("   - MacOS/Linux:  source .venv/bin/activate", file=sys.stderr)
    print("   - Windows:      .venv\\Scripts\\activate", file=sys.stderr)
    print("\n3. Install the required package:", file=sys.stderr)
    print("   pip install hidapi", file=sys.stderr)
    print("\nThen run your script again.\n", file=sys.stderr)
    sys.exit(1)
import ctypes
import threading
import time

# IOWkit Constants
IOWKIT_VENDOR_ID = 0x07c0

# Product IDs
IOWKIT_PRODUCT_ID_IOW40 = 0x1500
IOWKIT_PRODUCT_ID_IOW24 = 0x1501
IOWKIT_PRODUCT_ID_IOWPV1 = 0x1511
IOWKIT_PRODUCT_ID_IOWPV2 = 0x1512
IOWKIT_PRODUCT_ID_IOW_SENSI1 = 0x158a
IOWKIT_PRODUCT_ID_IOW56 = 0x1503
IOWKIT_PRODUCT_ID_IOW28 = 0x1504
IOWKIT_PRODUCT_ID_IOW28L = 0x1505
IOWKIT_PRODUCT_ID_IOW100 = 0x1506

# Pipe Names
IOW_PIPE_IO_PINS = 0
IOW_PIPE_SPECIAL_MODE = 1
IOW_PIPE_I2C_MODE = 2
IOW_PIPE_ADC_MODE = 3

# Report Sizes (Basic mapping, details depend on device)
IOWKIT_REPORT_SIZE = 5 # Generic 1 + 4 bytes
IOWKIT40_IO_REPORT_SIZE = 5
IOWKIT24_IO_REPORT_SIZE = 3
IOWKIT56_IO_REPORT_SIZE = 8
IOWKIT28_IO_REPORT_SIZE = 5
IOWKIT100_IO_REPORT_SIZE = 13

# Global definitions to manage state
_devices = []
_handles = {}

class IowKitDevice:
    def __init__(self, device_infos):
        # device_infos is a list of dicts for all interfaces of this device
        if not device_infos:
            raise ValueError("No device info provided")
            
        # We take the first one for general info (PID, VID, Serial should be same)
        self.info = device_infos[0]
        self.device_infos = device_infos # Store all for opening
        
        self.handles = {} # Map pipe_id (int) -> hid.device()
        
        self.pid = self.info['product_id']
        self.vid = self.info['vendor_id']
        self.serial = self.info['serial_number']
        self.revision = self.info['release_number']
        self.timeout = 1000 # Default timeout
        self.write_timeout = 1000 # Default write timeout
        
    def open(self):
        if self.handles:
            return True
            
        success_count = 0
        for info in self.device_infos:
            path = info['path']
            # Interface number usually maps to Pipe ID.
            # If interface_number is missing, assume 0.
            pipe_id = info.get('interface_number', 0)
            
            # If we already have a handle for this pipe_id, skip or close it first
            if pipe_id in self.handles:
                try:
                    self.handles[pipe_id].close()
                except:
                    pass
                    
            try:
                h = hid.device()
                h.open_path(path)
                h.set_nonblocking(0)
                self.handles[pipe_id] = h
                success_count += 1
            except Exception as e:
                # Pipe open failure - common if old process is still holding it 
                # or permission issues.
                print(f"Error opening pipe {pipe_id} path {path} (Serial={self.serial}): {e}")
        
        return success_count > 0

    def close(self):
        for h in self.handles.values():
            h.close()
        self.handles = {}

def IowKitOpenDevice():
    """
    Opens all detected IO-Warrior devices and returns a handle to the first one.
    Groups interfaces by Serial Number to form logical devices.
    """
    global _devices
    
    # CRITICAL: Close and clear existing devices first to avoid "open failed"
    # errors when re-opening the same HID paths on some OSs (like macOS).
    for dev in _devices:
        dev.close()
    _devices = []
    
    # Enumerate all Code Mercenaries devices
    device_list = hid.enumerate(IOWKIT_VENDOR_ID)
    
    # Group by Serial Number
    grouped_devices = {}
    
    for dev_info in device_list:
        serial = dev_info.get('serial_number')
        if not serial:
            # Fallback if no serial? IOW devices usually have one.
            # If not, maybe use path but path is unique per interface.
            # We skip devices without serial for reliable grouping or treat as unique.
            # Let's treat as separate if no serial (unlikely for IOW).
            continue
            
        if serial not in grouped_devices:
            grouped_devices[serial] = []
        grouped_devices[serial].append(dev_info)
    
    # Create IowKitDevice objects
    for serial, infos in grouped_devices.items():
        dev = IowKitDevice(infos)
        _devices.append(dev)
    
    # Open all devices
    opened_devices = []
    for dev in _devices:
        if dev.open():
            opened_devices.append(dev)
        else:
            print(f"Warning: Failed to open any pipes for device {dev.serial}. Is it already in use?")
            
    if not opened_devices:
        return None
        
    return opened_devices[0] # Return the first device object as the handle

def IowKitCloseDevice(devHandle):
    """
    Closes the device.
    """
    global _devices
    
    if devHandle in _devices:
        devHandle.close()
        # We don't remove from _devices list to match potential re-open logic or just state management
        pass

    # If None passed, close all (Legacy behavior support)
    if devHandle is None:
        for dev in _devices:
             dev.close()
        _devices = []

def IowKitWrite(devHandle, numPipe, buffer, length):
    """
    Writes to the device.
    devHandle: The device instance.
    numPipe: The pipe ID (IOW_PIPE_*).
    buffer: list or bytes to write.
    length: Number of bytes to write.
    """
    if not devHandle or not devHandle.handles:
        return 0
        
    # Get the specific handle for the requested pipe
    handle = devHandle.handles.get(numPipe)
    if not handle:
        # If pipe 1 (Special) is requested but not found, maybe strict error or return 0
        return 0
    
    # Report ID mapping
    # ... (same comments as before) ...
    
    data = []
    if isinstance(buffer, (bytes, bytearray)):
        data = list(buffer)
    elif isinstance(buffer, list):
        data = buffer
    else:
        pass
        
    if length > 0:
        # write requires a list of integers
        try:
             count = handle.write(data)
             return count
        except Exception as e:
            print(f"Write error on pipe {numPipe}: {e}")
            return 0
            
    return 0

def IowKitRead(devHandle, numPipe, buffer, length):
    """
    Reads from the device (blocking).
    """
    if not devHandle or not devHandle.handles:
        return 0
        
    handle = devHandle.handles.get(numPipe)
    if not handle:
        return 0
        
    try:
        # Use stored timeout
        timeout = getattr(devHandle, 'timeout', 1000)
        data = handle.read(length, timeout_ms=timeout)
        if data:
            read_len = len(data)
            if isinstance(buffer, (bytearray, list)):
                for i in range(min(len(buffer), read_len)):
                    # Explicit casting or assignment to handle different buffer types
                    if isinstance(buffer, (bytearray, list)):
                        buffer[i] = int(data[i])
            
            return read_len
    except Exception as e:
        # print(f"Read error on pipe {numPipe}: {e}")
        return 0
    return 0

def IowKitReadNonBlocking(devHandle, numPipe, buffer, length):
    """
    Reads from the device (non-blocking).
    """
    if not devHandle or not devHandle.handles:
        return 0
    
    handle = devHandle.handles.get(numPipe)
    if not handle:
        return 0

    try:
        data = handle.read(length, timeout_ms=0)
        if data:
            read_len = len(data)
            if isinstance(buffer, (bytearray, list)):
                for i in range(min(len(buffer), read_len)):
                    if isinstance(buffer, (bytearray, list)):
                        buffer[i] = int(data[i])
            return read_len
    except Exception as e:
        return 0
    return 0
    
def IowKitReadImmediate(devHandle, value_ptr):
    """
    Reads the current state of IO pins (cached).
    value_ptr: A ctypes buffer or object with a .value attribute to receive the 32-bit dword.
    """
    if not devHandle:
        return False
        
    # In a full implementation, we'd return the last cached state from a background thread.
    # Since we are a wrapper, we might not have a background thread HERE.
    # However, the iowp library maintains the state in the IowDevice objects.
    # But iowkit.py should be standalone.
    
    # Try a quick non-blocking read to get the latest state if possible
    # but that might consume a report intended for IowKitRead.
    # The real IOWkit DLL has a internal buffer.
    
    # For now, return False or if we have it cached in devHandle (we don't yet).
    return False

def IowKitGetNumDevs():
    """ Returns the number of successfully opened devices. """
    return len(_devices)

def IowKitGetDeviceHandle(numDevice):
    """ Returns the handle for the Nth device (1-based index usually). """
    # DLL usually maps 1..N. Let's check.
    # "GetDeviceHandle(ULONG numDevice)" where numDevice is 1..IowKitGetNumDevs() usually inside the loop.
    # Wait, typical iteration is 1 to GetNumDevs().
    
    idx = numDevice - 1
    if 0 <= idx < len(_devices):
        return _devices[idx]
    return None

def IowKitGetProductId(devHandle):
    if devHandle:
        return devHandle.pid
    return 0

def IowKitGetRevision(devHandle):
    if devHandle:
        return devHandle.revision
    return 0

def IowKitGetSerialNumber(devHandle, serialNumber):
    """
    Fills serialNumber with the wide string.
    """
    if devHandle and devHandle.serial:
        # serialNumber is expected to be a buffer.
        # In python, we might just return the string or fill a ctypes c_wchar_p?
        # Assuming ctypes buffer:
        try:
             # If it's a ctypes array
             val = devHandle.serial
             # We can't easily fill a python string in-place.
             # If user passed a ctypes buffer:
             if hasattr(serialNumber, 'value'):
                 serialNumber.value = val
             # If it's a list...
             return True
        except:
             pass
    return False

    return False

def IowKitSetTimeout(devHandle, timeout):
    """
    Sets the read timeout in milliseconds.
    """
    if not devHandle or not devHandle.handles:
        return False
    # hidapi doesn't store timeout on device handle for 'read' function usually, 
    # it is passed to read().
    # We can store it in our wrapper object.
    devHandle.timeout = timeout
    return True

def IowKitSetWriteTimeout(devHandle, timeout):
    """
    Sets the write timeout in milliseconds.
    """
    if not devHandle or not devHandle.handles:
        return False
    devHandle.write_timeout = timeout
    return True

def IowKitCancelIo(devHandle, numPipe):
    """
    Cancels pending IO.
    """
    # Not easily supported in hidapi cross-platform without closing/reopening or threading.
    # We return True to pretend success or False if critical.
    # Check if device matches
    if not devHandle or not devHandle.handles:
        return False
    return True

def IowKitVersion():
    return b"IOWKit Python Wrapper 1.0"
