#include "ShadowMap.h"
#include "Environment.h"
#include <iostream>
#include <cmath>

namespace ZL {

    // Build a look-at view matrix (column-major, same convention as the engine).
    static Eigen::Matrix4f lookAt(const Eigen::Vector3f& eye,
                                   const Eigen::Vector3f& center,
                                   const Eigen::Vector3f& up) {
        Eigen::Vector3f f = (center - eye).normalized();
        Eigen::Vector3f s = f.cross(up).normalized();
        Eigen::Vector3f u = s.cross(f);

        Eigen::Matrix4f m = Eigen::Matrix4f::Identity();
        m(0, 0) = s.x();  m(0, 1) = s.y();  m(0, 2) = s.z();
        m(1, 0) = u.x();  m(1, 1) = u.y();  m(1, 2) = u.z();
        m(2, 0) = -f.x(); m(2, 1) = -f.y(); m(2, 2) = -f.z();

        m(0, 3) = -s.dot(eye);
        m(1, 3) = -u.dot(eye);
        m(2, 3) = f.dot(eye);
        return m;
    }

    // Build an orthographic projection matrix that matches the engine's
    // MakeOrthoMatrix(xmin, xmax, ymin, ymax, zNear, zFar) exactly.
    static Eigen::Matrix4f ortho(float left, float right,
                                  float bottom, float top,
                                  float zNear, float zFar) {
        float width = right - left;
        float height = top - bottom;
        float depthRange = zFar - zNear;

        Eigen::Matrix4f m = Eigen::Matrix4f::Zero();
        m(0, 0) = 2.0f / width;
        m(1, 1) = 2.0f / height;
        m(2, 2) = -1.0f / depthRange;
        m(0, 3) = -(right + left) / width;
        m(1, 3) = -(top + bottom) / height;
        m(2, 3) = zNear / depthRange;
        m(3, 3) = 1.0f;
        return m;
    }

    ShadowMap::ShadowMap(int res, float orthoSz, float zNear, float zFar)
        : resolution(res)
        , orthoSize(orthoSz)
        , nearPlane(zNear)
        , farPlane(zFar)
        , lightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f).normalized())
        , lightViewMatrix(Eigen::Matrix4f::Identity())
        , lightProjectionMatrix(Eigen::Matrix4f::Identity())
        , lightSpaceMatrix(Eigen::Matrix4f::Identity())
    {
        glGenFramebuffers(1, &fbo);

        glGenTextures(1, &depthTexture);
        glBindTexture(GL_TEXTURE_2D, depthTexture);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
                     resolution, resolution, 0,
                     GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                               GL_TEXTURE_2D, depthTexture, 0);

        // No color buffer for this FBO — depth only.
        glDrawBuffer(GL_NONE);
        glReadBuffer(GL_NONE);

        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
            std::cerr << "Error: Shadow map framebuffer is not complete!" << std::endl;
        }

        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }

    ShadowMap::~ShadowMap() {
        if (fbo) glDeleteFramebuffers(1, &fbo);
        if (depthTexture) glDeleteTextures(1, &depthTexture);
    }

    void ShadowMap::setLightDirection(const Eigen::Vector3f& dir) {
        lightDirection = dir.normalized();
    }

    void ShadowMap::updateLightSpaceMatrix(const Eigen::Vector3f& sceneCenter) {
        // Place the light "camera" looking at the scene center from the light direction.
        Eigen::Vector3f lightPos = sceneCenter - lightDirection * (farPlane * 0.5f);

        // Choose an up vector that isn't parallel to the light direction.
        Eigen::Vector3f up(0.0f, 1.0f, 0.0f);
        if (std::abs(lightDirection.dot(up)) > 0.99f) {
            up = Eigen::Vector3f(0.0f, 0.0f, 1.0f);
        }

        lightViewMatrix = lookAt(lightPos, sceneCenter, up);

        lightProjectionMatrix = ortho(
            -orthoSize, orthoSize,
            -orthoSize, orthoSize,
            nearPlane, farPlane);

        lightSpaceMatrix = lightProjectionMatrix * lightViewMatrix;
    }

    void ShadowMap::bind() {
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        glViewport(0, 0, resolution, resolution);
        glClear(GL_DEPTH_BUFFER_BIT);
    }

    void ShadowMap::unbind() {
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glViewport(0, 0, Environment::width, Environment::height);
    }

} // namespace ZL
