#include "xr_input.h"

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

#include <android/log.h>

#include <GLES3/gl3.h>

#include "xr_overlay.h"
#include "xr_session.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_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;

static GLuint g_reticle_program = 0;
static GLint g_reticle_color_loc = -1;

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 GLuint compile_shader(GLenum type, const char *src) {
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &src, NULL);
    glCompileShader(shader);
    GLint compiled = GL_FALSE;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (!compiled) {
        char log[512];
        glGetShaderInfoLog(shader, sizeof(log), NULL, log);
        LOGE("XR: reticle shader compile failed: %s", log);
    }
    return shader;
}

// A standalone flat-color shader, independent of the engine's own
// textureShaderProgram (OpenGL.cc is a separate, C++-only translation
// unit, and shader state isn't shared across programs anyway) - position
// is emitted directly in clip space, matching the same [-1,1] local quad
// coordinates android_draw_surface_as_quad() (see android/engine-patches/
// 11-android-openxr-present.patch) already uses for its own vertex
// positions, so no view/projection matrix is needed here either.
static const char *kVertexSrc = "attribute vec3 position;\n"
                                 "void main() { gl_Position = vec4(position, 1.0); }\n";
static const char *kFragmentSrc = "precision mediump float;\n"
                                   "uniform vec4 color;\n"
                                   "void main() { gl_FragColor = color; }\n";

// Attribute 0 to match kVertexSrc's single "position" attribute - fine to
// reuse the same numeric index the engine's own immediate-mode drawing
// treats specially, since that's a per-program binding and this program is
// never current at the same time as gl4es's immediate-mode emulation runs;
// xr_input_sync_and_draw() disables the array again right after drawing,
// the same discipline android_draw_surface_as_quad() already established.
#define RETICLE_POSITION_LOC 0

static bool xr_input_init_reticle_program(void) {
    GLuint vs = compile_shader(GL_VERTEX_SHADER, kVertexSrc);
    GLuint fs = compile_shader(GL_FRAGMENT_SHADER, kFragmentSrc);
    g_reticle_program = glCreateProgram();
    glAttachShader(g_reticle_program, vs);
    glAttachShader(g_reticle_program, fs);
    glBindAttribLocation(g_reticle_program, RETICLE_POSITION_LOC, "position");
    glLinkProgram(g_reticle_program);
    GLint linked = GL_FALSE;
    glGetProgramiv(g_reticle_program, GL_LINK_STATUS, &linked);
    glDeleteShader(vs);
    glDeleteShader(fs);
    if (!linked) {
        char log[512];
        glGetProgramInfoLog(g_reticle_program, sizeof(log), NULL, log);
        LOGE("XR: reticle program link failed: %s", log);
        return false;
    }
    g_reticle_color_loc = glGetUniformLocation(g_reticle_program, "color");
    return true;
}

bool xr_input_init(XrInstance instance, XrSession session) {
    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;

    if (!xr_input_init_reticle_program())
        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 if this overlay is visible - claiming the
// hand's ray processing for this frame, regardless of whether the ray
// actually hits it - so the caller should stop trying other targets (menu/
// keyboard/game quad are mutually exclusive per hand, per frame).
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) {
    if (!xr_overlay_is_visible(overlay))
        return false;

    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;
    if (fabsf(fz) > 1e-5f) {
        float 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;
        }
    }

    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;
    }

    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 true;
    }

    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 true;
}

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;
    }

    // The GL reticle is drawn directly into whatever framebuffer is
    // currently bound - the game quad's swapchain image (see xr_frame_end(),
    // which calls this while that image is still bound). That only makes
    // sense while aiming at the game quad; the menu/keyboard overlays' own
    // cursors are drawn by their Java views instead (see
    // xr_overlay_update_cursor() below), composited into their Bitmaps the
    // same way their other content is.
    bool drawReticle = draw && !menuVisible && !keyboardVisible;
    if (drawReticle)
        glUseProgram(g_reticle_program);

    // 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 if neither is visible - the game
        // quad's own reticle below. A single ray only ever interacts with
        // one target per hand per frame.
        if (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))
            continue;
        if (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))
            continue;

        float distance, halfWidth, halfHeight;
        xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);

        bool hit = false;
        float u = 0.0f, v = 0.0f, cx = 0.0f, cy = 0.0f;
        if (fabsf(fz) > 1e-5f) {
            float t = (-distance - location.pose.position.z) / fz;
            if (t > 0.0f) {
                cx = (location.pose.position.x + t * fx) / halfWidth;
                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;
            }
        }

        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;
        }

        if (!hit || !drawReticle)
            continue;

        const float kSize = 0.03f;
        const float verts[] = {
            cx - kSize, cy, 0.0f, cx + kSize, cy, 0.0f, cx, cy - kSize, 0.0f, cx, cy + kSize, 0.0f,
        };
        glUniform4fv(g_reticle_color_loc, 1, kHandColor[hand]);
        glEnableVertexAttribArray(RETICLE_POSITION_LOC);
        glVertexAttribPointer(RETICLE_POSITION_LOC, 3, GL_FLOAT, GL_FALSE, 0, verts);
        glDrawArrays(GL_LINES, 0, 4);
        glDisableVertexAttribArray(RETICLE_POSITION_LOC);
    }

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

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;
    }
    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;

    if (g_reticle_program != 0)
        glDeleteProgram(g_reticle_program);
    g_reticle_program = 0;

    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_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_prev_menu = false;
    g_keyboard_dragging = false;
    g_keyboard_drag_hand = -1;
}
