# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.

import multiprocessing
import threading
import time
from ctypes import c_bool
from multiprocessing.sharedctypes import Value
from select import select

from fenrirscreenreader.core import debug
from fenrirscreenreader.core import inputData
from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.inputDriver import InputDriver as inputDriver

_evdevAvailable = False
_udevAvailable = False
_evdevAvailableError = ""
_udevAvailableError = ""
try:
    import evdev
    from evdev import InputDevice
    from evdev import UInput
    from evdev import ecodes as e

    _evdevAvailable = True

except Exception as e:
    _evdevAvailableError = str(e)

try:
    import pyudev

    _udevAvailable = True
except Exception as e:
    _udevAvailableError = str(e)


class driver(inputDriver):
    """Linux evdev input driver for Fenrir screen reader.
    
    This driver provides access to Linux input devices through the evdev interface,
    allowing Fenrir to capture keyboard input, manage device grabbing for exclusive
    access, and inject synthetic input events.
    
    Features:
    - Automatic device detection and hotplug support via udev
    - Device grabbing to prevent input from reaching other applications
    - Key event mapping and filtering
    - UInput support for synthetic key injection
    - Multi-device support with thread-safe access
    
    Attributes:
        iDevices (dict): Map of file descriptor to InputDevice objects
        iDevicesFD (list): Shared list of file descriptors for multiprocessing
        uDevices (dict): Map of file descriptor to UInput devices
        gDevices (dict): Map of file descriptor to grab status
        iDeviceNo (int): Total number of input devices
        UInputinject (UInput): Device for injecting synthetic events
        _deviceLock (Lock): Thread lock for device access
    """
    def __init__(self):
        inputDriver.__init__(self)
        self._manager = multiprocessing.Manager()
        self.iDevices = {}
        self.iDevicesFD = self._manager.list()
        self.uDevices = {}
        self.gDevices = {}
        self.iDeviceNo = 0
        self.watch_dog = Value(c_bool, True)
        self.UInputinject = (
            UInput() if _evdevAvailable and "UInput" in globals() else None
        )
        self._deviceLock = threading.Lock()

    def initialize(self, environment):
        """Initialize the evdev input driver.
        
        Sets up device monitoring, starts watchdog threads for device hotplug
        detection and input monitoring, and configures the input subsystem.
        
        Args:
            environment: Fenrir environment dictionary with runtime managers
            
        Note:
            Requires evdev and optionally pyudev libraries. Falls back gracefully
            if libraries are not available.
        """
        self.env = environment
        global _evdevAvailable
        global _udevAvailable
        global _evdevAvailableError
        global _udevAvailableError
        if not _udevAvailable:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputDriver:" + _udevAvailableError, debug.DebugLevel.ERROR
            )
        if not _evdevAvailable:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputDriver:" + _evdevAvailableError, debug.DebugLevel.ERROR
            )
            return

        if _udevAvailable:
            self.env["runtime"]["ProcessManager"].add_custom_event_thread(
                self.plug_input_device_watchdog_udev
            )
        self.env["runtime"]["ProcessManager"].add_custom_event_thread(
            self.input_watchdog
        )
        self._initialized = True

    def plug_input_device_watchdog_udev(self, active, event_queue):
        """Monitor for input device hotplug events via udev.
        
        Runs in a separate thread to detect when input devices are
        plugged/unplugged and generates appropriate events for device
        management.
        
        Args:
            active: Shared boolean controlling the watchdog loop
            event_queue: Queue for sending device events to main process
            
        Events Generated:
            FenrirEventType.plug_input_device: When new devices are detected
            
        Note:
            Filters out virtual devices and devices from assistive technologies
            like BRLTTY to avoid conflicts.
        """
        context = pyudev.Context()
        monitor = pyudev.Monitor.from_netlink(context)
        monitor.filter_by(subsystem="input")
        monitor.start()
        ignore_plug = False
        while active.value:
            valid_devices = []
            device = monitor.poll(1)
            while device:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "plug_input_device_watchdog_udev:" + str(device),
                    debug.DebugLevel.INFO,
                )
                try:
                    try:
                        # FIX: Check if attributes exist before accessing them
                        if (
                            hasattr(device, "name")
                            and device.name
                            and device.name.upper()
                            in ["", "SPEAKUP", "FENRIR-UINPUT"]
                        ):
                            ignore_plug = True
                        if (
                            hasattr(device, "phys")
                            and device.phys
                            and device.phys.upper()
                            in ["", "SPEAKUP", "FENRIR-UINPUT"]
                        ):
                            ignore_plug = True
                        if (
                            hasattr(device, "name")
                            and device.name
                            and "BRLTTY" in device.name.upper()
                        ):
                            ignore_plug = True
                    except Exception as e:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "plug_input_device_watchdog_udev CHECK NAME CRASH: "
                            + str(e),
                            debug.DebugLevel.ERROR,
                        )

                    if not ignore_plug:
                        virtual = (
                            "/sys/devices/virtual/input/" in device.sys_path
                        )
                        if device.device_node:
                            valid_devices.append(
                                {
                                    "device": device.device_node,
                                    "virtual": virtual,
                                }
                            )
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "plug_input_device_watchdog_udev APPEND CRASH: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )
                try:
                    poll_timeout = 1
                    device = monitor.poll(poll_timeout)
                except Exception:
                    device = None
                ignore_plug = False
            if valid_devices:
                event_queue.put(
                    {
                        "Type": FenrirEventType.plug_input_device,
                        "data": valid_devices,
                    }
                )
        return time.time()

    def input_watchdog(self, active, event_queue):
        """Main input monitoring loop.
        
        Monitors all registered input devices for key events using select().
        Processes key events, maps them to Fenrir's internal format, and
        forwards them to the main application.
        
        Args:
            active: Shared boolean controlling the watchdog loop  
            event_queue: Queue for sending input events to main process
            
        Events Generated:
            FenrirEventType.keyboard_input: For key press/release events
            
        Note:
            Uses thread-safe device access and handles device disconnection
            gracefully. Non-keyboard events are forwarded to UInput devices.
        """
        try:
            while active.value:
                # Get a snapshot of devices for select() to avoid lock
                # contention
                with self._deviceLock:
                    devices_snapshot = self.iDevices.copy()

                if not devices_snapshot:
                    time.sleep(0.1)
                    continue

                r, w, x = select(devices_snapshot, [], [], 0.8)
                event = None
                found_key_in_sequence = False
                foreward = False
                event_fired = False
                for fd in r:
                    # Check if device still exists before accessing
                    with self._deviceLock:
                        if fd not in self.iDevices:
                            continue
                        device = self.iDevices[fd]
                        udevice = self.uDevices.get(fd)

                    try:
                        event = device.read_one()
                    except Exception as e:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "evdevDriver handle_input_event: Error reading event: "
                            + str(e),
                            debug.DebugLevel.ERROR,
                        )
                        self.remove_device(fd)
                        continue
                    while event:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "input_watchdog: EVENT:" + str(event),
                            debug.DebugLevel.INFO,
                        )
                        self.env["input"]["event_buffer"].append(
                            [device, udevice, event]
                        )
                        if event.type == evdev.events.EV_KEY:
                            if not found_key_in_sequence:
                                found_key_in_sequence = True
                            if event.code != 0:
                                curr_map_event = self.map_event(event)
                                if not curr_map_event:
                                    event = device.read_one()
                                    continue
                                if not isinstance(
                                    curr_map_event["event_name"], str
                                ):
                                    event = device.read_one()
                                    continue
                                if curr_map_event["event_state"] in [0, 1, 2]:
                                    event_queue.put(
                                        {
                                            "Type": FenrirEventType.keyboard_input,
                                            "data": curr_map_event.copy(),
                                        }
                                    )
                                    event_fired = True
                        else:
                            # Forward non-keyboard events: 2=EV_REL, 3=EV_ABS, 17=EV_LED
                            if event.type in [2, 3, 17]:
                                foreward = True

                        event = device.read_one()
                    if not found_key_in_sequence:
                        if foreward and not event_fired:
                            self.write_event_buffer()
                            self.clear_event_buffer()
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "INPUT WATCHDOG CRASH: " + str(e), debug.DebugLevel.ERROR
            )

    def write_event_buffer(self):
        if not self._initialized:
            return
        for iDevice, uDevice, event in self.env["input"]["event_buffer"]:
            try:
                if uDevice:
                    if self.gDevices[iDevice.fd]:
                        self.write_u_input(uDevice, event)
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "evdevDriver write_event_buffer: Error writing event: "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )

    def write_u_input(self, uDevice, event):
        if not self._initialized:
            return
        uDevice.write_event(event)
        time.sleep(0.0000002)
        uDevice.syn()

    def update_input_devices(self, new_devices=None, init=False):
        """Update the list of monitored input devices.
        
        Scans for available input devices and adds suitable ones based on
        the configured device mode. Supports filtering by device type and
        name matching.
        
        Args:
            new_devices (list, optional): Specific devices to add
            init (bool): Whether this is initial device setup
            
        Device Modes:
            - 'ALL': Monitor all keyboard devices
            - 'NOMICE': Monitor keyboards but exclude pointing devices
            - Device names: Comma-separated list of specific device names
            
        Note:
            Automatically filters out virtual devices, assistive technology
            devices, and devices with insufficient key counts.
        """
        if init:
            self.remove_all_devices()

        device_file_list = None

        if new_devices and not init:
            if not isinstance(new_devices, list):
                new_devices = [new_devices]
            device_file_list = new_devices
        else:
            device_file_list = evdev.list_devices()
            if len(device_file_list) == self.iDeviceNo:
                return
        if not device_file_list:
            return

        mode = (
            self.env["runtime"]["SettingsManager"]
            .get_setting("keyboard", "device")
            .upper()
        )

        i_devices_files = []
        for device in self.iDevices:
            i_devices_files.append(self.iDevices[device].path)

        event_type = evdev.events
        for deviceFile in device_file_list:
            try:
                if not deviceFile:
                    continue
                if deviceFile == "":
                    continue
                if deviceFile in i_devices_files:
                    continue
                try:
                    with open(deviceFile) as f:
                        pass
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "evdevDriver update_input_devices: Error opening device file: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )
                    continue
                # 3 pos absolute
                # 2 pos relative
                # 1 Keys
                try:
                    curr_device = evdev.InputDevice(deviceFile)
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "evdevDriver createDeviceType: Error creating device: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )
                    continue
                try:
                    # FIX: Check if attributes exist before accessing them
                    if (
                        hasattr(curr_device, "name")
                        and curr_device.name
                        and curr_device.name.upper()
                        in ["", "SPEAKUP", "FENRIR-UINPUT"]
                    ):
                        continue
                    if (
                        hasattr(curr_device, "phys")
                        and curr_device.phys
                        and curr_device.phys.upper()
                        in ["", "SPEAKUP", "FENRIR-UINPUT"]
                    ):
                        continue
                    if (
                        hasattr(curr_device, "name")
                        and curr_device.name
                        and "BRLTTY" in curr_device.name.upper()
                    ):
                        continue
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "evdevDriver: Error checking device capabilities: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )
                cap = curr_device.capabilities()
                if mode in ["ALL", "NOMICE"]:
                    if event_type.EV_KEY in cap:
                        if (
                            116 in cap[event_type.EV_KEY]
                            and len(cap[event_type.EV_KEY]) < 10
                        ):
                            self.env["runtime"][
                                "DebugManager"
                            ].write_debug_out(
                                "Device Skipped (has 116):" + curr_device.name,
                                debug.DebugLevel.INFO,
                            )
                            continue
                        # Check if device has numpad keys - use lower threshold for dedicated numpads
                        numpad_keys = [
                            evdev.ecodes.KEY_KP0, evdev.ecodes.KEY_KP1, evdev.ecodes.KEY_KP2,
                            evdev.ecodes.KEY_KP3, evdev.ecodes.KEY_KP4, evdev.ecodes.KEY_KP5,
                            evdev.ecodes.KEY_KP6, evdev.ecodes.KEY_KP7, evdev.ecodes.KEY_KP8,
                            evdev.ecodes.KEY_KP9, evdev.ecodes.KEY_KPPLUS, evdev.ecodes.KEY_KPMINUS,
                            evdev.ecodes.KEY_KPASTERISK, evdev.ecodes.KEY_KPSLASH, evdev.ecodes.KEY_KPENTER,
                            evdev.ecodes.KEY_KPDOT
                        ]
                        
                        has_numpad_keys = any(key in cap[event_type.EV_KEY] for key in numpad_keys)
                        min_key_threshold = 10 if has_numpad_keys else 60
                        
                        if len(cap[event_type.EV_KEY]) < min_key_threshold:
                            threshold_type = "numpad" if has_numpad_keys else "keyboard"
                            self.env["runtime"][
                                "DebugManager"
                            ].write_debug_out(
                                f"Device Skipped (< {min_key_threshold} keys for {threshold_type}):"
                                + curr_device.name,
                                debug.DebugLevel.INFO,
                            )
                            continue
                        if mode == "ALL":
                            self.add_device(curr_device)
                            self.env["runtime"][
                                "DebugManager"
                            ].write_debug_out(
                                "Device added (ALL):"
                                + self.iDevices[curr_device.fd].name,
                                debug.DebugLevel.INFO,
                            )
                        elif mode == "NOMICE":
                            if not (
                                (event_type.EV_REL in cap)
                                or (event_type.EV_ABS in cap)
                            ):
                                self.add_device(curr_device)
                                self.env["runtime"][
                                    "DebugManager"
                                ].write_debug_out(
                                    "Device added (NOMICE):"
                                    + self.iDevices[curr_device.fd].name,
                                    debug.DebugLevel.INFO,
                                )
                            else:
                                self.env["runtime"][
                                    "DebugManager"
                                ].write_debug_out(
                                    "Device Skipped (NOMICE):"
                                    + curr_device.name,
                                    debug.DebugLevel.INFO,
                                )
                    else:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "Device Skipped (no EV_KEY):" + curr_device.name,
                            debug.DebugLevel.INFO,
                        )
                elif curr_device.name.upper() in mode.split(","):
                    self.add_device(curr_device)
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Device added (Name):"
                        + self.iDevices[curr_device.fd].name,
                        debug.DebugLevel.INFO,
                    )
            except Exception as e:
                try:
                    device_name = (
                        curr_device.name
                        if hasattr(curr_device, "name")
                        else "unknown"
                    )
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Device Skipped (Exception): "
                        + deviceFile
                        + " "
                        + device_name
                        + " "
                        + str(e),
                        debug.DebugLevel.INFO,
                    )
                except Exception as ex:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Device Skipped (Exception): "
                        + deviceFile
                        + " "
                        + str(ex),
                        debug.DebugLevel.INFO,
                    )
        self.iDeviceNo = len(evdev.list_devices())
        self.update_m_pi_devices_fd()

    def update_m_pi_devices_fd(self):
        try:
            for fd in self.iDevices:
                if fd not in self.iDevicesFD:
                    self.iDevicesFD.append(fd)
            for fd in self.iDevicesFD:
                if fd not in self.iDevices:
                    self.iDevicesFD.remove(fd)
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "evdevDriver update_m_pi_devices_fd: Error updating device file descriptors: "
                + str(e),
                debug.DebugLevel.ERROR,
            )

    def map_event(self, event):
        if not self._initialized:
            return None
        if not event:
            return None
        m_event = inputData.input_event
        try:
            # mute is a list = ['KEY_MIN_INTERESTING', 'KEY_MUTE']
            m_event["event_name"] = evdev.ecodes.keys[event.code]
            if isinstance(m_event["event_name"], list):
                if len(m_event["event_name"]) > 0:
                    m_event["event_name"] = m_event["event_name"][0]
                    if isinstance(m_event["event_name"], list):
                        if len(m_event["event_name"]) > 0:
                            m_event["event_name"] = m_event["event_name"][0]
            m_event["event_value"] = event.code
            m_event["event_sec"] = event.sec
            m_event["event_usec"] = event.usec
            m_event["event_state"] = event.value
            m_event["event_type"] = event.type
            return m_event
        except Exception as e:
            return None

    def get_led_state(self, led=0):
        if not self.has_i_devices():
            return False
        # 0 = Numlock
        # 1 = Capslock
        # 2 = Rollen
        
        # Use the first device with LED capability as authoritative source
        # to avoid inconsistent readings from multiple devices during initialization
        for fd, dev in self.iDevices.items():
            # Check if device has LED capability (capability 17)
            if 17 in dev.capabilities():
                return led in dev.leds()
        
        # Fallback to old behavior if no device has LED capability
        for fd, dev in self.iDevices.items():
            if led in dev.leds():
                return True
        return False

    def toggle_led_state(self, led=0):
        if not self.has_i_devices():
            return False
        led_state = self.get_led_state(led)
        for i in self.iDevices:
            if self.gDevices[i]:
                # 17 LEDs
                if 17 in self.iDevices[i].capabilities():
                    if led_state == 1:
                        self.iDevices[i].set_led(led, 0)
                    else:
                        self.iDevices[i].set_led(led, 1)

    def grab_all_devices(self):
        if not self._initialized:
            return True
        ok = True
        for fd in self.iDevices:
            if not self.gDevices[fd]:
                ok = ok and self.grab_device(fd)
        return ok

    def ungrab_all_devices(self):
        if not self._initialized:
            return True
        ok = True
        for fd in self.iDevices:
            if self.gDevices[fd]:
                ok = ok and self.ungrab_device(fd)
        return ok

    def create_u_input_dev(self, fd):
        if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "keyboard", "grab_devices"
        ):
            self.uDevices[fd] = None
            return
        try:
            test = self.uDevices[fd]
            return
        except KeyError:
            self.uDevices[fd] = None
        if self.uDevices[fd] is not None:
            return
        if "UInput" not in globals():
            return
        try:
            self.uDevices[fd] = UInput.from_device(
                self.iDevices[fd], name="fenrir-uinput", phys="fenrir-uinput"
            )
        except Exception as e:
            try:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "InputDriver evdev: compat fallback:  " + str(e),
                    debug.DebugLevel.WARNING,
                )
                dev = self.iDevices[fd]
                cap = dev.capabilities()
                del cap[0]
                self.uDevices[fd] = UInput(
                    cap, name="fenrir-uinput", phys="fenrir-uinput"
                )
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "InputDriver evdev: init Uinput not possible:  " + str(e),
                    debug.DebugLevel.ERROR,
                )
                return

    def add_device(self, newDevice):
        """Add a new input device to the monitoring list.
        
        Creates the necessary data structures for device monitoring,
        sets up UInput forwarding if device grabbing is enabled, and
        initializes device state.
        
        Args:
            newDevice: evdev.InputDevice object to add
            
        Note:
            Thread-safe operation. Automatically cleans up on failure.
        """
        self.env["runtime"]["DebugManager"].write_debug_out(
            "InputDriver evdev: device added:  "
            + str(newDevice.fd)
            + " "
            + str(newDevice),
            debug.DebugLevel.INFO,
        )
        with self._deviceLock:
            try:
                self.iDevices[newDevice.fd] = newDevice
                self.create_u_input_dev(newDevice.fd)
                self.gDevices[newDevice.fd] = False
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "InputDriver evdev: error adding device: " + str(e),
                    debug.DebugLevel.ERROR,
                )
                # if it doesnt work clean up
                try:
                    del self.iDevices[newDevice.fd]
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "evdevDriver remove_device: Error removing iDevice: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )
                try:
                    del self.uDevices[newDevice.fd]
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "evdevDriver remove_device: Error removing uDevice: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )
                try:
                    del self.gDevices[newDevice.fd]
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "evdevDriver add_device: Error cleaning up gDevice: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )

    def grab_device(self, fd):
        """Grab exclusive access to an input device.
        
        Takes exclusive control of the device, preventing other applications
        from receiving its input. Also resets modifier key states to prevent
        stuck keys.
        
        Args:
            fd (int): File descriptor of device to grab
            
        Returns:
            bool: True if grab successful, False otherwise
            
        Note:
            Only effective if grabDevices setting is enabled.
        """
        if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "keyboard", "grab_devices"
        ):
            return True

        # FIX: Handle exception variable scope correctly
        grab_error = None
        try:
            self.iDevices[fd].grab()
            self.gDevices[fd] = True
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputDriver evdev: grab device ("
                + str(self.iDevices[fd].name)
                + ")",
                debug.DebugLevel.INFO,
            )
            # Reset modifier keys on successful grab
            if self.uDevices[fd]:
                modifier_keys = [
                    e.KEY_LEFTCTRL,
                    e.KEY_RIGHTCTRL,
                    e.KEY_LEFTALT,
                    e.KEY_RIGHTALT,
                    e.KEY_LEFTSHIFT,
                    e.KEY_RIGHTSHIFT,
                ]
                for key in modifier_keys:
                    try:
                        self.uDevices[fd].write(e.EV_KEY, key, 0)  # 0 = key up
                        self.uDevices[fd].syn()
                    except Exception as mod_error:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "Failed to reset modifier key: " + str(mod_error),
                            debug.DebugLevel.WARNING,
                        )
        except IOError:
            if not self.gDevices[fd]:
                return False
        except Exception as ex:
            grab_error = ex

        if grab_error:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputDriver evdev: grabing not possible: " + str(grab_error),
                debug.DebugLevel.ERROR,
            )
            return False

        return True

    def ungrab_device(self, fd):
        """Release exclusive access to an input device.
        
        Returns control of the device to the system, allowing other
        applications to receive its input.
        
        Args:
            fd (int): File descriptor of device to ungrab
            
        Returns:
            bool: True if ungrab successful, False otherwise
        """
        if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "keyboard", "grab_devices"
        ):
            return True

        # FIX: Handle exception variable scope correctly
        ungrab_error = None
        try:
            self.iDevices[fd].ungrab()
            self.gDevices[fd] = False
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputDriver evdev: ungrab device ("
                + str(self.iDevices[fd].name)
                + ")",
                debug.DebugLevel.INFO,
            )
        except IOError:
            if self.gDevices[fd]:
                return False
        except Exception as ex:
            ungrab_error = ex

        if ungrab_error:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputDriver evdev: ungrabing not possible: "
                + str(ungrab_error),
                debug.DebugLevel.ERROR,
            )
            return False

        return True

    def remove_device(self, fd):
        """Remove an input device from monitoring.
        
        Cleanly removes a device by ungrabbing it, closing file handles,
        and cleaning up all associated data structures.
        
        Args:
            fd (int): File descriptor of device to remove
            
        Note:
            Thread-safe operation with comprehensive error handling.
        """
        with self._deviceLock:
            try:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "InputDriver evdev: device removed:  "
                    + str(fd)
                    + " "
                    + str(self.iDevices[fd]),
                    debug.DebugLevel.INFO,
                )
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "InputDriver evdev: device removed:  "
                    + str(fd)
                    + " Error: "
                    + str(e),
                    debug.DebugLevel.INFO,
                )
            self.clear_event_buffer()
            try:
                self.ungrab_device(fd)
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "evdevDriver remove_device: Error ungrabbing device "
                    + str(fd)
                    + ": "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
            try:
                self.iDevices[fd].close()
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "evdevDriver remove_device: Error closing iDevice "
                    + str(fd)
                    + ": "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
            try:
                self.uDevices[fd].close()
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "evdevDriver remove_device: Error closing uDevice "
                    + str(fd)
                    + ": "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
            try:
                del self.iDevices[fd]
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "evdevDriver remove_device: Error deleting iDevice "
                    + str(fd)
                    + ": "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
            try:
                del self.uDevices[fd]
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "evdevDriver remove_device: Error deleting uDevice "
                    + str(fd)
                    + ": "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
            try:
                del self.gDevices[fd]
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "evdevDriver remove_device: Error deleting gDevice "
                    + str(fd)
                    + ": "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
            self.update_m_pi_devices_fd()

    def has_i_devices(self):
        if not self._initialized:
            return False
        if not self.iDevices:
            return False
        if len(self.iDevices) == 0:
            return False
        return True

    def send_key(self, key, state):
        """Inject a synthetic key event.
        
        Sends a key press or release event using UInput. Used for
        features like key forwarding and macro execution.
        
        Args:
            key (str): Key name (e.g., 'KEY_A')
            state (int): Key state (0=release, 1=press, 2=repeat)
        """
        if not self._initialized:
            return
        if self.UInputinject is None:
            return
        try:
            self.UInputinject.write(e.EV_KEY, e.ecodes[key], state)
            self.UInputinject.syn()
        except Exception as ex:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "evdevDriver send_key: Error sending key "
                + str(key)
                + ": "
                + str(ex),
                debug.DebugLevel.ERROR,
            )

    def remove_all_devices(self):
        if not self.has_i_devices():
            return
        devices = self.iDevices.copy()
        for fd in devices:
            self.remove_device(fd)
        self.iDevices.clear()
        self.uDevices.clear()
        self.gDevices.clear()
        self.iDeviceNo = 0
