"""
Device detection module for ESPal

This module provides functionality to:
- Detect ESP32 devices connected via USB
- Monitor connection state
- Handle reconnection after resets
"""

import os
import time
import serial
import serial.tools.list_ports


class DeviceDetector:
    """Handles ESP32 device detection and connection monitoring"""
    
    # Common ESP32 USB identifiers
    ESP_VENDORS = {
        "10C4": "Silicon Labs",  # CP210x USB to UART Bridge
        "1A86": "QinHeng Electronics",  # CH340 Serial
        "0403": "FTDI",  # FTDI FT232
        "0483": "STMicroelectronics",  # ESP32-DevKitC 
        "303A": "Espressif"  # Directly by Espressif
    }
    
    def __init__(self, callback=None, specific_device=None):
        """
        Initialize device detector
        
        Args:
            callback: Function to call when device connection state changes
            specific_device: Specific device path to use instead of auto-detection
        """
        self.callback = callback
        self.specific_device = specific_device
        self.current_device = None
        self.monitoring = False
        self._monitoring_thread = None
    
    def find_device_by_vidpid(self, vid, pid):
        """
        Find device by specific VID:PID

        Args:
            vid: Vendor ID as hex string (e.g., "10c4")
            pid: Product ID as hex string (e.g., "ea60")

        Returns:
            str: Device path if found, None otherwise
        """
        if not vid or not pid:
            return None

        # Normalize VID/PID to lowercase
        vid = vid.lower()
        pid = pid.lower()

        ports = list(serial.tools.list_ports.comports())
        for port in ports:
            # Check if this port matches the VID/PID
            if hasattr(port, 'vid') and hasattr(port, 'pid') and port.vid and port.pid:
                port_vid = f"{port.vid:04x}"
                port_pid = f"{port.pid:04x}"

                if port_vid == vid and port_pid == pid:
                    # Try to open the port to verify it's accessible
                    try:
                        with serial.Serial(port.device, baudrate=115200, timeout=0.1) as ser:
                            # Successfully opened the port
                            self.current_device = port.device
                            return port.device
                    except (serial.SerialException, OSError):
                        # Can't open the port, it might be in use
                        # But still return it as it matches VID/PID
                        self.current_device = port.device
                        return port.device

        return None

    def find_device(self, vid=None, pid=None):
        """
        Find ESP32 device connected via USB

        Args:
            vid: Optional vendor ID to filter by (hex string)
            pid: Optional product ID to filter by (hex string)

        Returns:
            str: Device path if found, None otherwise
        """
        # If VID/PID specified, use specific detection
        if vid and pid:
            device = self.find_device_by_vidpid(vid, pid)
            if device:
                return device

        # Check for specific device first if provided
        if self.specific_device:
            if os.path.exists(self.specific_device):
                self.current_device = self.specific_device
                return self.specific_device

        # If we have a current device and it's still connected, keep using it
        if self.current_device and self.is_device_connected(self.current_device):
            return self.current_device

        # Current device not connected or not set, scan for available devices
        esp_devices = []

        # Scan for ESP32 devices based on vendor IDs
        ports = list(serial.tools.list_ports.comports())
        for port in ports:
            is_esp = False

            # Check by vendor ID
            if hasattr(port, 'vid') and port.vid:
                vid_str = f"{port.vid:04X}"
                if vid_str in self.ESP_VENDORS:
                    is_esp = True

            # Fallback to name-based detection for systems without USB details
            if port.device.startswith('/dev/ttyUSB') or \
               port.device.startswith('/dev/ttyACM') or \
               port.device.startswith('COM'):
                is_esp = True

            if is_esp:
                # Try to open the port to verify it's accessible
                try:
                    with serial.Serial(port.device, baudrate=115200, timeout=0.1) as ser:
                        # Successfully opened the port
                        esp_devices.append(port.device)
                except (serial.SerialException, OSError):
                    # Can't open the port, it might be in use or not a valid device
                    pass

        # If we found devices, use the first one
        if esp_devices:
            self.current_device = esp_devices[0]
            return self.current_device

        return None
    
    def is_device_connected(self, device_path=None):
        """
        Check if a specific device is still connected
        
        Args:
            device_path: Device path to check, defaults to current_device

        Returns:
            bool: True if connected, False otherwise
        """
        path = device_path or self.current_device
        if not path:
            return False
            
        # First check if the device path exists
        if not os.path.exists(path):
            return False
            
        # Then try to open the device to see if it's accessible
        try:
            with serial.Serial(path, baudrate=115200, timeout=0.1) as ser:
                return True
        except (serial.SerialException, OSError):
            return False
    
    def start_monitoring(self, interval=1.0):
        """
        Start monitoring device connection state
        
        Args:
            interval: Check interval in seconds
        """
        import threading
        
        if self.monitoring:
            return
            
        self.monitoring = True
        
        def monitor_thread():
            last_state = self.is_device_connected()
            last_device = self.current_device
            
            while self.monitoring:
                # First check if our current device is connected
                current_state = self.is_device_connected()
                
                # If device is disconnected, try to find another device
                if not current_state and self.current_device:
                    # Try to find a new device if the current one is disconnected
                    print(f"Device {self.current_device} disconnected, searching for available devices...")
                    new_device = self.find_device()
                    
                    if new_device and new_device != self.current_device:
                        print(f"Found new device: {new_device}")
                        # Device path changed
                        if self.callback:
                            # Report disconnect of old device
                            self.callback(False, last_device)
                            # Report connect of new device
                            self.callback(True, new_device)
                        
                        last_device = new_device
                        last_state = True
                        continue
                
                # Normal state change detection
                if current_state != last_state or (current_state and self.current_device != last_device):
                    # State changed or device path changed
                    if self.callback:
                        if not current_state:
                            # Device disconnected
                            self.callback(False, last_device)
                        else:
                            # Device connected or path changed
                            self.callback(True, self.current_device)
                    
                    last_state = current_state
                    last_device = self.current_device
                
                time.sleep(interval)
        
        self._monitoring_thread = threading.Thread(target=monitor_thread, daemon=True)
        self._monitoring_thread.start()
    
    def stop_monitoring(self):
        """Stop monitoring device connection state"""
        self.monitoring = False
        if self._monitoring_thread:
            self._monitoring_thread.join(timeout=2.0)
            self._monitoring_thread = None


def list_esp_devices():
    """
    List all ESP32 devices currently connected
    
    Returns:
        list: List of dictionaries with device info
    """
    result = []
    ports = list(serial.tools.list_ports.comports())
    
    for port in ports:
        is_esp = False
        vendor = "Unknown"
        
        # Check by vendor ID
        if hasattr(port, 'vid') and port.vid:
            vid = f"{port.vid:04X}"
            if vid in DeviceDetector.ESP_VENDORS:
                is_esp = True
                vendor = DeviceDetector.ESP_VENDORS[vid]
        
        # Check by name patterns
        if port.device.startswith('/dev/ttyUSB') or \
           port.device.startswith('/dev/ttyACM') or \
           port.device.startswith('COM'):
            is_esp = True
        
        if is_esp:
            result.append({
                'device': port.device,
                'description': port.description,
                'vendor': vendor,
                'hardware_id': port.hwid if hasattr(port, 'hwid') else None
            })
    
    return result


# Simple test function for module
if __name__ == "__main__":
    print("Scanning for ESP32 devices...")
    devices = list_esp_devices()
    
    if not devices:
        print("No ESP32 devices found.")
    else:
        print(f"Found {len(devices)} ESP32 device(s):")
        for i, device in enumerate(devices):
            print(f"{i+1}. {device['device']} - {device['description']} ({device['vendor']})")
    
    # Test connection monitoring
    if devices:
        def connection_callback(connected, device):
            status = "Connected" if connected else "Disconnected"
            print(f"\n[{status}] {device}")
            
        detector = DeviceDetector(callback=connection_callback)
        detector.current_device = devices[0]['device']
        
        print(f"\nMonitoring connection to {detector.current_device}...")
        print("Press Ctrl+C to stop")
        
        detector.start_monitoring()
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            detector.stop_monitoring()
            print("\nMonitoring stopped.")
