import cv2
import numpy as np
import mediapipe as mp
import time, math

from evdev import UInput, ecodes as e

# ----------------------------
# Tunables
# ----------------------------
CAM_INDEX = 0
SMOOTHING = 0.25
DEADZONE_PX = 4

PINCH_DOWN = 0.040
PINCH_UP   = 0.055

FRAME_MARGIN = 0.10
SHOW_DEBUG = True

# Cursor scaling for RELATIVE mode (Wayland-friendly, feels like trackpad)
REL_SPEED = 2200   # higher = faster cursor (depends on your screen + taste)
MAX_STEP  = 45     # max pixels per frame (limits crazy jumps)

CLICK_COOLDOWN = 0.12

# ----------------------------
# Helpers
# ----------------------------
def clamp(v, lo, hi): return max(lo, min(hi, v))
def lerp(a, b, t): return a + (b - a) * t
def dist2d(a, b): return math.hypot(a[0]-b[0], a[1]-b[1])

# ----------------------------
# Create a virtual mouse (uinput)
# ----------------------------
capabilities = {
    e.EV_REL: [e.REL_X, e.REL_Y, e.REL_WHEEL],
    e.EV_KEY: [e.BTN_LEFT, e.BTN_RIGHT],
}
ui = UInput(capabilities, name="HandGesture Virtual Mouse", version=0x3)

def move_rel(dx, dy):
    # dx,dy are pixels-ish; emit REL events
    ui.write(e.EV_REL, e.REL_X, int(dx))
    ui.write(e.EV_REL, e.REL_Y, int(dy))
    ui.syn()

def click_left(down: bool):
    ui.write(e.EV_KEY, e.BTN_LEFT, 1 if down else 0)
    ui.syn()

def click_right():
    ui.write(e.EV_KEY, e.BTN_RIGHT, 1); ui.syn()
    ui.write(e.EV_KEY, e.BTN_RIGHT, 0); ui.syn()

# ----------------------------
# MediaPipe setup
# ----------------------------
mp_hands = mp.solutions.hands
hands = mp_hands.Hands(
    static_image_mode=False,
    max_num_hands=1,
    model_complexity=1,
    min_detection_confidence=0.6,
    min_tracking_confidence=0.6,
)

cap = cv2.VideoCapture(CAM_INDEX)
cap.set(cv2.CAP_PROP_FRAME_WIDTH,  640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
if not cap.isOpened():
    raise RuntimeError("Camera did not open. Try CAM_INDEX=1 or check /dev/video*")

# We'll do RELATIVE movement using index fingertip deltas in normalized coordinates
prev_ix, prev_iy = None, None
s_dx, s_dy = 0.0, 0.0  # smoothed deltas

left_down = False
paused = False
last_click_time = 0.0

print("Wayland mode: virtual mouse via /dev/uinput")
print("Controls: pinch thumb+index = left click/drag, 'p' pause, 'q'/ESC quit")

try:
    while True:
        ret, frame = cap.read()
        if not ret:
            continue

        frame = cv2.flip(frame, 1)
        h, w = frame.shape[:2]
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        res = hands.process(rgb)

        if res.multi_hand_landmarks and not paused:
            lm = res.multi_hand_landmarks[0].landmark
            thumb = (lm[4].x, lm[4].y)
            index = (lm[8].x, lm[8].y)

            # crop margins for stability
            ix = clamp(index[0], FRAME_MARGIN, 1.0 - FRAME_MARGIN)
            iy = clamp(index[1], FRAME_MARGIN, 1.0 - FRAME_MARGIN)

            # normalize back to 0..1
            ix = (ix - FRAME_MARGIN) / (1.0 - 2.0 * FRAME_MARGIN)
            iy = (iy - FRAME_MARGIN) / (1.0 - 2.0 * FRAME_MARGIN)

            if prev_ix is None:
                prev_ix, prev_iy = ix, iy

            # delta in normalized units
            dxn = ix - prev_ix
            dyn = iy - prev_iy
            prev_ix, prev_iy = ix, iy

            # convert to pixels-ish with speed
            dx = dxn * REL_SPEED
            dy = dyn * REL_SPEED

            # clamp crazy jumps
            dx = clamp(dx, -MAX_STEP, MAX_STEP)
            dy = clamp(dy, -MAX_STEP, MAX_STEP)

            # deadzone
            if abs(dx) < DEADZONE_PX: dx = 0
            if abs(dy) < DEADZONE_PX: dy = 0

            # smoothing
            s_dx = lerp(s_dx, dx, SMOOTHING)
            s_dy = lerp(s_dy, dy, SMOOTHING)

            if s_dx != 0 or s_dy != 0:
                move_rel(s_dx, s_dy)

            # Pinch click/drag
            pinch = dist2d(thumb, index)
            now = time.time()

            if not left_down and pinch <= PINCH_DOWN and (now - last_click_time) > CLICK_COOLDOWN:
                click_left(True)
                left_down = True
                last_click_time = now

            if left_down and pinch >= PINCH_UP:
                click_left(False)
                left_down = False

            if SHOW_DEBUG:
                # show pinch + state
                cv2.putText(frame, f"pinch={pinch:.3f} left_down={left_down} paused={paused}",
                            (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 2)
                # draw margin box
                x1, y1 = int(FRAME_MARGIN*w), int(FRAME_MARGIN*h)
                x2, y2 = int((1-FRAME_MARGIN)*w), int((1-FRAME_MARGIN)*h)
                cv2.rectangle(frame, (x1,y1), (x2,y2), (255,255,255), 1)
        else:
            # if no hand, release click + reset tracking
            prev_ix, prev_iy = None, None
            if left_down:
                click_left(False)
                left_down = False

        cv2.putText(frame, "Wayland uinput | q/ESC quit | p pause",
                    (10, h-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 2)

        cv2.imshow("Hand Mouse (Wayland uinput)", frame)
        key = cv2.waitKey(1) & 0xFF
        if key in (27, ord('q')):
            break
        if key == ord('p'):
            paused = not paused

finally:
    cap.release()
    cv2.destroyAllWindows()
    hands.close()
    ui.close()
