// glad needs to be initialized before GLFW
#include <math.h>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stdio.h>
#include <StormLib.h>
#include "logger/log.h"
#include "util.h"
#include "logger/gl_log.h"
#include "renderer/matrix.h"
#include "renderer/mesh.h"
#include "renderer/shader.h"
#include "renderer/vector.h"
#include "wmo/wmo.h"
#include "wmo/wmo_structs.h"
#include "renderer/texture.h"



// reported window size maybe good to know for a few things
int g_win_width = 640;
int g_win_height = 480;
// keep track of framebuffer size for things like the viewport and the mouse cursor
int g_fb_width = 640;
int g_fb_height = 480;
// time tracking
double previous_seconds;
int frame_count;

// function to update the window title with a frame rate
void _update_fps_counter(GLFWwindow* window) {
    double current_seconds;
    double elapsed_seconds;
    current_seconds = glfwGetTime();
    elapsed_seconds = current_seconds - previous_seconds;
    /* limit text updates to 4 per second */
    if (elapsed_seconds > 0.25) {
        previous_seconds = current_seconds;
        char tmp[128];
        double fps = (double)frame_count / elapsed_seconds;
        // 1000 / fps = expected ms per frame
        // elapsed_seconds / frame_count * 1000 = time per frame in ms
        sprintf(tmp, "opengl @ fps: %.2f time per frame in milliseconds: %.4f", fps,
            ((elapsed_seconds / frame_count) * 1000));
        glfwSetWindowTitle(window, tmp);
        frame_count = 0;
    }
    frame_count++;
}

// clears stdin buffer, trailing '\n' causes issues when using scanf_s() and fgets() for file name parsing
void clear_input_buffer() {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

void glfw_error_callback(int error, const char *description) {
    gl_log_err("GLFW ERROR: code %i msg: %s\n", error, description);
    log_error("GLFW ERROR: code %i msg: %s", error, description);
}

void glfw_window_size_callback(GLFWwindow* window, int width, int height) {
    g_win_width = width;
    g_win_height = height;
    gl_log("Window Resolution changed: width: %d, height: %d\n");
}

void glfw_framebuffer_resize_callback(GLFWwindow* window, int width, int height) {
    g_fb_width = width;
    g_fb_height = height;
    gl_log("Framebuffer resized: width: %d, height: %d\n");

    // TODO: update any perspective matrices used here
    //mat4_make_perspective(67 * ONE_DEG_IN_RAD, g_fb_width/g_fb_height, 1.0f, 100.0f);
}

const char *GL_type_to_string(GLenum type) {
    switch (type) {
    case GL_BOOL: return "bool";
    case GL_INT: return "int";
    case GL_FLOAT: return "float";
    case GL_FLOAT_VEC2: return "vec2";
    case GL_FLOAT_VEC3: return "vec3";
    case GL_FLOAT_VEC4: return "vec4";
    case GL_FLOAT_MAT2: return "mat2";
    case GL_FLOAT_MAT3: return "mat3";
    case GL_FLOAT_MAT4: return "mat4";
    case GL_SAMPLER_2D: return "sampler2D";
    case GL_SAMPLER_3D: return "sampler3D";
    case GL_SAMPLER_CUBE: return "samplerCube";
    case GL_SAMPLER_2D_SHADOW: return "sampler2DShadow";
    default: break;
    }
    return "other";
}

void _print_shader_info_log(GLuint shader_index) {
    int max_length = 2048;
    int actual_length = 0;
    char log[2048];
    glGetShaderInfoLog(shader_index, max_length, &actual_length, log);
    printf("shader info log for GL index %u:\n%s\n", shader_index, log);
}

void _print_programme_info_log(GLuint programme) {
    int max_length = 2048;
    int actual_length = 0;
    char log[2048];
    glGetProgramInfoLog(programme, max_length, &actual_length, log);
    log_info("program info log for GL index %u:\n%s", programme, log);
}

void print_all(GLuint programme) {
    log_info("--------------------\nshader programme %i info:", programme);
    int params = -1;
    glGetProgramiv(programme, GL_LINK_STATUS, &params);
    log_info("GL_LINK_STATUS = %i", params);

    glGetProgramiv(programme, GL_ATTACHED_SHADERS, &params);
    log_info("GL_ATTACHED_SHADERS = %i", params);
    for (GLuint i = 0; i < (GLuint)params; i++) {
        char name[64];
        int max_length = 64;
        int actual_length = 0;
        int size = 0;
        GLenum type;
        glGetActiveAttrib(programme, i, max_length, &actual_length, &size, &type, name);
        if (size > 1) {
            for (int j = 0; j < size; j++) {
                char long_name[64];
                sprintf(long_name, "%s[%i]", name, j);
                int location = glGetAttribLocation(programme, long_name);
                log_info("  %i) type:%s name:%s location:%i", i, GL_type_to_string(type), long_name, location);
            }
        } else {
            int location = glGetAttribLocation(programme, name);
            log_info("  %i) type:%s name:%s location:%i", i, GL_type_to_string(type), name, location);
        }
    }

    glGetProgramiv(programme, GL_ACTIVE_UNIFORMS, &params);
    log_info("GL_ACTIVE_UNIFORMS = %i", params);
    for (GLuint i = 0; i < (GLuint)params; i++) {
        char name[64];
        int max_length = 64;
        int actual_length = 0;
        int size = 0;
        GLenum type;
        glGetActiveUniform(programme, i, max_length, &actual_length, &size, &type, name);
        if (size > 1) {
            for (int j = 0; j < size; j++) {
                char long_name[64];
                sprintf(long_name, "%s[%i]", name, j);
                int location = glGetUniformLocation(programme, long_name);
                log_info("  %i) type:%s name:%s location:%i", i, GL_type_to_string (type), long_name, location);
            }
        } else {
            int location = glGetUniformLocation(programme, name);
            log_info("  %i) type:%s name:%s location:%i", i, GL_type_to_string (type), name, location);
        }
    }

    _print_programme_info_log(programme);
}

// should only be used during development, quite computationally expensive
bool is_valid(GLuint programme) {
    glValidateProgram(programme);
    int params = -1;
    glGetProgramiv(programme, GL_VALIDATE_STATUS, &params);
    log_info("program %i GL_VALIDATE_STATUS = %i", programme, params);
    if (GL_TRUE != params) {
        _print_programme_info_log(programme);
        return false;
    }
    return true;
}

void init() {

    printf("What would you like to do?\n");
    printf("\t1. Open MPQ\n\t2. Create MPQ\n\t3. Extract BLPs from WMO\n\t4. Renderer\n");

    int input = 0;
    scanf_s("%d", &input);
    clear_input_buffer();

    switch (input) {
    case 1:
    {
        HANDLE hMPQ = NULL;
        //DWORD dwError = 0;
        HANDLE hMPQ_COMMON = NULL;

        ArchiveManager manager;
        //char mpqFilePath[MAX_PATH];
        // get_mpq_file_path(mpqFilePath, MAX_PATH);

        PathList list_container;
        char wowDataDir[MAX_PATH];
        get_data_dir(wowDataDir, MAX_PATH);


        init_archive_manager(&manager, wowDataDir);

        init_path_list(&list_container);

        recursively_scan_directory(wowDataDir, &list_container);
        for (int i = 0; i < list_container.count; i++) {
            log_info("Found path: '%s'", list_container.entries[i].path);
        }
        // free_path_list(&list_container);

        qsort(list_container.entries, list_container.count, sizeof(ArchiveEntry),
            archive_comparator);

        for (int i = 0; i < list_container.count; i++) {
            HANDLE hArchive;
            const char *mpqFilePath = list_container.entries[i].path;

            if (manager.count >= manager.capacity) {
                expand_archive_manager(&manager);
            }

            if (!SFileOpenArchive(
                mpqFilePath,
                0,
                MPQ_OPEN_READ_ONLY,
                &hArchive))
            {
                // Log error and continue to the next archive!
                log_error("Error opening MPQ '%s'. Error code: %lu\n", mpqFilePath, GetLastError());
                continue;
            }

            // Success: Store the handle in the ArchiveManager
            manager.archives[manager.count++] = hArchive;
            // TODO - honestly I should mount these as needed... check newest patch first -> oldest
            //  for files needed
            log_info("Successfully mounted archive #%zu: %s", manager.count, mpqFilePath);
        }

        /*
        if (!SFileOpenArchive(
            mpqFilePath,
            0,
            MPQ_OPEN_READ_ONLY,
            &hMPQ)) {
            log_error("Error opening MPQ '%s'. Error code: %lu\n", mpqFilePath, GetLastError());
            return;
            }

        if (!SFileOpenArchive("./common.MPQ", 0, MPQ_OPEN_READ_ONLY, &hMPQ_COMMON)) {
            DWORD dwError = GetLastError();
            log_error("Error opening `common.MPQ` at `%s`. Error code: %lu\n", "./common.MPQ", dwError);
            return;
        }

        log_info("Successfully opened MPQ file: '%s'\n", mpqFilePath);
        getchar();

        log_info("Getting (listfile) for test run...\n");
        get_file_size_in_mpq(get_file_in_mpq(hMPQ, "(listfile)"), "(listfile)");

        log_info("Getting (CTFNightelf_A.wmo) for WMO data test");
        //WMOData test_WMOdata = load_wmo_data(hMPQ, "World\\wmo\\PvP\\Buildings\\CTF\\CTFOrc_A.wmo");*/
        // "World\\wmo\\Dungeon\\MD_Caveden\\MD_VrykulDen.wmo"
        //char *wmoFileName = "World\\wmo\\PvP\\Buildings\\CTF\\CTFNightelf_A.wmo";
        //char *wmoFileName = "World\\wmo\\Dungeon\\MD_Caveden\\MD_VrykulDen.wmo";
        //char *wmoFileName = "World\\wmo\\Test\\test_petes_wmo_rotation_test.wmo";
        char *wmoFileName = "World\\wmo\\Dungeon\\Ulduar\\Ulduar_Raid.wmo";
        WMOData test_WMOdata = load_wmo_data(&manager, wmoFileName);

        const char *log_file_name = "wmo_geometry_export.obj.log";
        char log_file_path[MAX_PATH];
        snprintf(log_file_path, MAX_PATH, "%s.obj.log", log_file_name);

        FILE *log_file = fopen(log_file_name, "w");
        if (log_file == NULL) {
            log_error("Failed to open log file: '%s'. Check permissions or dir", log_file_name);
        }


        WMORootData wmo_root_data = {0};

        // TODO - SOURCE OF ERROR FOR BYTE MISMATCH?
        parse_wmo_chunks(&manager, test_WMOdata.data, test_WMOdata.size, &wmo_root_data, wmoFileName);

        // extract_wmo_geometry(&wmo_root_data, log_file);
        if (log_file != NULL) {
            fclose(log_file);
        }

        if (wmo_root_data.motx_data_ptr && wmo_root_data.momt_data_ptr) {
            log_info("Successfully located chunks.");
            // log_info("Located MVER chunk at offset %td", out_wmo_data.mver_data_ptr - wmo_buffer);
            // pass by reference
            parse_momt_and_extract_textures(&manager, &wmo_root_data);

        } else {
            log_error("Failed to find required MOMT and MOTX chunks.");
        }

        log_info("Testing struct data storage... MVER at %td, MOGN at %td",
            wmo_root_data.mver_data_ptr - test_WMOdata.data,
            wmo_root_data.mogn_data_ptr - test_WMOdata.data);

        if (!restart_gl_log()) {
            break;
        }

        gl_log("starting GLFW\n%s\n", glfwGetVersionString());
        glfwSetErrorCallback(glfw_error_callback);

        // Initialize GLFW helper library to start GL context and O/S window
        if (!glfwInit()) {
            log_error("Could not initialize glfw");
            return;
        }

        glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 6);
        glfwWindowHint (GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        glfwWindowHint (GLFW_SAMPLES, 4); // Set to "16" before taking screen shots?

        GLFWmonitor *mon = glfwGetPrimaryMonitor();
        const GLFWvidmode *vmode = glfwGetVideoMode(mon);
        // Create a windowed mode window and its OpenGL context
        GLFWwindow* window = glfwCreateWindow(vmode->width, vmode->height, "GLFW Window", mon, NULL);
        if (!window) {
            log_error("Could not create glfw window, Error: %d", GetLastError());
            glfwTerminate();
            return;
        }
        // Make window's context current
        glfwMakeContextCurrent(window);

        // Load all OpenGL function pointers with GLAD
        if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
            log_error("Failed to initialize GLAD");
            glfwTerminate();
            return;
        }

        glEnable(GL_MULTISAMPLE);

        // log GL params
        log_gl_params();

        // get version info
        const GLubyte* renderer = glGetString(GL_RENDERER);
        const GLubyte* version = glGetString(GL_VERSION);
        log_info("Renderer: %s", renderer);
        log_info("OpenGL version supported %s", version);

        glfwSetWindowSizeCallback(window, glfw_window_size_callback);
        glfwSetFramebufferSizeCallback(window, glfw_framebuffer_resize_callback);

        // tell GL to only draw onto a pixel if the shape is closer to the viewer
        glEnable(GL_DEPTH_TEST); // enable depth testing
        glDepthFunc(GL_LESS); // depth-testing interprets a smaller value as "closer"

        wmo_load_textures_to_gpu(&manager, &wmo_root_data);

        GroupMesh *meshes = (GroupMesh*)calloc(wmo_root_data.group_count, sizeof(GroupMesh));

        if (wmo_root_data.groups == NULL || wmo_root_data.group_count == 0) {
            log_error("No WMO groups loaded. Skipping mesh creation");
        } else {
            for (size_t i = 0; i < wmo_root_data.group_count; i++) {
                if (wmo_root_data.groups[i].movt_data_ptr != NULL) {
                    meshes[i] = create_mesh_from_group(&wmo_root_data.groups[i], &wmo_root_data);
                } else {
                    log_warn("Skipping empty group %zu", i);
                }
            }
        }

        /*
        GLfloat points [] = {
             0.0f,  0.5f, -1.0f,
             0.4f,  -0.5f, -1.0f,
             -0.4f, -0.5f, -1.0f,
            //-0.4f, -0.5f, 0.0f,
            //-0.4f,  0.5f, 0.0f,
             //0.4f, -0.5f, 0.0f,
        };

        GLfloat colours[] = {
            1.0f, 0.0f, 0.0f,
            0.0f, 1.0f, 0.0f,
            0.0f, 0.0f, 1.0f
        };

        float matrix[16] = {
            1.0f, 0.0f, 0.0f, 0.0f, // first column
            0.0f, 1.0f, 0.0f, 0.0f, // second column
            0.0f, 0.0f, 1.0f, 0.0f, // third column
            0.5f, 0.0f, 0.0f, 1.0f // fourth column
          };

        GLuint points_vbo = 0;
        glGenBuffers(1, &points_vbo);
        glBindBuffer(GL_ARRAY_BUFFER, points_vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);

        GLuint colours_vbo = 0;
        glGenBuffers(1, &colours_vbo);
        glBindBuffer(GL_ARRAY_BUFFER, colours_vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(colours), colours, GL_STATIC_DRAW);

        // bind vbos to ONE vertex array object
        // need to keep track of the VAO index for each type of mesh that is created
        GLuint vao = 0;
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
        //glEnableVertexAttribArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, points_vbo);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
        glBindBuffer(GL_ARRAY_BUFFER, colours_vbo);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);

        // this only affects the currently bound vertex array object.
        // only affects the attributes above basically
        // need to bind every new vertex array and repeat this procedure for those too
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);


        GLfloat rotation_x = 0.01;
        GLfloat rotation_y = 0.01;
        GLfloat rotation_z = 0.01;
        mat4_t scale_matrix = mat4_make_scale(1.0f, 1.0f, 1.0f);
        mat4_t translate_matrix = mat4_make_translation(1.0f, 1.0f, 5.0f);
        mat4_t rotation_matrix_x = mat4_make_rotation_x(rotation_x);
        mat4_t rotation_matrix_y = mat4_make_rotation_y(rotation_y);
        mat4_t rotation_matrix_z = mat4_make_rotation_z(rotation_z);
        */
        char *vertex_shader = ("../../shaders/wmo.vert");
        char *fragment_shader = ("../../shaders/wmo.frag");
        GLuint shader_programme = create_shader_program_from_files(vertex_shader, fragment_shader);
        //print_all(shader_programme);

        // get unique location of the variable "inputColour"
        //GLuint colour_location = glGetUniformLocation(shader_programme, "inputColour");
        int matrix_location = glGetUniformLocation(shader_programme, "matrix");
        glUseProgram(shader_programme);

        float matrix[16] = {
            1.0f, 0.0f, 0.0f, 0.0f, // first column
            0.0f, 1.0f, 0.0f, 0.0f, // second column
            0.0f, 0.0f, 1.0f, 0.0f, // third column
            0.5f, 0.0f, 0.0f, 1.0f // fourth column
          };
        //glUniformMatrix4fv(matrix_location, 1, GL_FALSE, matrix);
        // assigns an initial colour to the fragment shader
        // can change uniform variables with glUniform() but only affects shader
        // glUniform4f(colour_location, 1.0f, 0.0f, 0.0f, 1.0f);

        /* pass pointer to matrix array's first element*/


        // Optional: Set up the viewport and clear color once
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);
        glViewport(0, 0, width, height);
        glClearColor(0.8f, 0.8f, 0.8f, 1.0f);

        //glEnable(GL_CULL_FACE);
        //glCullFace(GL_BACK);
        //glFrontFace(GL_CW); // GL_CCW for counter clock-wise
        glDisable(GL_CULL_FACE);

        float speed = 10.0f; // 1 unit per second
        float last_position = 0.0f;
        float cam_speed = 30.0f; // 1 unit per sec
        float cam_yaw_speed = 1.0f; // 3.5 degrees per second
        vec3_t cam_pos = { 0.0f, 20.0f, 150.0f }; // don't start at zero, or we will be too close
        float cam_yaw = 0.0f; // y-rotation in degrees
        float cam_pitch = 45.0f;

        // TODO: fix issues with initial view matrix and view_matrix when camera is moved
        //mat4_t T = mat4_make_translation(-cam_pos.x, -cam_pos.y, -cam_pos.z);
        //mat4_t R = mat4_make_rotation_y(-cam_yaw);
        //mat4_t view_mat = mat4_mul_mat4(R, T);



        /*
        // projection matrix variables
        // input variables
        float znear = 0.1f; // clipping plane
        float zfar = 1000.0f; // clipping plane
        float fov = 67.0f * ONE_DEG_IN_RAD; // convert 67 degrees to radians
        float aspect = (float)width / (float)height; // aspect ratio
        // matrix components
        float range = tan(fov * 0.5f) * znear;
        //float Sx = (2.0f * znear) / (range * aspect + range * aspect);
        float Sy = znear / range;
        float Sx = Sy / aspect;
        float Sz = -(zfar + znear) / (zfar - znear);
        float Pz = -(2.0f * zfar * znear) / (zfar - znear);

        float proj_mat[] = {
            Sx, 0.0f, 0.0f, 0.0f,
            0.0f, Sy, 0.0f, 0.0f,
            0.0f, 0.0f, Sz, -1.0f,
            0.0f, 0.0f, Pz, 0.0f
          };*/

        float yaw_rad = TO_RAD(cam_yaw);
        float pitch_rad = TO_RAD(cam_pitch);

        vec3_t cam_front;
        cam_front.x = cosf(yaw_rad) * cosf(pitch_rad);
        cam_front.y = sinf(pitch_rad);
        cam_front.z = sinf(yaw_rad) * cosf(pitch_rad);

        vec3_normalize(&cam_front);

        vec3_t world_up = { 0.0f, 1.0f, 0.0f };

        vec3_t cam_right = vec3_cross(cam_front, world_up);
        vec3_normalize(&cam_right);

        vec3_t cam_up = vec3_cross(cam_right, cam_front);
        vec3_normalize(&cam_up);

        mat4_t view_mat = mat4_look_at(
            cam_pos.x, cam_pos.y, cam_pos.z,
            cam_pos.x + cam_front.x, cam_pos.y + cam_front.y, cam_pos.z + cam_front.z,
            cam_up.x, cam_up.y, cam_up.z
            );

        float fov_radians = 67.0f * ONE_DEG_IN_RAD;

        mat4_t projection = mat4_make_perspective(fov_radians, (float)width / (float)height, 0.1f, 5000.0f);


        //mat4_t rotation_correction = mat4_make_rotation_x(TO_RAD(-90.0f));
        mat4_t model_mat = mat4_identity();

        int view_mat_location = glGetUniformLocation (shader_programme, "view");
        glUseProgram (shader_programme);
        //glUniformMatrix4fv (view_mat_location, 1, GL_FALSE, view_mat.m);
        int proj_mat_location = glGetUniformLocation (shader_programme, "proj");
        glUseProgram (shader_programme);
        //glUniformMatrix4fv (proj_mat_location, 1, GL_FALSE, proj_mat);

        glUniformMatrix4fv(matrix_location, 1, GL_FALSE, model_mat.m);
        glUniformMatrix4fv(view_mat_location, 1, GL_FALSE, view_mat.m);
        glUniformMatrix4fv(proj_mat_location, 1, GL_FALSE, projection.m);

        print_all(shader_programme);
        // call `glPolygonMode` before rendering if you want to use wire-frame rendering mode
        // Loop until the user closes the window
        while(!glfwWindowShouldClose(window)) {
            _update_fps_counter(window);

            // add a timer for doing animation
            static double previous_seconds = 0;
            double current_seconds = glfwGetTime();
            double elapsed_seconds = current_seconds - previous_seconds;
            previous_seconds = current_seconds;

            if (fabs(last_position) > 1.0f) {
                speed = -speed;
            }
            //rotation_z += 0.1;
            // update the matrix
            //matrix[12] = elapsed_seconds * speed + last_position;
            //last_position = matrix[12];
            //glUseProgram(shader_programme);
            //glUniformMatrix4fv(matrix_location, 1, GL_FALSE, rotation_matrix_z.m);

            double current_time = glfwGetTime();
            /*if (current_time > 5000) {
                mat4_t new_scale_matrix = mat4_make_scale(0.5f, 2.0f, 1.0f);
                glUseProgram(shader_programme);
                glUniformMatrix4fv(matrix_location, 1, GL_FALSE, new_scale_matrix.m);
            }*/
            /*
            if (current_time > 5000) {
                // Scale the vectors, rotate, then translate
                mat4_t S = mat4_make_scale(1.5f, 1.5f, 1.5f);
                mat4_t Rz = mat4_make_rotation_z(M_PI / 4.0f);
                mat4_t T = mat4_make_translation(1.0f, 0.0f, 0.0f);

                mat4_t temp_matrix = mat4_mul_mat4(Rz, S);
                mat4_t final_matrix = mat4_mul_mat4(T, temp_matrix);

                glUseProgram(shader_programme);
                glUniformMatrix4fv(matrix_location, 1, GL_FALSE, final_matrix.m);
            }*/

            yaw_rad = TO_RAD(cam_yaw);
            pitch_rad = TO_RAD(cam_pitch);

            vec3_t cam_front;
            cam_front.x = cosf(yaw_rad) * cosf(pitch_rad);
            cam_front.y = sinf(pitch_rad);
            cam_front.z = sinf(yaw_rad) * cosf(pitch_rad);

            vec3_normalize(&cam_front);

            vec3_t world_up = { 0.0f, 1.0f, 0.0f };

            vec3_t cam_right = vec3_cross(cam_front, world_up);
            vec3_normalize(&cam_right);

            vec3_t cam_up = vec3_cross(cam_right, cam_front);
            vec3_normalize(&cam_up);

            float move_dist = cam_speed * elapsed_seconds;

            view_mat = mat4_look_at(
                cam_pos.x, cam_pos.y, cam_pos.z,
                cam_pos.x + cam_front.x, cam_pos.y + cam_front.y, cam_pos.z + cam_front.z,
                cam_up.x, cam_up.y, cam_up.z
                );

            // Render here
            // wipe the drawing surface clear
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            // Update viewport size for scaling
            glViewport(0, 0, g_fb_width, g_fb_height);

            for (size_t i = 0; i < wmo_root_data.group_count; i++) {
                if (meshes[i].VAO != 0) {
                    //glActiveTexture(GL_TEXTURE0);
                    //glBindTexture(GL_TEXTURE_2D, meshes[i].textureID);

                    draw_group_mesh(meshes[i]);
                }
            }


            // update other events like input handling
            glfwPollEvents();
            // Swap front and back buffers
            glfwSwapBuffers(window);
            // Check if we should exit
            if (GLFW_PRESS == glfwGetKey(window, GLFW_KEY_ESCAPE)) {
                glfwSetWindowShouldClose(window, 1);
            } else if (GLFW_PRESS == glfwGetKey(window, GLFW_KEY_9)) {
                glfwSetWindowMonitor(window, NULL, 480, 270, vmode->width / 2, vmode->height / 2, GLFW_DONT_CARE);
            } else if (GLFW_PRESS == glfwGetKey(window, GLFW_KEY_8)) {
                glfwSetWindowMonitor(window, mon, 0, 0, vmode->width, vmode->height, vmode->refreshRate);
            } else if (GLFW_PRESS == glfwGetKey(window, GLFW_KEY_7)) {
                shader_reload(&shader_programme, vertex_shader, fragment_shader);

                int new_matrix_loc = glGetUniformLocation(shader_programme, "matrix");
                int new_view_loc = glGetUniformLocation(shader_programme, "view");
                int new_proj_loc = glGetUniformLocation(shader_programme, "proj");

                glUseProgram(shader_programme);
                glUniformMatrix4fv(new_matrix_loc, 1, GL_FALSE, model_mat.m);
                glUniformMatrix4fv(new_view_loc, 1, GL_FALSE, view_mat.m);
                glUniformMatrix4fv(new_proj_loc, 1, GL_FALSE, projection.m);
            }


            // control keys
            bool cam_moved = false;
            if (glfwGetKey (window, GLFW_KEY_A)) {
                //cam_pos[0] -= cam_speed * elapsed_seconds;
                vec3_t move_vec = vec3_mul(cam_right, move_dist);
                cam_pos = vec3_sub(cam_pos, move_vec);
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_D)) {
                //cam_pos[0] += cam_speed * elapsed_seconds;
                vec3_t move_vec = vec3_mul(cam_right, move_dist);
                cam_pos = vec3_add(cam_pos, move_vec);
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_PAGE_UP)) {
                //cam_pos[1] += cam_speed * elapsed_seconds;
                cam_pos.y += move_dist;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_PAGE_DOWN)) {
                //cam_pos[1] -= cam_speed * elapsed_seconds;
                cam_pos.y -= move_dist;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_W)) {
                //cam_pos[2] -= cam_speed * elapsed_seconds;
                vec3_t move_vec = vec3_mul(cam_front, move_dist);

                cam_pos = vec3_add(cam_pos, move_vec);
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_S)) {
                //cam_pos[2] += cam_speed * elapsed_seconds;
                vec3_t move_vec = vec3_mul(cam_front, move_dist);

                cam_pos = vec3_sub(cam_pos, move_vec);

                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_LEFT)) {
                cam_yaw += cam_yaw_speed * elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_RIGHT)) {
                cam_yaw -= cam_yaw_speed * elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_UP)) {
                cam_pitch += cam_yaw_speed *elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_DOWN)) {
                cam_pitch -= cam_yaw_speed * elapsed_seconds;
                cam_moved = true;
            }


            if (cam_pitch > 89.0f) {
                cam_pitch = 89.0f;
            }
            if (cam_pitch < -89.0f) {
                cam_pitch = -89.0f;
            }

            // update view matrix
            if (cam_moved) {
                //T = mat4_make_translation(-cam_pos.x, -cam_pos.y, -cam_pos.z);
                //R = mat4_make_rotation_y(-cam_yaw);
                //view_mat = mat4_mul_mat4(R, T);
                view_mat = mat4_look_at(
                cam_pos.x, cam_pos.y, cam_pos.z,
                cam_pos.x + cam_front.x, cam_pos.y + cam_front.y, cam_pos.z + cam_front.z,
                cam_up.x, cam_up.y, cam_up.z
                );
                glUniformMatrix4fv (view_mat_location, 1, GL_FALSE, view_mat.m);
            }
        }
        free_group_mesh(meshes);
        glfwTerminate();
        break;
    }
    case 2:
        printf("TODO!!");
        break;
    case 3:
        // printf("Enter a WMO filename or path\n");

        FILE *file_handle = NULL;
        char wmo_file_path[MAX_PATH];
        get_wmo_file_path(wmo_file_path, MAX_PATH);

        if (load_wmo_file(wmo_file_path, &file_handle) == 0) {
            log_info("Successfully loaded WMO file '%s'!", wmo_file_path);
        } else {
            log_error("Could not open WMO file '%s': %d", wmo_file_path, GetLastError());
        }

        WMOData test_WMOdata = load_wmo_data_from_file(&file_handle);

        WMORootData wmo_root_data = {0};
        HANDLE hMPQ = NULL;
        HANDLE hMPQ_COMMON = NULL;
        // parse_wmo_chunks(hMPQ, test_WMOdata.data, test_WMOdata.size, hMPQ_COMMON, &wmo_root_data, wmo_file_path);

        log_info("Sizeof SMOGroupInfo: %d", sizeof(SMOGroupInfo));

        if (wmo_root_data.motx_data_ptr && wmo_root_data.momt_data_ptr) {
            log_info("Successfully located chunks.");
            // log_info("Located MVER chunk at offset %td", out_wmo_data.mver_data_ptr - wmo_buffer);
            // pass by reference
            // TODO: this currently uses stormlib for extracting out of the MPQ's when loaded
            //  when we do it manually loading, there will be 0 BLP's unless we load from data dir
            //  which is kinda redundant then, we could just export a list of BLP's through text...?
            //  but the only way to do it I imagine with actual BLP files is loading the MPQ's themselves...
            //  unless the MPQ is already fully extracted then we could just read from that 'cache'... otherwise we would just
            //  read from MPQ memory!! only problem is the startup time is long while reading with Stormlib...
            //  probably should just read header data of MPQ's and confirm if the file exists with the listfile instead of loading the whole
            //  mpq into memory as we won't need EVERYTHING in there at the current moment (reduce debugging time)
            // parse_momt_and_extract_textures(hMPQ, &wmo_root_data, hMPQ_COMMON);
        } else {
            log_error("Failed to find required MOMT and MOTX chunks.");
        }

        break;
    case 4:
        if (!restart_gl_log()) {
            break;
        }

        gl_log("starting GLFW\n%s\n", glfwGetVersionString());
        glfwSetErrorCallback(glfw_error_callback);

        // Initialize GLFW helper library to start GL context and O/S window
        if (!glfwInit()) {
            log_error("Could not initialize glfw");
            return;
        }

        glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 6);
        glfwWindowHint (GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        glfwWindowHint (GLFW_SAMPLES, 4); // Set to "16" before taking screen shots?

        GLFWmonitor *mon = glfwGetPrimaryMonitor();
        const GLFWvidmode *vmode = glfwGetVideoMode(mon);
        // Create a windowed mode window and its OpenGL context
        GLFWwindow* window = glfwCreateWindow(vmode->width, vmode->height, "GLFW Window", mon, NULL);
        if (!window) {
            log_error("Could not create glfw window, Error: %d", GetLastError());
            glfwTerminate();
            return;
        }
        // Make window's context current
        glfwMakeContextCurrent(window);

        // Load all OpenGL function pointers with GLAD
        if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
            log_error("Failed to initialize GLAD");
            glfwTerminate();
            return;
        }

        glEnable(GL_MULTISAMPLE);

        // log GL params
        log_gl_params();

        // get version info
        const GLubyte* renderer = glGetString(GL_RENDERER);
        const GLubyte* version = glGetString(GL_VERSION);
        log_info("Renderer: %s", renderer);
        log_info("OpenGL version supported %s", version);

        glfwSetWindowSizeCallback(window, glfw_window_size_callback);
        glfwSetFramebufferSizeCallback(window, glfw_framebuffer_resize_callback);

        // tell GL to only draw onto a pixel if the shape is closer to the viewer
        glEnable(GL_DEPTH_TEST); // enable depth testing
        glDepthFunc(GL_LESS); // depth-testing interprets a smaller value as "closer"

        GLfloat points [] = {
             0.0f,  0.5f, -1.0f,
             0.4f,  -0.5f, -1.0f,
             -0.4f, -0.5f, -1.0f,
            //-0.4f, -0.5f, 0.0f,
            //-0.4f,  0.5f, 0.0f,
             //0.4f, -0.5f, 0.0f,
        };

        GLfloat colours[] = {
            1.0f, 0.0f, 0.0f,
            0.0f, 1.0f, 0.0f,
            0.0f, 0.0f, 1.0f
        };

        float matrix[16] = {
            1.0f, 0.0f, 0.0f, 0.0f, // first column
            0.0f, 1.0f, 0.0f, 0.0f, // second column
            0.0f, 0.0f, 1.0f, 0.0f, // third column
            0.5f, 0.0f, 0.0f, 1.0f // fourth column
          };

        GLuint points_vbo = 0;
        glGenBuffers(1, &points_vbo);
        glBindBuffer(GL_ARRAY_BUFFER, points_vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);

        GLuint colours_vbo = 0;
        glGenBuffers(1, &colours_vbo);
        glBindBuffer(GL_ARRAY_BUFFER, colours_vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(colours), colours, GL_STATIC_DRAW);

        // bind vbos to ONE vertex array object
        // need to keep track of the VAO index for each type of mesh that is created
        GLuint vao = 0;
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
        glEnableVertexAttribArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, points_vbo);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
        glBindBuffer(GL_ARRAY_BUFFER, colours_vbo);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);

        // this only affects the currently bound vertex array object.
        // only affects the attributes above basically
        // need to bind every new vertex array and repeat this procedure for those too
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);

        GLfloat rotation_x = 0.01;
        GLfloat rotation_y = 0.01;
        GLfloat rotation_z = 0.01;
        mat4_t scale_matrix = mat4_make_scale(1.0f, 1.0f, 1.0f);
        mat4_t translate_matrix = mat4_make_translation(1.0f, 1.0f, 5.0f);
        mat4_t rotation_matrix_x = mat4_make_rotation_x(rotation_x);
        mat4_t rotation_matrix_y = mat4_make_rotation_y(rotation_y);
        mat4_t rotation_matrix_z = mat4_make_rotation_z(rotation_z);

        char *vertex_shader = ("../../shaders/wmo.vert");
        char *fragment_shader = ("../../shaders/wmo.frag");
        GLuint shader_programme = create_shader_program_from_files(vertex_shader, fragment_shader);
        // print_all(shader_programme);

        // get unique location of the variable "inputColour"
        GLuint colour_location = glGetUniformLocation(shader_programme, "inputColour");
        int matrix_location = glGetUniformLocation(shader_programme, "matrix");
        glUseProgram(shader_programme);
        glUniformMatrix4fv(matrix_location, 1, GL_FALSE, matrix);
        // assigns an initial colour to the fragment shader
        // can change uniform variables with glUniform() but only affects shader
        glUniform4f(colour_location, 1.0f, 0.0f, 0.0f, 1.0f);
        /* pass pointer to matrix array's first element*/

        // Optional: Set up the viewport and clear color once
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);
        glViewport(0, 0, width, height);
        glClearColor(0.8f, 0.8f, 0.8f, 1.0f);

        //glEnable(GL_CULL_FACE);
        //glCullFace(GL_BACK);
        //glFrontFace(GL_CW); // GL_CCW for counter clock-wise
        glDisable(GL_CULL_FACE);
        glDisable(GL_DEPTH_TEST);

        float speed = 1.0f; // 1 unit per second
        float last_position = 0.0f;
        float cam_speed = 1.0f; // 1 unit per sec
        float cam_yaw_speed = 1.0f; // 10 degrees per second
        float cam_pos[] = { 0.0f, 0.0f, 2.0f }; // don't start at zero, or we will be too close
        float cam_yaw = 0.0f; // y-rotation in degrees
        mat4_t T = mat4_make_translation(-cam_pos[0], -cam_pos[1], -cam_pos[2]);
        mat4_t R = mat4_make_rotation_y(-cam_yaw);
        mat4_t view_mat = mat4_mul_mat4(R, T);

        // input variables
        float znear = 0.1f; // clipping plane
        float zfar = 100.0f; // clipping plane
        float fov = 67.0f * ONE_DEG_IN_RAD; // convert 67 degrees to radians
        float aspect = (float)width / (float)height; // aspect ratio
        // matrix components
        float range = tan(fov * 0.5f) * znear;
        //float Sx = (2.0f * znear) / (range * aspect + range * aspect);
        float Sy = znear / range;
        float Sx = Sy / aspect;
        float Sz = -(zfar + znear) / (zfar - znear);
        float Pz = -(2.0f * zfar * znear) / (zfar - znear);

        float proj_mat[] = {
            Sx, 0.0f, 0.0f, 0.0f,
            0.0f, Sy, 0.0f, 0.0f,
            0.0f, 0.0f, Sz, -1.0f,
            0.0f, 0.0f, Pz, 0.0f
          };

        int view_mat_location = glGetUniformLocation (shader_programme, "view");
        glUseProgram (shader_programme);
        glUniformMatrix4fv (view_mat_location, 1, GL_FALSE, view_mat.m);
        int proj_mat_location = glGetUniformLocation (shader_programme, "proj");
        glUseProgram (shader_programme);
        glUniformMatrix4fv (proj_mat_location, 1, GL_FALSE, proj_mat);

        // call `glPolygonMode` before rendering if you want to use wire-frame rendering mode
        // Loop until the user closes the window
        while(!glfwWindowShouldClose(window)) {
            _update_fps_counter(window);

            // add a timer for doing animation
            static double previous_seconds = 0;
            double current_seconds = glfwGetTime();
            double elapsed_seconds = current_seconds - previous_seconds;
            previous_seconds = current_seconds;

            if (fabs(last_position) > 1.0f) {
                speed = -speed;
            }

            double current_time = glfwGetTime();

            // Render here
            // wipe the drawing surface clear
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            // Update viewport size for scaling
            glViewport(0, 0, g_fb_width, g_fb_height);

            // draw points from the currently bound VAO with current in-use shader
            // GL_POINTS looks ideal for debugging vertex locations if unsure
            glUseProgram(shader_programme);
            //glBindVertexArray(vao);
            glDrawArrays(GL_TRIANGLES, 0, 3);

            // update other events like input handling
            glfwPollEvents();
            // Swap front and back buffers
            glfwSwapBuffers(window);
            // Check if we should exit
            if (GLFW_PRESS == glfwGetKey(window, GLFW_KEY_ESCAPE)) {
                glfwSetWindowShouldClose(window, 1);
            } else if (GLFW_PRESS == glfwGetKey(window, GLFW_KEY_9)) {
                glfwSetWindowMonitor(window, NULL, 480, 270, vmode->width / 2, vmode->height / 2, GLFW_DONT_CARE);
            } else if (GLFW_PRESS == glfwGetKey(window, GLFW_KEY_8)) {
                glfwSetWindowMonitor(window, mon, 0, 0, vmode->width, vmode->height, vmode->refreshRate);
            } else if (GLFW_PRESS == glfwGetKey(window, GLFW_KEY_7)) {
                shader_reload(&shader_programme, vertex_shader, fragment_shader);

                int new_matrix_loc = glGetUniformLocation(shader_programme, "matrix");
                int new_view_loc = glGetUniformLocation(shader_programme, "view");
                int new_proj_loc = glGetUniformLocation(shader_programme, "proj");

                glUseProgram(shader_programme);
                glUniformMatrix4fv(new_matrix_loc, 1, GL_FALSE, matrix);
                glUniformMatrix4fv(new_view_loc, 1, GL_FALSE, view_mat.m);
                glUniformMatrix4fv(new_proj_loc, 1, GL_FALSE, proj_mat);
            }


            // control keys
            bool cam_moved = false;
            if (glfwGetKey (window, GLFW_KEY_A)) {
                cam_pos[0] -= cam_speed * elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_D)) {
                cam_pos[0] += cam_speed * elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_PAGE_UP)) {
                cam_pos[1] += cam_speed * elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_PAGE_DOWN)) {
                cam_pos[1] -= cam_speed * elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_W)) {
                cam_pos[2] -= cam_speed * elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_S)) {
                cam_pos[2] += cam_speed * elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_LEFT)) {
                cam_yaw += cam_yaw_speed * elapsed_seconds;
                cam_moved = true;
            }
            if (glfwGetKey (window, GLFW_KEY_RIGHT)) {
                cam_yaw -= cam_yaw_speed * elapsed_seconds;
                cam_moved = true;
            }
            // update view matrix
            if (cam_moved) {
                T = mat4_make_translation(-cam_pos[0], -cam_pos[1], -cam_pos[2]);
                R = mat4_make_rotation_y(-cam_yaw);
                view_mat = mat4_mul_mat4(R, T);
                glUniformMatrix4fv (view_mat_location, 1, GL_FALSE, view_mat.m);
            }
        }
        glfwTerminate();
        break;
    default:
        printf("DEFAULT TODO!");
    }
}

int main() {
    log_set_level(1);
    log_info("Starting program and initializing...");

    init();


    // hard coded path or './' source path works currently
    // const char *mpqFileName = "F:\\C Projects\\BLPExtractor\\patch-3.MPQ";

    return 0;
}