#include "vk_scene.h"

#include "core/device/swapchain.h"
#include "core/context.h"
#include "mesh_bvh.h"

#include "glm/gtx/transform.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include "glm/gtx/norm.inl"

#include <algorithm>
#include <array>
#include <cmath>
#include <limits>

namespace
{
    struct BoundsHitDebug
    {
        bool usedBVH = false;
        bool bvhHit = false;
        bool fallbackBox = false;
    };

    // Ray / oriented-box intersection in world space using object-local AABB.
    // Returns true when hit; outWorldHit is the closest hit point in world space.
    bool intersect_ray_box(const glm::vec3 &rayOrigin,
                           const glm::vec3 &rayDir,
                           const Bounds &bounds,
                           const glm::mat4 &worldTransform,
                           glm::vec3 &outWorldHit)
    {
        if (glm::length2(rayDir) < 1e-8f)
        {
            return false;
        }

        // Transform ray into local space of the bounds for precise box test.
        glm::mat4 invM = glm::inverse(worldTransform);
        glm::vec3 localOrigin = glm::vec3(invM * glm::vec4(rayOrigin, 1.0f));
        glm::vec3 localDir = glm::vec3(invM * glm::vec4(rayDir, 0.0f));

        if (glm::length2(localDir) < 1e-8f)
        {
            return false;
        }
        localDir = glm::normalize(localDir);

        glm::vec3 minB = bounds.origin - bounds.extents;
        glm::vec3 maxB = bounds.origin + bounds.extents;

        float tMin = 0.0f;
        float tMax = std::numeric_limits<float>::max();

        for (int axis = 0; axis < 3; ++axis)
        {
            float o = localOrigin[axis];
            float d = localDir[axis];
            if (std::abs(d) < 1e-8f)
            {
                // Ray parallel to slab: must be inside to intersect.
                if (o < minB[axis] || o > maxB[axis])
                {
                    return false;
                }
            }
            else
            {
                float invD = 1.0f / d;
                float t1 = (minB[axis] - o) * invD;
                float t2 = (maxB[axis] - o) * invD;
                if (t1 > t2)
                {
                    std::swap(t1, t2);
                }

                tMin = std::max(tMin, t1);
                tMax = std::min(tMax, t2);

                if (tMax < tMin)
                {
                    return false;
                }
            }
        }

        if (tMax < 0.0f)
        {
            return false;
        }

        // Choose the closest intersection in front of the ray origin.
        // If the ray starts inside the box (tMin <= 0), use the exit point tMax.
        float tHit = tMin;
        if (tHit <= 0.0f)
        {
            tHit = tMax;
        }
        if (tHit <= 0.0f)
        {
            return false;
        }

        glm::vec3 localHit = localOrigin + tHit * localDir;
        glm::vec3 worldHit = glm::vec3(worldTransform * glm::vec4(localHit, 1.0f));

        outWorldHit = worldHit;
        return true;
    }

    // Quick conservative ray / bounding-sphere test in world space.
    // Returns false when the ray misses the sphere; on hit, outT is the
    // closest positive intersection distance along the ray direction.
    bool intersect_ray_sphere(const glm::vec3 &rayOrigin,
                              const glm::vec3 &rayDir,
                              const Bounds &bounds,
                              const glm::mat4 &worldTransform,
                              float &outT)
    {
        // Sphere center is bounds.origin transformed to world.
        glm::vec3 centerWorld = glm::vec3(worldTransform * glm::vec4(bounds.origin, 1.0f));

        // Approximate world-space radius by scaling with the maximum axis scale.
        glm::vec3 sx = glm::vec3(worldTransform[0]);
        glm::vec3 sy = glm::vec3(worldTransform[1]);
        glm::vec3 sz = glm::vec3(worldTransform[2]);
        float maxScale = std::max({glm::length(sx), glm::length(sy), glm::length(sz)});
        float radiusWorld = bounds.sphereRadius * maxScale;
        if (radiusWorld <= 0.0f)
        {
            return false;
        }

        glm::vec3 oc = rayOrigin - centerWorld;
        float b = glm::dot(oc, rayDir);
        float c = glm::dot(oc, oc) - radiusWorld * radiusWorld;
        float disc = b * b - c;
        if (disc < 0.0f)
        {
            return false;
        }
        float s = std::sqrt(disc);
        float t0 = -b - s;
        float t1 = -b + s;
        float t = t0 >= 0.0f ? t0 : t1;
        if (t < 0.0f)
        {
            return false;
        }
        outT = t;
        return true;
    }

    // Ray / capsule intersection in world space. Capsule is aligned with local Y axis
    // and reconstructed from Bounds.origin/extents, assuming extents.x/z ~= radius
    // and extents.y ~= halfHeight + radius (AABB center/half-size convention).
    bool intersect_ray_capsule(const glm::vec3 &rayOrigin,
                               const glm::vec3 &rayDir,
                               const Bounds &bounds,
                               const glm::mat4 &worldTransform,
                               glm::vec3 &outWorldHit)
    {
        if (glm::length2(rayDir) < 1e-8f)
        {
            return false;
        }

        // Transform ray into object-local space.
        glm::mat4 invM = glm::inverse(worldTransform);
        glm::vec3 localOrigin = glm::vec3(invM * glm::vec4(rayOrigin, 1.0f));
        glm::vec3 localDir = glm::vec3(invM * glm::vec4(rayDir, 0.0f));
        if (glm::length2(localDir) < 1e-8f)
        {
            return false;
        }
        localDir = glm::normalize(localDir);

        // Work in capsule-local space where Bounds.origin is at (0,0,0).
        glm::vec3 ro = localOrigin - bounds.origin;
        glm::vec3 rd = localDir;

        float radius = std::max(bounds.extents.x, bounds.extents.z);
        if (radius <= 0.0f)
        {
            return false;
        }
        // extents.y is (halfCylinder + radius) for a symmetric capsule.
        float halfSegment = std::max(bounds.extents.y - radius, 0.0f);

        float tHit = std::numeric_limits<float>::max();
        bool hit = false;

        // 1) Cylinder part around Y axis: x^2 + z^2 = r^2, |y| <= halfSegment.
        float a = rd.x * rd.x + rd.z * rd.z;
        float b = 2.0f * (ro.x * rd.x + ro.z * rd.z);
        float c = ro.x * ro.x + ro.z * ro.z - radius * radius;

        if (std::abs(a) > 1e-8f)
        {
            float disc = b * b - 4.0f * a * c;
            if (disc >= 0.0f)
            {
                float s = std::sqrt(disc);
                float invDen = 0.5f / a;
                float t0 = (-b - s) * invDen;
                float t1 = (-b + s) * invDen;
                if (t0 > t1) std::swap(t0, t1);

                auto tryCylHit = [&](float t)
                {
                    if (t < 0.0f || t >= tHit)
                    {
                        return;
                    }
                    glm::vec3 p = ro + rd * t;
                    if (std::abs(p.y) <= halfSegment + 1e-4f)
                    {
                        tHit = t;
                        hit = true;
                    }
                };

                tryCylHit(t0);
                tryCylHit(t1);
            }
        }

        // 2) Spherical caps at y = +/- halfSegment.
        auto intersectCap = [&](float capY)
        {
            glm::vec3 center(0.0f, capY, 0.0f);
            glm::vec3 oc = ro - center;
            float b2 = glm::dot(oc, rd);
            float c2 = glm::dot(oc, oc) - radius * radius;
            float disc = b2 * b2 - c2;
            if (disc < 0.0f)
            {
                return;
            }
            float s = std::sqrt(disc);
            float t0 = -b2 - s;
            float t1 = -b2 + s;

            auto tryCapHit = [&](float t)
            {
                if (t < 0.0f || t >= tHit)
                {
                    return;
                }
                tHit = t;
                hit = true;
            };

            if (t0 >= 0.0f) tryCapHit(t0);
            if (t1 >= 0.0f) tryCapHit(t1);
        };

        intersectCap(+halfSegment);
        intersectCap(-halfSegment);

        if (!hit)
        {
            return false;
        }

        glm::vec3 localHit = ro + rd * tHit;
        glm::vec3 worldHit = glm::vec3(worldTransform * glm::vec4(localHit + bounds.origin, 1.0f));

        if (glm::dot(worldHit - rayOrigin, rayDir) <= 0.0f)
        {
            return false;
        }

        outWorldHit = worldHit;
        return true;
    }

    // Ray / oriented-bounds intersection in world space using object-local shape.
    // For non-mesh shapes we use a quick world-space bounding-sphere pretest;
    // for mesh bounds we go directly to the mesh BVH (which already has a root AABB).
    // Returns true when hit; outWorldHit is the closest hit point in world space.
    bool intersect_ray_bounds(const glm::vec3 &rayOrigin,
                              const glm::vec3 &rayDir,
                              const RenderObject &obj,
                              glm::vec3 &outWorldHit,
                              BoundsHitDebug *debug,
                              MeshBVHPickHit *outMeshHit)
    {
        const Bounds &bounds = obj.bounds;
        const glm::mat4 &worldTransform = obj.transform;

        if (outMeshHit)
        {
            *outMeshHit = {};
        }

        // Non-pickable object.
        if (bounds.type == BoundsType::None)
        {
            return false;
        }

        if (glm::length2(rayDir) < 1e-8f)
        {
            return false;
        }

        switch (bounds.type)
        {
            case BoundsType::Sphere:
            {
                // Early reject using bounding sphere in world space.
                float sphereT = 0.0f;
                if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT))
                {
                    return false;
                }
                // We already have the hit distance along the ray from the sphere test.
                outWorldHit = rayOrigin + rayDir * sphereT;
                return true;
            }
            case BoundsType::Capsule:
            {
                float sphereT = 0.0f;
                if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT))
                {
                    return false;
                }
                return intersect_ray_capsule(rayOrigin, rayDir, bounds, worldTransform, outWorldHit);
            }
            case BoundsType::Mesh:
            {
                // For mesh bounds we rely solely on the CPU mesh BVH.
                // If there is no BVH or the BVH misses, the object is
                // treated as not hit by this ray (no coarse box fallback).
                if (!obj.sourceMesh || !obj.sourceMesh->bvh)
                {
                    return false;
                }

                if (debug)
                {
                    debug->usedBVH = true;
                }

                MeshBVHPickHit meshHit{};
                if (!intersect_ray_mesh_bvh(*obj.sourceMesh->bvh, worldTransform, rayOrigin, rayDir, meshHit))
                {
                    if (debug)
                    {
                        // BVH was queried but produced no hit.
                        debug->fallbackBox = true;
                    }
                    return false;
                }

                if (debug)
                {
                    debug->bvhHit = true;
                }

                outWorldHit = meshHit.worldPos;
                if (outMeshHit)
                {
                    *outMeshHit = meshHit;
                }
                return true;
            }
            case BoundsType::Box:
            default:
            {
                float sphereT = 0.0f;
                if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT))
                {
                    return false;
                }
                return intersect_ray_box(rayOrigin, rayDir, bounds, worldTransform, outWorldHit);
            }
        }
    }

    // Test whether the clip-space box corners of an object intersect a 2D NDC rectangle.
    // ndcMin/ndcMax are in [-1,1]x[-1,1]. Returns true if any visible corner projects inside.
    bool box_overlaps_ndc_rect(const RenderObject &obj,
                               const glm::mat4 &viewproj,
                               const glm::vec2 &ndcMin,
                               const glm::vec2 &ndcMax)
    {
        const glm::vec3 o = obj.bounds.origin;
        const glm::vec3 e = obj.bounds.extents;
        const glm::mat4 m = viewproj * obj.transform; // world -> clip

        const std::array<glm::vec3, 8> corners{
            glm::vec3{+1, +1, +1}, glm::vec3{+1, +1, -1}, glm::vec3{+1, -1, +1}, glm::vec3{+1, -1, -1},
            glm::vec3{-1, +1, +1}, glm::vec3{-1, +1, -1}, glm::vec3{-1, -1, +1}, glm::vec3{-1, -1, -1},
        };

        for (const glm::vec3 &c : corners)
        {
            glm::vec3 pLocal = o + c * e;
            glm::vec4 clip = m * glm::vec4(pLocal, 1.f);
            if (clip.w <= 0.0f)
            {
                continue;
            }
            float x = clip.x / clip.w;
            float y = clip.y / clip.w;
            float z = clip.z / clip.w; // Vulkan Z0: 0..1
            if (z < 0.0f || z > 1.0f)
            {
                continue;
            }
            if (x >= ndcMin.x && x <= ndcMax.x &&
                y >= ndcMin.y && y <= ndcMax.y)
            {
                return true;
            }
        }
        return false;
    }
} // namespace

bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject, glm::vec3 &outWorldPos)
{
    if (_context == nullptr)
    {
        return false;
    }

    SwapchainManager *swapchain = _context->getSwapchain();
    if (swapchain == nullptr)
    {
        return false;
    }

    VkExtent2D extent = swapchain->windowExtent();
    if (extent.width == 0 || extent.height == 0)
    {
        return false;
    }

    float width = static_cast<float>(extent.width);
    float height = static_cast<float>(extent.height);

    // Convert from window coordinates (top-left origin) to NDC in [-1, 1].
    float ndcX = (2.0f * mousePosPixels.x / width) - 1.0f;
    float ndcY = 1.0f - (2.0f * mousePosPixels.y / height);

    float fovRad = glm::radians(mainCamera.fovDegrees);
    float tanHalfFov = std::tan(fovRad * 0.5f);
    float aspect = width / height;

    // Build ray in camera space using -Z forward convention.
    glm::vec3 dirCamera(ndcX * aspect * tanHalfFov,
                        ndcY * tanHalfFov,
                        -1.0f);
    dirCamera = glm::normalize(dirCamera);

    glm::vec3 rayOrigin = mainCamera.position;
    glm::mat4 camRotation = mainCamera.getRotationMatrix();
    glm::vec3 rayDir = glm::normalize(glm::vec3(camRotation * glm::vec4(dirCamera, 0.0f)));

    bool anyHit = false;
    float bestDist2 = std::numeric_limits<float>::max();
    glm::vec3 bestHitPos{};

    // Reset debug info for this pick.
    pickingDebug = {};

    auto testList = [&](const std::vector<RenderObject> &list)
    {
        for (const RenderObject &obj: list)
        {
            glm::vec3 hitPos{};
            BoundsHitDebug localDebug{};
            MeshBVHPickHit localMeshHit{};
            if (!intersect_ray_bounds(rayOrigin, rayDir, obj, hitPos, &localDebug, &localMeshHit))
            {
                continue;
            }

            float d2 = glm::length2(hitPos - rayOrigin);
            if (d2 < bestDist2)
            {
                bestDist2 = d2;
                bestHitPos = hitPos;
                outObject = obj;

                // If we have a precise mesh BVH hit, refine the picked
                // primitive to the exact triangle instead of the whole surface.
                if (localMeshHit.hit && outObject.sourceMesh && outObject.sourceMesh->bvh)
                {
                    outObject.firstIndex = localMeshHit.firstIndex;
                    outObject.indexCount = 3;
                    outObject.surfaceIndex = localMeshHit.surfaceIndex;
                }

                anyHit = true;

                // Capture debug info for the best hit so far.
                pickingDebug.usedMeshBVH = localDebug.usedBVH;
                pickingDebug.meshBVHHit = localDebug.bvhHit;
                pickingDebug.meshBVHFallbackBox = localDebug.fallbackBox;
                if (obj.sourceMesh && obj.sourceMesh->bvh)
                {
                    pickingDebug.meshBVHPrimCount =
                        static_cast<uint32_t>(obj.sourceMesh->bvh->primitives.size());
                    pickingDebug.meshBVHNodeCount =
                        static_cast<uint32_t>(obj.sourceMesh->bvh->nodes.size());
                }
                else
                {
                    pickingDebug.meshBVHPrimCount = 0;
                    pickingDebug.meshBVHNodeCount = 0;
                }
            }
        }
    };

    testList(mainDrawContext.OpaqueSurfaces);
    testList(mainDrawContext.TransparentSurfaces);

    if (anyHit)
    {
        outWorldPos = bestHitPos;
    }

    return anyHit;
}

bool SceneManager::resolveObjectID(uint32_t id, RenderObject &outObject) const
{
    if (id == 0)
    {
        return false;
    }

    auto findIn = [&](const std::vector<RenderObject> &list) -> bool
    {
        for (const RenderObject &obj : list)
        {
            if (obj.objectID == id)
            {
                outObject = obj;
                return true;
            }
        }
        return false;
    };

    if (findIn(mainDrawContext.OpaqueSurfaces))
    {
        return true;
    }
    if (findIn(mainDrawContext.TransparentSurfaces))
    {
        return true;
    }
    return false;
}

void SceneManager::selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vector<RenderObject> &outObjects) const
{
    if (!_context || !_context->getSwapchain())
    {
        return;
    }

    VkExtent2D extent = _context->getSwapchain()->windowExtent();
    if (extent.width == 0 || extent.height == 0)
    {
        return;
    }

    float width = static_cast<float>(extent.width);
    float height = static_cast<float>(extent.height);

    // Convert from window coordinates (top-left origin) to NDC in [-1, 1].
    auto toNdc = [&](const glm::vec2 &p) -> glm::vec2
    {
        float ndcX = (2.0f * p.x / width) - 1.0f;
        float ndcY = 1.0f - (2.0f * p.y / height);
        return glm::vec2{ndcX, ndcY};
    };

    glm::vec2 ndc0 = toNdc(p0);
    glm::vec2 ndc1 = toNdc(p1);
    glm::vec2 ndcMin = glm::min(ndc0, ndc1);
    glm::vec2 ndcMax = glm::max(ndc0, ndc1);

    const glm::mat4 vp = sceneData.viewproj;

    auto testList = [&](const std::vector<RenderObject> &list)
    {
        for (const RenderObject &obj : list)
        {
            if (box_overlaps_ndc_rect(obj, vp, ndcMin, ndcMax))
            {
                outObjects.push_back(obj);
            }
        }
    };

    testList(mainDrawContext.OpaqueSurfaces);
    testList(mainDrawContext.TransparentSurfaces);
}

