#!/usr/bin/env python3
import socket
import time
import threading

SERVER_IP = "192.168.1.17"
SERVER_PORT = 65432

hid_lock = threading.Lock()
mouse_down_active = False
current_mode = "off"  # "off" | "macro1" | "macro2" | "macro3"

# -------- Tunables --------
RECOIL_INTERVAL_S = 0.05  # time between mouse reports (20 Hz)

# Macro 1 (original)
M1_PHASE1_STEPS = 15
M1_PHASE2_STEPS = 10
M1_PHASE1_DY = 2
M1_PHASE2_DY = 2
M1_LATE_DY   = 1
M1_SWAY_PERIOD = 8  # horizontal sway change every N steps

# Macro 2 (stronger early pull + faster sway)
M2_PHASE1_STEPS = 18
M2_PHASE2_STEPS = 12
M2_PHASE1_DY = 3
M2_PHASE2_DY = 2
M2_LATE_DY   = 1
M2_SWAY_PERIOD = 6

# Macro 3 (Home): left/right oscillation with steady downward pull
# Idea: hold a horizontal direction for M3_HOLD_STEPS, then flip direction,
# while adding a constant downward dy every tick.
M3_SWAY_AMPL     = 6   # pixels left/right each report
M3_HOLD_STEPS    = 3   # reports to keep the same horizontal direction before flipping
M3_VERTICAL_DY   = 4   # constant downward pull each report

# ----------------------------- Low-level HID -----------------------------
def send_mouse_event(buttons: int, x: int, y: int, wheel: int = 0):
    with hid_lock:
        with open("/dev/hidg0", "wb") as hid:
            hid.write(bytearray([buttons, x & 0xFF, y & 0xFF, wheel & 0xFF]))

# ----------------------------- Patterns -----------------------------
def step_for_mode(mode: str, step: int):
    """Return dx, dy for this step in the given mode."""
    if mode == "macro1":
        if step < M1_PHASE1_STEPS:
            return 0, M1_PHASE1_DY
        elif step < M1_PHASE1_STEPS + M1_PHASE2_STEPS:
            return 0, M1_PHASE2_DY
        else:
            sway_step = (step - M1_PHASE1_STEPS - M1_PHASE2_STEPS) // M1_SWAY_PERIOD
            dx = -1 if (sway_step % 2 == 0) else 1
            return dx, M1_LATE_DY

    elif mode == "macro2":
        if step < M2_PHASE1_STEPS:
            return 0, M2_PHASE1_DY
        elif step < M2_PHASE1_STEPS + M2_PHASE2_STEPS:
            return 0, M2_PHASE2_DY
        else:
            sway_step = (step - M2_PHASE1_STEPS - M2_PHASE2_STEPS) // M2_SWAY_PERIOD
            dx = -1 if (sway_step % 2 == 0) else 1
            return dx, M2_LATE_DY

    elif mode == "macro3":
        # Repeated left/right by the same distance while pulling down.
        # Direction flips every M3_HOLD_STEPS.
        segment = (step // M3_HOLD_STEPS) % 2   # 0,1,0,1,...
        dx = M3_SWAY_AMPL if segment == 0 else -M3_SWAY_AMPL
        dy = M3_VERTICAL_DY
        return dx, dy

    # default/off
    return 0, 0

# ----------------------------- Anti-recoil worker -----------------------------
def anti_recoil_pattern(mode_fn, mode_supplier, stop_evt: threading.Event):
    """
    Runs while LMB is held. If the mode changes while running, we switch pattern on-the-fly.
    mode_fn(step, mode) -> (dx, dy)
    mode_supplier() -> current_mode string
    """
    print("[RECOIL] Anti-recoil pattern started")
    step = 0
    while not stop_evt.is_set():
        mode = mode_supplier()
        dx, dy = mode_fn(step, mode)
        send_mouse_event(0x00, dx, dy)
        step += 1
        time.sleep(RECOIL_INTERVAL_S)
    print("[RECOIL] Anti-recoil pattern stopped")

# ----------------------------- Server loop -----------------------------
def main():
    global mouse_down_active, current_mode
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind((SERVER_IP, SERVER_PORT))
        s.listen()
        print(f"[SERVER] Listening on {SERVER_IP}:{SERVER_PORT}")

        conn, addr = s.accept()
        with conn:
            print(f"[SERVER] Connected by {addr}")

            recoil_stop_evt = threading.Event()
            recoil_thread = None

            def get_mode():
                return current_mode

            while True:
                data = conn.recv(1024)
                if not data:
                    break
                cmd = data.decode().strip().lower()
                print(f"[SERVER] Received: {cmd}")

                if cmd.startswith("mode "):
                    # mode off | mode macro1 | mode macro2 | mode macro3
                    _, new_mode = cmd.split(" ", 1)
                    if new_mode in ("off", "macro1", "macro2", "macro3"):
                        prev = current_mode
                        current_mode = new_mode
                        print(f"[SERVER] Mode changed: {prev} -> {current_mode}")

                        # If LMB is currently held, switch pattern immediately by restarting thread
                        if mouse_down_active:
                            recoil_stop_evt.set()
                            if recoil_thread is not None:
                                recoil_thread.join()
                            recoil_stop_evt.clear()
                            recoil_thread = threading.Thread(
                                target=anti_recoil_pattern,
                                args=(lambda step, mode: step_for_mode(mode, step), get_mode, recoil_stop_evt),
                                daemon=True
                            )
                            recoil_thread.start()

                elif cmd == "mouse left down":
                    if not mouse_down_active:
                        mouse_down_active = True
                        recoil_stop_evt.clear()
                        recoil_thread = threading.Thread(
                            target=anti_recoil_pattern,
                            args=(lambda step, mode: step_for_mode(mode, step), get_mode, recoil_stop_evt),
                            daemon=True
                        )
                        recoil_thread.start()

                elif cmd == "mouse left up":
                    if mouse_down_active:
                        mouse_down_active = False
                        recoil_stop_evt.set()
                        if recoil_thread is not None:
                            recoil_thread.join()
                            recoil_thread = None

                # Acknowledge every command
                conn.sendall(b"OK")

        # Cleanup on disconnect
        if mouse_down_active:
            mouse_down_active = False

if __name__ == "__main__":
    main()
