#pragma once

#include <array>
#include <algorithm>
#include <cmath>
#include <vector>

#include <glm/glm.hpp>
#include <glm/geometric.hpp>

#include <ptvc/assets/GltfAsset.h>
#include <ptvc/scene/Component.h>

namespace CodeDrawPTVC
{
    struct FrustumPlanes
    {
        std::array<glm::vec4, 6> planes{};
    };

    class FrustumCullingComponent final : public Component
    {
    public:
        bool enabled = true;
        glm::vec3 localCenter{0.0f};
        float localRadius = 0.0f;

        bool SetLocalBoundsFromVertices(const std::vector<Vertex>& vertices)
        {
            if (vertices.empty()) {
                localCenter = glm::vec3(0.0f);
                localRadius = 0.0f;
                return false;
            }

            glm::vec3 minP(vertices.front().px, vertices.front().py, vertices.front().pz);
            glm::vec3 maxP = minP;

            for (const Vertex& v : vertices) {
                const glm::vec3 p(v.px, v.py, v.pz);
                minP = glm::min(minP, p);
                maxP = glm::max(maxP, p);
            }

            localCenter = (minP + maxP) * 0.5f;
            localRadius = 0.0f;

            for (const Vertex& v : vertices) {
                const glm::vec3 p(v.px, v.py, v.pz);
                localRadius = std::max(localRadius, glm::length(p - localCenter));
            }

            return localRadius > 0.0f;
        }

        glm::vec3 WorldCenter(const glm::mat4& model) const
        {
            return glm::vec3(model * glm::vec4(localCenter, 1.0f));
        }

        float WorldRadius(const glm::mat4& model) const
        {
            const float scaleX = glm::length(glm::vec3(model[0]));
            const float scaleY = glm::length(glm::vec3(model[1]));
            const float scaleZ = glm::length(glm::vec3(model[2]));
            const float maxScale = std::max(scaleX, std::max(scaleY, scaleZ));
            return localRadius * maxScale;
        }

        bool Intersects(const FrustumPlanes& frustum, const glm::mat4& model) const
        {
            return SphereIntersectsFrustum(frustum, WorldCenter(model), WorldRadius(model));
        }

        static FrustumPlanes ExtractFrustumPlanes(const glm::mat4& viewProj)
        {
            const glm::vec4 r0 = MatrixRow(viewProj, 0);
            const glm::vec4 r1 = MatrixRow(viewProj, 1);
            const glm::vec4 r2 = MatrixRow(viewProj, 2);
            const glm::vec4 r3 = MatrixRow(viewProj, 3);

            FrustumPlanes out{};
            out.planes[0] = NormalizePlane(r3 + r0);
            out.planes[1] = NormalizePlane(r3 - r0);
            out.planes[2] = NormalizePlane(r3 + r1);
            out.planes[3] = NormalizePlane(r3 - r1);
            out.planes[4] = NormalizePlane(r3 + r2);
            out.planes[5] = NormalizePlane(r3 - r2);
            return out;
        }

        static bool SphereIntersectsFrustum(const FrustumPlanes& frustum, const glm::vec3& center, float radius)
        {
            radius = std::max(0.0f, radius);
            for (const glm::vec4& plane : frustum.planes) {
                const float distance = glm::dot(glm::vec3(plane), center) + plane.w;
                if (distance < -radius) {
                    return false;
                }
            }
            return true;
        }

    private:
        static glm::vec4 MatrixRow(const glm::mat4& m, int row)
        {
            return glm::vec4(m[0][row], m[1][row], m[2][row], m[3][row]);
        }

        static glm::vec4 NormalizePlane(glm::vec4 plane)
        {
            const float len = glm::length(glm::vec3(plane));
            if (len <= 0.00001f) {
                return plane;
            }
            return plane / len;
        }
    };
}
