#include <ptvc/components/lensFlare/LensFlareComponent.h>

#include <ptvc/platform/Gl.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>

namespace CodeDrawPTVC::components::lensFlare {
    namespace {
        struct BlendState {
            GLboolean enabled = GL_FALSE;
            GLint srcRgb = GL_ONE;
            GLint dstRgb = GL_ZERO;
            GLint srcAlpha = GL_ONE;
            GLint dstAlpha = GL_ZERO;
            GLint equationRgb = GL_FUNC_ADD;
            GLint equationAlpha = GL_FUNC_ADD;
        };

        struct RenderState {
            GLint program = 0;
            GLint vao = 0;
            GLboolean depthTest = GL_FALSE;
            GLboolean depthMask = GL_TRUE;
            GLboolean cullFace = GL_FALSE;
            BlendState blend{};
        };

        RenderState CaptureRenderState()
        {
            RenderState s{};
            glGetIntegerv(GL_CURRENT_PROGRAM, &s.program);
            glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &s.vao);
            s.depthTest = glIsEnabled(GL_DEPTH_TEST);
            glGetBooleanv(GL_DEPTH_WRITEMASK, &s.depthMask);
            s.cullFace = glIsEnabled(GL_CULL_FACE);

            s.blend.enabled = glIsEnabled(GL_BLEND);
            glGetIntegerv(GL_BLEND_SRC_RGB, &s.blend.srcRgb);
            glGetIntegerv(GL_BLEND_DST_RGB, &s.blend.dstRgb);
            glGetIntegerv(GL_BLEND_SRC_ALPHA, &s.blend.srcAlpha);
            glGetIntegerv(GL_BLEND_DST_ALPHA, &s.blend.dstAlpha);
            glGetIntegerv(GL_BLEND_EQUATION_RGB, &s.blend.equationRgb);
            glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &s.blend.equationAlpha);
            return s;
        }

        void RestoreRenderState(const RenderState& s)
        {
            if (s.blend.enabled) {
                glEnable(GL_BLEND);
            } else {
                glDisable(GL_BLEND);
            }
            glBlendFuncSeparate(
                static_cast<GLenum>(s.blend.srcRgb),
                static_cast<GLenum>(s.blend.dstRgb),
                static_cast<GLenum>(s.blend.srcAlpha),
                static_cast<GLenum>(s.blend.dstAlpha));
            glBlendEquationSeparate(
                static_cast<GLenum>(s.blend.equationRgb),
                static_cast<GLenum>(s.blend.equationAlpha));

            if (s.depthTest) {
                glEnable(GL_DEPTH_TEST);
            } else {
                glDisable(GL_DEPTH_TEST);
            }
            glDepthMask(s.depthMask);

            if (s.cullFace) {
                glEnable(GL_CULL_FACE);
            } else {
                glDisable(GL_CULL_FACE);
            }

            glBindVertexArray(static_cast<GLuint>(s.vao));
            glUseProgram(static_cast<GLuint>(s.program));
        }

        float ClampFinite(float value, float fallback, float lo, float hi)
        {
            if (!std::isfinite(value)) {
                return fallback;
            }
            return std::clamp(value, lo, hi);
        }

        glm::vec3 ClampColor(const glm::vec3& value)
        {
            glm::vec3 color = value;
            if (!std::isfinite(color.x)) {
                color.x = 1.0f;
            }
            if (!std::isfinite(color.y)) {
                color.y = 1.0f;
            }
            if (!std::isfinite(color.z)) {
                color.z = 1.0f;
            }
            return glm::clamp(color, glm::vec3(0.0f), glm::vec3(8.0f));
        }

        float EdgeFade(const glm::vec2 ndc, float edgeFadeNdc)
        {
            const float insideX = 1.0f - std::abs(ndc.x);
            const float insideY = 1.0f - std::abs(ndc.y);
            return std::clamp(std::min(insideX, insideY) / std::max(0.001f, edgeFadeNdc), 0.0f, 1.0f);
        }

        void DrawQuad(
            const GpuProgram& program,
            const glm::vec2& centerNdc,
            const glm::vec2& halfSizeNdc,
            const glm::vec3& color,
            float intensity,
            int kind)
        {
            glUniform2f(program.GetUniformLocation("uCenterNdc"), centerNdc.x, centerNdc.y);
            glUniform2f(program.GetUniformLocation("uHalfSizeNdc"), halfSizeNdc.x, halfSizeNdc.y);
            glUniform3f(program.GetUniformLocation("uColor"), color.r, color.g, color.b);
            glUniform1f(program.GetUniformLocation("uIntensity"), intensity);
            glUniform1i(program.GetUniformLocation("uKind"), kind);
            glDrawArrays(GL_TRIANGLES, 0, 6);
        }

        glm::vec2 SizePxToHalfNdc(float widthPx, float heightPx, int viewportW, int viewportH)
        {
            return {
                std::max(1.0f, widthPx) / std::max(1, viewportW),
                std::max(1.0f, heightPx) / std::max(1, viewportH)
            };
        }
    }


    float FocalLengthYPixels(float viewportHeightPx, float fovYRadians)
    {
        const float safeHeight = std::max(1.0f, viewportHeightPx);
        const float safeFovY = std::max(0.001f, fovYRadians);
        return 0.5f * safeHeight / std::tan(0.5f * safeFovY);
    }

    float AngularRadiusPixels(float angularRadiusRadians, float focalLengthYPx)
    {
        return std::max(0.0f, angularRadiusRadians) * std::max(1.0f, focalLengthYPx);
    }

    float EmitterRadiusPixels(float emitterRadiusWorld, float distanceWorld, float focalLengthYPx)
    {
        const float safeDistance = std::max(0.001f, distanceWorld);
        const float angularRadius = std::atan(std::max(0.0f, emitterRadiusWorld) / safeDistance);
        return AngularRadiusPixels(angularRadius, focalLengthYPx);
    }
    LensFlareRenderer::~LensFlareRenderer()
    {
        Shutdown();
    }

    void LensFlareRenderer::Initialize()
    {
        if (_program.Handle() == 0) {
            _program.LoadFromFiles(
                "engine/components/lensFlare/assets/shaders/lensFlare.vert",
                "engine/components/lensFlare/assets/shaders/lensFlare.frag");
        }

        if (_vao == 0) {
            glGenVertexArrays(1, &_vao);
        }
    }

    void LensFlareRenderer::Shutdown()
    {
        if (_vao != 0) {
            glDeleteVertexArrays(1, &_vao);
            _vao = 0;
        }
        _program.Destroy();
    }

    void LensFlareRenderer::Render(
        const glm::mat4& viewProj,
        std::span<const Source> sources,
        int viewportW,
        int viewportH,
        const Config& config)
    {
        if (!config.enabled || sources.empty() || viewportW <= 0 || viewportH <= 0) {
            return;
        }

        Initialize();
        if (_program.Handle() == 0 || _vao == 0) {
            return;
        }

        const float globalIntensity = ClampFinite(config.globalIntensity, 1.0f, 0.0f, 10.0f);
        const float sourceDotIntensity = ClampFinite(config.sourceDotIntensity, 1.0f, 0.0f, 20.0f);
        const float sourceDotRadiusScale = ClampFinite(config.sourceDotRadiusScale, 1.0f, 0.0f, 50.0f);
        const float artifactRadiusScale = ClampFinite(config.artifactRadiusScale, 1.0f, 0.0f, 50.0f);
        const float edgeFadeNdc = ClampFinite(config.edgeFadeNdc, 0.22f, 0.001f, 1.0f);
        const float ghostIntensity = ClampFinite(config.ghostIntensity, 0.42f, 0.0f, 4.0f);
        const float haloIntensity = ClampFinite(config.haloIntensity, 0.26f, 0.0f, 4.0f);
        const float streakIntensity = ClampFinite(config.streakIntensity, 0.18f, 0.0f, 4.0f);

        const RenderState saved = CaptureRenderState();

        glDisable(GL_DEPTH_TEST);
        glDepthMask(GL_FALSE);
        glDisable(GL_CULL_FACE);
        glEnable(GL_BLEND);
        glBlendEquation(GL_FUNC_ADD);
        glBlendFunc(GL_ONE, GL_ONE);

        _program.Use();
        glBindVertexArray(_vao);

        const glm::vec2 screenCenter(0.0f);
        constexpr std::array<float, 4> GhostOffsets{{-0.72f, -0.38f, 0.32f, 0.82f}};
        constexpr std::array<float, 4> GhostScales{{0.62f, 0.38f, 0.44f, 0.76f}};
        const std::array<glm::vec3, 4> GhostTints{{
            glm::vec3(0.82f, 0.95f, 1.00f),
            glm::vec3(1.00f, 0.56f, 0.42f),
            glm::vec3(0.40f, 0.88f, 1.00f),
            glm::vec3(1.00f, 0.86f, 0.42f)
        }};

        for (const Source& source : sources) {
            if (!source.enabled) {
                continue;
            }

            const float sourceIntensity = ClampFinite(source.intensity, 0.0f, 0.0f, 100.0f);
            const float radiusPx = ClampFinite(source.radiusPx, 80.0f, 1.0f, 2000.0f);
            const float visibility = ClampFinite(source.visibility, 1.0f, 0.0f, 1.0f);
            if (sourceIntensity <= 0.0001f) {
                continue;
            }

            const glm::vec4 clip = viewProj * glm::vec4(source.worldPosition, 1.0f);
            if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w) ||
                clip.w <= 0.0001f) {
                continue;
            }

            const glm::vec3 ndc3 = glm::vec3(clip) / clip.w;
            const glm::vec2 sourceNdc(ndc3.x, ndc3.y);
            const float fade = EdgeFade(sourceNdc, edgeFadeNdc);
            if (fade <= 0.0001f || ndc3.z < -1.0f || ndc3.z > 1.0f) {
                continue;
            }

            const glm::vec3 color = ClampColor(source.color);
            const float baseIntensity = sourceIntensity * visibility * globalIntensity * fade;
            if (baseIntensity <= 0.0001f) {
                continue;
            }

            const float sourceDotRadiusPx = radiusPx * sourceDotRadiusScale;
            const glm::vec2 coreSize = SizePxToHalfNdc(sourceDotRadiusPx, sourceDotRadiusPx, viewportW, viewportH);
            DrawQuad(_program, sourceNdc, coreSize, color, baseIntensity * sourceDotIntensity, 0);

            const glm::vec2 axis = screenCenter - sourceNdc;
            if (config.halo && haloIntensity > 0.0f) {
                const float haloRadiusPx = radiusPx * artifactRadiusScale * 2.65f;
                const glm::vec2 haloSize = SizePxToHalfNdc(haloRadiusPx, haloRadiusPx, viewportW, viewportH);
                DrawQuad(_program, sourceNdc + axis * 0.10f, haloSize, color, baseIntensity * haloIntensity, 2);
            }

            if (config.ghosts && ghostIntensity > 0.0f) {
                for (size_t i = 0; i < GhostOffsets.size(); ++i) {
                    const glm::vec2 ghostCenter = sourceNdc + axis * GhostOffsets[i];
                    const float ghostFade = EdgeFade(ghostCenter, edgeFadeNdc * 1.4f);
                    if (ghostFade <= 0.0001f) {
                        continue;
                    }

                    const float scale = radiusPx * artifactRadiusScale * GhostScales[i];
                    const glm::vec2 ghostSize = SizePxToHalfNdc(scale, scale, viewportW, viewportH);
                    const glm::vec3 ghostColor = color * GhostTints[i];
                    DrawQuad(_program, ghostCenter, ghostSize, ghostColor, baseIntensity * ghostIntensity * ghostFade, 1);
                }
            }

            if (config.streaks && streakIntensity > 0.0f) {
                const glm::vec2 streakSize = SizePxToHalfNdc(
                    radiusPx * artifactRadiusScale * 3.8f,
                    radiusPx * artifactRadiusScale * 0.42f,
                    viewportW,
                    viewportH
                );
                DrawQuad(_program, sourceNdc, streakSize, color, baseIntensity * streakIntensity, 3);
            }
        }

        RestoreRenderState(saved);
    }
}
