#include <sstream>
#include <thread>
#include <JGL/JGL.h>
#include <Redacted3D/engine/engine.h>
#include <Redacted3D/types/entity/camera.h>
#include <jlog/jlog.hpp>

using namespace J3ML;

std::vector<float> perspective(float fov, float aspect, float nearPlane, float farPlane) {
    std::vector<float> result(16);
    float f = 1.0f / tan(fov * 0.5f * M_PI / 180.0f);
    result[0] = f / aspect;
    result[5] = f;
    result[10] = (farPlane + nearPlane) / (nearPlane - farPlane);
    result[11] = -1.0f;
    result[14] = (2.0f * farPlane * nearPlane) / (nearPlane - farPlane);
    return result;
}

void Engine::quit() const {
    //window->destroyWindow();
    exit(0);
}

void outputErrorCode() {
    switch (engine->getError().error) {
        case ENGINE_ERROR_CODE::NO_ERROR:
            std::cerr << "NO ERROR" << std::endl;
            break;
        case ENGINE_ERROR_CODE::ANIM_OUT_OF_BOUNDS:
            std::cerr << "ANIM OUT OF BOUNDS" << std::endl;
            break;
        case ENGINE_ERROR_CODE::TEXTURE_NOT_FOUND:
            std::cerr << "TEXTURE NOT FOUND" << std::endl;
            break;
        case ENGINE_ERROR_CODE::SHADER_COMPILATION_ERROR:
            std::cerr << "SHADER COMPILATION ERROR" << std::endl;
            break;
        case ENGINE_ERROR_CODE::SHADER_LINKING_ERROR:
            std::cerr << "SHADER LINKING ERROR" << std::endl;
            break;
        case ENGINE_ERROR_CODE::INVALID_SHADER_TYPE:
            std::cerr << "INVALID SHADER TYPE" << std::endl;
            break;
        case ENGINE_ERROR_CODE::SHADER_NOT_FOUND:
            std::cerr << "SHADER NOT FOUND" << std::endl;
            break;
        case ENGINE_ERROR_CODE::MODEL_NOT_FOUND:
            std::cerr << "MODEL NOT FOUND" << std::endl;
            break;
        case ENGINE_ERROR_CODE::MULTI_TEXTURE_SIZE_EXCEEDS:
            std::cerr << "MULTI-TEXTURE SIZE EXCEEDS" << std::endl;
            break;
        case ENGINE_ERROR_CODE::TEXTURE_ERASED_WHILE_IN_USE:
            std::cerr << "TEXTURE ERASED WHILE IN USE" << std::endl;
        case ENGINE_ERROR_CODE::VERTEX_ARRAY_ERASED_WHILE_IN_USE:
            std::cerr << "VERTEX ARRAY ERASED WHILE IN USE" << std::endl;
            break;
    }
}

float Engine::getGLVersion() {
    const std::string gl_major = std::string(((const char*) (glGetString(GL_VERSION)))).substr(0, 3);
    return std::stof(gl_major);
}

void Engine::initGL() const {
    gladLoadGL();
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    auto window_size = window->getSize();
    auto aspect = (float) window_size[0] / (float) window_size[1];
    //glMultMatrixf(Matrix4x4::OpenGLPerspProjRH(nearPlane, farPlane, window_size.x, window_size.y).ptr());
    glMultMatrixf(perspective(fov, aspect, nearPlane, farPlane).data());
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glClearColor(0.f, 0.f, 0.f, 0.f);
    glViewport(0,0,window_size.x,window_size.y);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    glDepthMask(GL_TRUE);
    glEnable(GL_CULL_FACE);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
    glEnable(GL_LIGHT0);
}

void Engine::init() {
    engine->window->Open();
    engine->initGL();
    engine->loadConfig();
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
}

void Engine::preRender() {
    engine->window->pollEvents();
    engine->frameCount++;
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    for (auto& e : world->GetChildren())
        e->pre_render();
}

void Engine::render() {
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    if(world->getActiveCamera() != nullptr)
        world->getActiveCamera()->render();

    for (auto& e : world->GetChildren())
        if (e->draw)
            if (auto* c = dynamic_cast<Camera*>(e); c == nullptr) //If it's not a camera.
                e->render();

    int glError = glGetError();
    if (glError != GL_NO_ERROR) {
        FATAL("GL Error: " + std::to_string(glError))
        exit(0);
    }
}

void Engine::jglRenderPass() {
    JGL::Update(engine->window->getSize());
    glDisable(GL_LIGHTING);
    ///If elements are attached to a camera that's not the active one then they shouldn't be drawn.
    if(world->getActiveCamera() != nullptr)
        world->getActiveCamera()->jglRenderPass();

    for (auto& e : world->GetChildren())
        if (e->draw)
            if (auto* c = dynamic_cast<Camera*>(e); c == nullptr) //If it's not a camera.
                e->jglRenderPass();
    glEnable(GL_LIGHTING);
}

void Engine::postRender() {
    for (auto& e : engine->world->GetChildren())
        e->post_render();
    ReWindow::RWindow::glSwapBuffers();
}


void Engine::renderPass() {
    preRender();
    render();
    jglRenderPass();
    postRender();
}

[[noreturn]] void Engine::renderLoop() {
    while (true) {
        auto start = std::chrono::high_resolution_clock::now();
        renderPass();
        auto stop = std::chrono::high_resolution_clock::now();
        auto dT = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);

        if (dT < std::chrono::microseconds(1000)) {
            auto remaining = std::chrono::microseconds(1000) - dT;
            std::this_thread::sleep_for(remaining);
            dT += remaining;
        }

        // Decrease framerate when focus is lost.
        if (!engine->window->getFlag(RWindowFlags::IN_FOCUS)) {
            auto remaining = std::chrono::microseconds((int) 41666.67f) - dT;
            std::this_thread::sleep_for(remaining);
            dT += remaining;
        }
        engine->frameDelta = dT.count() / 1000000.0f;
    }
}

float Engine::framerate() const {
    return 1.f / frameDelta;
}

void Engine::loadConfig() {
    std::ifstream file("cfg/engine.cfg");
    if (!file.is_open()) {
        std::cerr << "Couldn't load engine config." << std::endl;
        quit();
    }
    std::string line;
    while (std::getline(file, line)) {
        std::istringstream stream(line);
        std::string prefix;
        stream >> prefix;
        //if (prefix == "Resolution:") {
            //int x, y;
            //stream >> x >> y;
            //window->setSize(x,y);
            //glViewport(0,0,x,y);
        //}
        if (prefix == "Fullscreen:")
            stream >> fullscreen;
        if (prefix == "Debug:")
            stream >> debug;
        if (prefix == "CameraFOV:")
            stream >> fov;
    }
    //If we have a window already.
    //if (window != NULL) {
        //window->destroyWindow();
        //Some window managers won't handle destroying and remaking the window with zero delay very well.
        //std::this_thread::sleep_for(std::chrono::milliseconds(500));
        //initVideo();
        //initGL();
    //}

    if (debug) {
        std::cout << "Fullscreen: " << fullscreen << std::endl;
        std::cout << "Resolution: " << window->getSize()[0] << "x" << window->getSize()[1] << std::endl;
        std::cout << "Camera FOV: " << fov << std::endl;
        std::cout << "Debug: " << debug << std::endl;
    }
}

void Engine::setError(ENGINE_ERROR_CODE code, bool critical) {
    error.error = code;
    error.critical = critical;
    outputErrorCode();
    if (error.critical)
        engine->quit(code);
}

EngineError Engine::getError() {
    return error;
}

void Engine::quit(ENGINE_ERROR_CODE code) const {
    //window->destroyWindow();
    exit((int) code);
}

void Engine::takeScreenshot() {
    int width = (int) engine->window->getSize().x;
    int height = (int) engine->window->getSize().y;
    std::vector<unsigned char> frameBuffer(3 * width * height);

    glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, frameBuffer.data());

    std::string fileName = std::to_string(engine->frameCount) + ".bmp";
    std::ofstream file(fileName, std::ios::out | std::ios::binary);

    int fileSize = 54 + 3 * width * height;
    unsigned char bmpfileheader[14] = {'B', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0};
    unsigned char bmpinfoheader[40] = {40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 24, 0};
    bmpfileheader[2] = (unsigned char) (fileSize);
    bmpfileheader[3] = (unsigned char) (fileSize >> 8);
    bmpfileheader[4] = (unsigned char) (fileSize >> 16);
    bmpfileheader[5] = (unsigned char) (fileSize >> 24);
    bmpinfoheader[4] = (unsigned char) (width);
    bmpinfoheader[5] = (unsigned char) (width >> 8);
    bmpinfoheader[6] = (unsigned char) (width >> 16);
    bmpinfoheader[7] = (unsigned char) (width >> 24);
    bmpinfoheader[8] = (unsigned char) (height);
    bmpinfoheader[9] = (unsigned char) (height >> 8);
    bmpinfoheader[10] = (unsigned char) (height >> 16);
    bmpinfoheader[11] = (unsigned char) (height >> 24);

    file.write((char*)bmpfileheader, 14);
    file.write((char*)bmpinfoheader, 40);

    int padding = (4 - (width * 3) % 4) % 4;
    for (int y = height - 1; y >= 0; --y) {
        for (int x = 0; x < width; ++x) {
            unsigned char pixel[3];
            pixel[0] = frameBuffer[(y * width + x) * 3 + 2];
            pixel[1] = frameBuffer[(y * width + x) * 3 + 1];
            pixel[2] = frameBuffer[(y * width + x) * 3];
            file.write((char*)pixel, 3);
        }
        for (int p = 0; p < padding; ++p) {
            file.write((char*)"\0", 1);
        }
    }

    file.close();
}