#include "xr_swapchain.h"

#include <dlfcn.h>

#include <android/log.h>

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

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

// This file's other GL calls otherwise resolve to gl4es (the only GL
// symbol provider linked into this binary - see android/engine-patches/
// 02-android-opengl-es.patch), which is fine for anything shared with the
// engine's own gl4es-routed rendering. But the swapchain images OpenXR
// hands us are real driver texture objects gl4es never created itself,
// and gl4es's own glFramebufferTexture2D can't attach a texture it has no
// tracked metadata for. So the FBO *container* is created via gl4es's own
// glGenFramebuffers/glBindFramebuffer (so gl4es recognizes the id as its
// own and its own per-frame glBindFramebuffer succeeds), while the
// texture-attach step - the specifically foreign part - goes through the
// real driver directly, via dlsym against libGLESv2.so.
typedef void (*PFNQSFRAMEBUFFERTEXTURE2D)(GLenum, GLenum, GLenum, GLuint, GLint);
typedef GLenum (*PFNQSCHECKFRAMEBUFFERSTATUS)(GLenum);

static PFNQSFRAMEBUFFERTEXTURE2D real_glFramebufferTexture2D;
static PFNQSCHECKFRAMEBUFFERSTATUS real_glCheckFramebufferStatus;
static bool g_real_gles_loaded = false;

static bool xr_swapchain_load_real_gles(void) {
    if (g_real_gles_loaded)
        return true;
    void *lib = dlopen("libGLESv2.so", RTLD_NOW | RTLD_LOCAL);
    if (lib == NULL) {
        LOGE("XR: swapchain dlopen(libGLESv2.so) failed: %s", dlerror());
        return false;
    }
    real_glFramebufferTexture2D = (PFNQSFRAMEBUFFERTEXTURE2D)dlsym(lib, "glFramebufferTexture2D");
    real_glCheckFramebufferStatus =
        (PFNQSCHECKFRAMEBUFFERSTATUS)dlsym(lib, "glCheckFramebufferStatus");
    g_real_gles_loaded = real_glFramebufferTexture2D && real_glCheckFramebufferStatus;
    return g_real_gles_loaded;
}

bool xr_swapchain_create(XrInstance instance, XrSession session, int64_t format, int width,
                          int height, XrSwapchainState *out) {
    if (!xr_swapchain_load_real_gles()) {
        LOGE("XR: swapchain couldn't resolve real GLES FBO functions via dlsym");
        return false;
    }

    out->width = width;
    out->height = height;

    XrSwapchainCreateInfo swapchainInfo = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
    swapchainInfo.usageFlags =
        XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT;
    swapchainInfo.format = format;
    swapchainInfo.sampleCount = 1;
    swapchainInfo.width = (uint32_t)width;
    swapchainInfo.height = (uint32_t)height;
    swapchainInfo.faceCount = 1;
    swapchainInfo.arraySize = 1;
    swapchainInfo.mipCount = 1;
    if (!xr_check(instance, xrCreateSwapchain(session, &swapchainInfo, &out->swapchain),
                  "xrCreateSwapchain"))
        return false;

    uint32_t imageCount = 0;
    xrEnumerateSwapchainImages(out->swapchain, 0, &imageCount, NULL);
    if (imageCount > XR_SWAPCHAIN_MAX_IMAGES) {
        LOGE("XR: swapchain reports %u images, only room for %d", imageCount,
             XR_SWAPCHAIN_MAX_IMAGES);
        return false;
    }
    // Zero-initialized, not just `.type` set per element - these structs
    // also carry a `next` field the runtime may read, and an uninitialized
    // stack array would leave it as garbage.
    XrSwapchainImageOpenGLESKHR images[XR_SWAPCHAIN_MAX_IMAGES] = {0};
    for (uint32_t i = 0; i < imageCount; i++)
        images[i].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
    if (!xr_check(instance,
                  xrEnumerateSwapchainImages(out->swapchain, imageCount, &imageCount,
                                              (XrSwapchainImageBaseHeader *)images),
                  "xrEnumerateSwapchainImages"))
        return false;
    out->image_count = imageCount;

    // Wraps each swapchain-provided texture in its own framebuffer, matching
    // how OpenGL.cc's own CreateFrameBuffer() wraps backupBuffer - just
    // without a depth/stencil attachment, since the final composite draw
    // (see opengl_swap_and_restore) never needs one. The Gen/Bind calls are
    // gl4es's own (linked, not dlsym'd) - see the comment above
    // real_glFramebufferTexture2D for why.
    bool all_complete = true;
    for (uint32_t i = 0; i < imageCount; i++) {
        glGenFramebuffers(1, &out->fbos[i]);
        glBindFramebuffer(GL_FRAMEBUFFER, out->fbos[i]);
        real_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                                     images[i].image, 0);
        GLenum status = real_glCheckFramebufferStatus(GL_FRAMEBUFFER);
        if (status != GL_FRAMEBUFFER_COMPLETE) {
            LOGE("XR: swapchain FBO %u incomplete: 0x%x", i, status);
            all_complete = false;
        }
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    return all_complete;
}

void xr_swapchain_destroy(XrSwapchainState *state) {
    for (uint32_t i = 0; i < state->image_count; i++) {
        if (state->fbos[i] != 0)
            glDeleteFramebuffers(1, &state->fbos[i]);
    }
    state->image_count = 0;
    if (state->swapchain != XR_NULL_HANDLE)
        xrDestroySwapchain(state->swapchain);
    state->swapchain = XR_NULL_HANDLE;
}

// Acquires the next image of the given swapchain, waits for the runtime to
// finish with it, and binds its framebuffer (sized to exactly fill it) as
// the current render target.
bool xr_swapchain_acquire(XrInstance instance, XrSwapchainState *state) {
    uint32_t imageIndex = 0;
    XrSwapchainImageAcquireInfo acquireInfo = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
    if (!xr_check(instance, xrAcquireSwapchainImage(state->swapchain, &acquireInfo, &imageIndex),
                  "xrAcquireSwapchainImage"))
        return false;

    XrSwapchainImageWaitInfo waitImageInfo = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
    waitImageInfo.timeout = XR_INFINITE_DURATION;
    if (!xr_check(instance, xrWaitSwapchainImage(state->swapchain, &waitImageInfo),
                  "xrWaitSwapchainImage"))
        return false;

    // gl4es's own (linked, not dlsym'd) bind - this targets an FBO id gl4es
    // itself created (see xr_swapchain_create()), so its own "current FBO"
    // bookkeeping updates correctly and its immediate-mode draw calls
    // (android_draw_surface_as_quad(), the laser reticle) land in the right
    // place.
    glBindFramebuffer(GL_FRAMEBUFFER, state->fbos[imageIndex]);
    glViewport(0, 0, state->width, state->height);
    return true;
}

void xr_swapchain_release(XrInstance instance, XrSwapchainState *state) {
    XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
    xr_check(instance, xrReleaseSwapchainImage(state->swapchain, &releaseInfo),
              "xrReleaseSwapchainImage");
}
