/***********************************************/
/*           Copyright (c) 2025 Belmu          */
/*             All Rights Reserved             */
/***********************************************/

/*
    [Credits]:
        JNNGL - help with fixing lens tracing issues (https://github.com/JNNGL)

    [References]:
        Pharr, M., Jakob, W., & Humphreys, G. (2016). Physically Based Rendering: From theory to implementation (3rd ed.). Morgan Kaufmann. Section 6.4, “Realistic Cameras.” Retrieved July 11, 2025, from https://pbr-book.org/3ed‑2018/Camera_Models/Realistic_Cameras
*/

#include "/include/camera/lens/intersections.glsl"
#include "/include/camera/lens/focus.glsl"

bool traceLensSystem(const float startZ, const int startElement, float wavelength, inout float throughput, inout vec3 position, inout vec3 direction) {
    float opticalAxisZ     = startZ;
    float prevOpticalAxisZ = startZ;

    int i = startElement;

    int scatteringOrder = 0;

    complexFloat n1 = complexFloat(airIOR, 0.0);

    while (i >= 0 && i < lensElementsCount) {

        #if RUSSIAN_ROULETTE == 1
            if (throughput < randF()) { 
                throughput = 0.0;
                break;
            }
            throughput /= saturate(throughput);
        #endif

        if (scatteringOrder >= MAX_LENS_SCATTERING_ORDERS) {
            return false;
        }

        LensElement element = lensSystem[i];

        bool backwards = direction.z > 0.0;

        opticalAxisZ += backwards ? lensSystem[i - 1].thickness * float(i > 0) : -element.thickness;

        vec3 normal;

        if (element.radius == 0.0) {
            if (!intersectApertureStop(position, direction, opticalAxisZ, element, normal))
                return false;
        } else {
            if (element.asphericalCoefficients == emptyCoefficients) {
                if (!intersectSphericalLensElement(position, direction, opticalAxisZ, element, normal))
                    return false;
            }
            else if (!intersectAsphericalLensElement(position, direction, opticalAxisZ, prevOpticalAxisZ, element, normal))
                return false;
        }

        if (!element.isApertureStop) {
            complexFloat n2;
            n2.r = sellmeier(backwards ? element.coefficients : (i > 0 ? lensSystem[i - 1].coefficients : N_AIR), wavelength);
            n2.i = 0.0;

            float NdotV = abs(dot(normal, -direction));

            float fresnel_R = fresnelComplex_R(NdotV, n1, n2);
            float fresnel_T = fresnelComplex_T(NdotV, n1, n2);

            float specularBounceProbability = fresnel_R;

            if (specularBounceProbability > randF()) {
                throughput /= specularBounceProbability;
                throughput *= fresnel_R;

                direction = reflect(direction, normal);
            } else {
                throughput /= 1.0 - specularBounceProbability;
                throughput *= fresnel_T;
                
                direction = refract(direction, normal, n1.r / n2.r);

                if (direction == vec3(0.0))
                    return false;

                n1 = n2;
            }
        }

        i += (backwards ? 1 : -1);

        prevOpticalAxisZ = opticalAxisZ;

        scatteringOrder++;
    }

    if (i >= lensElementsCount)
        return false;

    return true;
}

void generateThickLensCameraRay(vec2 coords, float wavelength, inout float throughput, inout vec3 position, out vec3 direction) {
    vec2 sensorExtent = (sensorSize / LENS_ZOOM) * vec2(1.0, gbufferProjection[0][0] / gbufferProjection[1][1]);

    vec2 sensorPlaneCoords = (coords + rand2F() * texelSize) * 2.0 - 1.0;
    vec3 sensorRayOrigin   = vec3(sensorPlaneCoords * -sensorExtent, 0.0);

    #if MANUAL_CAMERA == 0
        float focusDistance = getAutoFocusDistance(position, normalize(-gbufferModelViewInverse[2].xyz));

        float focusShift = estimateLensFocusShift(wavelength, focusDistance);
    #else
        float focusShift = FOCAL * mm_2_m;
    #endif

    vec3 rearPosition = vec3(sampleDisk(rand2F()) * rearLensElement.aperture, -rearLensElement.thickness - focusShift);

    position  = sensorRayOrigin;
    direction = normalize(rearPosition - sensorRayOrigin);

    bool validCameraRay = traceLensSystem(-focusShift, lensElementsCount - 1, wavelength, throughput, position, direction);

    if (!validCameraRay) {
        throughput = 0.0;
        return;
    }

    throughput *= direction.z * direction.z * rearLensElement.aperture * rearLensElement.aperture * PI / lengthSqr(rearPosition - sensorRayOrigin);

    position  = viewToWorld(position);
    direction = normalize(mat3(gbufferModelViewInverse) * direction);
}
