//
// Created by Bram on 10/11/2024.
//

#include <cassert>
#include "HitTest.h"

float min(float a, float b, float c) {
    return std::min(a, std::min(b, c));
}

float max(float a, float b, float c) {
    return std::max(a, std::max(b, c));
}

float CrossZ(const Vector3& p0, const Vector3& p1, const Vector3& point)
{
    return (p1.x - p0.x) * (point.y - p0.y)
           - (p1.y - p0.y) * (point.x - p0.x);
}


template<typename Type>
Type interpolate(const Type& val0, const Type& val1, const Type& val2, float depth, float weight)
{
    return {
            val0 * depth * weight +
            val1 * depth * weight +
            val2 * depth * weight
    };
}

std::optional<Sample> HitTest::TriangleHitTest(const Vector3& fragPos, const Vertex& v0, const Vertex& v1, const Vertex& v2)
{
    Vector3 weights;

    weights.x = CrossZ(v2.position, v1.position, fragPos);
    if ( weights.x > 0 )
        return std::nullopt;

    weights.y = CrossZ(v0.position, v2.position, fragPos);
    if ( weights.y > 0 )
        return std::nullopt;

    weights.z = CrossZ(v1.position, v0.position, fragPos);
    if ( weights.z > 0 )
        return std::nullopt;

    const float totalWeight{ weights.x + weights.y + weights.z };

    const float invTotalWeight{ 1.0f / totalWeight };
    const Vector3 normWeights{
            weights.x * invTotalWeight,
            weights.y * invTotalWeight,
            weights.z * invTotalWeight
    };

    const float depth =
            1 / (normWeights.x / v0.position.w +
                 normWeights.y / v1.position.w +
                 normWeights.z / v2.position.w);

    const Vector2 uv =
            v0.uv * depth * normWeights.x / v0.position.w +
            v1.uv * depth * normWeights.y / v1.position.w +
            v2.uv * depth * normWeights.z / v2.position.w;

    const float interpolatedDepth = 1 / (normWeights.x / v0.position.w + normWeights.y / v1.position.w + normWeights.z / v2.position.w);

    auto interpolate  =
            [&]<typename Type>(const Type& val0, const Type& val1, const Type& val2) -> Type
            {
                return (val0 / v0.position.w * normWeights.x +
                        val1 / v1.position.w * normWeights.y +
                        val2 / v2.position.w * normWeights.z) * interpolatedDepth;

            };

    //Invert the normal
    const Vector3 normal = interpolate(v0.normal, v1.normal, v2.normal).Normalized();
    const Vector3 tangent = interpolate(v0.tangent, v1.tangent, v2.tangent).Normalized();
    const Vector3 viewDir = interpolate(v0.viewDir, v1.viewDir, v2.viewDir).Normalized();

    return Sample{
        .uv = uv,
        .normal = normal,
        .tangent = tangent,
        .viewDirection = viewDir,
        .depth = depth,
        .weight = normWeights
    };
}



HitTest::HitTestSample HitTest::Triangle(const Vector3 &pos, const std::vector<Vertex> &vertices) {
    float minX = min(vertices[0].position.x, vertices[1].position.x, vertices[2].position.x);
    float maxX = max(vertices[0].position.x, vertices[1].position.x, vertices[2].position.x);

    float minY = min(vertices[0].position.y, vertices[1].position.y, vertices[2].position.y);
    float maxY = max(vertices[0].position.y, vertices[1].position.y, vertices[2].position.y);

    if (pos.x < minX || pos.x > maxX || pos.y < minY || pos.y > maxY) {
        return HitTestSample{ColorRGB{1, 0, 0}, 0};
    }

    // first edge
    double w0{ CrossZ(vertices[1].position, vertices[2].position, pos) };
    const bool inEdge0 = w0 <= 0;

    double w1{ CrossZ(vertices[2].position, vertices[0].position, pos) };
    const bool inEdge1 = w1 <= 0;

    double w2{ CrossZ(vertices[0].position, vertices[1].position, pos) };
    const bool inEdge2 = w2 <= 0;

    if(!inEdge0 || !inEdge1 || !inEdge2){
        return {ColorRGB{0, 0, 1}, 0};
    }

    const double totalWeight{ w0 + w1 + w2 };
    const float invTotalWeight{ static_cast<float>(1.0f / totalWeight) };
    w0 *= invTotalWeight;
    w1 *= invTotalWeight;
    w2 *= invTotalWeight;


    assert(std::abs(w0 + w1 + w2 - 1) < 0.0001);

    float v0z = vertices[0].position.z;
    float v1z = vertices[1].position.z;
    float v2z = vertices[2].position.z;

    float depth = w0 * v0z + w1 * v1z + w2 * v2z;

    if(depth < 0 || depth > 1) {
        return HitTestSample{ColorRGB{0, 0, 0}, 0};
    }

    Vector4 color{};

    color += Vector4(float(w0) * Vector3(vertices[0].color.r, vertices[0].color.g, vertices[0].color.b) , 0);
    color += Vector4(float(w1) * Vector3(vertices[1].color.r, vertices[1].color.g, vertices[1].color.b) , 0);
    color += Vector4(float(w2) * Vector3(vertices[2].color.r, vertices[2].color.g, vertices[2].color.b) , 0);


    return HitTestSample{ColorRGB{
            static_cast<float>(color.x),
            static_cast<float>(color.y),
            static_cast<float>(color.z)
        }, depth};
}
