#!/usr/bin/env python3
# Plays one two-finger touchpad stroke through uinput for the native case_touchpad run.
import argparse
import fcntl
import os
import struct
import sys
import time

UINPUT = "/dev/uinput"

UI_SET_EVBIT = 0x40045564
UI_SET_KEYBIT = 0x40045565
UI_SET_ABSBIT = 0x40045567
UI_SET_PROPBIT = 0x4004556E
# Bit-setter request numbers from linux/uinput.h, one _IOW per bit class.
UI_SET_EVBIT_NR = 100
UI_SET_KEYBIT_NR = 101
UI_SET_ABSBIT_NR = 103
UI_SET_PROPBIT_NR = 110
UI_ABS_SETUP = 0x401C5504
UI_DEV_SETUP = 0x405C5503
UINPUT_SETUP_FMT = "HHHH80sI"
# uinput_abs_setup is a u16 code, its pad and input_absinfo's six ints, 28 bytes.
UINPUT_ABS_SETUP_FMT = "HHiiiiii"
UI_DEV_CREATE = 0x5501
UI_DEV_DESTROY = 0x5502

EV_SYN, EV_KEY, EV_ABS = 0, 1, 3
SYN_REPORT = 0
BTN_TOOL_FINGER = 325
BTN_TOUCH = 330
BTN_TOOL_DOUBLETAP = 333
ABS_X, ABS_Y = 0, 1
ABS_MT_SLOT = 47
ABS_MT_POSITION_X, ABS_MT_POSITION_Y = 53, 54
ABS_MT_TRACKING_ID = 57
INPUT_PROP_POINTER = 0

SPAN, RES = 1000, 10
BUS_USB = 0x03
DEVICE_VENDOR, DEVICE_PRODUCT, DEVICE_VERSION = 0x4711, 0x4712, 1
MT_SLOT_MAX = 9
MAX_TRACKING_ID = 65535
EVENT_STEP_MS = 8
CREATE_SETTLE_S = 0.5
POST_SETTLE_S = 0.3


def emit(fd, kind, code, value):
    os.write(fd, struct.pack("qqHHi", 0, 0, kind, code, value))


def report(fd):
    emit(fd, EV_SYN, SYN_REPORT, 0)


def finger(fd, slot, tracking, x, y):
    emit(fd, EV_ABS, ABS_MT_SLOT, slot)
    emit(fd, EV_ABS, ABS_MT_TRACKING_ID, tracking)
    if tracking >= 0:
        emit(fd, EV_ABS, ABS_MT_POSITION_X, x)
        emit(fd, EV_ABS, ABS_MT_POSITION_Y, y)
    if slot == 0:
        emit(fd, EV_ABS, ABS_X, x)
        emit(fd, EV_ABS, ABS_Y, y)


def setup_abs(fd, code, maximum, resolution):
    fcntl.ioctl(fd, UI_ABS_SETUP, struct.pack(UINPUT_ABS_SETUP_FMT, code, 0, 0, 0, maximum, 0, 0, resolution))


def create():
    fd = os.open(UINPUT, os.O_WRONLY | os.O_NONBLOCK)
    for bit in (EV_SYN, EV_KEY, EV_ABS):
        fcntl.ioctl(fd, UI_SET_EVBIT, bit)
    for bit in (BTN_TOUCH, BTN_TOOL_FINGER, BTN_TOOL_DOUBLETAP):
        fcntl.ioctl(fd, UI_SET_KEYBIT, bit)
    for bit in (ABS_X, ABS_Y, ABS_MT_SLOT, ABS_MT_TRACKING_ID,
                ABS_MT_POSITION_X, ABS_MT_POSITION_Y):
        fcntl.ioctl(fd, UI_SET_ABSBIT, bit)
    fcntl.ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_POINTER)
    # Sample input: uinput_setup is input_id, name[80] and ff_effects_max, 92 bytes.
    setup = struct.pack(UINPUT_SETUP_FMT, BUS_USB, DEVICE_VENDOR, DEVICE_PRODUCT,
                        DEVICE_VERSION, b"flea-touchpad", 0)
    fcntl.ioctl(fd, UI_DEV_SETUP, setup)
    setup_abs(fd, ABS_X, SPAN, RES)
    setup_abs(fd, ABS_Y, SPAN, RES)
    setup_abs(fd, ABS_MT_SLOT, MT_SLOT_MAX, 0)
    setup_abs(fd, ABS_MT_TRACKING_ID, MAX_TRACKING_ID, 0)
    setup_abs(fd, ABS_MT_POSITION_X, SPAN, RES)
    setup_abs(fd, ABS_MT_POSITION_Y, SPAN, RES)
    fcntl.ioctl(fd, UI_DEV_CREATE)
    return fd


def swipe(fd, dx_mm, dy_mm, ms, hold_ms):
    dx, dy = int(dx_mm * RES), int(dy_mm * RES)
    x0, y0 = SPAN // 2, SPAN // 2
    steps = max(2, ms // EVENT_STEP_MS)
    finger(fd, 0, 1, x0, y0)
    finger(fd, 1, 2, x0, y0)
    emit(fd, EV_KEY, BTN_TOUCH, 1)
    emit(fd, EV_KEY, BTN_TOOL_FINGER, 0)
    emit(fd, EV_KEY, BTN_TOOL_DOUBLETAP, 1)
    report(fd)
    for step in range(1, steps + 1):
        x = x0 + dx * step // steps
        y = y0 + dy * step // steps
        finger(fd, 0, 1, x, y)
        finger(fd, 1, 2, x, y)
        report(fd)
        time.sleep(ms / 1000 / steps)
    if hold_ms > 0:
        time.sleep(hold_ms / 1000)
    finger(fd, 0, -1, x0 + dx, y0 + dy)
    finger(fd, 1, -1, x0 + dx, y0 + dy)
    emit(fd, EV_KEY, BTN_TOUCH, 0)
    emit(fd, EV_KEY, BTN_TOOL_DOUBLETAP, 0)
    report(fd)
    return steps


def check_api():
    # _IOW('U', nr, struct) is 0x40000000 | size << 16 | 'U' << 8 | nr, checked with no device touched.
    failures = []

    def check(label, actual, expected):
        if actual != expected:
            failures.append("%s: got %#x, want %#x" % (label, actual, expected))
        else:
            print("ok   %s (%d)" % (label, actual))

    setup_size = struct.calcsize(UINPUT_SETUP_FMT)
    abs_size = struct.calcsize(UINPUT_ABS_SETUP_FMT)
    int_size = struct.calcsize("i")
    check("struct uinput_setup size", setup_size, 92)
    check("struct uinput_abs_setup size", abs_size, 28)
    check("UI_DEV_SETUP request", UI_DEV_SETUP,
          0x40000000 | (setup_size << 16) | (ord("U") << 8) | 3)
    check("UI_ABS_SETUP request", UI_ABS_SETUP,
          0x40000000 | (abs_size << 16) | (ord("U") << 8) | 4)
    # Each UI_SET_* enables one plain int bit, so each request is _IOW('U', its nr, int).
    check("UI_SET_EVBIT request", UI_SET_EVBIT,
          0x40000000 | (int_size << 16) | (ord("U") << 8) | UI_SET_EVBIT_NR)
    check("UI_SET_KEYBIT request", UI_SET_KEYBIT,
          0x40000000 | (int_size << 16) | (ord("U") << 8) | UI_SET_KEYBIT_NR)
    check("UI_SET_ABSBIT request", UI_SET_ABSBIT,
          0x40000000 | (int_size << 16) | (ord("U") << 8) | UI_SET_ABSBIT_NR)
    check("UI_SET_PROPBIT request", UI_SET_PROPBIT,
          0x40000000 | (int_size << 16) | (ord("U") << 8) | UI_SET_PROPBIT_NR)
    if failures:
        for failure in failures:
            print("FAIL " + failure, file=sys.stderr)
        return 1
    print("flea-touchpad: uinput API check passed")
    return 0


def main(argv):
    parser = argparse.ArgumentParser(prog="flea-touchpad")
    sub = parser.add_subparsers(dest="command", required=True)
    play = sub.add_parser("swipe", help="play one two-finger stroke, then remove the device")
    play.add_argument("--dx-mm", type=float, default=0.0)
    play.add_argument("--dy-mm", type=float, default=40.0)
    play.add_argument("--ms", type=int, default=120)
    play.add_argument("--hold-ms", type=int, default=0)
    sub.add_parser("check", help="assert the packed uinput structs and request numbers, no device needed")
    args = parser.parse_args(argv)
    if args.command == "check":
        return check_api()
    if not os.access(UINPUT, os.W_OK):
        print("flea-touchpad: %s is not writable, cannot create a touchpad" % UINPUT,
              file=sys.stderr)
        return 2
    fd = create()
    try:
        time.sleep(CREATE_SETTLE_S)
        steps = swipe(fd, args.dx_mm, args.dy_mm, args.ms, args.hold_ms)
        time.sleep(POST_SETTLE_S)
    finally:
        try:
            fcntl.ioctl(fd, UI_DEV_DESTROY)
        except OSError:
            pass
        os.close(fd)
    print("flea-touchpad: swipe dx=%gmm dy=%gmm ms=%d hold=%dms steps=%d" % (
        args.dx_mm, args.dy_mm, args.ms, args.hold_ms, steps))
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
