#include "Renderer.hpp"

#include "UI.hpp"
#include "../../../core/world/materials.hpp"
#include "../../control/Camera.hpp"
#include "../../Window.hpp"
#include "../../../core/utils/logger.hpp"
#include <GL/gl3w.h>

#include <TracyOpenGL.hpp>

using namespace render::gl;

constexpr auto GL_MAJOR = 4;
constexpr auto GL_MINOR_MIN = 2;
constexpr auto GL_MINOR_BEST = 6;

#define CONTENT_DIR "content/"
#define TEXTURES_DIR CONTENT_DIR "textures/"

Renderer::Renderer(const render::renderOptions& options):
    IndicatorCubeBuffer(Shape::LINE_CUBE), IndicatorSphereBuffer(Shape::LINE_SPHERE)
{
    glGenVertexArrays(1, &VertexArrayID);
    glBindVertexArray(VertexArrayID);

    reloadShaders(options.voxel);
    SkyPass = std::make_unique<pass::SkyProgram>();
    SkyEnable = options.skybox;
    IndicatorPass = std::make_unique<pass::ColorProgram>();

    FogColor = glm::vec3(options.clear_color.x, options.clear_color.y, options.clear_color.z);
    loadTextures(options.textures, options.getMipmapLodBias(), options.getAnisotropy());
    setFillMode(options.wireframe);
}

Renderer::~Renderer() {
    render::UI::Unload();
    unloadTextures();
    glDeleteVertexArrays(1, &VertexArrayID);
}

void framebuffer_size_callback(GLFWwindow *, int width, int height) {
    glViewport(0, 0, width, height);
}

bool Renderer::Load(Window& window, const render::renderOptions& opt, const windowOptions& windOpt) {
    Window::CreateInfo windowInfo;
    windowInfo.pfnResize = framebuffer_size_callback;
    windowInfo.client = {Window::CreateInfo::Client::Type::GL, GL_MAJOR, GL_MINOR_MIN};
    windowInfo.samples = windOpt.getSamples();
    windowInfo.fps = windOpt.targetFPS;
    windowInfo.fullscreen = windOpt.fullscreen;

    if (!window.create(windowInfo))
        return false;

    if (gl3wInit() != GL3W_OK) {
        LOG_E("Failed to initialize OpenGL");
        return false;
    }
    if (!gl3wIsSupported(GL_MAJOR, GL_MINOR_BEST)) {
        if (gl3wIsSupported(GL_MAJOR, GL_MINOR_MIN)) {
            LOG_W("OpenGL " << GL_MAJOR << "." << GL_MINOR_BEST << " not supported. Some feature like texture sharpness may not work");
        } else {
            LOG_E("OpenGL " << GL_MAJOR << "." << GL_MINOR_MIN << " not supported");
            return false;
        }
    }
    LOG_D("OpenGL " << glGetString(GL_VERSION) << ", GLSL " << glGetString(GL_SHADING_LANGUAGE_VERSION));

    // Enable depth test
    glEnable(GL_DEPTH_TEST);
    // Accept fragment if it closer to the camera than the former one
    glDepthFunc(GL_LEQUAL);

    // Cull triangles which normal is not towards the camera
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    GLint smp;
    glGetIntegerv(GL_SAMPLES, &smp);
    if (smp > 0) {
        glEnable(GL_MULTISAMPLE);
    }

    glClearColor(opt.clear_color.x, opt.clear_color.y, opt.clear_color.z, opt.clear_color.w);

    TracyGpuContext;

    sInstance = new Renderer(opt);
    sInstance->setVSync(windOpt.targetFPS < Window::MIN_FPS);
    Model::MakeDefault();
    LodModel::MakeDefault();
    return true;
}

void Renderer::loadUI(Window& w) {
    UI::Load(w);
}


void Renderer::beginFrame() {
    TracyGpuZone("Render");
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_FRAMEBUFFER_SRGB);
}

std::function<size_t(render::LodModel *const, glm::mat4, glm::vec4, float)> Renderer::beginWorldPass(bool solid) {
    if (!solid)
        glEnable(GL_BLEND);
    WorldPass->useIt();
    WorldPass->start(this);
    return [&](render::LodModel *const buf, glm::mat4 model, glm::vec4 sph, float curv) {
        WorldPass->setup(model, sph, curv);
        return dynamic_cast<LodModel *const>(buf)->draw();
    };
}

std::function<size_t(render::Model *const, const std::vector<glm::mat4> &)> Renderer::beginEntityPass() {
    EntityPass->useIt();
    EntityPass->start(this);
    return [&](render::Model *const buf, const std::vector<glm::mat4> &models) {
        EntityPass->setup(models);
        return dynamic_cast<Model *const>(buf)->drawInstanced(models.size());
    };
}

std::function<size_t(glm::mat4, world::action::Shape, glm::vec4)> Renderer::beginIndicatorPass() {
    IndicatorPass->useIt();
    return [&](glm::mat4 model, world::action::Shape shape, glm::vec4 color) {
        IndicatorPass->setup(this, model, color);
        return shape == world::action::Shape::Cube ?
            IndicatorCubeBuffer.draw(GL_LINES) : IndicatorSphereBuffer.draw(GL_LINES);
    };
}

void Renderer::postProcess() {
    glDisable(GL_FRAMEBUFFER_SRGB);
    glDisable(GL_BLEND);
    if(SkyEnable) {
        SkyPass->draw(this);
    }
}
void Renderer::endFrame() { }

void Renderer::swapBuffer(Window& w) {
    TracyGpuZone("Swap");
    glfwSwapBuffers(w.getPtr());
    TracyGpuCollect;
}

void Renderer::reloadShaders(const pass::VoxelProgram::options& options) {
    WorldPass = std::make_unique<pass::WorldProgram>(options);
    EntityPass = std::make_unique<pass::EntityProgram>(options);
}
void Renderer::reloadTextures(const std::string& texturePath, float mipMapLOD, int anisotropy) {
    unloadTextures();
    loadTextures(texturePath, mipMapLOD, anisotropy);
}

void Renderer::unloadTextures() {
    HOSAtlas.reset();
    NormalAtlas.reset();
    TextureAtlas.reset();
}
void Renderer::loadTextures(const std::string& texturePath, float mipMapLOD, int anisotropy) {
    std::vector<std::string> terrainTextures;
    auto makePaths = [&](const std::string& suffix) {
        terrainTextures.clear();
        for(const auto& texture: world::materials::textures) {
            terrainTextures.emplace_back(TEXTURES_DIR + texturePath + "/terrain/" + texture + suffix + ".dds");
        }
        return terrainTextures;
    };
    auto sampling = Texture::sampling{true, true, Texture::Wrap::REPEAT, anisotropy, true, mipMapLOD};
    TextureAtlas = TextureArray::LoadFromFiles(makePaths(""), sampling);
    NormalAtlas = TextureArray::LoadFromFiles(makePaths(".nrm"), sampling);
    HOSAtlas = TextureArray::LoadFromFiles(makePaths(".hos"), sampling);

    Skybox = TextureCube::LoadFromFiles(TEXTURES_DIR + texturePath + "/sky/Space_tray.cube", {});
}

void Renderer::lookFrom(const Camera& camera) {
    ProjectionMatrix = camera.getProjectionMatrix();
    ViewMatrix = camera.getViewMatrix();
    FogDepth = camera.getDepth();
}

void Renderer::setClearColor(glm::vec4 c) {
    FogColor = c;
    glClearColor(c.r, c.g, c.b, c.a);
}

void Renderer::setFillMode(bool wireframe) {
    glPolygonMode(GL_FRONT_AND_BACK, wireframe ? GL_LINE : GL_FILL);
}

void Renderer::setVSync(bool vSync) {
    glfwSwapInterval(static_cast<int>(vSync));
}