#include "xr_input.h"

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

#include <android/log.h>

#include <GLES3/gl3.h>

#include "xr_menu.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 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.
static bool g_prev_select[2] = {false, false};
static bool g_prev_menu = false;
static bool g_prev_hit[2] = {false, false};
// Tracks which hand currently has an in-flight synthetic touch down on the
// menu (so a later trigger-up edge only dispatches a matching touch-up if
// a touch-down was actually sent for that hand).
static bool g_menu_touch_active[2] = {false, false};

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

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");

    bool menuVisible = xr_menu_is_visible();

    float quadCenterX = 0.0f, quadCenterY = 0.0f, distance, halfWidth, halfHeight;
    if (menuVisible)
        xr_menu_get_quad_extent(&quadCenterX, &quadCenterY, &distance, &halfWidth, &halfHeight);
    else
        xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);

    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 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 quad's content comes from
    // MenuOverlay's rendered Bitmap instead (xr_menu.c), uploaded on a
    // separate swapchain, so no reticle is drawn for it here.
    bool drawReticle = draw && !menuVisible;
    if (drawReticle)
        glUseProgram(g_reticle_program);

    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");
                xr_menu_toggle_visible();
                // Avoid a stale hit/touch-active carrying over from
                // whichever quad was being tested against before the
                // toggle - the next frame re-evaluates against the new
                // target from a clean state.
                g_prev_hit[LEFT] = g_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);

        // The quad's plane is z = -distance (facing the viewer along -Z,
        // see QUAD_DISTANCE_METERS/xr_menu.c's MENU_DISTANCE_METERS) - a
        // ray parallel to it (fz ~ 0) never crosses. cx/cy are the hit
        // point in the quad's own [-1,1] local space (the same space
        // android_draw_surface_as_quad() draws its own vertex positions
        // in); u/v are the equivalent 0..1, top-left-origin coordinates
        // MenuOverlay's pixel grid and the on-quad hit logs both use.
        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 - quadCenterX) / halfWidth;
                cy = (location.pose.position.y + t * fy - quadCenterY) / halfHeight;
                hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
                u = (cx + 1.0f) * 0.5f;
                v = (1.0f - cy) * 0.5f;
            }
        }

        if (menuVisible) {
            if (hit != g_prev_hit[hand]) {
                LOGI("XR: %s aim ray %s menu quad (u=%.2f v=%.2f)", kHandName[hand],
                     hit ? "entered" : "left", u, v);
                g_prev_hit[hand] = hit;
            }
            if (selectDownEdge && hit) {
                xr_menu_touch(u, v, true);
                g_menu_touch_active[hand] = true;
            } else if (selectUpEdge && g_menu_touch_active[hand]) {
                xr_menu_touch(u, v, false);
                g_menu_touch_active[hand] = false;
            }
            continue;
        }

        if (hit != g_prev_hit[hand]) {
            LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand],
                 hit ? "entered" : "left", u, v);
            g_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);
    }
}

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_prev_hit, 0, sizeof(g_prev_hit));
    memset(g_menu_touch_active, 0, sizeof(g_menu_touch_active));
    g_prev_menu = false;
}
