#!/usr/bin/env python3
"""Virtual Xbox-360 gamepad over /dev/uinput (OpenFUT FIFA-17 recon).

FIFA 17 runs under Proton and reads input via evdev/SDL (a real controller),
NOT via X11 XTEST — so xdotool keystrokes never reach it. This creates a
kernel-level virtual pad whose events are indistinguishable from hardware, so
FIFA's native gamepad path picks them up. Prototype (pure ctypes, no deps) to
PROVE the approach; port to a Rust driver once confirmed.

  ./vgamepad.py daemon        create the pad + hold it open, read commands from
                              the FIFO /tmp/vpad.fifo until killed
  ./vgamepad.py <cmd> [...]   send command(s) to the running daemon, e.g.
                              ./vgamepad.py a         (A / confirm)
                              ./vgamepad.py b         (B / back)
                              ./vgamepad.py up down left right
                              ./vgamepad.py lb rb start back guide

NOTE: FIFA enumerates controllers at launch, so the daemon must be running
BEFORE FIFA starts (or FIFA relaunched) for the pad to be seen.
"""
import os, sys, time, struct, fcntl

FIFO = "/tmp/vpad.fifo"
UINPUT = "/dev/uinput"

# ---- ioctl numbers (x86_64) ------------------------------------------------
UI_SET_EVBIT  = 0x40045564
UI_SET_KEYBIT = 0x40045565
UI_SET_ABSBIT = 0x40045567
UI_DEV_CREATE = 0x5501
UI_DEV_DESTROY = 0x5502

EV_SYN, EV_KEY, EV_ABS = 0x00, 0x01, 0x03
SYN_REPORT = 0
BUS_USB = 0x03

# Xbox-360 button codes
BTN = {
    "a": 0x130, "b": 0x131, "x": 0x133, "y": 0x134,
    "lb": 0x136, "rb": 0x137, "back": 0x13a, "start": 0x13b,
    "guide": 0x13c, "l3": 0x13d, "r3": 0x13e,
}
ABS_X, ABS_Y, ABS_Z, ABS_RX, ABS_RY, ABS_RZ = 0, 1, 2, 3, 4, 5
ABS_HAT0X, ABS_HAT0Y = 0x10, 0x11
STICKS = [ABS_X, ABS_Y, ABS_RX, ABS_RY]      # -32768..32767
TRIGGERS = [ABS_Z, ABS_RZ]                    # 0..255
HATS = [ABS_HAT0X, ABS_HAT0Y]                 # -1..1

# d-pad direction -> (hat axis, value)
DPAD = {
    "up": (ABS_HAT0Y, -1), "down": (ABS_HAT0Y, 1),
    "left": (ABS_HAT0X, -1), "right": (ABS_HAT0X, 1),
}


def _ev(fd, etype, code, value):
    # struct input_event { timeval time(16); u16 type; u16 code; s32 value; }
    os.write(fd, struct.pack("llHHi", 0, 0, etype, code, value))


def _syn(fd):
    _ev(fd, EV_SYN, SYN_REPORT, 0)


def create_device():
    fd = os.open(UINPUT, os.O_WRONLY | os.O_NONBLOCK)
    fcntl.ioctl(fd, UI_SET_EVBIT, EV_KEY)
    fcntl.ioctl(fd, UI_SET_EVBIT, EV_ABS)
    fcntl.ioctl(fd, UI_SET_EVBIT, EV_SYN)
    for code in BTN.values():
        fcntl.ioctl(fd, UI_SET_KEYBIT, code)
    for ax in STICKS + TRIGGERS + HATS:
        fcntl.ioctl(fd, UI_SET_ABSBIT, ax)

    # legacy uinput_user_dev: name[80], input_id{bus,vendor,product,version}(u16*4),
    # ff_effects_max(u32), absmax/min/fuzz/flat[64] each s32
    name = b"Microsoft X-Box 360 pad".ljust(80, b"\0")
    idv = struct.pack("HHHH", BUS_USB, 0x045e, 0x028e, 0x0114)
    ff = struct.pack("I", 0)
    absmax = [0] * 64; absmin = [0] * 64; absfuzz = [0] * 64; absflat = [0] * 64
    for ax in STICKS:
        absmax[ax] = 32767; absmin[ax] = -32768; absflat[ax] = 128
    for ax in TRIGGERS:
        absmax[ax] = 255; absmin[ax] = 0
    for ax in HATS:
        absmax[ax] = 1; absmin[ax] = -1
    payload = (name + idv + ff
               + struct.pack("64i", *absmax) + struct.pack("64i", *absmin)
               + struct.pack("64i", *absfuzz) + struct.pack("64i", *absflat))
    os.write(fd, payload)
    fcntl.ioctl(fd, UI_DEV_CREATE)
    time.sleep(0.3)   # let udev create /dev/input/eventN + jsN
    return fd


def do(fd, cmd):
    cmd = cmd.strip().lower()
    if not cmd:
        return
    if cmd in BTN:
        _ev(fd, EV_KEY, BTN[cmd], 1); _syn(fd); time.sleep(0.08)
        _ev(fd, EV_KEY, BTN[cmd], 0); _syn(fd)
    elif cmd in DPAD:
        ax, val = DPAD[cmd]
        _ev(fd, EV_ABS, ax, val); _syn(fd); time.sleep(0.10)
        _ev(fd, EV_ABS, ax, 0); _syn(fd)
    elif cmd.startswith("hold_") and cmd[5:] in BTN:      # hold_lb etc. (no auto-release)
        _ev(fd, EV_KEY, BTN[cmd[5:]], 1); _syn(fd)
    elif cmd.startswith("rel_") and cmd[4:] in BTN:
        _ev(fd, EV_KEY, BTN[cmd[4:]], 0); _syn(fd)
    else:
        sys.stderr.write("unknown cmd: %s\n" % cmd)
    time.sleep(0.12)


def daemon():
    if os.path.exists(FIFO):
        os.unlink(FIFO)
    os.mkfifo(FIFO)
    fd = create_device()
    sys.stderr.write("[vgamepad] device created, listening on %s\n" % FIFO)
    sys.stderr.flush()
    try:
        while True:
            with open(FIFO, "r") as f:      # blocks until a writer sends a line
                for line in f:
                    for cmd in line.split():
                        do(fd, cmd)
    finally:
        try:
            fcntl.ioctl(fd, UI_DEV_DESTROY)
        except Exception:
            pass
        os.close(fd)
        if os.path.exists(FIFO):
            os.unlink(FIFO)


def send(cmds):
    if not os.path.exists(FIFO):
        sys.stderr.write("!! daemon not running (no %s). Start: vgamepad.py daemon\n" % FIFO)
        sys.exit(2)
    with open(FIFO, "w") as f:
        f.write(" ".join(cmds) + "\n")
    print("sent: %s" % " ".join(cmds))


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)
    if sys.argv[1] == "daemon":
        daemon()
    else:
        send(sys.argv[1:])
