#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <emscripten.h>
#include <emscripten/vr.h>

static int gDisplay = -1;

static int renderLoopCalled = 0;
static bool renderLoopArgCalled = false;
static void* renderLoopArgArg = NULL;

void report_result(int result) {
    emscripten_cancel_main_loop();
    if (result == 0) {
        printf("Test successful!\n");
    } else {
        printf("Test failed!\n");
    }
#ifdef REPORT_RESULT
    REPORT_RESULT(result);
#endif
}

static void printMatrix(float* m) {
    printf("{%f, %f, %f, %f,\n"
           " %f, %f, %f, %f,\n"
           " %f, %f, %f, %f,\n"
           " %f, %f, %f, %f}\n",
           m[0],  m[1],  m[2],  m[3],
           m[4],  m[5],  m[6],  m[7],
           m[8],  m[9],  m[10], m[11],
           m[12], m[13], m[14], m[15]);
}

/* Render loop with an argument, set in `renderLoop()` */
static void renderLoopArg(void* arg) {
    emscripten_vr_cancel_display_render_loop(gDisplay);

    renderLoopArgCalled = true;
    renderLoopArgArg = arg;

    emscripten_vr_exit_present(gDisplay);

    printf("Render loop with argument was called.\n");

    return;
}

static void requestPresentCallback(void* userData) {
    // Will likely never happen as the callback is not called
    // from a user-gesture in this test.
    printf("Request present callback called.\n");
}

/* Render loop without argument, set in `mainLoop()` */
static void renderLoop() {
    renderLoopCalled++;
    if(renderLoopCalled == 1) {
        /* First iteration */
        VRLayerInit init = {
            "#canvas",
            VR_LAYER_DEFAULT_LEFT_BOUNDS,
            VR_LAYER_DEFAULT_RIGHT_BOUNDS
        };
        if (!emscripten_vr_request_present(gDisplay, &init, 1, requestPresentCallback, NULL)) {
            printf("Request present with default canvas failed.\n");
            report_result(1);
            return;
        }

        if(emscripten_vr_display_presenting(gDisplay)) {
            /* request present needs to be called from a user gesture callback and
             * should have failed to make the display present. */
            printf("Error: Expected display not to be presenting.\n");
            report_result(1);
            return;
        }

        VRFrameData data;
        if (!emscripten_vr_get_frame_data(gDisplay, &data)) {
            printf("Could not get frame data. (first iteration)\n");
            report_result(1);
            return;
        }
        if(!emscripten_vr_submit_frame(gDisplay)) {
            printf("Error: Failed to submit frame to VR display %d (first iteration)\n", gDisplay);
            report_result(1);
            return;
        }
        return;
    } else if (renderLoopCalled > 2) {
        printf("Error: Unexpected render loop iteration\n");
        report_result(1);
        return; // only two iterations should run code
    }

    /* Second iteration */

    emscripten_vr_cancel_display_render_loop(gDisplay);

    VRFrameData data;
    if (!emscripten_vr_get_frame_data(gDisplay, &data)) {
        printf("Could not get frame data.\n");
        report_result(1);
    }

    /* Print list of properties which are reported as valid */
    printf("The following properties are valid:\n[");
    if(data.pose.poseFlags & VR_POSE_POSITION) {
        printf("position, ");
    }
    if(data.pose.poseFlags & VR_POSE_LINEAR_VELOCITY) {
        printf("linear vel, ");
    }
    if(data.pose.poseFlags & VR_POSE_LINEAR_ACCELERATION) {
        printf("linear accel, ");
    }
    if(data.pose.poseFlags & VR_POSE_ORIENTATION) {
        printf("orientation, ");
    }
    if(data.pose.poseFlags & VR_POSE_ANGULAR_VELOCITY) {
        printf("angular vel, ");
    }
    if(data.pose.poseFlags & VR_POSE_ANGULAR_ACCELERATION) {
        printf("angular accel");
    }
    printf("]\n");

    /* Print all values (independent of validity) */
    printf("Position: \t\t%f %f %f\n",
            data.pose.position.x,
            data.pose.position.y,
            data.pose.position.z);

    printf("Linear Velocity: \t%f %f %f\n",
            data.pose.linearVelocity.x,
            data.pose.linearVelocity.y,
            data.pose.linearVelocity.z);

    printf("Linear Acceleration: \t%f %f %f\n",
            data.pose.linearAcceleration.x,
            data.pose.linearAcceleration.y,
            data.pose.linearAcceleration.z);

    printf("Orientation: \t\t%f %f %f %f\n",
            data.pose.orientation.x,
            data.pose.orientation.y,
            data.pose.orientation.z,
            data.pose.orientation.w);

    printf("Angular Velocity: \t%f %f %f\n",
            data.pose.angularVelocity.x,
            data.pose.angularVelocity.y,
            data.pose.angularVelocity.z);

    printf("Angular Acceleration: \t%f %f %f\n",
            data.pose.angularAcceleration.x,
            data.pose.angularAcceleration.y,
            data.pose.angularAcceleration.z);

    printf("Left Projection Matrix:\n");
    printMatrix(data.leftProjectionMatrix);

    printf("Right Projection Matrix:\n");
    printMatrix(data.rightProjectionMatrix);

    printf("Left View Matrix:\n");
    printMatrix(data.leftViewMatrix);

    printf("Right View Matrix:\n");
    printMatrix(data.rightViewMatrix);

    if(!emscripten_vr_submit_frame(gDisplay)) {
        printf("Error: Failed to submit frame to VR display %d (second iteration)\n", gDisplay);
        report_result(1);
    }

    printf("Submitted frame.\n");

    if (!emscripten_vr_set_display_render_loop_arg(gDisplay, renderLoopArg, (void*) 42)) {
        printf("Error: Failed to dereference handle while settings display render loop of device %d\n", gDisplay);
        report_result(1);
    }

    printf("Set main loop with argument to be called next.\n");
}

/* emscripten main loop */
static void mainloop() {
    static int loopcount = 0;

    if (!emscripten_vr_ready()) {
        printf("VR not ready\n");
        return;
    }

    if(gDisplay == -1) {
        int numDisplays = emscripten_vr_count_displays();
        if (numDisplays == 0) {
            printf("No VR displays found!\n");
            report_result(0);
            return;
        }

        printf("%d VR displays found\n", numDisplays);

        int id = -1;
        char *devName;
        for (int i = 0; i < numDisplays; ++i) {
            VRDisplayHandle handle = emscripten_vr_get_display_handle(i);
            if (gDisplay == -1) {
                /* Save first found display for more testing */
                gDisplay = handle;
                devName = emscripten_vr_get_display_name(handle);
                printf("Using VRDisplay '%s' (displayId '%d')\n", devName, gDisplay);

                VRDisplayCapabilities caps;
                if(!emscripten_vr_get_display_capabilities(handle, &caps)) {
                    printf("Error: failed to get display capabilities.\n");
                    report_result(1);
                    return;
                }
                if(!emscripten_vr_display_connected(gDisplay)) {
                    printf("Error: expected display to be connected.\n");
                    report_result(1);
                    return;
                }
                printf("Display Capabilities:\n"
                       "{hasPosition: %d, hasExternalDisplay: %d, canPresent: %d, maxLayers: %lu}\n",
                       caps.hasPosition, caps.hasExternalDisplay, caps.canPresent, caps.maxLayers);
            }
        }

        if (gDisplay == -1) {
            printf("Couln't find a VR display even though at least one was found.\n");
            report_result(1);
            return;
        }

        VREyeParameters leftParams, rightParams;
        emscripten_vr_get_eye_parameters(gDisplay, VREyeLeft, &leftParams);
        emscripten_vr_get_eye_parameters(gDisplay, VREyeRight, &rightParams);

        VREyeParameters* p = &leftParams;
        printf("Left eye params: {offset: [%f, %f, %f], renderWidth: %lu, renderHeight: %lu}\n", p->offset.x, p->offset.y, p->offset.z, p->renderWidth, p->renderHeight);

        p = &rightParams;
        printf("Right eye params: {offset: [%f, %f, %f], renderWidth: %lu, renderHeight: %lu}\n", p->offset.x, p->offset.y, p->offset.z, p->renderWidth, p->renderHeight);

        if (!emscripten_vr_set_display_render_loop(gDisplay, renderLoop)) {
            printf("Error: Failed to dereference handle while settings display render loop of device %d\n", gDisplay);
            report_result(1);
        }
    }

    if (loopcount++ > 10) {
        if (renderLoopCalled != 2) {
            printf("Render loop was not called twice (expected 2, was %d)\n", renderLoopCalled);
            report_result(1);
        }
        if (!renderLoopArgCalled) {
            printf("Render loop with argument was never called\n");
            report_result(1);
        }
        if ((int)renderLoopArgArg != 42) {
            printf("Argument to emscripten_vr_set_display_render_loop_arg was not passed on correctly\n");
            report_result(1);
        }
        report_result(0);
    }
}

int main(int argc, char **argv) {
    if (!emscripten_vr_init()) {
        printf("Skipping: Browser does not support WebVR\n");
        return 0;
    }

    printf("Browser is running WebVR version %d.%d\n",
           emscripten_vr_version_major(),
           emscripten_vr_version_minor());

    /* 2fps -- no rAF */
    emscripten_set_main_loop(mainloop, 2, 0);
}
