#!/usr/bin/env python3
import sys
import os
import signal
import atexit

global_grabbed_mouse = None

def ungrab_mouse_cleanup():
    global global_grabbed_mouse
    if global_grabbed_mouse:
        try:
            global_grabbed_mouse.ungrab()
        except Exception:
            pass
        global_grabbed_mouse = None

atexit.register(ungrab_mouse_cleanup)

if hasattr(signal, 'SIGPIPE'):
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)
if hasattr(signal, 'SIGINT'):
    signal.signal(signal.SIGINT, lambda s, f: (ungrab_mouse_cleanup(), sys.exit(0)))
if hasattr(signal, 'SIGTERM'):
    signal.signal(signal.SIGTERM, lambda s, f: (ungrab_mouse_cleanup(), sys.exit(0)))

import json
import time
import select
import string
import threading
import math
from evdev import UInput, ecodes as e, InputDevice, list_devices, AbsInfo

def find_physical_pointers():
    pointers = []
    try:
        for path in list_devices():
            try:
                dev = InputDevice(path)
                if 'SDO' in dev.name or 'Virtual' in dev.name:
                    continue
                caps = dev.capabilities()
                is_rel = (e.EV_REL in caps and e.REL_X in caps[e.EV_REL])
                is_abs = (e.EV_ABS in caps and e.ABS_X in caps[e.EV_ABS])
                is_touch = (e.EV_KEY in caps and any(btn in caps[e.EV_KEY] for btn in [getattr(e, 'BTN_TOUCH', 0x14a), getattr(e, 'BTN_TOOL_FINGER', 0x145), getattr(e, 'BTN_LEFT', 0x110)]))
                if is_rel or is_abs or is_touch:
                    pointers.append(dev)
            except Exception:
                continue
    except Exception:
        pass
    return pointers

def find_physical_keyboards():
    keyboards = []
    try:
        for path in list_devices():
            try:
                dev = InputDevice(path)
                if 'SDO' in dev.name or 'Virtual' in dev.name:
                    continue
                caps = dev.capabilities()
                if e.EV_KEY in caps and e.KEY_ENTER in caps[e.EV_KEY] and e.KEY_SPACE in caps[e.EV_KEY]:
                    keyboards.append(dev)
            except Exception:
                continue
    except Exception:
        pass
    return keyboards

def build_char_map():
    cmap = {
        ' ': (e.KEY_SPACE, False), '\n': (e.KEY_ENTER, False), '\t': (e.KEY_TAB, False),
        '.': (e.KEY_DOT, False), ',': (e.KEY_COMMA, False), '/': (e.KEY_SLASH, False),
        ';': (e.KEY_SEMICOLON, False), "'": (e.KEY_APOSTROPHE, False),
        '[': (e.KEY_LEFTBRACE, False), ']': (e.KEY_RIGHTBRACE, False),
        '-': (e.KEY_MINUS, False), '=': (e.KEY_EQUAL, False),
        '`': (e.KEY_GRAVE, False), '\\': (e.KEY_BACKSLASH, False),
        '>': (e.KEY_DOT, True), '<': (e.KEY_COMMA, True), '?': (e.KEY_SLASH, True),
        ':': (e.KEY_SEMICOLON, True), '"': (e.KEY_APOSTROPHE, True),
        '{': (e.KEY_LEFTBRACE, True), '}': (e.KEY_RIGHTBRACE, True),
        '_': (e.KEY_MINUS, True), '+': (e.KEY_EQUAL, True),
        '~': (e.KEY_GRAVE, True), '|': (e.KEY_BACKSLASH, True),
    }
    for c in string.ascii_lowercase:
        cmap[c] = (getattr(e, 'KEY_' + c.upper()), False)
    for c in string.ascii_uppercase:
        cmap[c] = (getattr(e, 'KEY_' + c), True)
    
    num_row = ['1','2','3','4','5','6','7','8','9','0']
    sym_row = ['!','@','#','$','%','^','&','*','(',')']
    for c in num_row:
        cmap[c] = (getattr(e, 'KEY_' + c), False)
    for i, c in enumerate(sym_row):
        cmap[c] = (getattr(e, 'KEY_' + num_row[i]), True)
    return cmap

CHAR_MAP = build_char_map()

def main():
    pointers = find_physical_pointers()
    keyboards = find_physical_keyboards()
    
    screen_w, screen_h = 1920, 1080
    
    used_keys = {e.BTN_LEFT, e.BTN_RIGHT, e.BTN_MIDDLE, e.BTN_SIDE, e.BTN_EXTRA}
    for attr in dir(e):
        if (attr.startswith('KEY_') or attr.startswith('BTN_')) and not attr.endswith('_MAX') and not attr.endswith('_CNT') and not attr.endswith('_RESERVED'):
            try:
                val = getattr(e, attr)
                if isinstance(val, int) and 0 < val < getattr(e, 'KEY_MAX', 0x2ff):
                    used_keys.add(val)
            except Exception:
                pass
    for code, shift in CHAR_MAP.values():
        used_keys.add(code)
        
    mouse_caps = {
        e.EV_REL: (e.REL_X, e.REL_Y, e.REL_WHEEL, e.REL_HWHEEL),
        e.EV_KEY: (e.BTN_LEFT, e.BTN_RIGHT, e.BTN_MIDDLE, e.BTN_SIDE, e.BTN_EXTRA),
        e.EV_ABS: [
            (e.ABS_X, AbsInfo(value=0, min=0, max=screen_w, fuzz=0, flat=0, resolution=0)),
            (e.ABS_Y, AbsInfo(value=0, min=0, max=screen_h, fuzz=0, flat=0, resolution=0))
        ]
    }

    kbd_keys = set()
    for attr in dir(e):
        if attr.startswith('KEY_') and not attr.endswith('_MAX') and not attr.endswith('_CNT') and not attr.endswith('_RESERVED'):
            try:
                val = getattr(e, attr)
                if isinstance(val, int) and 0 < val < getattr(e, 'KEY_MAX', 0x2ff):
                    kbd_keys.add(val)
            except Exception:
                pass
    for code, shift in CHAR_MAP.values():
        kbd_keys.add(code)

    kbd_caps = {
        e.EV_KEY: list(kbd_keys)
    }

    try:
        ui_mouse = UInput(events=mouse_caps, name='SDO-Virtual-Mouse', version=0x2)
        ui_kbd = UInput(events=kbd_caps, name='SDO-Virtual-Keyboard', version=0x2)
    except Exception as ex:
        print(f'{{"status": "error", "message": "Failed to create uinput devices: {ex}. Running in dummy mode."}}')
        ui_mouse = None
        ui_kbd = None

    x, y = screen_w // 2, screen_h // 2

    pynput_mouse = None
    pynput_kbd = None
    try:
        from pynput.mouse import Controller as MouseController, Button as MouseButton
        from pynput.keyboard import Controller as KeyboardController, Key as PynputKey, KeyCode as PynputKeyCode
        pynput_mouse = MouseController()
        pynput_kbd = KeyboardController()
        cur = pynput_mouse.position
        if cur and len(cur) >= 2:
            x, y = int(cur[0]), int(cur[1])
    except Exception:
        pynput_mouse = None
        pynput_kbd = None

    print('{"status": "ready"}')
    sys.stdout.flush()

    try:
        import pyautogui
        pyautogui.FAILSAFE = False
    except Exception:
        pyautogui = None

    def update_pos(new_x, new_y, origin="SYSTEM"):
        nonlocal x, y
        nx = max(0, min(screen_w, int(new_x)))
        ny = max(0, min(screen_h, int(new_y)))
        if nx != x or ny != y:
            dx = nx - x
            dy = ny - y
            x, y = nx, ny
            if pynput_mouse:
                try:
                    pynput_mouse.position = (int(x), int(y))
                except Exception:
                    pass
            elif pyautogui:
                try:
                    pyautogui.moveTo(x, y)
                except Exception:
                    pass
            if ui_mouse:
                try:
                    ui_mouse.write(e.EV_ABS, e.ABS_X, int(x))
                    ui_mouse.write(e.EV_ABS, e.ABS_Y, int(y))
                    if dx != 0: ui_mouse.write(e.EV_REL, e.REL_X, dx)
                    if dy != 0: ui_mouse.write(e.EV_REL, e.REL_Y, dy)
                    ui_mouse.syn()
                except Exception:
                    pass
            print(json.dumps({"type": "position", "x": x, "y": y, "origin": origin}))
            sys.stdout.flush()

    update_pos(x, y)
    
    # Reflex arc state
    state_lock = threading.Lock()
    target_x = None
    target_y = None
    start_x = None
    start_y = None
    start_time = None
    move_duration = 0.0
    human_override_until = 0.0

    def reflex_arc_loop():
        nonlocal x, y, target_x, target_y, move_duration, human_override_until
        while True:
            if time.time() < human_override_until:
                time.sleep(0.016)
                continue

            with state_lock:
                tx, ty = target_x, target_y
                md = move_duration
                
            if tx is not None and ty is not None:
                if pynput_mouse:
                    try:
                        cur = pynput_mouse.position
                        if cur and len(cur) >= 2:
                            x, y = int(cur[0]), int(cur[1])
                    except Exception:
                        pass
                dx = tx - x
                dy = ty - y
                dist = math.hypot(dx, dy)
                if dist > 300 and pynput_mouse:
                    update_pos(tx, ty, origin="SDO")
                    with state_lock:
                        target_x = None
                        target_y = None
                elif dist > 0.5:
                    # Simple P-controller / low-pass filter for smooth tracking
                    speed = min(1.0, 0.08 / max(0.01, md))
                    cur_x = x + dx * speed
                    cur_y = y + dy * speed
                    update_pos(cur_x, cur_y, origin="SDO")
                else:
                    update_pos(tx, ty, origin="SDO")
                    with state_lock:
                        target_x = None
                        target_y = None
                    
            time.sleep(0.016)

    t = threading.Thread(target=reflex_arc_loop, daemon=True)
    t.start()
    
    def tap_key(code, shift=False):
        if not ui_kbd: return
        if shift:
            ui_kbd.write(e.EV_KEY, e.KEY_LEFTSHIFT, 1)
            ui_kbd.syn()
        ui_kbd.write(e.EV_KEY, code, 1)
        ui_kbd.syn()
        ui_kbd.write(e.EV_KEY, code, 0)
        ui_kbd.syn()
        if shift:
            ui_kbd.write(e.EV_KEY, e.KEY_LEFTSHIFT, 0)
            ui_kbd.syn()
            
    last_human_report_time = 0.0
    
    while True:
        rlist = [sys.stdin]
        for p in pointers:
            rlist.append(p.fd)
        for k in keyboards:
            rlist.append(k.fd)
        r, w, x_fds = select.select(rlist, [], [])
        
        if sys.stdin in r:
            line = sys.stdin.readline()
            if not line:
                break
            try:
                cmd = json.loads(line)
                action = cmd.get('type')

                # Check if human override is active
                if time.time() < human_override_until:
                    continue

                if action == 'expect':
                    tx = cmd.get('target_x', x)
                    ty = cmd.get('target_y', y)
                    adr = cmd.get('adrenaline', 0.0)
                    ser = cmd.get('serotonin', 0.0)
                    
                    # Calculate duration based on distance and neurochemistry
                    dist = math.hypot(tx - x, ty - y)
                    # Base time: 100ms + 1.5ms per pixel
                    base_duration = 0.1 + (dist * 0.0015)
                    # Adrenaline speeds it up, serotonin slows it down
                    duration = base_duration * (1.0 - (adr * 0.3)) * (1.0 + (ser * 0.3))
                    
                    with state_lock:
                        target_x, target_y = tx, ty
                        move_duration = max(0.05, duration)
                        
                elif action == 'move':
                    with state_lock:
                        # Interrupt smooth movement if a direct relative move comes in
                        target_x = None
                        target_y = None
                    update_pos(x + cmd.get('dx', 0), y + cmd.get('dy', 0), origin="SDO")
                elif action == 'click':
                    btn = e.BTN_RIGHT if cmd.get('button') == 'right' else e.BTN_LEFT
                    if ui_mouse:
                        try:
                            ui_mouse.write(e.EV_KEY, btn, 1)
                            ui_mouse.syn()
                            time.sleep(0.05)
                            ui_mouse.write(e.EV_KEY, btn, 0)
                            ui_mouse.syn()
                        except Exception:
                            pass
                    elif pynput_mouse:
                        try:
                            from pynput.mouse import Button as MouseButton
                            pynput_mouse.click(MouseButton.right if cmd.get('button') == 'right' else MouseButton.left)
                        except Exception:
                            pass
                elif action == 'scroll':
                    dx = cmd.get('dx', 0)
                    dy = cmd.get('dy', 0)
                    if ui_mouse:
                        if dy != 0:
                            ui_mouse.write(e.EV_REL, e.REL_WHEEL, dy)
                        if dx != 0:
                            ui_mouse.write(e.EV_REL, e.REL_HWHEEL, dx)
                        ui_mouse.syn()
                    elif pynput_mouse:
                        try:
                            pynput_mouse.scroll(dx, dy)
                        except Exception:
                            pass
                elif action == 'combo':
                    mods = cmd.get('mods', [])
                    key_str = cmd.get('key')
                    if ui_kbd:
                        try:
                            for mod in mods:
                                code = getattr(e, mod, None)
                                if code: ui_kbd.write(e.EV_KEY, code, 1)
                            ui_kbd.syn()
                            if key_str:
                                k_code = getattr(e, key_str, None)
                                if k_code:
                                    ui_kbd.write(e.EV_KEY, k_code, 1)
                                    ui_kbd.syn()
                                    ui_kbd.write(e.EV_KEY, k_code, 0)
                                    ui_kbd.syn()
                            for mod in reversed(mods):
                                code = getattr(e, mod, None)
                                if code: ui_kbd.write(e.EV_KEY, code, 0)
                            ui_kbd.syn()
                        except Exception:
                            pass
                    elif pynput_kbd:
                        try:
                            # Map mod keys
                            for mod in mods:
                                if mod in ('KEY_LEFTMETA', 'KEY_RIGHTMETA'): pynput_kbd.press(PynputKey.cmd)
                                elif mod in ('KEY_LEFTSHIFT', 'KEY_RIGHTSHIFT'): pynput_kbd.press(PynputKey.shift)
                                elif mod in ('KEY_LEFTCTRL', 'KEY_RIGHTCTRL'): pynput_kbd.press(PynputKey.ctrl)
                                elif mod in ('KEY_LEFTALT', 'KEY_RIGHTALT'): pynput_kbd.press(PynputKey.alt)
                            if key_str:
                                clean = key_str.replace('KEY_', '').lower()
                                if clean == 'space': pynput_kbd.press(PynputKey.space); pynput_kbd.release(PynputKey.space)
                                elif clean == 'enter': pynput_kbd.press(PynputKey.enter); pynput_kbd.release(PynputKey.enter)
                                elif clean == 'backspace': pynput_kbd.press(PynputKey.backspace); pynput_kbd.release(PynputKey.backspace)
                                elif clean == 'tab': pynput_kbd.press(PynputKey.tab); pynput_kbd.release(PynputKey.tab)
                                elif clean == 'esc': pynput_kbd.press(PynputKey.esc); pynput_kbd.release(PynputKey.esc)
                                elif len(clean) == 1:
                                    k_obj = PynputKeyCode.from_char(clean)
                                    pynput_kbd.press(k_obj)
                                    pynput_kbd.release(k_obj)
                            for mod in reversed(mods):
                                if mod in ('KEY_LEFTMETA', 'KEY_RIGHTMETA'): pynput_kbd.release(PynputKey.cmd)
                                elif mod in ('KEY_LEFTSHIFT', 'KEY_RIGHTSHIFT'): pynput_kbd.release(PynputKey.shift)
                                elif mod in ('KEY_LEFTCTRL', 'KEY_RIGHTCTRL'): pynput_kbd.release(PynputKey.ctrl)
                                elif mod in ('KEY_LEFTALT', 'KEY_RIGHTALT'): pynput_kbd.release(PynputKey.alt)
                        except Exception:
                            pass
                elif action == 'type':
                    text = cmd.get('text', '')
                    if ui_kbd:
                        for c in text:
                            if c in CHAR_MAP:
                                code, shift = CHAR_MAP[c]
                                tap_key(code, shift)
                    elif pynput_kbd:
                        try:
                            pynput_kbd.type(text)
                        except Exception:
                            pass
            except Exception as ex:
                pass
                
        now_time = time.time()
        for pdev in list(pointers):
            if pdev.fd in r:
                try:
                    for event in pdev.read():
                        human_override_until = time.time() + 2.5
                        with state_lock:
                            target_x = None
                            target_y = None

                        if event.type == e.EV_REL:
                            if event.code == e.REL_X:
                                update_pos(x + event.value, y, origin="HUMAN")
                            elif event.code == e.REL_Y:
                                update_pos(x, y + event.value, origin="HUMAN")
                            elif event.code in (e.REL_WHEEL, e.REL_HWHEEL):
                                if ui_mouse:
                                    ui_mouse.write(e.EV_REL, event.code, event.value)
                                    ui_mouse.syn()
                        elif event.type == e.EV_ABS:
                            # Touchpad / absolute coordinate update
                            if pynput_mouse:
                                try:
                                    cur = pynput_mouse.position
                                    if cur and len(cur) >= 2:
                                        update_pos(int(cur[0]), int(cur[1]), origin="HUMAN")
                                except Exception:
                                    pass
                        elif event.type == e.EV_KEY:
                            if ui_mouse:
                                ui_mouse.write(e.EV_KEY, event.code, event.value)
                                ui_mouse.syn()
                            if event.value in (1, 0): # Press or release
                                btn_name = "LEFT" if event.code in (e.BTN_LEFT, getattr(e, 'BTN_TOUCH', 0x14a)) else ("RIGHT" if event.code == e.BTN_RIGHT else "MIDDLE")
                                print(json.dumps({
                                    "type": "click",
                                    "button": btn_name,
                                    "code": event.code,
                                    "value": event.value,
                                    "x": x,
                                    "y": y,
                                    "origin": "HUMAN"
                                }), flush=True)
                except Exception:
                    if pdev in pointers:
                        pointers.remove(pdev)
                        
        for kdev in list(keyboards):
            if kdev.fd in r:
                try:
                    for event in kdev.read():
                        human_override_until = time.time() + 2.5
                        with state_lock:
                            target_x = None
                            target_y = None

                        if event.type == e.EV_KEY and event.value == 1: # Key press
                            key_name = e.KEY.get(event.code, f"KEY_{event.code}")
                            if isinstance(key_name, list):
                                key_name = key_name[0]
                            char_repr = None
                            for ch, (code, _) in CHAR_MAP.items():
                                if code == event.code and len(ch) == 1:
                                    char_repr = ch
                                    break
                            print(json.dumps({
                                "type": "keystroke",
                                "key": str(key_name),
                                "char": char_repr,
                                "code": event.code,
                                "value": event.value,
                                "x": x,
                                "y": y,
                                "origin": "HUMAN"
                            }), flush=True)
                except Exception:
                    if kdev in keyboards:
                        keyboards.remove(kdev)

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        sys.exit(0)
