#define RENDER_SHADOWCOMP

// Define the workgroup size: 8 threads in each dimension (x, y, z)
layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in;

// Set workgroup counts based on LPV_SIZE
#if LPV_SIZE == 8
// For LPV_SIZE 8, use 32x32x32 workgroups
const ivec3 workGroups = ivec3(32, 32, 32);
#elif LPV_SIZE == 7
// For LPV_SIZE 7, use 16x16x16 workgroups
const ivec3 workGroups = ivec3(16, 16, 16);
#elif LPV_SIZE == 6
// For LPV_SIZE 6, use 8x8x8 workgroups
const ivec3 workGroups = ivec3(8, 8, 8);
#endif

#ifdef LPV_ENABLED
// Shared arrays for pre-loading LPV light data and voxel block IDs for faster access
shared vec4 lpvSharedData[10 * 10 * 10];
shared uint voxelSharedData[10 * 10 * 10];

// Constants for sky light falloff and index flattening
const vec2 LpvBlockSkyFalloff = vec2(0.96, 0.96);
const ivec3 lpvFlatten = ivec3(1, 10, 100);

    // Include external GLSL libraries that provide additional functions and definitions
    #include "/lib/lpv/blocks.glsl"
    #include "/lib/lpv/lpv_common.glsl"
    #include "/lib/lpv/lpv_blocks.glsl"
    #include "/lib/lpv/lpv_buffer.glsl"
    #include "/lib/lpv/voxel_common.glsl"

//-------------------------------------------------------------------------------
// Function: HsvToRgb
// Converts a color from HSV space to RGB space.
//-------------------------------------------------------------------------------
vec3 HsvToRgb(const in vec3 c)
{
    // Constant vector used for calculations
    const vec4 K = vec4(3.0, 2.0, 1.0, 9.0) / 3.0;

    // Compute intermediate values using fractional and absolute operations
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    // Mix between base constant and clamped p based on saturation (c.y) then scale by value (c.z)
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0, 1), c.y);
}

//-------------------------------------------------------------------------------
// Function: RgbToHsv
// Converts a color from RGB space to HSV space.
//-------------------------------------------------------------------------------
vec3 RgbToHsv(const in vec3 c)
{
    // Constants for computing the HSV components
    const vec4 K = vec4(0.0, -1.0, 2.0, -3.0) / 3.0;
    const float e = 1.0e-10;

    // Use mix and step functions to handle branching without actual conditional statements
    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));

    // Compute the difference for the saturation calculation
    float d = q.x - min(q.w, q.y);
    // Return HSV: hue, saturation, and value
    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

//-------------------------------------------------------------------------------
// Function: srgbToLinear
// Converts a color from sRGB space to linear color space.
//-------------------------------------------------------------------------------
vec3 srgbToLinear(vec3 srgb)
{
    // Use a mix between linear scaling and gamma correction based on a threshold
    return mix(
        srgb / 12.92,
        pow(.947867 * srgb + .0521327, vec3(2.4)),
        step(.04045, srgb));
}

//-------------------------------------------------------------------------------
// Function: sumOf
// Helper function that returns the sum of the three components of an ivec3.
//-------------------------------------------------------------------------------
int sumOf(ivec3 vec)
{
    return vec.x + vec.y + vec.z;
}

//-------------------------------------------------------------------------------
// Function: getSharedIndex
// Flattens a 3D index into a single 1D index using the lpvFlatten vector.
//-------------------------------------------------------------------------------
int getSharedIndex(ivec3 pos)
{
    return sumOf(pos * lpvFlatten);
}

//-------------------------------------------------------------------------------
// Function: GetLpvValue
// Retrieves a light propagation volume (LPV) value from an image buffer and adjusts it.
//-------------------------------------------------------------------------------
vec4 GetLpvValue(in ivec3 texCoord)
{
    // Ensure the texture coordinates are within valid bounds of the LPV grid
    if (clamp(texCoord, ivec3(0), ivec3(LpvSize) - 1) != texCoord)
        return vec4(0.0);

    // Choose between two LPV image buffers based on frame counter (double-buffering)
    vec4 lpvSample = (frameCounter % 2) == 0
                         ? imageLoad(imgLpv2, texCoord)
                         : imageLoad(imgLpv1, texCoord);

    // Convert the sampled RGB value to HSV
    vec4 hsv_sky = vec4(RgbToHsv(lpvSample.rgb), lpvSample.a);
    // Adjust the brightness range using an exponential function based on LpvBlockSkyRange
    hsv_sky.zw = exp2(hsv_sky.zw * LpvBlockSkyRange) - 1.0;
    // Convert the adjusted HSV color back to RGB
    lpvSample = vec4(HsvToRgb(hsv_sky.xyz), hsv_sky.w);

    return lpvSample;
}

//-------------------------------------------------------------------------------
// Function: GetVoxelBlock
// Retrieves the voxel block identifier from the voxel mask image.
//-------------------------------------------------------------------------------
uint GetVoxelBlock(const in ivec3 voxelPos)
{
    // Check if the voxel position is within the valid volume bounds
    if (clamp(voxelPos, ivec3(0), ivec3(VoxelSize3 - 1u)) != voxelPos)
        return BLOCK_EMPTY;

    // Return the block ID from the voxel mask image at the given position
    return imageLoad(imgVoxelMask, voxelPos).r;
}

//-------------------------------------------------------------------------------
// Function: PopulateSharedIndex
// Populates the shared memory arrays with LPV values and voxel block data.
//-------------------------------------------------------------------------------
void PopulateSharedIndex(const in ivec3 imgCoordOffset, const in ivec3 workGroupOffset, const in uint i)
{
    // Calculate a 3D position in the shared block using the index and workgroup offset
    ivec3 pos = workGroupOffset + ivec3(i / lpvFlatten) % 10;

    // Load the LPV light value into shared memory
    lpvSharedData[i] = GetLpvValue(imgCoordOffset + pos);
    // Load the voxel block ID into shared memory
    voxelSharedData[i] = GetVoxelBlock(pos);
}

//-------------------------------------------------------------------------------
// Function: sampleShared
// Samples a light value from shared memory and applies a mask to it.
//-------------------------------------------------------------------------------
vec4 sampleShared(ivec3 pos, int mask_index)
{
    // Compute the flattened index for the shared data, offset by 1 for boundary handling
    int shared_index = getSharedIndex(pos + 1);

    float mixWeight = 1.0;
    uint mask = 0xFFFF; // Default mask value
    uint blockId = voxelSharedData[shared_index];

    // If the block is valid (non-zero and not empty), load additional block data for mask extraction
    if (blockId > 0 && blockId != BLOCK_EMPTY)
    {
        uvec2 blockData = imageLoad(imgBlockData, int(blockId)).rg;
        // Extract the mask bits from the block data (using a bit shift and mask)
        mask = (blockData.g >> 24) & 0xFFFF;
    }

    // Return the LPV sample scaled by the mask bit at the given index
    return lpvSharedData[shared_index] * ((mask >> mask_index) & 1u);
}

//-------------------------------------------------------------------------------
// Function: mixNeighbours
// Blends light contributions from the six neighboring voxels.
//-------------------------------------------------------------------------------
vec4 mixNeighbours(const in ivec3 fragCoord, const in uint mask)
{
    // Extract individual mask bits for neighbor sampling for each axis
    uvec3 m1 = (uvec3(mask) >> uvec3(0, 2, 4)) & uvec3(1u);
    uvec3 m2 = (uvec3(mask) >> uvec3(1, 3, 5)) & uvec3(1u);

    // Sample neighboring voxels along the X, Y, and Z axes, applying the appropriate mask bits
    vec4 sX1 = sampleShared(fragCoord + ivec3(-1, 0, 0), 1) * m1.x;
    vec4 sX2 = sampleShared(fragCoord + ivec3(1, 0, 0), 0) * m2.x;
    vec4 sY1 = sampleShared(fragCoord + ivec3(0, -1, 0), 3) * m1.y;
    vec4 sY2 = sampleShared(fragCoord + ivec3(0, 1, 0), 2) * m2.y;
    vec4 sZ1 = sampleShared(fragCoord + ivec3(0, 0, -1), 5) * m1.z;
    vec4 sZ2 = sampleShared(fragCoord + ivec3(0, 0, 1), 4) * m2.z;

    // Compute an average falloff factor to apply to the combined neighbors
    const vec4 avgFalloff = (1.0 / 6.0) * LpvBlockSkyFalloff.xxxy;
    // Return the blended light value from all six neighbors, scaled by the falloff
    return (sX1 + sX2 + sY1 + sY2 + sZ1 + sZ2) * avgFalloff;
}
#endif // End of LPV_ENABLED block

////////////////////////////// MAIN FUNCTION //////////////////////////////

void main()
{
#ifdef LPV_ENABLED
    // Calculate the starting position (chunkPos) of the current workgroup in the LPV grid
    uvec3 chunkPos = gl_WorkGroupID * gl_WorkGroupSize;
    // If the workgroup's position is outside the LPV volume, exit early
    if (any(greaterThanEqual(chunkPos, LpvSize3)))
        return;

    // Pre-populate shared memory for improved sampling performance
    uint i = uint(gl_LocalInvocationIndex) * 2u;
    if (i < 1000u)
    {
        // Calculate an offset based on the change in camera position (current minus previous)
        ivec3 imgCoordOffset = ivec3(floor(cameraPosition) - floor(previousCameraPosition));
        // Determine the workgroup offset in the global grid, subtracting 1 for boundary handling
        ivec3 workGroupOffset = ivec3(gl_WorkGroupID * gl_WorkGroupSize) - 1;

        // Populate two entries in the shared memory per local invocation
        PopulateSharedIndex(imgCoordOffset, workGroupOffset, i);
        PopulateSharedIndex(imgCoordOffset, workGroupOffset, i + 1u);
    }

    // Synchronize all invocations to ensure shared memory is fully populated before continuing
    barrier();

    // Calculate the global image coordinate for the current invocation
    ivec3 imgCoord = ivec3(gl_GlobalInvocationID);
    // If the global coordinate is outside the LPV volume, exit early
    if (any(greaterThanEqual(imgCoord, LpvSize3)))
        return;

    // Initialize variables for light accumulation and decoding
    vec4 lightValue = vec4(0.0);
    vec3 lightColor = vec3(0.0);
    vec3 tintColor = vec3(1.0);
    float lightRange = 0.0;
    uint mixMask = 0xFFFF;

    // Retrieve the block ID for the current voxel from shared memory
    uint blockId = voxelSharedData[getSharedIndex(ivec3(gl_LocalInvocationID) + 1)];

    // If a valid block exists, decode its lighting properties
    if (blockId > 0u)
    {
        // Load block data (e.g., light color and range) from the block data image
        uvec2 blockData = imageLoad(imgBlockData, int(blockId)).rg;
        // Unpack and convert the light color (and range) from sRGB to linear color space
        vec4 lightColorRange = unpackUnorm4x8(blockData.r);
        lightColor = srgbToLinear(lightColorRange.rgb);
        // Optionally, additional voxel color data can be added (this line is commented out)
        // lightColor += srgbToLinear(unpackUnorm4x8(imageLoad(imgVoxelColor, int(blockId)).r).rgb);
        // Derive the light range from the unpacked alpha component (scaled)
        lightRange = lightColorRange.a * 255.0;
        // Unpack the tint color mask and convert it from sRGB to linear space
        vec4 tintColorMask = unpackUnorm4x8(blockData.g);
        tintColor = srgbToLinear(tintColorMask.rgb);
        // Extract the mix mask used for neighbor blending from the block data
        mixMask = (blockData.g >> 24) & 0xFFFF;
    }

    // If the tint color is non-zero, blend the light from neighboring voxels
    if (any(greaterThan(tintColor, vec3(0.0))))
    {
        // Mix light from neighboring voxels using the computed mix mask
        vec4 lightMixed = mixNeighbours(ivec3(gl_LocalInvocationID), mixMask);
        // Scale the mixed light by the tint color
        lightMixed.rgb *= tintColor;
        // Accumulate the neighbor light contribution into the overall light value
        lightValue += lightMixed;
    }

    // If the current voxel emits light (lightRange > 0), add its direct light contribution
    if (lightRange > 0.0)
    {
        // Convert the light color to HSV for brightness adjustment
        vec3 hsv = RgbToHsv(lightColor);
        // Adjust the brightness (value channel) using an exponential function based on light range
        hsv.z = exp2(lightRange) - 1.0;
        // Convert the adjusted HSV color back to RGB and add it to the light value
        lightValue.rgb += HsvToRgb(hsv);
    }

    // Final conversion: adjust the accumulated light value's dynamic range by converting to HSV,
    // applying a logarithmic function, and converting back to RGB
    vec4 hsv_sky = vec4(RgbToHsv(lightValue.rgb), lightValue.a);
    hsv_sky.zw = log2(hsv_sky.zw + 1.0) / LpvBlockSkyRange;
    lightValue = vec4(HsvToRgb(hsv_sky.xyz), hsv_sky.w);

    // Write the final light value into one of two LPV image buffers (double-buffering via frameCounter)
    if (frameCounter % 2 == 0)
        imageStore(imgLpv1, imgCoord, lightValue);
    else
        imageStore(imgLpv2, imgCoord, lightValue);
#endif // End of LPV_ENABLED in main
}
