/*
 * hid/inject_timer.c
 *
 * Periodic DISPATCH_LEVEL injection timer. Consumes shared-memory
 * seq counters, builds HID reports, and pushes them into the
 * pending-read queue.
 *
 * Concurrency — classic seqlock, consumer side:
 *
 *   The user-mode producer uses a two-phase commit on each lane's
 *   Seq counter:
 *
 *       InterlockedIncrement(&Seq);   // Seq -> odd (write in progress)
 *       <write all transient fields>
 *       ReleaseFence();
 *       InterlockedIncrement(&Seq);   // Seq -> even (committed)
 *
 *   An even Seq therefore always denotes a stable snapshot; an odd
 *   Seq means a publisher is mid-write and the fields may be torn.
 *
 *   The kernel consumer implements the matching retry protocol:
 *
 *       attempt:
 *           seq1 = InterlockedOr(&Seq, 0)      (acquire)
 *           if (seq1 is odd)         -> writer busy; bail this tick
 *           if (seq1 == LastSeq)     -> nothing new since last post
 *           snapshot transient fields
 *           ReadBarrier()
 *           seq2 = InterlockedOr(&Seq, 0)      (acquire)
 *           if (seq2 != seq1)        -> torn during snapshot; retry
 *           post HID report; LastSeq = seq1; ConsumedSeq = seq1
 *
 *   A bounded retry count (VHID_SEQLOCK_MAX_ATTEMPTS) guards against
 *   a pathological producer that keeps the lane constantly in flux;
 *   on exhaustion we leave LastSeq untouched so the next 1 ms tick
 *   tries again.
 *
 *   The callback runs at DISPATCH_LEVEL with AutomaticSerialization
 *   = FALSE. It touches only:
 *
 *     - The shared-memory view (read-only except for ConsumedSeq).
 *     - Per-device LastMouseSeq / LastKbdSeq which are only touched
 *       here (the timer is a single-instance object, so there is at
 *       most one in-flight callback per device).
 *     - VhidkmReportQueuePost, which is documented as DISPATCH-safe
 *       and takes its own WDFSPINLOCK.
 *
 * Report mapping:
 *
 *   Mouse AbsMode == VHID_ABSMODE_RELATIVE
 *       -> VHID_MOUSE_REL_REPORT (8 bytes, ReportId 0x02)
 *
 *   Mouse AbsMode == VHID_ABSMODE_ABSOLUTE
 *       -> VHID_MOUSE_ABS_REPORT (8 bytes, ReportId 0x03)
 *
 *   Keyboard snapshot regardless of content
 *       -> VHID_KEYBOARD_INPUT_REPORT (9 bytes, ReportId 0x01)
 *
 * Out-of-range values are clamped at the boundary to match the
 * declared descriptor field widths. HidParser would otherwise reject
 * the report and hidclass.sys would NAK the read with
 * STATUS_DEVICE_DATA_ERROR.
 */

#include "driver.h"
#include "device.h"
#include "inject_timer.h"
#include "report_queue.h"
#include "shared_mem.h"
#include "trace.h"
#include "inject_timer.tmh"

#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, VhidkmInjectTimerStart)
#pragma alloc_text(PAGE, VhidkmInjectTimerStop)
#endif

EVT_WDF_TIMER VhidkmInjectTimerTick;

/*
 * Upper bound on per-tick seqlock retries. A single producer normally
 * completes its two-phase commit in well under a microsecond, so a
 * handful of attempts covers both the "writer mid-commit" case (which
 * resolves inside the 1 ms heartbeat) and transient tearing from a
 * writer on a different core. If every attempt observes a tear we
 * bail out of this tick and let the next one try — LastSeq is left
 * untouched so no event is skipped.
 */
#define VHID_SEQLOCK_MAX_ATTEMPTS   4u

static FORCEINLINE
SHORT
ClampShort(
    _In_ INT32 Value
    )
{
    if (Value >  32767) { return  32767; }
    if (Value < -32768) { return -32768; }
    return (SHORT)Value;
}

static FORCEINLINE
USHORT
ClampAbs(
    _In_ INT32 Value
    )
{
    if (Value < 0)                  { return 0; }
    if (Value > VHID_MOUSE_ABS_MAX) { return (USHORT)VHID_MOUSE_ABS_MAX; }
    return (USHORT)Value;
}

static
_IRQL_requires_(DISPATCH_LEVEL)
VOID
InjectMouseIfChanged(
    _In_ PVHIDKM_DEVICE_CONTEXT DevCtx,
    _In_ PVHID_SHARED_STATE     Shared
    )
{
    UCHAR  absMode = 0;
    UCHAR  buttons = 0;
    INT8   wheel   = 0;
    INT8   hwheel  = 0;
    INT32  dx      = 0;
    INT32  dy      = 0;
    INT32  absX    = 0;
    INT32  absY    = 0;
    LONG   seq1    = 0;
    LONG   seq2    = 0;
    ULONG  attempt;
    BOOLEAN stable = FALSE;

    for (attempt = 0; attempt < VHID_SEQLOCK_MAX_ATTEMPTS; attempt++) {
        /*
         * Acquire-read the commit counter. An odd value means the
         * producer is between its two Interlocked increments, i.e.
         * mid-write — we must not read the payload fields now.
         */
        seq1 = InterlockedOr(&Shared->Mouse.Seq, 0);
        if ((seq1 & 1) != 0) {
            return;
        }
        if (seq1 == DevCtx->LastMouseSeq) {
            return;
        }

        absMode = Shared->Mouse.AbsMode;
        buttons = Shared->Mouse.Buttons & VHID_MOUSE_BTN_MASK;
        wheel   = Shared->Mouse.Wheel;
        hwheel  = Shared->Mouse.HWheel;
        dx      = Shared->Mouse.Dx;
        dy      = Shared->Mouse.Dy;
        absX    = Shared->Mouse.AbsX;
        absY    = Shared->Mouse.AbsY;

        _ReadBarrier();

        /*
         * Re-read the counter. If it moved, the producer committed a
         * new event (or started another one) while we were snapshotting
         * the fields — the observed payload may be torn across two
         * generations and must be discarded.
         */
        seq2 = InterlockedOr(&Shared->Mouse.Seq, 0);
        if (seq2 == seq1) {
            stable = TRUE;
            break;
        }
    }

    if (!stable) {
        /*
         * Too many torn snapshots in one tick. Leave LastMouseSeq
         * untouched so the next 1 ms callback re-tries cleanly.
         */
        return;
    }

    if (absMode == VHID_ABSMODE_ABSOLUTE) {
        VHID_MOUSE_ABS_REPORT r;
        r.ReportId = VHID_REPORTID_MOUSE_ABS;
        r.Buttons  = buttons;
        r.X        = ClampAbs(absX);
        r.Y        = ClampAbs(absY);
        r.Wheel    = wheel;
        r.HWheel   = hwheel;
        (VOID)VhidkmReportQueuePost(DevCtx,
            (const UCHAR*)&r, (UCHAR)sizeof(r));
    } else {
        VHID_MOUSE_REL_REPORT r;
        r.ReportId = VHID_REPORTID_MOUSE_REL;
        r.Buttons  = buttons;
        r.X        = ClampShort(dx);
        r.Y        = ClampShort(dy);
        r.Wheel    = wheel;
        r.HWheel   = hwheel;
        (VOID)VhidkmReportQueuePost(DevCtx,
            (const UCHAR*)&r, (UCHAR)sizeof(r));
    }

    DevCtx->LastMouseSeq = seq1;
    InterlockedExchange(&Shared->Mouse.ConsumedSeq, seq1);
}

static
_IRQL_requires_(DISPATCH_LEVEL)
VOID
InjectKeyboardIfChanged(
    _In_ PVHIDKM_DEVICE_CONTEXT DevCtx,
    _In_ PVHID_SHARED_STATE     Shared
    )
{
    UCHAR  modifiers = 0;
    UCHAR  keys[VHID_KBD_MAX_KEYS];
    LONG   seq1      = 0;
    LONG   seq2      = 0;
    ULONG  attempt;
    BOOLEAN stable = FALSE;

    RtlZeroMemory(keys, sizeof(keys));

    for (attempt = 0; attempt < VHID_SEQLOCK_MAX_ATTEMPTS; attempt++) {
        seq1 = InterlockedOr(&Shared->Keyboard.Seq, 0);
        if ((seq1 & 1) != 0) {
            return;
        }
        if (seq1 == DevCtx->LastKbdSeq) {
            return;
        }

        modifiers = Shared->Keyboard.Modifiers;
        RtlCopyMemory(keys, Shared->Keyboard.Keys, sizeof(keys));

        _ReadBarrier();

        seq2 = InterlockedOr(&Shared->Keyboard.Seq, 0);
        if (seq2 == seq1) {
            stable = TRUE;
            break;
        }
    }

    if (!stable) {
        return;
    }

    VHID_KEYBOARD_INPUT_REPORT r;
    r.ReportId  = VHID_REPORTID_KEYBOARD_INPUT;
    r.Modifiers = modifiers;
    r.Reserved  = 0;
    RtlCopyMemory(r.Keys, keys, sizeof(r.Keys));

    (VOID)VhidkmReportQueuePost(DevCtx,
        (const UCHAR*)&r, (UCHAR)sizeof(r));

    DevCtx->LastKbdSeq = seq1;
    InterlockedExchange(&Shared->Keyboard.ConsumedSeq, seq1);
}

_Use_decl_annotations_
VOID
VhidkmInjectTimerTick(
    WDFTIMER Timer
    )
{
    PVHIDKM_DEVICE_CONTEXT ctx = VhidkmDeviceGetContext(
        WdfTimerGetParentObject(Timer));
    PVHID_SHARED_STATE     s   = ctx->Shared.View;

    if (s == NULL) {
        return;
    }

    InjectMouseIfChanged(ctx, s);
    InjectKeyboardIfChanged(ctx, s);
}

_Use_decl_annotations_
NTSTATUS
VhidkmInjectTimerStart(
    PVHIDKM_DEVICE_CONTEXT DevCtx
    )
{
    WDF_TIMER_CONFIG       cfg;
    WDF_OBJECT_ATTRIBUTES  attr;
    NTSTATUS               status;

    PAGED_CODE();

    if (DevCtx->InjectTimer != NULL) {
        /* Already created; just make sure it is running. */
        (VOID)WdfTimerStart(DevCtx->InjectTimer,
            WDF_REL_TIMEOUT_IN_MS(VHID_SHARED_HEARTBEAT_MS));
        return STATUS_SUCCESS;
    }

    WDF_TIMER_CONFIG_INIT_PERIODIC(&cfg, VhidkmInjectTimerTick,
                                   VHID_SHARED_HEARTBEAT_MS);
    /*
     * AutomaticSerialization = FALSE:
     *   The default (TRUE) would force the timer callback to acquire
     *   the parent device's presentation lock, demoting it to
     *   PASSIVE_LEVEL on devices whose ExecutionLevel is Passive.
     *   We want DISPATCH_LEVEL unconditionally.
     *
     * TolerableDelay = 0:
     *   Ask the kernel to honour the 1 ms period as closely as
     *   possible. On modern Windows this maps to the HIGH_RES timer
     *   tick (~0.5–1 ms) without the user-mode timeBeginPeriod dance.
     */
    cfg.AutomaticSerialization = FALSE;
    cfg.TolerableDelay         = 0;
    cfg.UseHighResolutionTimer = WdfTrue;

    WDF_OBJECT_ATTRIBUTES_INIT(&attr);
    attr.ParentObject    = DevCtx->Device;
    attr.ExecutionLevel  = WdfExecutionLevelDispatch;

    status = WdfTimerCreate(&cfg, &attr, &DevCtx->InjectTimer);
    if (!NT_SUCCESS(status)) {
        TraceError("WdfTimerCreate(inject) failed %!STATUS!", status);
        DevCtx->InjectTimer = NULL;
        return status;
    }

    (VOID)WdfTimerStart(DevCtx->InjectTimer,
        WDF_REL_TIMEOUT_IN_MS(VHID_SHARED_HEARTBEAT_MS));
    TracePnp("inject timer started @ %u ms", VHID_SHARED_HEARTBEAT_MS);
    return STATUS_SUCCESS;
}

_Use_decl_annotations_
VOID
VhidkmInjectTimerStop(
    PVHIDKM_DEVICE_CONTEXT DevCtx
    )
{
    PAGED_CODE();

    if (DevCtx->InjectTimer == NULL) {
        return;
    }
    /* Wait = TRUE so no callback is in flight when we return. */
    (VOID)WdfTimerStop(DevCtx->InjectTimer, TRUE);
    TracePnp("inject timer stopped");
}
