#include "Skybox.hpp"
#include "assets/Assets.hpp"
#include "graphics/core/Shader.hpp"
#include "graphics/core/Mesh.hpp"
#include "graphics/core/Batch3D.hpp"
#include "graphics/core/Texture.hpp"
#include "graphics/core/Cubemap.hpp"
#include "graphics/core/Framebuffer.hpp"
#include "graphics/core/DrawContext.hpp"
#include "window/Window.hpp"
#include "window/Camera.hpp"
#include "maths/UVRegion.hpp"

#include <cmath>
#include <iostream>
#include <GL/glew.h>
#include <glm/glm.hpp>

#ifndef M_PI
#define M_PI 3.141592
#endif // M_PI

const int STARS_COUNT = 3000;
const int STARS_SEED = 632;

Skybox::Skybox(uint size, Shader* shader) 
  : size(size), 
    shader(shader), 
    batch3d(std::make_unique<Batch3D>(4096)) 
{
    auto cubemap = std::make_unique<Cubemap>(size, size, ImageFormat::rgb888);

    uint fboid;
    glGenFramebuffers(1, &fboid);
    fbo = std::make_unique<Framebuffer>(fboid, 0, std::move(cubemap));

    float vertices[] {
        -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f,
        -1.0f, -1.0f,  1.0f, 1.0f, 1.0f, -1.0f
    };
    vattr attrs[] {{2}, {0}};
    mesh = std::make_unique<Mesh>(vertices, 6, attrs);

    sprites.push_back(skysprite {
        "misc/moon",
        M_PI*0.5f,
        4.0f,
        false
    });

    sprites.push_back(skysprite {
        "misc/sun",
        M_PI*1.5f,
        4.0f,
        true
    });
}

Skybox::~Skybox() = default;

void Skybox::drawBackground(Camera* camera, Assets* assets, int width, int height) {
    auto backShader = assets->get<Shader>("background");
    backShader->use();
    backShader->uniformMatrix("u_view", camera->getView(false));
    backShader->uniform1f("u_zoom", camera->zoom*camera->getFov()/(M_PI*0.5f));
    backShader->uniform1f("u_ar", float(width)/float(height));
    backShader->uniform1i("u_cubemap", 1);
    bind();
    mesh->draw();
    unbind();
}

void Skybox::drawStars(float angle, float opacity) {
    batch3d->texture(nullptr);
    random.setSeed(STARS_SEED);
    for (int i = 0; i < STARS_COUNT; i++) {
        float rx = (random.randFloat()) - 0.5f;
        float ry = (random.randFloat()) - 0.5f;
        float z = (random.randFloat()) - 0.5f;
        float x = rx * std::sin(angle) + ry * -std::cos(angle);
        float y = rx * std::cos(angle) + ry * std::sin(angle);

        float sopacity = random.randFloat();
        if (y < 0.0f)
            continue;

        sopacity *= (0.2f+std::sqrt(std::cos(angle))*0.5f) - 0.05f;
        glm::vec4 tint (1,1,1, sopacity * opacity);
        batch3d->point(glm::vec3(x, y, z), tint);
    }
    batch3d->flushPoints();
}

void Skybox::draw(
    const DrawContext& pctx, 
    Camera* camera, 
    Assets* assets, 
    float daytime,
    float fog) 
{
    const Viewport& viewport = pctx.getViewport();
    int width = viewport.getWidth();
    int height = viewport.getHeight();

    drawBackground(camera, assets, width, height);

    DrawContext ctx = pctx.sub();
    ctx.setBlendMode(BlendMode::addition);

    auto p_shader = assets->get<Shader>("ui3d");
    p_shader->use();
    p_shader->uniformMatrix("u_projview", camera->getProjView(false));
    p_shader->uniformMatrix("u_apply", glm::mat4(1.0f));
    batch3d->begin();

    float angle = daytime * float(M_PI) * 2.0f;
    float opacity = glm::pow(1.0f-fog, 7.0f);
    
    for (auto& sprite : sprites) {
        batch3d->texture(assets->get<Texture>(sprite.texture));

        float sangle = daytime * float(M_PI)*2.0 + sprite.phase;
        float distance = sprite.distance;

        glm::vec3 pos(-std::cos(sangle)*distance, std::sin(sangle)*distance, 0);
        glm::vec3 up(-std::sin(-sangle), std::cos(-sangle), 0.0f);
        glm::vec4 tint (1,1,1, opacity);
        if (!sprite.emissive) {
            tint *= 0.6f+std::cos(angle)*0.4;
        }
        batch3d->sprite(pos, glm::vec3(0, 0, 1), 
                        up, 1, 1, UVRegion(), tint);
    }

    drawStars(angle, opacity);
}

void Skybox::refresh(const DrawContext& pctx, float t, float mie, uint quality) {
    DrawContext ctx = pctx.sub();
    ctx.setDepthMask(false);
    ctx.setDepthTest(false);
    ctx.setFramebuffer(fbo.get());
    ctx.setViewport(Viewport(size, size));

    auto cubemap = dynamic_cast<Cubemap*>(fbo->getTexture());
    assert(cubemap != nullptr);

    ready = true;
    glActiveTexture(GL_TEXTURE1);
    cubemap->bind();
    shader->use();

    const glm::vec3 xaxs[] = {
        {0.0f, 0.0f, -1.0f},
        {0.0f, 0.0f, 1.0f},
        {-1.0f, 0.0f, 0.0f},

        {-1.0f, 0.0f, 0.0f},
        {-1.0f, 0.0f, 0.0f},
        {1.0f, 0.0f, 0.0f},
    };
    const glm::vec3 yaxs[] = {
        {0.0f, 1.0f, 0.0f},
        {0.0f, 1.0f, 0.0f},
        {0.0f, 0.0f, -1.0f},
        
        {0.0f, 0.0f, 1.0f},
        {0.0f, 1.0f, 0.0f},
        {0.0f, 1.0f, 0.0f},
    };

    const glm::vec3 zaxs[] = {
        {1.0f, 0.0f, 0.0f},
        {-1.0f, 0.0f, 0.0f},
        {0.0f, -1.0f, 0.0f},
        
        {0.0f, 1.0f, 0.0f},
        {0.0f, 0.0f, -1.0f},
        {0.0f, 0.0f, 1.0f},
    };
    t *= M_PI*2.0f;
    
    shader->uniform1i("u_quality", quality);
    shader->uniform1f("u_mie", mie);
    shader->uniform1f("u_fog", mie - 1.0f);
    shader->uniform3f("u_lightDir", glm::normalize(glm::vec3(sin(t), -cos(t), 0.0f)));
    for (uint face = 0; face < 6; face++) {
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, cubemap->getId(), 0);
        shader->uniform3f("u_xaxis", xaxs[face]);
        shader->uniform3f("u_yaxis", yaxs[face]);
        shader->uniform3f("u_zaxis", zaxs[face]);
        mesh->draw();
    }
    cubemap->unbind();
    glActiveTexture(GL_TEXTURE0);
}

void Skybox::bind() const {
    glActiveTexture(GL_TEXTURE1);
    fbo->getTexture()->bind();
    glActiveTexture(GL_TEXTURE0);
}

void Skybox::unbind() const {
    glActiveTexture(GL_TEXTURE1);
    fbo->getTexture()->unbind();
    glActiveTexture(GL_TEXTURE0);
}
