#include "xr_input.h"

#include <math.h>
#include <string.h>

#include <android/log.h>

#include <GLES3/gl3.h>

#include "xr_mouse.h"
#include "xr_overlay.h"
#include "xr_session.h"
#include "xr_swapchain.h"

#define TAG "QuestShock"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)

#define LEFT 0
#define RIGHT 1

static const char *kHandName[2] = {"left", "right"};
static const float kHandColor[2][4] = {
    {0.2f, 1.0f, 1.0f, 1.0f}, // left: cyan
    {1.0f, 0.85f, 0.1f, 1.0f}, // right: amber
};

// The keyboard overlay's title bar (drag handle + Close button) is handled
// entirely here, before a hit would normally be dispatched to
// KeyboardOverlay as a touch - these fractions must agree with
// KeyboardOverlay.java's TITLE_BAR_HEIGHT/CLOSE_BUTTON_WIDTH constants (in
// pixels, out of KeyboardOverlay.WIDTH/HEIGHT there). A hit in the top
// TITLE_BAR_V_FRACTION of the quad, at or left of CLOSE_BUTTON_U_FRACTION,
// is the drag handle; right of that (still within the title bar) is the
// Close button, which is dispatched as an ordinary click instead. The menu
// launcher overlay has no title bar - it's a plain click-only panel.
#define TITLE_BAR_V_FRACTION (90.0f / 768.0f)
#define CLOSE_BUTTON_U_FRACTION (1.0f - 160.0f / 1024.0f)

static XrInstance g_instance = XR_NULL_HANDLE;
static XrSession g_session = XR_NULL_HANDLE;
static XrActionSet g_action_set = XR_NULL_HANDLE;
static XrAction g_aim_pose_action = XR_NULL_HANDLE;
static XrAction g_select_click_action = XR_NULL_HANDLE;
static XrAction g_menu_toggle_action = XR_NULL_HANDLE;
static XrPath g_hand_path[2];
static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};

// Edge-detection state, so touch dispatch and logging only fire on actual
// state changes, not every frame. Each interaction target (game quad, menu
// launcher, keyboard) tracks its own "was this hand's ray hitting it last
// frame" independently, since the menu and keyboard overlays can now both
// be visible at once.
static bool g_prev_select[2] = {false, false};
static bool g_prev_menu = false;
static bool g_game_prev_hit[2] = {false, false};
static bool g_game_touch_active[2] = {false, false};
static bool g_menu_prev_hit[2] = {false, false};
static bool g_menu_touch_active[2] = {false, false};
static bool g_keyboard_prev_hit[2] = {false, false};
static bool g_keyboard_touch_active[2] = {false, false};

// Drag state for repositioning the keyboard overlay via its title bar (see
// TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION above) - the menu launcher
// has no title bar, so it's never draggable. g_keyboard_drag_hand is -1
// when g_keyboard_dragging is false. The offset is the vector from the
// quad's center to the grabbed point at drag-start, kept constant for the
// whole gesture so the quad follows the hand rigidly rather than
// re-centering under the ray each frame.
static bool g_keyboard_dragging = false;
static int g_keyboard_drag_hand = -1;
static float g_keyboard_drag_offset_x = 0.0f;
static float g_keyboard_drag_offset_y = 0.0f;

// A "view" reference space, located once per frame (not per hand) to get
// an approximate head position for the laser-beam billboard math below -
// the only consumer of a head pose in this file. Both LOCAL (g_local_space,
// owned by xr_session.c) and VIEW reference spaces are mandated by core
// OpenXR, so no capability check is needed to create this.
static XrSpace g_view_space = XR_NULL_HANDLE;

// Per-hand laser-beam state: a thin, billboarded quad spanning from the
// controller to wherever that hand's ray currently crosses the plane of
// whichever target (keyboard, menu, or the game quad) claimed it this
// frame - see xr_input_build_beam()/xr_input_get_beam_layer(). Each hand
// gets its own tiny solid-color swapchain (no shared tint on XrCompositionLayerQuad,
// so two separately-colored textures is simplest). g_beam_quad_valid is
// reset to false at the top of every xr_input_sync_and_draw() call and
// only set back to true if that hand actually claims a beam this frame.
#define BEAM_TEX_SIZE 2
#define BEAM_THICKNESS_METERS 0.004f
#define MAX_BEAM_LENGTH_METERS 2.0f

static XrSwapchainState g_beam_swapchain[2];
static XrCompositionLayerQuad g_beam_quad[2];
static bool g_beam_quad_valid[2] = {false, false};

static bool xr_check(XrResult result, const char *what) {
    if (XR_SUCCEEDED(result))
        return true;
    char resultString[XR_MAX_RESULT_STRING_SIZE] = {0};
    if (g_instance != XR_NULL_HANDLE)
        xrResultToString(g_instance, result, resultString);
    LOGE("XR: %s failed: %s (%d)", what, resultString[0] ? resultString : "?", (int)result);
    return false;
}

// Standard quaternion-vector rotation (v + 2*cross(q.xyz, cross(q.xyz, v) +
// q.w*v)) - used to turn the aim pose's orientation into its local -Z
// ("forward") axis, the ray direction per the OpenXR aim-pose convention.
static void quat_rotate_vec(const XrQuaternionf *q, float vx, float vy, float vz, float *outx,
                             float *outy, float *outz) {
    float cx = q->y * vz - q->z * vy + q->w * vx;
    float cy = q->z * vx - q->x * vz + q->w * vy;
    float cz = q->x * vy - q->y * vx + q->w * vz;
    *outx = vx + 2.0f * (q->y * cz - q->z * cy);
    *outy = vy + 2.0f * (q->z * cx - q->x * cz);
    *outz = vz + 2.0f * (q->x * cy - q->y * cx);
}

static void vec3_sub(const float a[3], const float b[3], float out[3]) {
    out[0] = a[0] - b[0];
    out[1] = a[1] - b[1];
    out[2] = a[2] - b[2];
}

static float vec3_dot(const float a[3], const float b[3]) {
    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}

static void vec3_cross(const float a[3], const float b[3], float out[3]) {
    out[0] = a[1] * b[2] - a[2] * b[1];
    out[1] = a[2] * b[0] - a[0] * b[2];
    out[2] = a[0] * b[1] - a[1] * b[0];
}

// Returns false (out left untouched) if v is too close to zero-length to
// normalize safely - callers use this to detect a degenerate billboard
// axis and fall back to another reference vector.
static bool vec3_normalize(const float v[3], float out[3]) {
    float len = sqrtf(vec3_dot(v, v));
    if (len < 1e-6f)
        return false;
    out[0] = v[0] / len;
    out[1] = v[1] / len;
    out[2] = v[2] / len;
    return true;
}

// Converts an orthonormal local->world rotation matrix (given as its
// columns - x/y/z, each the world-space direction of that local axis) to
// an XrQuaternionf, via the standard trace-based (Shepperd) method.
static void mat3_to_quat(const float x[3], const float y[3], const float z[3], XrQuaternionf *q) {
    float m00 = x[0], m10 = x[1], m20 = x[2];
    float m01 = y[0], m11 = y[1], m21 = y[2];
    float m02 = z[0], m12 = z[1], m22 = z[2];
    float trace = m00 + m11 + m22;
    if (trace > 0.0f) {
        float s = sqrtf(trace + 1.0f) * 2.0f;
        q->w = 0.25f * s;
        q->x = (m21 - m12) / s;
        q->y = (m02 - m20) / s;
        q->z = (m10 - m01) / s;
    } else if (m00 > m11 && m00 > m22) {
        float s = sqrtf(1.0f + m00 - m11 - m22) * 2.0f;
        q->w = (m21 - m12) / s;
        q->x = 0.25f * s;
        q->y = (m01 + m10) / s;
        q->z = (m02 + m20) / s;
    } else if (m11 > m22) {
        float s = sqrtf(1.0f + m11 - m00 - m22) * 2.0f;
        q->w = (m02 - m20) / s;
        q->x = (m01 + m10) / s;
        q->y = 0.25f * s;
        q->z = (m12 + m21) / s;
    } else {
        float s = sqrtf(1.0f + m22 - m00 - m11) * 2.0f;
        q->w = (m10 - m01) / s;
        q->x = (m02 + m20) / s;
        q->y = (m12 + m21) / s;
        q->z = 0.25f * s;
    }
}

bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format) {
    g_instance = instance;
    g_session = session;

    xrStringToPath(instance, "/user/hand/left", &g_hand_path[LEFT]);
    xrStringToPath(instance, "/user/hand/right", &g_hand_path[RIGHT]);

    XrActionSetCreateInfo setInfo = {XR_TYPE_ACTION_SET_CREATE_INFO};
    strncpy(setInfo.actionSetName, "gameplay", XR_MAX_ACTION_SET_NAME_SIZE - 1);
    strncpy(setInfo.localizedActionSetName, "Gameplay", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE - 1);
    setInfo.priority = 0;
    if (!xr_check(xrCreateActionSet(instance, &setInfo, &g_action_set), "xrCreateActionSet"))
        return false;

    XrActionCreateInfo aimInfo = {XR_TYPE_ACTION_CREATE_INFO};
    strncpy(aimInfo.actionName, "aim_pose", XR_MAX_ACTION_NAME_SIZE - 1);
    strncpy(aimInfo.localizedActionName, "Aim Pose", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
    aimInfo.actionType = XR_ACTION_TYPE_POSE_INPUT;
    aimInfo.countSubactionPaths = 2;
    aimInfo.subactionPaths = g_hand_path;
    if (!xr_check(xrCreateAction(g_action_set, &aimInfo, &g_aim_pose_action),
                  "xrCreateAction(aim_pose)"))
        return false;

    XrActionCreateInfo selectInfo = {XR_TYPE_ACTION_CREATE_INFO};
    strncpy(selectInfo.actionName, "select_click", XR_MAX_ACTION_NAME_SIZE - 1);
    strncpy(selectInfo.localizedActionName, "Select", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
    selectInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
    selectInfo.countSubactionPaths = 2;
    selectInfo.subactionPaths = g_hand_path;
    if (!xr_check(xrCreateAction(g_action_set, &selectInfo, &g_select_click_action),
                  "xrCreateAction(select_click)"))
        return false;

    // The touch_controller profile's "menu" component (the three-line
    // hamburger icon) only exists on the left controller - there's no
    // right-hand equivalent.
    XrActionCreateInfo menuInfo = {XR_TYPE_ACTION_CREATE_INFO};
    strncpy(menuInfo.actionName, "menu_toggle", XR_MAX_ACTION_NAME_SIZE - 1);
    strncpy(menuInfo.localizedActionName, "Menu Toggle", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
    menuInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
    menuInfo.countSubactionPaths = 1;
    menuInfo.subactionPaths = &g_hand_path[LEFT];
    if (!xr_check(xrCreateAction(g_action_set, &menuInfo, &g_menu_toggle_action),
                  "xrCreateAction(menu_toggle)"))
        return false;

    XrPath profilePath;
    xrStringToPath(instance, "/interaction_profiles/oculus/touch_controller", &profilePath);

    // touch_controller has no .../trigger/click component, only the float
    // .../trigger/value - binding this boolean action straight to it
    // relies on OpenXR's standard action-type equivalence (runtimes
    // auto-convert float inputs to boolean via an internal threshold), the
    // same pattern Khronos' own hello_xr sample uses for its "select"
    // action. Suggesting a binding to a component the profile doesn't
    // have at all (like a nonexistent .../trigger/click) fails the entire
    // xrSuggestInteractionProfileBindings call, not just that one entry.
    XrPath aimPoseLeftPath, aimPoseRightPath, triggerLeftPath, triggerRightPath, menuClickPath;
    xrStringToPath(instance, "/user/hand/left/input/aim/pose", &aimPoseLeftPath);
    xrStringToPath(instance, "/user/hand/right/input/aim/pose", &aimPoseRightPath);
    xrStringToPath(instance, "/user/hand/left/input/trigger/value", &triggerLeftPath);
    xrStringToPath(instance, "/user/hand/right/input/trigger/value", &triggerRightPath);
    xrStringToPath(instance, "/user/hand/left/input/menu/click", &menuClickPath);

    XrActionSuggestedBinding bindings[] = {
        {g_aim_pose_action, aimPoseLeftPath},   {g_aim_pose_action, aimPoseRightPath},
        {g_select_click_action, triggerLeftPath}, {g_select_click_action, triggerRightPath},
        {g_menu_toggle_action, menuClickPath},
    };
    XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
    suggested.interactionProfile = profilePath;
    suggested.countSuggestedBindings = sizeof(bindings) / sizeof(bindings[0]);
    suggested.suggestedBindings = bindings;
    if (!xr_check(xrSuggestInteractionProfileBindings(instance, &suggested),
                  "xrSuggestInteractionProfileBindings"))
        return false;

    for (int hand = 0; hand < 2; hand++) {
        XrActionSpaceCreateInfo spaceInfo = {XR_TYPE_ACTION_SPACE_CREATE_INFO};
        spaceInfo.action = g_aim_pose_action;
        spaceInfo.subactionPath = g_hand_path[hand];
        spaceInfo.poseInActionSpace.orientation.w = 1.0f;
        if (!xr_check(xrCreateActionSpace(session, &spaceInfo, &g_aim_space[hand]),
                      "xrCreateActionSpace(aim)"))
            return false;
    }

    XrSessionActionSetsAttachInfo attachInfo = {XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
    attachInfo.countActionSets = 1;
    attachInfo.actionSets = &g_action_set;
    if (!xr_check(xrAttachSessionActionSets(session, &attachInfo), "xrAttachSessionActionSets"))
        return false;

    XrReferenceSpaceCreateInfo viewSpaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
    viewSpaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
    viewSpaceInfo.poseInReferenceSpace.orientation.w = 1.0f;
    if (!xr_check(xrCreateReferenceSpace(session, &viewSpaceInfo, &g_view_space),
                  "xrCreateReferenceSpace(view)"))
        return false;

    for (int hand = 0; hand < 2; hand++) {
        if (!xr_swapchain_create(instance, session, swapchain_format, BEAM_TEX_SIZE, BEAM_TEX_SIZE,
                                  &g_beam_swapchain[hand])) {
            LOGE("XR: beam swapchain setup failed for hand %d", hand);
            return false;
        }
    }

    LOGI("XR: input action set ready (aim pose + trigger + menu-toggle)");
    return true;
}

// Hit-tests/dispatches this hand's ray against one overlay quad - shared
// by the menu launcher and keyboard overlays in xr_input_sync_and_draw()
// below. dragCapable enables the keyboard-only title-bar/drag-handle
// handling (see TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION); the menu
// launcher has no title bar, so it's always false there and every hit is a
// plain click. Returns true only when this hand's ray is actually
// relevant to this overlay this frame - landing on it now, or continuing
// a touch/drag begun on a previous frame for this hand that hasn't been
// released yet - not merely because the overlay is visible. The caller
// should only stop trying other targets (menu/keyboard/game quad) when
// this returns true; a visible-but-unclaimed overlay falls through so a
// lower-priority target (ultimately the game quad) can still be aimed at.
static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
                                  const XrSpaceLocation *location, float fx, float fy, float fz,
                                  bool selectDownEdge, bool selectUpEdge, bool *touchActive,
                                  bool *prevHit, const char *quadName, float *outCursorU,
                                  float *outCursorV, bool *outCursorHit, bool *outPlaneHit,
                                  float *outPlaneDistance) {
    *outPlaneHit = false;
    if (!xr_overlay_is_visible(overlay))
        return false;

    // Captured before this call's own down/up-edge handling below can
    // mutate them, so a hand with a pending down-touch (waiting for its
    // matching up) or an in-progress drag on *this* overlay still claims
    // the ray this frame even if it has strayed off the panel's current
    // bounds - see `claims` below.
    bool hadTouch = *touchActive;
    bool hadDrag = dragCapable && g_keyboard_dragging && g_keyboard_drag_hand == hand;

    float quadCenterX, quadCenterY, distance, halfWidth, halfHeight;
    xr_overlay_get_quad_extent(overlay, &quadCenterX, &quadCenterY, &distance, &halfWidth,
                                &halfHeight);

    // The quad's plane is z = -distance (facing the viewer along -Z) - a
    // ray parallel to it (fz ~ 0) never crosses. worldX/Y is the raw
    // world-space intersection point, needed as-is for dragging (which can
    // legitimately move the quad to where the ray isn't currently hitting
    // it); cx/cy are that same point in the quad's own [-1,1] local space;
    // hit/u/v (the equivalent 0..1, top-left-origin coordinates the Java
    // view's pixel grid and the on-quad hit logs use) are only meaningful
    // within the quad's current bounds, unlike planeHit.
    bool planeHit = false, hit = false;
    float u = 0.0f, v = 0.0f, worldX = 0.0f, worldY = 0.0f, t = 0.0f;
    if (fabsf(fz) > 1e-5f) {
        t = (-distance - location->pose.position.z) / fz;
        if (t > 0.0f) {
            planeHit = true;
            worldX = location->pose.position.x + t * fx;
            worldY = location->pose.position.y + t * fy;
            float cx = (worldX - quadCenterX) / halfWidth;
            float cy = (worldY - quadCenterY) / halfHeight;
            hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
            u = (cx + 1.0f) * 0.5f;
            v = (1.0f - cy) * 0.5f;
        }
    }
    *outPlaneHit = planeHit;
    *outPlaneDistance = t;

    if (hit != *prevHit) {
        LOGI("XR: %s aim ray %s %s quad (u=%.2f v=%.2f)", kHandName[hand],
             hit ? "entered" : "left", quadName, u, v);
        *prevHit = hit;
    }
    if (hit) {
        *outCursorHit = true;
        *outCursorU = u;
        *outCursorV = v;
    }

    // Only claim (and thus route/beam) this hand's ray to this overlay
    // when it's actually relevant this frame: landing on it now, or
    // continuing a touch/drag that started on it - never merely because
    // it's visible.
    bool claims = hit || hadTouch || hadDrag;

    if (!dragCapable) {
        if (selectDownEdge && hit) {
            xr_overlay_touch(overlay, u, v, true);
            *touchActive = true;
        } else if (selectUpEdge && *touchActive) {
            xr_overlay_touch(overlay, u, v, false);
            *touchActive = false;
        }
        return claims;
    }

    bool onTitleBar = hit && v < TITLE_BAR_V_FRACTION;
    bool onDragHandle = onTitleBar && u <= CLOSE_BUTTON_U_FRACTION;

    if (selectDownEdge) {
        if (onDragHandle) {
            g_keyboard_dragging = true;
            g_keyboard_drag_hand = hand;
            g_keyboard_drag_offset_x = worldX - quadCenterX;
            g_keyboard_drag_offset_y = worldY - quadCenterY;
        } else if (hit) {
            xr_overlay_touch(overlay, u, v, true);
            *touchActive = true;
        }
    } else if (selectUpEdge) {
        if (g_keyboard_dragging && g_keyboard_drag_hand == hand) {
            g_keyboard_dragging = false;
            g_keyboard_drag_hand = -1;
        } else if (*touchActive) {
            xr_overlay_touch(overlay, u, v, false);
            *touchActive = false;
        }
    }

    // Re-intersects against the same fixed-distance plane every frame the
    // drag continues, so the quad tracks the hand in real time rather than
    // only jumping on the down/up edges above.
    if (g_keyboard_dragging && g_keyboard_drag_hand == hand && planeHit) {
        xr_overlay_set_position(overlay, worldX - g_keyboard_drag_offset_x,
                                 worldY - g_keyboard_drag_offset_y);
    }
    return claims;
}

// Builds this hand's laser-beam quad - a thin, billboarded ribbon from the
// controller (location->pose.position) along the aim ray (fx,fy,fz,
// already unit length) to length_m meters out, oriented so it reads as a
// line from the viewer's eye (headLoc->pose.position) regardless of angle:
// one in-plane axis follows the ray direction, the other is the thin
// "width", and the quad's normal is billboarded toward the eye
// (orthogonalized against the ray axis, so the ribbon only rotates around
// its own long axis as the hand moves, never twists). Acquires/clears/
// releases this hand's tiny beam swapchain and leaves g_beam_quad[hand]
// ready for xr_input_get_beam_layer() - doesn't set space/eyeVisibility/
// layerFlags (see that function's doc comment in xr_input.h).
static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float fx, float fy,
                                 float fz, float length_m, const XrSpaceLocation *headLoc) {
    float origin[3] = {location->pose.position.x, location->pose.position.y,
                        location->pose.position.z};
    float dirY[3] = {fx, fy, fz};
    float endpoint[3] = {origin[0] + length_m * fx, origin[1] + length_m * fy,
                          origin[2] + length_m * fz};
    float center[3] = {(origin[0] + endpoint[0]) * 0.5f, (origin[1] + endpoint[1]) * 0.5f,
                        (origin[2] + endpoint[2]) * 0.5f};
    float head[3] = {headLoc->pose.position.x, headLoc->pose.position.y,
                      headLoc->pose.position.z};

    float toEyeRaw[3], toEye[3];
    vec3_sub(head, center, toEyeRaw);
    if (!vec3_normalize(toEyeRaw, toEye)) {
        // Head is (almost) exactly at the beam's midpoint - astronomically
        // unlikely, but fall back to a fixed direction rather than divide
        // by ~0.
        toEye[0] = 0.0f;
        toEye[1] = 0.0f;
        toEye[2] = 1.0f;
    }

    // Orthogonalize toEye against the ray axis to get the billboard
    // normal - if the ray points almost straight at/away from the eye,
    // that leaves ~nothing to normalize, so fall back to world-up then
    // world-Z, each reprojected the same way.
    float normalZ[3];
    float d = vec3_dot(toEye, dirY);
    float proj[3] = {toEye[0] - dirY[0] * d, toEye[1] - dirY[1] * d, toEye[2] - dirY[2] * d};
    if (!vec3_normalize(proj, normalZ)) {
        static const float kWorldUp[3] = {0.0f, 1.0f, 0.0f};
        d = vec3_dot(kWorldUp, dirY);
        proj[0] = kWorldUp[0] - dirY[0] * d;
        proj[1] = kWorldUp[1] - dirY[1] * d;
        proj[2] = kWorldUp[2] - dirY[2] * d;
        if (!vec3_normalize(proj, normalZ)) {
            static const float kWorldZ[3] = {0.0f, 0.0f, 1.0f};
            d = vec3_dot(kWorldZ, dirY);
            proj[0] = kWorldZ[0] - dirY[0] * d;
            proj[1] = kWorldZ[1] - dirY[1] * d;
            proj[2] = kWorldZ[2] - dirY[2] * d;
            if (!vec3_normalize(proj, normalZ)) {
                // dirY parallel to both world-up and world-Z is impossible
                // for two non-parallel vectors - unreachable in practice,
                // but keep the beam well-defined regardless.
                normalZ[0] = 1.0f;
                normalZ[1] = 0.0f;
                normalZ[2] = 0.0f;
            }
        }
    }

    float axisX[3];
    vec3_cross(dirY, normalZ, axisX);
    if (!vec3_normalize(axisX, axisX)) {
        axisX[0] = 1.0f;
        axisX[1] = 0.0f;
        axisX[2] = 0.0f;
    }
    float axisZ[3];
    vec3_cross(axisX, dirY, axisZ); // already unit length - axisX/dirY are orthonormal

    XrQuaternionf orientation;
    mat3_to_quat(axisX, dirY, axisZ, &orientation);

    if (xr_swapchain_acquire(g_instance, &g_beam_swapchain[hand])) {
        // Premultiplied alpha (matches OverlayPanel.compositeAndPublish()'s
        // convention, and XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT's
        // assumption - see xr_session.c) - kHandColor's RGB scaled by this
        // beam's own alpha, not the opaque RGB itself.
        const float alpha = 0.55f;
        const float *c = kHandColor[hand];
        glClearColor(c[0] * alpha, c[1] * alpha, c[2] * alpha, alpha);
        glClear(GL_COLOR_BUFFER_BIT);
        xr_swapchain_release(g_instance, &g_beam_swapchain[hand]);
    }

    XrCompositionLayerQuad *quad = &g_beam_quad[hand];
    quad->type = XR_TYPE_COMPOSITION_LAYER_QUAD;
    quad->next = NULL;
    quad->subImage.swapchain = g_beam_swapchain[hand].swapchain;
    quad->subImage.imageRect.offset.x = 0;
    quad->subImage.imageRect.offset.y = 0;
    quad->subImage.imageRect.extent.width = BEAM_TEX_SIZE;
    quad->subImage.imageRect.extent.height = BEAM_TEX_SIZE;
    quad->subImage.imageArrayIndex = 0;
    quad->pose.position.x = center[0];
    quad->pose.position.y = center[1];
    quad->pose.position.z = center[2];
    quad->pose.orientation = orientation;
    quad->size.width = BEAM_THICKNESS_METERS;
    quad->size.height = length_m;

    g_beam_quad_valid[hand] = true;
}

// Hit-tests this hand's ray against the game quad (xr_get_game_quad_extent())
// - the lowest-priority target, tried only once neither the keyboard nor
// menu overlay claimed the ray this frame (or, symmetrically, once this
// hand already has a pending mouse-down on the game quad - see
// g_game_touch_active and the keyboard/menu call sites in
// xr_input_sync_and_draw()). outU/outV (only meaningful when the
// returned hit is true) let the caller drive the game's own mouse cursor
// via xr_mouse.h. Returns plain `hit` - drag/touch dispatch for actual
// clicks is the caller's responsibility (xr_mouse_click()), not this
// function's, unlike xr_input_try_overlay().
static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, float fx, float fy,
                                    float fz, bool *outPlaneHit, float *outPlaneDistance,
                                    float *outU, float *outV) {
    float distance, halfWidth, halfHeight;
    xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);

    bool planeHit = false, hit = false;
    float u = 0.0f, v = 0.0f, t = 0.0f;
    if (fabsf(fz) > 1e-5f) {
        t = (-distance - location->pose.position.z) / fz;
        if (t > 0.0f) {
            planeHit = true;
            float cx = (location->pose.position.x + t * fx) / halfWidth;
            float cy = (location->pose.position.y + t * fy) / halfHeight;
            hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
            u = (cx + 1.0f) * 0.5f;
            v = (1.0f - cy) * 0.5f;
        }
    }
    *outPlaneHit = planeHit;
    *outPlaneDistance = t;
    *outU = u;
    *outV = v;

    if (hit != g_game_prev_hit[hand]) {
        LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand],
             hit ? "entered" : "left", u, v);
        g_game_prev_hit[hand] = hit;
    }
    return hit;
}

void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
    if (g_action_set == XR_NULL_HANDLE)
        return;

    XrActiveActionSet activeSet = {g_action_set, XR_NULL_PATH};
    XrActionsSyncInfo syncInfo = {XR_TYPE_ACTIONS_SYNC_INFO};
    syncInfo.countActiveActionSets = 1;
    syncInfo.activeActionSets = &activeSet;
    xr_check(xrSyncActions(g_session, &syncInfo), "xrSyncActions");

    XrOverlay *menuOverlay = xr_session_get_menu_overlay();
    XrOverlay *keyboardOverlay = xr_session_get_keyboard_overlay();
    bool menuVisible = xr_overlay_is_visible(menuOverlay);
    bool keyboardVisible = xr_overlay_is_visible(keyboardOverlay);

    // A drag can't get stuck active across a hide/reopen - e.g. Close
    // being hit on one hand mid-drag on the other, or the session ending.
    if (!keyboardVisible) {
        g_keyboard_dragging = false;
        g_keyboard_drag_hand = -1;
    }

    // A hand only ever gets a laser-beam quad this frame if it actually
    // claims the keyboard or menu overlay's ray below (see the
    // xr_input_build_beam() calls) - reset here so a hand that doesn't
    // claim one this frame doesn't keep showing last frame's beam.
    g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;

    // The billboard math needs an approximate head position - only bother
    // locating it on frames where a beam could possibly be drawn at all
    // (i.e. any drawn frame - the game quad can claim a beam on its own
    // even with both overlays closed).
    bool needBeams = draw;
    XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
    bool haveHead = false;
    if (needBeams) {
        const XrSpaceLocationFlags neededHead =
            XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
        haveHead = xr_check(xrLocateSpace(g_view_space, baseSpace, time, &headLoc),
                             "xrLocateSpace(view)") &&
                   (headLoc.locationFlags & neededHead) == neededHead;
    }

    // Fed to xr_overlay_update_cursor() after the loop below - whichever
    // hand's ray hits a given overlay last wins if both do, good enough
    // for a single on-quad cursor per overlay (only ever updated when hit
    // is true, so a later hand that misses doesn't hide an earlier hand's
    // hit).
    bool menuCursorHit = false, keyboardCursorHit = false;
    float menuCursorU = 0.0f, menuCursorV = 0.0f;
    float keyboardCursorU = 0.0f, keyboardCursorV = 0.0f;

    for (int hand = 0; hand < 2; hand++) {
        XrActionStateGetInfo selectInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
        selectInfo.action = g_select_click_action;
        selectInfo.subactionPath = g_hand_path[hand];
        XrActionStateBoolean selectState = {XR_TYPE_ACTION_STATE_BOOLEAN};
        xrGetActionStateBoolean(g_session, &selectInfo, &selectState);
        bool selectDown = selectState.isActive && selectState.currentState;
        bool selectDownEdge = selectDown && !g_prev_select[hand];
        bool selectUpEdge = !selectDown && g_prev_select[hand];
        if (selectDown != g_prev_select[hand]) {
            LOGI("XR: %s trigger %s", kHandName[hand], selectDown ? "DOWN" : "UP");
            g_prev_select[hand] = selectDown;
        }

        if (hand == LEFT) {
            XrActionStateGetInfo menuInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
            menuInfo.action = g_menu_toggle_action;
            menuInfo.subactionPath = g_hand_path[LEFT];
            XrActionStateBoolean menuState = {XR_TYPE_ACTION_STATE_BOOLEAN};
            xrGetActionStateBoolean(g_session, &menuInfo, &menuState);
            bool menuDown = menuState.isActive && menuState.currentState;
            if (menuDown && !g_prev_menu) {
                LOGI("XR: menu-toggle button DOWN");
                // Only ever affects the menu launcher - the keyboard is
                // fully independent (its own Close button is the only way
                // to hide it, see KeyboardOverlay.java).
                if (menuOverlay != NULL) {
                    xr_overlay_toggle_visible(menuOverlay);
                    g_menu_prev_hit[LEFT] = g_menu_prev_hit[RIGHT] = false;
                    g_menu_touch_active[LEFT] = g_menu_touch_active[RIGHT] = false;
                }
            } else if (!menuDown && g_prev_menu) {
                LOGI("XR: menu-toggle button UP");
            }
            g_prev_menu = menuDown;
        }

        XrActionStateGetInfo poseInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
        poseInfo.action = g_aim_pose_action;
        poseInfo.subactionPath = g_hand_path[hand];
        XrActionStatePose poseState = {XR_TYPE_ACTION_STATE_POSE};
        xrGetActionStatePose(g_session, &poseInfo, &poseState);
        if (!poseState.isActive)
            continue;

        XrSpaceLocation location = {XR_TYPE_SPACE_LOCATION};
        if (!xr_check(xrLocateSpace(g_aim_space[hand], baseSpace, time, &location), "xrLocateSpace"))
            continue;
        const XrSpaceLocationFlags needed =
            XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
        if ((location.locationFlags & needed) != needed)
            continue;

        float fx, fy, fz;
        quat_rotate_vec(&location.pose.orientation, 0.0f, 0.0f, -1.0f, &fx, &fy, &fz);

        // Keyboard first (it's the more likely target while it's up), then
        // the menu launcher, then - only once neither actually claims the
        // ray this frame (not merely "isn't visible" - see
        // xr_input_try_overlay()'s doc comment) - the game quad itself.
        // Whichever target claims the ray gets a laser-beam quad built for
        // it (if a head pose is available and the ray actually crosses
        // that target's plane) - see xr_input_build_beam(). Keyboard/menu
        // are additionally gated on !g_game_touch_active[hand]: a pending
        // mouse-down on the game quad (trigger still held since a
        // down-edge dispatched there) must keep claiming the ray even if
        // it strays onto another panel before release, the cross-target
        // analogue of xr_input_try_overlay()'s own hadTouch/hadDrag - the
        // eventual mouse-up has to reach the game quad, not whatever the
        // ray happens to be over on the release frame.
        bool keyboardPlaneHit = false;
        float keyboardPlaneDistance = 0.0f;
        if (!g_game_touch_active[hand] &&
            xr_input_try_overlay(keyboardOverlay, true, hand, &location, fx, fy, fz,
                                  selectDownEdge, selectUpEdge, &g_keyboard_touch_active[hand],
                                  &g_keyboard_prev_hit[hand], "keyboard", &keyboardCursorU,
                                  &keyboardCursorV, &keyboardCursorHit, &keyboardPlaneHit,
                                  &keyboardPlaneDistance)) {
            if (haveHead && keyboardPlaneHit)
                xr_input_build_beam(hand, &location, fx, fy, fz,
                                     fminf(keyboardPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
            continue;
        }
        bool menuPlaneHit = false;
        float menuPlaneDistance = 0.0f;
        if (!g_game_touch_active[hand] &&
            xr_input_try_overlay(menuOverlay, false, hand, &location, fx, fy, fz, selectDownEdge,
                                  selectUpEdge, &g_menu_touch_active[hand], &g_menu_prev_hit[hand],
                                  "menu", &menuCursorU, &menuCursorV, &menuCursorHit, &menuPlaneHit,
                                  &menuPlaneDistance)) {
            if (haveHead && menuPlaneHit)
                xr_input_build_beam(hand, &location, fx, fy, fz,
                                     fminf(menuPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
            continue;
        }

        // Lowest priority (for a fresh hit): the game quad itself - drives
        // the engine's own mouse cursor/clicks via xr_mouse.h, exactly
        // like a desktop mouse (absolute position + left button), plus the
        // same laser-beam visual the other two targets get.
        bool gamePlaneHit = false;
        float gamePlaneDistance = 0.0f, gameU = 0.0f, gameV = 0.0f;
        bool gameHit = xr_input_try_game_quad(hand, &location, fx, fy, fz, &gamePlaneHit,
                                               &gamePlaneDistance, &gameU, &gameV);
        if (gameHit) {
            int gameWidth, gameHeight;
            xr_get_game_resolution(&gameWidth, &gameHeight);
            int px = (int)(gameU * (float)gameWidth);
            int py = (int)(gameV * (float)gameHeight);
            if (px < 0)
                px = 0;
            else if (px >= gameWidth)
                px = gameWidth - 1;
            if (py < 0)
                py = 0;
            else if (py >= gameHeight)
                py = gameHeight - 1;
            xr_mouse_move(px, py);
        }
        if (selectDownEdge && gameHit) {
            xr_mouse_click(true);
            g_game_touch_active[hand] = true;
        } else if (selectUpEdge && g_game_touch_active[hand]) {
            xr_mouse_click(false);
            g_game_touch_active[hand] = false;
        }
        // gameHit implies gamePlaneHit (both only ever set together above),
        // kept as a separate out-param for symmetry with
        // xr_input_try_overlay()'s outPlaneHit/outPlaneDistance pair.
        if (haveHead && gameHit)
            xr_input_build_beam(hand, &location, fx, fy, fz,
                                 fminf(gamePlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
    }

    if (menuVisible)
        xr_overlay_update_cursor(menuOverlay, menuCursorU, menuCursorV, menuCursorHit);
    if (keyboardVisible)
        xr_overlay_update_cursor(keyboardOverlay, keyboardCursorU, keyboardCursorV,
                                  keyboardCursorHit);
}

bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad) {
    if (hand < 0 || hand > 1 || !g_beam_quad_valid[hand])
        return false;
    *out_quad = g_beam_quad[hand];
    return true;
}

void xr_input_shutdown(void) {
    for (int hand = 0; hand < 2; hand++) {
        if (g_aim_space[hand] != XR_NULL_HANDLE)
            xrDestroySpace(g_aim_space[hand]);
        g_aim_space[hand] = XR_NULL_HANDLE;
        xr_swapchain_destroy(&g_beam_swapchain[hand]);
    }
    if (g_view_space != XR_NULL_HANDLE)
        xrDestroySpace(g_view_space);
    g_view_space = XR_NULL_HANDLE;
    if (g_action_set != XR_NULL_HANDLE)
        xrDestroyActionSet(g_action_set);
    g_action_set = XR_NULL_HANDLE;
    g_aim_pose_action = XR_NULL_HANDLE;
    g_select_click_action = XR_NULL_HANDLE;
    g_menu_toggle_action = XR_NULL_HANDLE;

    g_instance = XR_NULL_HANDLE;
    g_session = XR_NULL_HANDLE;
    memset(g_prev_select, 0, sizeof(g_prev_select));
    memset(g_game_prev_hit, 0, sizeof(g_game_prev_hit));
    memset(g_game_touch_active, 0, sizeof(g_game_touch_active));
    memset(g_menu_prev_hit, 0, sizeof(g_menu_prev_hit));
    memset(g_menu_touch_active, 0, sizeof(g_menu_touch_active));
    memset(g_keyboard_prev_hit, 0, sizeof(g_keyboard_prev_hit));
    memset(g_keyboard_touch_active, 0, sizeof(g_keyboard_touch_active));
    g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
    g_prev_menu = false;
    g_keyboard_dragging = false;
    g_keyboard_drag_hand = -1;
}
