#pragma once

#include <cmath>

#include "foundation/math/CameraFrame.h"
#include "foundation/math/Geometry.h"

namespace ta::foundation::math
{
    namespace detail
    {
        struct HomogeneousVector
        {
            float x = 0.0f;
            float y = 0.0f;
            float z = 0.0f;
            float w = 0.0f;
        };

        [[nodiscard]] inline HomogeneousVector multiply(
            const Matrix4x4& matrix,
            const HomogeneousVector& value) noexcept
        {
            const float* values = matrix.values;
            return {
                values[0] * value.x + values[4] * value.y + values[8] * value.z + values[12] * value.w,
                values[1] * value.x + values[5] * value.y + values[9] * value.z + values[13] * value.w,
                values[2] * value.x + values[6] * value.y + values[10] * value.z + values[14] * value.w,
                values[3] * value.x + values[7] * value.y + values[11] * value.z + values[15] * value.w
            };
        }
    }

    [[nodiscard]] inline bool project_visible(
        const ta::foundation::math::CameraFrame& camera,
        const Vector3& world_position,
        const float viewport_width,
        const float viewport_height,
        Vector3& out_screen_position) noexcept
    {
        if (!camera.valid || viewport_width <= 0.0f || viewport_height <= 0.0f)
            return false;

        const detail::HomogeneousVector world{
            world_position.x, world_position.y, world_position.z, 1.0f
        };
        const auto view = detail::multiply(camera.world_to_camera, world);
        const auto clip = detail::multiply(camera.projection, view);
        if (!std::isfinite(clip.w) || clip.w <= 0.01f)
            return false;

        const float inverse_w = 1.0f / clip.w;
        const float ndc_x = clip.x * inverse_w;
        const float ndc_y = clip.y * inverse_w;
        const float ndc_z = clip.z * inverse_w;
        if (!std::isfinite(ndc_x) || !std::isfinite(ndc_y) || !std::isfinite(ndc_z))
            return false;

        float x = (ndc_x + 1.0f) * 0.5f * viewport_width;
        float y = (1.0f - ndc_y) * 0.5f * viewport_height;
        if (camera.zoom > 1.0f)
        {
            const float center_x = viewport_width * 0.5f;
            const float center_y = viewport_height * 0.5f;
            x = (x - center_x) * camera.zoom + center_x;
            y = (y - center_y) * camera.zoom + center_y;
        }

        out_screen_position = {x, y, ndc_z};
        return x > -100.0f && y > -100.0f &&
               x < viewport_width + 100.0f && y < viewport_height + 100.0f;
    }
}
