#pragma only_renderers d3d11 vulkan metal glcore
#define UNIFIED_RT_GROUP_SIZE_X 64
#define UNIFIED_RT_GROUP_SIZE_Y 1
#define UNIFIED_RT_RAYGEN_FUNC AccumulateInternal

#include "PathTracing.hlsl"
#include "LightmapIntegrationHelpers.hlsl"

int g_AccumulateDirectional;
int g_SampleOffset;
float g_PushOff;
RWStructuredBuffer<float4> g_ExpandedOutput;
RWStructuredBuffer<float4> g_ExpandedDirectional;

// additional instance flags to deal with lightmap lods
#define CURRENT_LOD_FOR_LIGHTMAP_INSTANCE 8u
#define LOD_ZERO_FOR_LIGHTMAP_INSTANCE 16u
#define CURRENT_LOD_FOR_LIGHTMAP_INSTANCE_SHADOW 32u
#define LOD_ZERO_FOR_LIGHTMAP_INSTANCE_SHADOW 64u

float3 EstimateLightmapRadiance(UnifiedRT::DispatchInfo dispatchInfo, UnifiedRT::Ray ray, inout PathTracingSampler rngState)
{
    UnifiedRT::RayTracingAccelStruct accelStruct = UNIFIED_RT_GET_ACCEL_STRUCT(g_SceneAccelStruct);

    PathIterator pathIter;
    InitPathIterator(pathIter, ray);

    // LOD handling: Rays starting from the lightmapped object use an accel struct built with that same LOD level
    uint rayLightmapLodMask = CURRENT_LOD_FOR_LIGHTMAP_INSTANCE;
    uint shadowRayLightmapLodMask = CURRENT_LOD_FOR_LIGHTMAP_INSTANCE_SHADOW;

    int transparencyBounce = 0;
    // We start at bounce index 1, as bounce index is defined relative to the camera for this path tracer.
    // Since this function is used for baking, we already implicitly have the first "hit", and are about
    // to process the second path segment.
    for (int bounceIndex = 1; bounceIndex <= g_BounceCount && transparencyBounce < MAX_TRANSMISSION_BOUNCES; bounceIndex++)
    {
        // The first path segment is special for the indirect pass - we should not add radiance from the
        // environment or emission, as these are already explicitly sampled in the direct pass.
        bool isFirstPathSegment = bounceIndex == 1;

        uint pathRayMask = RayMask(bounceIndex == 0) | rayLightmapLodMask;
        uint traceResult = TraceBounceRay(pathIter, bounceIndex, pathRayMask, dispatchInfo, accelStruct, rngState);

        if (traceResult == TRACE_HIT)
        {
            uint hitInstanceMask = UnifiedRT::GetInstance(pathIter.hitResult.instanceID).instanceMask;

            // LOD handling: If the ray hits a surface that is not the current lightmap instance,
            // then, for the rest of the path, we can replace it in the scene accel struct by one that using lod 0.
            if (!(hitInstanceMask & CURRENT_LOD_FOR_LIGHTMAP_INSTANCE))
            {
                rayLightmapLodMask = LOD_ZERO_FOR_LIGHTMAP_INSTANCE;
                shadowRayLightmapLodMask = LOD_ZERO_FOR_LIGHTMAP_INSTANCE_SHADOW;
            }

            uint shadowRayMask = ShadowRayMask() | shadowRayLightmapLodMask;
            if (!isFirstPathSegment)
                AddEmissionRadiance(pathIter, accelStruct, g_AccelStructInstanceList, false);
            AddRadianceFromDirectIllumination(pathIter, shadowRayMask, dispatchInfo, accelStruct, g_AccelStructInstanceList, rngState, false);
        }

        if (traceResult == TRACE_MISS)
        {
            if (!isFirstPathSegment)
                AddEnvironmentRadiance(pathIter, false);
            break;
        }

        if (traceResult == TRACE_TRANSMISSION)
        {
            bounceIndex--;
            transparencyBounce++;
            pathIter.ray.origin = pathIter.hitGeo.NextTransmissionRayOrigin();
            pathIter.throughput *= pathIter.material.transmission;
            rngState.NextBounce();
            continue;
        }

        if (!Scatter(pathIter, rngState))
            break;

        if (bounceIndex >= RUSSIAN_ROULETTE_MIN_BOUNCES)
        {
            float p = max(pathIter.throughput.x, max(pathIter.throughput.y, pathIter.throughput.z));
            if (rngState.GetFloatSample(RAND_DIM_RUSSIAN_ROULETTE) > p)
                break;
            else
                pathIter.throughput /= p;
        }

        rngState.NextBounce();
    }

    return pathIter.radianceSample;
}


void AccumulateInternal(UnifiedRT::DispatchInfo dispatchInfo)
{
    float3 worldPosition = 0.f;
    float3 worldNormal = 0.f;
    float3 worldFaceNormal = 0.f;
    uint localSampleOffset = 0;
    uint2 instanceTexelPos = 0;
    const bool gotSample = GetExpandedSample(dispatchInfo.dispatchThreadID.x, localSampleOffset, instanceTexelPos, worldPosition, worldNormal, worldFaceNormal);
    if (!gotSample)
        return;

    UnifiedRT::RayTracingAccelStruct accelStruct = UNIFIED_RT_GET_ACCEL_STRUCT(g_SceneAccelStruct);

    // now sample irradiance with the gBuffer data
    const uint sampleOffset = g_SampleOffset + localSampleOffset;
    PathTracingSampler rngState;
    rngState.Init(instanceTexelPos, sampleOffset);
    UnifiedRT::Ray ray;
    ray.origin = OffsetRayOrigin(worldPosition, worldFaceNormal, g_PushOff);
    ray.direction = CosineSample(float2(rngState.GetFloatSample(RAND_DIM_SURF_SCATTER_X), rngState.GetFloatSample(RAND_DIM_SURF_SCATTER_Y)), worldNormal);
    ray.tMin = 0;
    ray.tMax = Max_float();
    rngState.NextBounce();

    float3 sampleRadiance = EstimateLightmapRadiance(dispatchInfo, ray, rngState);

    const float sampleLuminance = Luminance(sampleRadiance.xyz);
    const float reciprocalProbabilityDensityMulipliedByCosine = PI;

    // store new accumulated radiance
    g_ExpandedOutput[dispatchInfo.dispatchThreadID.x] += float4(sampleRadiance * reciprocalProbabilityDensityMulipliedByCosine, 1.0f);
    // the cosine term from the PDF cancels with the cosine term for the integrand.
    if (g_AccumulateDirectional > 0)
        g_ExpandedDirectional[dispatchInfo.dispatchThreadID.x] += float4(normalize(ray.direction), 1.f) * sampleLuminance;
}