#pragma only_renderers d3d11 vulkan metal glcore
#define UNIFIED_RT_GROUP_SIZE_X 128
#define UNIFIED_RT_GROUP_SIZE_Y 1
#define UNIFIED_RT_RAYGEN_FUNC IntegrateIndirectRadiance

#include "PathTracing.hlsl"
#include "SphericalHarmonicsUtils.hlsl"

RWStructuredBuffer<float3> g_Positions;
RWStructuredBuffer<float> g_RadianceShl2;
uint g_PositionsOffset;
uint g_SampleOffset;
uint g_SampleCount;

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

    PathIterator pathIter;
    InitPathIterator(pathIter, ray);

    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);
        uint shadowRayMask = ShadowRayMask();

        uint traceResult = TraceBounceRay(pathIter, bounceIndex, pathRayMask, dispatchInfo, accelStruct, rngState);

        if (traceResult == TRACE_HIT)
        {
            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 IntegrateIndirectRadiance(UnifiedRT::DispatchInfo dispatchInfo)
{
    const uint threadIdx = dispatchInfo.dispatchThreadID.x;
    const uint inProbeIdx = threadIdx / g_SampleCount + g_PositionsOffset;
    const uint inProbeSampleIdx = threadIdx % g_SampleCount;
    const uint outProbeIdx = threadIdx;

    PathTracingSampler rngState;
    rngState.Init(inProbeIdx, g_SampleOffset + inProbeSampleIdx);

    // TODO(pema.malling): This works but that is sort of by accident. Avoid coupling to AA (which is unrelated to probe integration). https://jira.unity3d.com/browse/LIGHT-1687
    const float3 uniformSphereDir = MapSquareToSphere(float2(rngState.GetFloatSample(RAND_DIM_AA_X), rngState.GetFloatSample(RAND_DIM_AA_Y)));

    UnifiedRT::Ray ray;
    ray.origin = g_Positions[inProbeIdx];
    ray.direction = uniformSphereDir;
    ray.tMin = 0;
    ray.tMax = K_T_MAX;

    float3 radiance = EstimateProbeRadiance(dispatchInfo, ray, rngState);

    // Local array to accumulate radiance into, using SoA layout.
    float3 accumulatedRadianceSH[SH_COEFFICIENTS_PER_CHANNEL];
    accumulatedRadianceSH[0] = radiance * SHL0();
    accumulatedRadianceSH[1] = radiance * SHL1_1(uniformSphereDir);
    accumulatedRadianceSH[2] = radiance * SHL10(uniformSphereDir);
    accumulatedRadianceSH[3] = radiance * SHL11(uniformSphereDir);

    accumulatedRadianceSH[4] = radiance * SHL2_2(uniformSphereDir);
    accumulatedRadianceSH[5] = radiance * SHL2_1(uniformSphereDir);
    accumulatedRadianceSH[6] = radiance * SHL20(uniformSphereDir);
    accumulatedRadianceSH[7] = radiance * SHL21(uniformSphereDir);
    accumulatedRadianceSH[8] = radiance * SHL22(uniformSphereDir);

    const float reciprocalSampleCount = 1.0f / (float) g_SampleCount;
    const float reciprocalUniformSphereDensity = 4.0f * PI;
    const float monteCarloNormalization = reciprocalSampleCount * reciprocalUniformSphereDensity;
    for (uint channel = 0; channel < SH_COLOR_CHANNELS; ++channel)
    {
        for (uint i = 0; i < SH_COEFFICIENTS_PER_CHANNEL; ++i)
        {
            g_RadianceShl2[SHIndex(outProbeIdx, channel, i)] = accumulatedRadianceSH[i][channel] * monteCarloNormalization;
        }
    }
}
