#include <cstddef>
#include <cstdint>
#include <handleapi.h>
#include <thread>
#include <windows.h>
#include "include/glad/glad.h"
#include "include/glfw/glfw3.h"
#include <iostream>
#include "include/glm/gtc/quaternion.hpp"
#include "include/imgui/imgui.h"
#include "include/imgui/imgui_impl_glfw.h"
#include "include/imgui/imgui_impl_opengl3.h"
#include "serialcomm.hpp"
#include <string>
#include <mutex>
#include "visualization.hpp"
#include <vector>
#include "include/glm/glm.hpp"
#include "include/glm/gtc/matrix_transform.hpp"
#include "include/glm/gtc/type_ptr.hpp"
#define STB_IMAGE_IMPLEMENTATION
#include "include/stb/stb_image.h"
#include "include/glm/gtc/type_ptr.hpp"


// global variables for the communication between threads (gui, serial)
std::mutex g_mutex;
Quaternion g_currQuat = {1.0f, 0.0f, 0.0f, 0.0f};
std::vector<Quaternion> g_quadHistory;
bool g_running = false;
HANDLE g_serialHandle = INVALID_HANDLE_VALUE;


// shader
const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "layout (location = 1) in vec2 aTexCoord;\n"
    "uniform mat4 transformMatrix;\n"

    "uniform mat4 model;\n"
    "uniform mat4 view;\n"
    "uniform mat4 projection;\n" 

    "out vec2 TexCoord;\n"

    "void main()\n"
    "{\n"
    "   gl_Position = projection * view * model *vec4(aPos, 1.0);\n"
    "   TexCoord = aTexCoord;\n"
    "}\0";

const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "in vec2 TexCoord;\n"
    "uniform sampler2D ourTexture;\n"

    "void main()\n"
    "{\n"
    "   FragColor = texture(ourTexture, TexCoord);\n"
    "}\0";


// handle the serial communication wioth the stm32 in a separate thread
void serialParserThread() {
    // define communication variables
    const char* portName = "COM3"; 
    DWORD baudRate = CBR_115200;   
    char buffer[64];
    DWORD bytesRead;
    std::string incompleteDataBuffer;

    // establish communication to stm32
    g_serialHandle = initSerialPort(portName, baudRate);
    if (g_serialHandle == INVALID_HANDLE_VALUE){
        g_running = false;
        std::cout << "Serial connection FAILED" << std::endl;
        return;
    } else {std::cout << "Serial connection succesfully established" << std::endl;}

    // serial reading loop
    while (g_running) {
        if (readSerialData(g_serialHandle, buffer, sizeof(buffer), bytesRead)) {
                if (bytesRead > 0) {
                // add newly arrived data to buffer
                buffer[bytesRead] = '\0';
                incompleteDataBuffer += buffer;

                // process any complete lines in the buffer
                size_t pos;
                while ((pos = incompleteDataBuffer.find('\n')) != std::string::npos) {
                    // create substring of completed line and erase it from incompleteDataBuffer
                    std::string completeLine = incompleteDataBuffer.substr(0, pos);
                    incompleteDataBuffer.erase(0, pos + 1);

                    // now parse the quaternions from line
                    Quaternion quat;
                    if (parseQuaternion(completeLine, quat)) {
                        // lock mutex while updating it
                        std::lock_guard<std::mutex> lock(g_mutex);
                        g_currQuat = quat;
                        g_quadHistory.push_back(quat);
                        if (g_quadHistory.size() > 200) {
                            g_quadHistory.erase(g_quadHistory.begin());
                        }

                        std::cout << "w: " << g_currQuat.w << "x: " << g_currQuat.x << "y: " << g_currQuat.y << "z: " << g_currQuat.z << std::endl;
                    }
                }
                
            } 
        }

    }

    // close and exit the thread
    CloseHandle(g_serialHandle);
    std::cout << "Serial communication thread exiting" << std::endl;

}




int main() {
    // Locap copy of quaternion
    Quaternion lQuad;

    // initialize main window
    GLFWwindow* window = initWindow();
    if (!window) {
        std::cerr << "Window initialization failed in mainthread" << std::endl;
        return 1;
    } else {std::cout << "Window initialization succesful in mainthread" << std::endl;}

    // compile the shaders
    int success;
    char errorLog[512];
    unsigned int vertexShader;
    unsigned int fragmentShader;

    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);

    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if(!success) {
        glGetShaderInfoLog(vertexShader, 512, NULL, errorLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << errorLog << std::endl;
    }

    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if(!success) {
        glGetShaderInfoLog(fragmentShader, 512, NULL, errorLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << errorLog << std::endl;
    }


    // link the shaders
    unsigned int shaderProgramm = glCreateProgram();
    glAttachShader(shaderProgramm, vertexShader);
    glAttachShader(shaderProgramm, fragmentShader);
    glLinkProgram(shaderProgramm);
    glGetProgramiv(shaderProgramm, GL_LINK_STATUS, &success);
    if(!success) {
        glGetShaderInfoLog(shaderProgramm, 512, NULL, errorLog);
        std::cout << "ERROR::LINKING::SHADER::COMPILATION_FAILED\n" << errorLog << std::endl;
    }
    glUseProgram(shaderProgramm);
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // texture stuff
    unsigned int texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    // texture wrapping and filtering
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // now load and create the texture
    int imWidth, imHeigth, imNrChannels;
    stbi_set_flip_vertically_on_load(true);
    unsigned char *data = stbi_load("kekusmaximus.jpg", &imWidth, &imHeigth, &imNrChannels, 0);
    if (data) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, imWidth, imHeigth, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    } else {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);

    // configure depth settings for 3D
    glEnable(GL_DEPTH_TEST);

    // only screen coordinates between -1 and 1 are visible, (0, 0, 0) being display centerpoint
    float vertexData[] = {
        // Front face
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,  // 0
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,  // 1
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,  // 2
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,  // 3
        
        // Back face
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,  // 4
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,  // 5
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,  // 6
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,  // 7
        
        // Left face
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,  // 8
        -0.5f, -0.5f,  0.5f,  1.0f, 0.0f,  // 9
        -0.5f,  0.5f,  0.5f,  1.0f, 1.0f,  // 10
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,  // 11
        
        // Right face
        0.5f, -0.5f,  0.5f,  0.0f, 0.0f,  // 12
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,  // 13
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,  // 14
        0.5f,  0.5f,  0.5f,  0.0f, 1.0f,  // 15
        
        // Bottom face
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,  // 16
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,  // 17
        0.5f, -0.5f,  0.5f,  1.0f, 1.0f,  // 18
        -0.5f, -0.5f,  0.5f,  0.0f, 1.0f,  // 19
        
        // Top face
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,  // 20
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,  // 21
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,  // 22
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f   // 23
    };

    unsigned int vertexIndices[] = {
        // Front face
        0, 1, 2,  2, 3, 0,
        // Back face
        4, 5, 6,  6, 7, 4,
        // Left face
        8, 9, 10,  10, 11, 8,
        // Right face
        12, 13, 14,  14, 15, 12,
        // Bottom face
        16, 17, 18,  18, 19, 16,
        // Top face
        20, 21, 22,  22, 23, 20
    };

    // create vertex/element buffer object VBO to store data on GPU
    unsigned int VBO, VAO, EBO;

    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    glBindVertexArray(VAO);

    // bind vertex to VBO buffer
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

    // bind indices to EBO buffer
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(vertexIndices), vertexIndices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // add texture coordinates
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);


    // setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO(); (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableSetMousePos;

    // setup platform/renderer backends
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init();

    // Start serial parsing thread
    // pass the address of the code in memory so that thread can start its instance of the function
    //std::thread parserThread(serialParserThread);
    std::thread* parserThread = nullptr;

    // set rendering state of window
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);

    // glm quad to test purposes
    

    // main rendering loop for the ui
    while (!glfwWindowShouldClose(window)) {
        // process new events from window evevnt queue
        glfwPollEvents();
        // clear the color buffer == clear the screen / / GL_DEPTH_BUFFER_BIT would be for 3D rendering
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        // activate and bind texture before draw
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture);

        glUseProgram(shaderProgramm);

        // use the texture actually
        glUniform1i(glGetUniformLocation(shaderProgramm, "outTexture"), 0);

        // create the transformations - TRS order (translate, rotate, scale) for proper transformation
        // create identity matrix
        glm::mat4 transformMatrix = glm::mat4(1.0f);
        transformMatrix = glm::translate(transformMatrix, glm::vec3(0.3f, 0.1f, 0.0f));
        transformMatrix = glm::rotate(transformMatrix, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));

        glm::mat4 model = glm::mat4(1.0f);
        glm::mat4 view = glm::mat4(1.0f);
        glm::mat4 projection = glm::mat4(1.0f);
        // apply MVP
        //model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
        
        // this needs    to be deleted and redone
        glm::quat kekus_q(g_currQuat.w, g_currQuat.x, g_currQuat.y, g_currQuat.z);

        
        model = glm::mat4_cast(kekus_q);
        view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
        projection = glm::perspective(glm::radians(45.0f), (float)1280 / (float)720, 0.1f, 100.f);


        // get uniform matrix location and set matrix
        // unsigned int transformLoc = glGetUniformLocation(shaderProgramm, "transformMatrix");
        // glad_glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transformMatrix));
        unsigned int modelLoc = glGetUniformLocation(shaderProgramm, "model");
        unsigned int viewLoc = glGetUniformLocation(shaderProgramm, "view");
        unsigned int projLoc = glGetUniformLocation(shaderProgramm, "projection");
        //shaderProgramm.setMat4("projection", projection);
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

       // OpenGL 3D Render       
        glBindVertexArray(VAO);
        //glDrawArrays(GL_TRIANGLES, 0, 3);
        glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
        //glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        // start imgui frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();


        // accessing g_currQuat through the mutex protection
        // limited scope block just for the mutex operations
        {
            std::lock_guard<std::mutex> lock(g_mutex);
            lQuad = g_currQuat;

            static int frame_count = 0;
            if (frame_count++ % 60 == 0) {
                std::cout << "Rendering frame with quaternion: "
                          << "w: " << g_currQuat.w << " x: " << g_currQuat.x 
                          << " y: " << g_currQuat.y << " z: " << g_currQuat.z << std::endl;
            }
        }



        ImGui::Text("Hello Daddy, I'm here to serve");
        ImGui::Text("w: %.4f x: %.4f y: %.4f z: %.4f", lQuad.w, lQuad.x, lQuad.y, lQuad.z);
        ImGui::Text("Connection to MCU: ");

        ImGui::BeginDisabled(parserThread != nullptr);
        if (ImGui::Button("Connect")) {
            g_running = true;
            parserThread = new std::thread(serialParserThread);
        } 
        ImGui::EndDisabled();

        ImGui::SameLine();
        
        ImGui::BeginDisabled(parserThread == nullptr);
        if (ImGui::Button("Disconnect")) {
            g_running = false;
            parserThread -> join();
            delete parserThread;
            parserThread = nullptr;
        } 
        ImGui::EndDisabled();


        // create scrolling section for last 200 measurements
        ImGui::BeginChild("History", ImVec2(0, 300), true, ImGuiWindowFlags_HorizontalScrollbar);
        // loop through quadHistory vector and display each element as line
        for (uint32_t i = 0; i < g_quadHistory.size(); i++) {
            const Quaternion& q = g_quadHistory[i];
            ImGui::Text("w: %.4f x: %.4f y: %.4f z: %.4f", q.w, q.x, q.y, q.z);
        }
        // enable autoscroll to newest entry
        if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
            ImGui::SetScrollHereY(1.0f);
        }
        ImGui::EndChild();

        // render the frame
        ImGui::Render();
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
        // swap the buffers to display what was rendered
        glfwSwapBuffers(window);
        glfwPollEvents();


        std::this_thread::sleep_for(std::chrono::milliseconds(16));
    }

    // set the parsing thread to exit and wait for it
   // g_running = false;
    //if (parserThread.joinable()) {
    //    parserThread.join();
    //}

    // clean up ImGui
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    // clean up OpenGL
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteProgram(shaderProgramm);

    // clean up the window
    glfwDestroyWindow(window);
    glfwTerminate();



    return 0;
}