#include "utilities/window.hpp"
#include <chrono>
#include <GLFW/glfw3.h>
#include <glad/glad.h>
#include <SFML/Audio/SoundBuffer.hpp>
#include <SFML/Audio/Sound.hpp>
#include <glm/vec3.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/transform.hpp>
#include <fmt/format.h>
#include <iostream>
#include <string>
#include <vector>

#include <utilities/shader.hpp>
#include <utilities/timeutils.h>
#include <utilities/mesh.h>
#include <utilities/shapes.h>
#include <utilities/glutils.h>
#include <utilities/imageLoader.hpp>
#include <utilities/glfont.h>

enum KeyFrameAction { BOTTOM, TOP };

#include "gamelogic.h"
#include "sceneGraph.hpp"
#include <timestamps.h>

struct LightSource {
    glm::vec3 position;
    glm::vec3 color;
};

const glm::vec3 boxDimensions(180, 90, 90);
const glm::vec3 padDimensions(30, 3, 40);
const double    ballRadius = 3.0;

// modify to start music further into the track (seconds)
const float debugStartTime = 0;

SceneNode* rootNode;
SceneNode* boxNode;
SceneNode* ballNode;
SceneNode* padNode;
SceneNode* textureNode;

std::vector<SceneNode*> lightNodes;
std::vector<LightSource> lights;

glm::vec3 cameraPosition;
glm::vec3 ballPosition(0, ballRadius + padDimensions.y, boxDimensions.z / 2);
glm::vec3 ballDirection(1, 1, 0.2f);

double padPositionX = 0;
double padPositionZ = 0;

unsigned int currentKeyFrame  = 0;
unsigned int previousKeyFrame = 0;

bool hasStarted        = false;
bool hasLost           = false;
bool jumpedToNextFrame = false;
bool isPaused          = false;

bool mouseLeftPressed   = false;
bool mouseLeftReleased  = false;
bool mouseRightPressed  = false;
bool mouseRightReleased = false;

double totalElapsedTime = debugStartTime;
double gameElapsedTime  = debugStartTime;

sf::SoundBuffer* buffer;
sf::Sound*       sound;
Gloom::Shader*   shader;
CommandLineOptions options;

Gloom::Shader* textureShader;

double mouseSensitivity = 1.0;
double lastMouseX = windowWidth  / 2;
double lastMouseY = windowHeight / 2;

void mouseCallback(GLFWwindow* window, double x, double y) {
    int w, h;
    glfwGetWindowSize(window, &w, &h);
    glViewport(0, 0, w, h);

    double dx = x - lastMouseX;
    double dy = y - lastMouseY;

    padPositionX = glm::clamp(padPositionX - mouseSensitivity * dx / w, 0.0, 1.0);
    padPositionZ = glm::clamp(padPositionZ - mouseSensitivity * dy / h, 0.0, 1.0);

    glfwSetCursorPos(window, w / 2, h / 2);
}

unsigned int createTextureFromImage(PNGImage *img) {
    unsigned int tid;
    glGenTextures(1, &tid);
    glBindTexture(GL_TEXTURE_2D, tid);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img->width, img->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img->pixels.data());
    glGenerateMipmap(GL_TEXTURE_2D);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    return tid;
}

void initGame(GLFWwindow* window, CommandLineOptions gameOptions) {
    options = gameOptions;

    buffer = new sf::SoundBuffer();
    if (!buffer->loadFromFile("../res/Hall of the Mountain King.ogg")) return;

    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
    glfwSetCursorPosCallback(window, mouseCallback);

    shader = new Gloom::Shader();
    shader->makeBasicShader("../res/shaders/simple.vert", "../res/shaders/simple.frag");

    Mesh padMesh  = cube(padDimensions, glm::vec2(30, 40), true);
    Mesh boxMesh  = cube(boxDimensions, glm::vec2(90), true, true);  // inverted for interior view
    Mesh ballMesh = generateSphere(1.0, 40, 40);

    unsigned int padVAO  = generateBuffer(padMesh);
    unsigned int boxVAO  = generateBuffer(boxMesh);
    unsigned int ballVAO = generateBuffer(ballMesh);

    rootNode = createSceneNode();
    boxNode  = createSceneNode();
    padNode  = createSceneNode();
    ballNode = createSceneNode();

    rootNode->children.push_back(boxNode);
    rootNode->children.push_back(padNode);
    rootNode->children.push_back(ballNode);

    boxNode->vertexArrayObjectID  = boxVAO;
    boxNode->VAOIndexCount        = boxMesh.indices.size();

    padNode->vertexArrayObjectID  = padVAO;
    padNode->VAOIndexCount        = padMesh.indices.size();

    ballNode->vertexArrayObjectID = ballVAO;
    ballNode->VAOIndexCount       = ballMesh.indices.size();

    for (int i = 0; i < 3; i++) {
        auto* light = new SceneNode();
        light->nodeType = POINT_LIGHT;
        light->lightIdx = i;
        lightNodes.push_back(light);
    }

    auto* ceilingLight = lightNodes[0];
    auto* padLight     = lightNodes[1];
    auto* ballLight    = lightNodes[2];

    boxNode->children.push_back(ceilingLight);
    ceilingLight->position = { 0, 10, 0 };

    padNode->children.push_back(padLight);
    padLight->position = { 0, 10, 0 };

    ballNode->children.push_back(ballLight);
    ballLight->position = { 0, 0, 0 };

    std::cout << "loading textures..." << std::endl;

    textureShader = new Gloom::Shader();
    textureShader->makeBasicShader("../res/shaders/texture.vert", "../res/shaders/texture.frag");
    textureNode = createSceneNode();

    auto img = loadPNGFile("../res/textures/charmap.png");
    textureNode->textureID = createTextureFromImage(&img);
    auto textureMesh = generateTextGeometryBuffer("foo", 39.0 / 29, windowWidth);
    textureNode->vertexArrayObjectID = generateBuffer(textureMesh);
    textureNode->VAOIndexCount = textureMesh.indices.size();
    textureNode->nodeType = FLAT_GEOMETRY;

    rootNode->children.push_back(textureNode);
    textureNode->position = {0, 0, 0};

    std::cout << "loaded textures" << std::endl;

    getTimeDeltaSeconds();  // prime the timer

    std::cout << fmt::format("Initialized scene with {} SceneNodes.", totalChildren(rootNode)) << std::endl;
    std::cout << "Ready. Click to start!" << std::endl;
}

void updateFrame(GLFWwindow* window) {
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    double dt = getTimeDeltaSeconds();

    const float cameraWallOffset = 30;
    const float ballBottomY = boxNode->position.y - boxDimensions.y/2 + ballRadius + padDimensions.y;
    const float ballTopY    = boxNode->position.y + boxDimensions.y/2 - ballRadius;
    const float travelY     = ballTopY - ballBottomY;

    const float ballMinX = boxNode->position.x - boxDimensions.x/2 + ballRadius;
    const float ballMaxX = boxNode->position.x + boxDimensions.x/2 - ballRadius;
    const float ballMinZ = boxNode->position.z - boxDimensions.z/2 + ballRadius;
    const float ballMaxZ = boxNode->position.z + boxDimensions.z/2 - ballRadius - cameraWallOffset;

    mouseLeftReleased  = !glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) && mouseLeftPressed;
    mouseLeftPressed   =  glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1);
    mouseRightReleased = !glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) && mouseRightPressed;
    mouseRightPressed  =  glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2);

    if (!hasStarted) {
        if (mouseLeftPressed) {
            if (options.enableMusic) {
                sound = new sf::Sound();
                sound->setBuffer(*buffer);
                sound->setPlayingOffset(sf::seconds(debugStartTime));
                sound->play();
            }
            totalElapsedTime = debugStartTime;
            gameElapsedTime  = debugStartTime;
            hasStarted = true;
        }
        ballPosition.x = ballMinX + (1 - padPositionX) * (ballMaxX - ballMinX);
        ballPosition.y = ballBottomY;
        ballPosition.z = ballMinZ + (1 - padPositionZ) * (ballMaxZ + cameraWallOffset - ballMinZ);

    } else {
        totalElapsedTime += dt;

        if (hasLost) {
            if (mouseLeftReleased) {
                hasLost = hasStarted = false;
                currentKeyFrame = previousKeyFrame = 0;
            }
        } else if (isPaused) {
            if (mouseRightReleased) {
                isPaused = false;
                if (options.enableMusic) sound->play();
            }
        } else {
            gameElapsedTime += dt;
            if (mouseRightReleased) {
                isPaused = true;
                if (options.enableMusic) sound->pause();
            }

            for (unsigned int i = currentKeyFrame; i < keyFrameTimeStamps.size(); i++) {
                if (gameElapsedTime >= keyFrameTimeStamps[i]) currentKeyFrame = i;
            }
            jumpedToNextFrame = (currentKeyFrame != previousKeyFrame);
            previousKeyFrame  = currentKeyFrame;

            double t0 = keyFrameTimeStamps[currentKeyFrame];
            double t1 = keyFrameTimeStamps[currentKeyFrame + 1];
            double frac = (gameElapsedTime - t0) / (t1 - t0);

            KeyFrameAction from = keyFrameDirections[currentKeyFrame];
            KeyFrameAction to   = keyFrameDirections[currentKeyFrame + 1];

            double ballY = ballBottomY;
            if      (from == BOTTOM && to == TOP)    ballY = ballBottomY + travelY * frac;
            else if (from == TOP    && to == BOTTOM) ballY = ballBottomY + travelY * (1 - frac);
            else if (from == TOP    && to == TOP)    ballY = ballTopY;

            const float ballSpeed = 60.0f;
            ballPosition.x += dt * ballSpeed * ballDirection.x;
            ballPosition.y  = ballY;
            ballPosition.z += dt * ballSpeed * ballDirection.z;

            if (ballPosition.x < ballMinX) { ballPosition.x = ballMinX; ballDirection.x *= -1; }
            if (ballPosition.x > ballMaxX) { ballPosition.x = ballMaxX; ballDirection.x *= -1; }
            if (ballPosition.z < ballMinZ) { ballPosition.z = ballMinZ; ballDirection.z *= -1; }
            if (ballPosition.z > ballMaxZ) { ballPosition.z = ballMaxZ; ballDirection.z *= -1; }

            if (options.enableAutoplay) {
                padPositionX = 1 - (ballPosition.x - ballMinX) / (ballMaxX - ballMinX);
                padPositionZ = 1 - (ballPosition.z - ballMinZ) / (ballMaxZ + cameraWallOffset - ballMinZ);
            }

            if (jumpedToNextFrame && from == BOTTOM && to == TOP) {
                double padL = boxNode->position.x - boxDimensions.x/2 + (1 - padPositionX) * (boxDimensions.x - padDimensions.x);
                double padR = padL + padDimensions.x;
                double padF = boxNode->position.z - boxDimensions.z/2 + (1 - padPositionZ) * (boxDimensions.z - padDimensions.z);
                double padB = padF + padDimensions.z;

                if (ballPosition.x < padL || ballPosition.x > padR ||
                    ballPosition.z < padF || ballPosition.z > padB) {
                    hasLost = true;
                    if (options.enableMusic) { sound->stop(); delete sound; }
                }
            }
        }
    }

    cameraPosition = glm::vec3(0, 2, -20);
    float lookRot  = -0.6f / (1 + exp(-5 * (padPositionX - 0.5))) + 0.3f;

    glm::mat4 view = glm::rotate(0.3f + 0.2f * float(-padPositionZ * padPositionZ), glm::vec3(1,0,0))
                   * glm::rotate(lookRot, glm::vec3(0,1,0))
                   * glm::translate(-cameraPosition);

    glm::mat4 proj = glm::perspective(glm::radians(80.0f),
                                      float(windowWidth) / float(windowHeight),
                                      0.1f, 350.f);
    glm::mat4 VP = proj * view;

    boxNode->position = { 0, -10, -80 };

    ballNode->position = ballPosition;
    ballNode->scale    = glm::vec3(ballRadius);
    ballNode->rotation = { 0, totalElapsedTime * 2, 0 };

    padNode->position = {
        boxNode->position.x - boxDimensions.x/2 + padDimensions.x/2 + (1 - padPositionX) * (boxDimensions.x - padDimensions.x),
        boxNode->position.y - boxDimensions.y/2 + padDimensions.y/2,
        boxNode->position.z - boxDimensions.z/2 + padDimensions.z/2 + (1 - padPositionZ) * (boxDimensions.z - padDimensions.z)
    };

    updateNodeTransformations(rootNode, VP, glm::mat4(1.0f));

    // update lights
    lights.clear();
    for (auto* node : lightNodes) {
        LightSource l;
        l.position = node->worldPosition;

        switch (node->lightIdx) {
            case 0: l.color = glm::vec3(1, 0, 0); break;  // red (ceiling)
            case 1: l.color = glm::vec3(0, 1, 0); break;  // green (pad)
            case 2: l.color = glm::vec3(0, 0, 1); break;  // blue (ball)
        }
        lights.push_back(l);
    }
}

void updateNodeTransformations(SceneNode* node, glm::mat4 vpSoFar, glm::mat4 modelSoFar) {
    if (node->nodeType != FLAT_GEOMETRY) {
        glm::mat4 local = glm::translate(node->position)
                        * glm::translate(node->referencePoint)
                        * glm::rotate(node->rotation.y, glm::vec3(0,1,0))
                        * glm::rotate(node->rotation.x, glm::vec3(1,0,0))
                        * glm::rotate(node->rotation.z, glm::vec3(0,0,1))
                        * glm::scale(node->scale)
                        * glm::translate(-node->referencePoint);

        node->currentTransformationMatrix = vpSoFar   * local;  // MVP
        node->modelMatrix                 = modelSoFar * local;  // world transform
        node->normalMatrix = glm::transpose(glm::inverse(glm::mat3(node->modelMatrix)));

        if (node->nodeType == POINT_LIGHT) {
            node->worldPosition = glm::vec3(node->modelMatrix * glm::vec4(0, 0, 0, 1));
        }
    }
    for (auto* child : node->children) {
        updateNodeTransformations(child, node->currentTransformationMatrix, node->modelMatrix);
    }
}

void renderNode(SceneNode* node) {
    if (node->nodeType != FLAT_GEOMETRY) {
        glUniformMatrix4fv(3, 1, GL_FALSE, glm::value_ptr(node->currentTransformationMatrix));
        glUniformMatrix4fv(4, 1, GL_FALSE, glm::value_ptr(node->modelMatrix));
        glUniformMatrix3fv(5, 1, GL_FALSE, glm::value_ptr(node->normalMatrix));

        glUniform3fv(shader->getUniformFromName("cameraPosition"), 1, glm::value_ptr(cameraPosition));
        glUniform3fv(shader->getUniformFromName("ballPosition"),   1, glm::value_ptr(ballPosition));

        for (size_t i = 0; i < lights.size(); i++) {
            std::string base = "lights[" + std::to_string(i) + "]";
            glUniform3fv(shader->getUniformFromName((base + ".position").c_str()), 1, glm::value_ptr(lights[i].position));
            glUniform3fv(shader->getUniformFromName((base + ".color").c_str()),    1, glm::value_ptr(lights[i].color));
        }

        if (node->nodeType == GEOMETRY && node->vertexArrayObjectID != -1) {
            glBindVertexArray(node->vertexArrayObjectID);
            glDrawElements(GL_TRIANGLES, node->VAOIndexCount, GL_UNSIGNED_INT, nullptr);
        }
    }
    for (auto* child : node->children) {
        renderNode(child);
    }
}

void renderTexture(SceneNode *node) {
    auto orthoMatrix = glm::ortho(0.0f, static_cast<float>(windowWidth), 0.0f, static_cast<float>(windowHeight));
    GLint orthoLoc = textureShader->getUniformFromName("orthoMatrix");
    glUniformMatrix4fv(orthoLoc, 1, GL_FALSE, glm::value_ptr(orthoMatrix));
    if (node->nodeType == FLAT_GEOMETRY) {
        glBindTextureUnit(0, node->textureID);
        glBindVertexArray(node->vertexArrayObjectID);
        glDrawElements(GL_TRIANGLES, node->VAOIndexCount, GL_UNSIGNED_INT, nullptr);
    }
    for (auto* child : node->children) {
        renderTexture(child);
    }
}

void renderFrame(GLFWwindow* window) {
    int w, h;
    glfwGetWindowSize(window, &w, &h);
    glViewport(0, 0, w, h);

    shader->activate();
    renderNode(rootNode);

    textureShader->activate();
    renderTexture(rootNode);
}
