"""
SendEvent Injector — Kernel-Level Touch Event Injection via ADB.

Injects raw MotionEvent sequences through `adb shell sendevent` to produce
touch events that are indistinguishable from real finger input at the kernel level.

Key advantages over `input swipe`:
- Supports pressure variation (ABS_MT_PRESSURE)
- Supports touch contact area (ABS_MT_TOUCH_MAJOR)
- Produces SOURCE_TOUCHSCREEN events (not SOURCE_UNKNOWN)
- Multi-point non-linear paths

Falls back to `input swipe` if sendevent device detection fails.

Note: sendevent codes are device-specific. This module auto-detects the
correct /dev/input/eventX and the axis ranges on first use.
"""

import logging
import re

logger = logging.getLogger(__name__)


class SendEventInjector:
    """
    Injects touch events via adb shell sendevent for organic gesture simulation.

    Uses a batched shell command approach: all events for one gesture are piped
    into a single `adb shell` invocation to minimize latency.
    """

    _instance = None

    # Standard Linux input event types/codes
    EV_ABS = 3
    EV_SYN = 0
    EV_KEY = 1

    # Multitouch protocol B codes (most modern Android devices)
    ABS_MT_TRACKING_ID = 0x39  # 57
    ABS_MT_POSITION_X = 0x35  # 53
    ABS_MT_POSITION_Y = 0x36  # 54
    ABS_MT_PRESSURE = 0x3A  # 58
    ABS_MT_TOUCH_MAJOR = 0x30  # 48

    SYN_REPORT = 0
    BTN_TOUCH = 0x14A  # 330

    def __init__(self, device):
        self.device = device
        self.event_device = None
        self.x_max = 1080
        self.y_max = 2400
        self.pressure_max = 255
        self.touch_major_max = 30
        self._fallback_mode = False
        self._detected = False

    @classmethod
    def get_instance(cls, device):
        """Returns a singleton injector for the device."""
        if cls._instance is None:
            cls._instance = cls(device)
            cls._instance._detect_touch_device()
        return cls._instance

    @classmethod
    def reset(cls):
        """Reset for testing / device change."""
        cls._instance = None

    def _detect_touch_device(self):
        """
        Auto-detects the touchscreen input device and its axis ranges
        by parsing `getevent -pl` output.
        """
        try:
            result = self.device.shell("getevent -pl")
            if not isinstance(result, str):
                result = str(result)

            # Find device with ABS_MT_POSITION_X
            current_device = None
            for line in result.split("\n"):
                line = line.strip()

                # Device header: /dev/input/eventX
                dev_match = re.match(r"add device \d+:\s*(/dev/input/event\d+)", line)
                if dev_match:
                    current_device = dev_match.group(1)

                # Check for multitouch capability
                if current_device and "ABS_MT_POSITION_X" in line:
                    self.event_device = current_device
                    logger.info(f"🖐️ [SendEvent] Touch device detected: {self.event_device}")

                    # Parse axis ranges from the same section
                    self._parse_axis_ranges(result, current_device)
                    self._detected = True
                    return

            # If no MT device found, try fallback pattern
            logger.debug("⚠️ [SendEvent] No multitouch device found. " "Falling back to `input swipe` mode.")
            self._fallback_mode = True

        except Exception as e:
            logger.warning(f"⚠️ [SendEvent] Device detection failed: {e}. " f"Falling back to `input swipe` mode.")
            self._fallback_mode = True

    def _parse_axis_ranges(self, getevent_output, device_path):
        """
        Parses axis max values from getevent output.
        Lines look like: ABS_MT_POSITION_X : value 0, min 0, max 1079, ...
        """
        try:
            in_device = False
            for line in getevent_output.split("\n"):
                if device_path in line:
                    in_device = True
                    continue
                if in_device and line.strip().startswith("add device"):
                    break  # Next device

                if in_device:
                    if "ABS_MT_POSITION_X" in line:
                        m = re.search(r"max\s+(\d+)", line)
                        if m:
                            self.x_max = int(m.group(1))
                    elif "ABS_MT_POSITION_Y" in line:
                        m = re.search(r"max\s+(\d+)", line)
                        if m:
                            self.y_max = int(m.group(1))
                    elif "ABS_MT_PRESSURE" in line:
                        m = re.search(r"max\s+(\d+)", line)
                        if m:
                            self.pressure_max = int(m.group(1))
                    elif "ABS_MT_TOUCH_MAJOR" in line:
                        m = re.search(r"max\s+(\d+)", line)
                        if m:
                            self.touch_major_max = int(m.group(1))

            logger.debug(
                f"🖐️ [SendEvent] Axis ranges: X=0-{self.x_max}, "
                f"Y=0-{self.y_max}, P=0-{self.pressure_max}, "
                f"TM=0-{self.touch_major_max}"
            )
        except Exception as e:
            logger.debug(f"[SendEvent] Axis parsing error: {e}")

    def inject_gesture(self, points, timing_intervals, touch_major=6):
        """
        Injects a complete gesture (touch-down → move → touch-up) using sendevent.

        Args:
            points: List of (x, y, pressure) tuples from BezierGesture
            timing_intervals: List of inter-point delays in seconds
            touch_major: Contact area size

        Falls back to `input swipe` if sendevent is unavailable.
        """
        if self._fallback_mode or not self.event_device:
            return self._fallback_input_swipe(points, timing_intervals)

        if len(points) < 2:
            return

        try:
            dev = self.event_device

            # Scale coordinates from display space to input device space
            try:
                info = self.device.get_info()
                display_w = int(info.get("displayWidth", 1080)) if isinstance(info, dict) else 1080
                display_h = int(info.get("displayHeight", 2400)) if isinstance(info, dict) else 2400
            except (TypeError, ValueError):
                display_w, display_h = 1080, 2400
            scale_x = self.x_max / display_w
            scale_y = self.y_max / display_h

            # Build batch command list
            cmds = []

            # --- Touch Down (first point) ---
            x, y, pressure = points[0]
            ix = int(x * scale_x)
            iy = int(y * scale_y)
            ip = int(pressure * self.pressure_max)
            itm = min(touch_major, self.touch_major_max)

            cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_TRACKING_ID} 0")
            cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_X} {ix}")
            cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_Y} {iy}")
            cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_PRESSURE} {ip}")
            cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_TOUCH_MAJOR} {itm}")
            cmds.append(f"sendevent {dev} {self.EV_KEY} {self.BTN_TOUCH} 1")
            cmds.append(f"sendevent {dev} {self.EV_SYN} {self.SYN_REPORT} 0")

            # --- Move through intermediate points ---
            for i in range(1, len(points) - 1):
                if i - 1 < len(timing_intervals):
                    delay = timing_intervals[i - 1]
                    if delay > 0.001:
                        cmds.append(f"sleep {delay:.3f}")

                x, y, pressure = points[i]
                ix = int(x * scale_x)
                iy = int(y * scale_y)
                ip = int(pressure * self.pressure_max)

                cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_X} {ix}")
                cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_Y} {iy}")
                cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_PRESSURE} {ip}")
                cmds.append(f"sendevent {dev} {self.EV_SYN} {self.SYN_REPORT} 0")

            # --- Touch Up (last point) ---
            if len(timing_intervals) >= len(points) - 1:
                delay = timing_intervals[-1]
            else:
                delay = 0.01

            if delay > 0.001:
                cmds.append(f"sleep {delay:.3f}")

            x, y, pressure = points[-1]
            ix = int(x * scale_x)
            iy = int(y * scale_y)

            cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_X} {ix}")
            cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_Y} {iy}")
            cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_PRESSURE} 0")
            cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_TRACKING_ID} -1")
            cmds.append(f"sendevent {dev} {self.EV_KEY} {self.BTN_TOUCH} 0")
            cmds.append(f"sendevent {dev} {self.EV_SYN} {self.SYN_REPORT} 0")

            # Execute ALL events in one atomic batch to eliminate ADB latency
            self.device.shell(" && ".join(cmds))

        except Exception as e:
            logger.warning(f"⚠️ [SendEvent] Injection failed: {e}. Falling back.")
            self._fallback_input_swipe(points, timing_intervals)

    def _fallback_input_swipe(self, points, timing_intervals):
        """
        Fallback: Uses adb `input swipe` with first and last point.
        Loses pressure and curvature but maintains timing.
        """
        if len(points) < 2:
            return

        sx, sy, _ = points[0]
        ex, ey, _ = points[-1]
        total_ms = int(sum(timing_intervals) * 1000) if timing_intervals else 300

        dist_x = abs(ex - sx)
        dist_y = abs(ey - sy)

        # Android sometimes interprets a low-duration swipe with minimal movement as a long press or cancels it.
        # If it's physically a tap (minimal movement, short duration), use native input tap.
        if dist_x < 15 and dist_y < 15 and total_ms < 150:
            self.device.shell(f"input tap {int(sx)} {int(sy)}")
        else:
            self.device.shell(f"input swipe {int(sx)} {int(sy)} {int(ex)} {int(ey)} {total_ms}")
