#ifndef SURFACE_CACHE_PATCH_UTIL
#define SURFACE_CACHE_PATCH_UTIL

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Macros.hlsl"
#include "VectorLogic.hlsl"
#include "Common.hlsl"
#include "RingBuffer.hlsl"

#if defined(PATCH_UTIL_USE_RW_IRRADIANCE_BUFFER)
#define IrradianceBufferType RWStructuredBuffer<SphericalHarmonics::RGBL1>
#else
#define IrradianceBufferType StructuredBuffer<SphericalHarmonics::RGBL1>
#endif

#if defined(PATCH_UTIL_USE_RW_CELL_INDEX_BUFFER)
#define CellPatchIndexBufferType RWStructuredBuffer<uint>
#else
#define CellPatchIndexBufferType StructuredBuffer<uint>
#endif

#if defined(PATCH_UTIL_USE_RW_CELL_ALLOCATION_MARK_BUFFER)
#define CellAllocationMarkBufferType RWStructuredBuffer<uint>
#else
#define CellAllocationMarkBufferType StructuredBuffer<uint>
#endif

namespace PatchUtil
{
    static const uint invalidPatchIndex = UINT_MAX; // Must match C# side.
    static const uint invalidCellIndex = UINT_MAX; // Must match C# side.
    static const uint gridCellAngularResolution = 4; // Must match C# side.
    static const float3 invalidIrradiance = float3(-1, -1, -1);
    static const uint updateMax = 32;

    struct PatchGeometry
    {
        float3 position;
        float3 normal;
    };

    struct PatchStatisticsSet
    {
        float3 mean;
        float3 variance;
    };

    struct PatchCounterSet
    {
        uint data;
    };

    void Reset(inout PatchCounterSet set)
    {
        set.data = 0;
    }

    uint GetUpdateCount(PatchCounterSet set)
    {
        return set.data & 0xFFFF;
    }

    uint GetLastAccessFrame(PatchCounterSet set)
    {
        return set.data >> 16;
    }

    void SetUpdateCount(inout PatchCounterSet set, uint updateCount)
    {
        set.data = updateCount | (set.data & 0xFFFF0000);
    }

    void SetLastAccessFrame(inout PatchCounterSet set, uint lastAccessFrame)
    {
        set.data = (lastAccessFrame << 16) | (set.data & 0xFFFF);
    }

    bool IsEqual(PatchCounterSet a, PatchCounterSet b)
    {
        return a.data == b.data;
    }

    void WriteLastFrameAccess(RWStructuredBuffer<PatchUtil::PatchCounterSet> counterSets, uint patchIdx, uint frameIdx)
    {
        PatchCounterSet counterSet = counterSets[patchIdx];
        SetLastAccessFrame(counterSet, frameIdx);
        counterSets[patchIdx] = counterSet;
    }

    float GetVoxelSize(float voxelMinSize, uint cascadeIdx)
    {
        return voxelMinSize * (1u << cascadeIdx);
    }

    float2 OctWrap(float2 v)
    {
        return (1.0 - abs(v.yx)) * VECTOR_LOGIC_SELECT(v.xy >= 0.0, 1.0, -1.0);
    }

    float2 SphereToSquare(float3 n)
    {
        n /= (abs(n.x) + abs(n.y) + abs(n.z));
        n.xy = VECTOR_LOGIC_SELECT(n.z >= 0.0, n.xy, OctWrap(n.xy));
        n.xy = n.xy * 0.5 + 0.5;
        return n.xy;
    }

    struct VolumePositionResolution
    {
        uint cascadeIdx;
        uint3 positionGridSpace;

        void markInvalid()
        {
            positionGridSpace = UINT_MAX;
        }

        bool isValid()
        {
            return all(positionGridSpace != UINT_MAX);
        }
    };

    uint GetCellIndex(uint cascadeIdx, uint3 positionStorageSpace, uint directionIndex, uint gridSize, uint angularResolution)
    {
        const uint angularResolutionSquared = angularResolution * angularResolution;
        const uint gridSizeSquared = gridSize * gridSize;

        const uint cellsPerCascade = gridSizeSquared * gridSize * angularResolutionSquared;
        const uint withinCascadeIdx = angularResolutionSquared * (positionStorageSpace.x * gridSizeSquared + positionStorageSpace.y * gridSize + positionStorageSpace.z) + directionIndex;
        return cellsPerCascade * cascadeIdx + withinCascadeIdx;
    }

    uint GetDirectionIndex(float3 direction, uint angularResolution)
    {
        // To avoid discontinuities near the cardinal axis directions, we apply an arbitrary rotation.
        // This is based on the assumption that surfaces oriented along the cardinal axis directions
        // are most likely in a scene compared to other directions.
        const float3x3 arbitraryRotation = float3x3(
            float3(0.34034f, -0.30925f, 0.888f),
            float3(-0.30925f, 0.85502f, 0.41629f),
            float3(-0.888f, -0.41629f, 0.19536f));
        const float3 rotatedDirection = mul(arbitraryRotation, direction);

        const uint2 angularSquarePos = min(uint2(3, 3), SphereToSquare(rotatedDirection) * angularResolution);
        return angularSquarePos.y * angularResolution + angularSquarePos.x;
    }

    // Unlike the regular HLSL % operator where both operands must both be signed or unsigned,
    // this function additionally supports the case where the first argument is negative and
    // the second argument is positive.
    uint3 SignedIntegerModulo(int3 x, uint modulus)
    {
        const uint3 remainder = uint3(abs(x)) % modulus;
        return VECTOR_LOGIC_SELECT(VECTOR_LOGIC_AND(x < 0, remainder != 0), modulus - remainder, remainder);
    }

    uint3 ConvertGridSpaceToStorageSpace(uint3 posGridSpace, uint gridSize, int3 cascadeOffset)
    {
        return SignedIntegerModulo(int3(posGridSpace) + cascadeOffset, gridSize);
    }

    uint3 ConvertStorageSpaceToGridSpace(uint3 posStorageSpace, uint gridSize, int3 cascadeOffset)
    {
        return SignedIntegerModulo(int3(posStorageSpace) - cascadeOffset, gridSize);
    }

    bool IsInsideCascade(float3 gridTargetPos, float3 queryPos, float cascadeVoxelSize, uint gridSize)
    {
        const float3 dif = gridTargetPos - queryPos;
        const float difSquaredLength = dot(dif, dif);
        // We subtract 0.5 here to account for the fact that the Grid Target Pos can move up to
        // 0.499... voxel sizes away from the cascade center in any dimension without causing the
        // cascade to move.
        const float threshold = cascadeVoxelSize * (float(gridSize) * 0.5f - 0.5f);
        const float squaredThreshold = threshold * threshold;
        return difSquaredLength < squaredThreshold;
    }

    VolumePositionResolution ResolveVolumePosition(float3 queryPos, float3 gridTargetPos, uint gridSize, StructuredBuffer<int3> cascadeOffsets, uint cascadeCount, float voxelMinSize, uint startCascadeIdx = 0)
    {
        VolumePositionResolution resolution = (VolumePositionResolution)0; // Zero initialization is strictly not required but this silences a shader compiler warning.

        resolution.markInvalid();
        const float halfGridSize = float(gridSize) * 0.5f;
        for (uint cascadeIdx = startCascadeIdx; cascadeIdx < cascadeCount; ++cascadeIdx)
        {
            const float cascadeVoxelSize = GetVoxelSize(voxelMinSize, cascadeIdx);
            if (IsInsideCascade(gridTargetPos, queryPos, cascadeVoxelSize, gridSize))
            {
                const int3 cascadeOffset = cascadeOffsets[cascadeIdx];
                const float3 centerRelativePositionSpatialGridSpace = queryPos / cascadeVoxelSize - cascadeOffset;
                resolution.positionGridSpace = centerRelativePositionSpatialGridSpace + halfGridSize;
                resolution.cascadeIdx = cascadeIdx;
                break;
            }
        }

        return resolution;
    }

    int ResolveCascadeIndex(float3 gridTargetPos, float3 queryPos, uint gridSize, uint cascadeCount, float voxelMinSize)
    {
        int result = -1;
        for (uint cascadeIdx = 0; cascadeIdx < cascadeCount; ++cascadeIdx)
        {
            const float cascadeVoxelSize = GetVoxelSize(voxelMinSize, cascadeIdx);
            if (IsInsideCascade(gridTargetPos, queryPos, cascadeVoxelSize, gridSize))
            {
                result = cascadeIdx;
                break;
            }
        }
        return result;
    }

    static const uint patchIndexResolutionCodeLookup = 0;
    static const uint patchIndexResolutionCodeAllocationSuccess = 1;
    static const uint patchIndexResolutionCodeAllocationFailure = 2;

    struct PatchIndexResolutionResult
    {
        uint code;
        uint patchIdx;
    };

    PatchIndexResolutionResult ResolvePatchIndex(RWStructuredBuffer<uint> ringConfigBuffer, uint ringConfigOffset, RWStructuredBuffer<uint> cellPatchIndices, RWStructuredBuffer<uint> cellAllocationMarks, uint cellIdx)
    {
        PatchIndexResolutionResult result;
        result.patchIdx = invalidPatchIndex;

        uint existingPatchIndex = cellPatchIndices[cellIdx];
        if (existingPatchIndex != invalidPatchIndex)
        {
            result.patchIdx = existingPatchIndex;
            result.code = patchIndexResolutionCodeLookup;
        }
        else
        {
            result.code = patchIndexResolutionCodeAllocationFailure;

            uint existingAllocationMark;
            InterlockedExchange(cellAllocationMarks[cellIdx], 1, existingAllocationMark);
            if (existingAllocationMark == 0)
            {
                uint countBeforeAllocation;
                InterlockedAdd(ringConfigBuffer[ringConfigOffset + RingBuffer::countConfigIndex], 1, countBeforeAllocation);
                if (countBeforeAllocation < patchCapacity)
                {
                    uint newPatchIdx;
                    InterlockedAdd(ringConfigBuffer[ringConfigOffset + RingBuffer::endConfigIndex], 1, newPatchIdx);
                    newPatchIdx %= patchCapacity; // Here we exploit the requirement that UINT_MAX is a multiple of patchCapacity.

                    result.code = patchIndexResolutionCodeAllocationSuccess;
                    result.patchIdx = newPatchIdx;
                    cellPatchIndices[cellIdx] = newPatchIdx;
                }
                else
                {
                    // Allocation failed, no room. Backing out.
                    ringConfigBuffer[ringConfigOffset + RingBuffer::countConfigIndex] = patchCapacity;
                    cellAllocationMarks[cellIdx] = 0;
                }
            }
        }

        return result;
    }

    bool ReadHemisphericalIrradiance(IrradianceBufferType patchIrradiances, CellPatchIndexBufferType cellPatchIndices, uint gridSize, uint cascadeIdx, uint3 gridSpacePosition, float3 worldNormal, out SphericalHarmonics::RGBL1 resultIrradiance)
    {
        const uint directionIdx = GetDirectionIndex(worldNormal, gridCellAngularResolution);
        const uint cellIdx = GetCellIndex(cascadeIdx, gridSpacePosition, directionIdx, gridSize, gridCellAngularResolution);

        bool resultBool = false;
        const uint patchIdx = cellPatchIndices[cellIdx];
        resultIrradiance = (SphericalHarmonics::RGBL1)0; // Setting value only to silence shader compilation warning.
        if (patchIdx != invalidPatchIndex)
        {
            resultIrradiance = patchIrradiances[patchIdx];
            resultBool = true;
        }
        return resultBool;
    }

    uint FindPatchIndex(float3 gridTargetPos, StructuredBuffer<uint> cellPatchIndices, uint gridSize, StructuredBuffer<int3> cascadeOffsets, uint cascadeCount, float voxelMinSize, float3 worldPosition, float3 worldNormal)
    {
        VolumePositionResolution posResolution = ResolveVolumePosition(worldPosition, gridTargetPos, gridSize, cascadeOffsets, cascadeCount, voxelMinSize);
        if (posResolution.isValid())
        {
            const uint directionIdx = GetDirectionIndex(worldNormal, gridCellAngularResolution);
            const uint3 positionStorageSpace = ConvertGridSpaceToStorageSpace(posResolution.positionGridSpace, gridSize, cascadeOffsets[posResolution.cascadeIdx]);
            const uint cellIdx = GetCellIndex(posResolution.cascadeIdx, positionStorageSpace, directionIdx, gridSize, gridCellAngularResolution);
            const uint patchIdx = cellPatchIndices[cellIdx];
            if (patchIdx != invalidPatchIndex)
            {
                return patchIdx;
            }
            else
            {
                return invalidPatchIndex;
            }
        }
        else
        {
            return invalidPatchIndex;
        }
    }

    uint FindPatchIndexAndUpdateLastAccess(float3 gridTargetPos, StructuredBuffer<uint> cellPatchIndices, uint gridSize, StructuredBuffer<int3> cascadeOffsets, RWStructuredBuffer<PatchUtil::PatchCounterSet> patchCounterSets, uint cascadeCount, float voxelMinSize, float3 worldPosition, float3 worldNormal, uint frameIdx)
    {
        const uint patchIdx = FindPatchIndex(gridTargetPos, cellPatchIndices, gridSize, cascadeOffsets, cascadeCount, voxelMinSize,worldPosition, worldNormal);
        if (patchIdx != invalidPatchIndex)
        {
            WriteLastFrameAccess(patchCounterSets, patchIdx, frameIdx);
        }
        return patchIdx;
    }

    bool ReadHemisphericalIrradiance(IrradianceBufferType patchIrradiances, CellPatchIndexBufferType cellPatchIndices, uint gridSize, StructuredBuffer<int3> cascadeOffsets, float3 cascadeFocusPos, uint cascadeCount, float voxelMinSize, float3 worldPosition, float3 worldNormal, uint startCascadeIdx, out SphericalHarmonics::RGBL1 resultIrradiance)
    {
        VolumePositionResolution posResolution = ResolveVolumePosition(worldPosition, cascadeFocusPos, gridSize, cascadeOffsets, cascadeCount, voxelMinSize, startCascadeIdx);
        bool resultBool = false;

        resultIrradiance = (SphericalHarmonics::RGBL1)0; // Theoretically not required but added to silence a shader compilation warning.

        if (posResolution.isValid())
        {
            const uint3 positionStorageSpace = ConvertGridSpaceToStorageSpace(posResolution.positionGridSpace, gridSize, cascadeOffsets[posResolution.cascadeIdx]);
            resultBool = ReadHemisphericalIrradiance(patchIrradiances, cellPatchIndices, gridSize, posResolution.cascadeIdx, positionStorageSpace, worldNormal, resultIrradiance);
        }

        return resultBool;
    }

    bool ReadHemisphericalIrradiance(IrradianceBufferType patchIrradiances, CellPatchIndexBufferType cellPatchIndices, uint gridSize, StructuredBuffer<int3> cascadeOffsets, float3 cascadeFocusPos, uint cascadeCount, float voxelMinSize, float3 worldPosition, float3 worldNormal, out SphericalHarmonics::RGBL1 resultIrradiance)
    {
        const uint conservativeStartCascadeIdx = 0;
        return ReadHemisphericalIrradiance(
            patchIrradiances,
            cellPatchIndices,
            gridSize,
            cascadeOffsets,
            cascadeFocusPos,
            cascadeCount,
            voxelMinSize,
            worldPosition,
            worldNormal,
            conservativeStartCascadeIdx,
            resultIrradiance);
    }

    float3 ReadPlanarIrradiance(IrradianceBufferType patchIrradiances, CellPatchIndexBufferType cellPatchIndices, uint gridSize, uint cascadeIdx, uint3 gridSpacePosition, float3 worldNormal)
    {
        SphericalHarmonics::RGBL1 resultIrradiance;
        bool resultBool = ReadHemisphericalIrradiance(patchIrradiances, cellPatchIndices, gridSize, cascadeIdx, gridSpacePosition, worldNormal, resultIrradiance);
        if (resultBool)
            return max(0, SphericalHarmonics::Eval(resultIrradiance, worldNormal));
        else
            return invalidIrradiance;
    }

    float3 ReadPlanarIrradiance(float3 gridTargetPos, IrradianceBufferType patchIrradiances, CellPatchIndexBufferType cellPatchIndices, uint gridSize, StructuredBuffer<int3> cascadeOffsets, uint cascadeCount, float voxelMinSize, float3 worldPosition, float3 worldNormal)
    {
        VolumePositionResolution posResolution = ResolveVolumePosition(worldPosition, gridTargetPos, gridSize, cascadeOffsets, cascadeCount, voxelMinSize);
        if (posResolution.isValid())
        {
            const uint3 positionStorageSpace = ConvertGridSpaceToStorageSpace(posResolution.positionGridSpace, gridSize, cascadeOffsets[posResolution.cascadeIdx]);
            return ReadPlanarIrradiance(patchIrradiances, cellPatchIndices, gridSize, posResolution.cascadeIdx, positionStorageSpace, worldNormal);
        }
        else
        {
            return invalidIrradiance;
        }
    }

    void MarkInvalid(inout SphericalHarmonics::RGBL1 irradiance)
    {
        irradiance.l0 = -1.0f;
    }

    bool IsValid(inout SphericalHarmonics::RGBL1 irradiance)
    {
        return all(irradiance.l0 != -1.0f);
    }
}

#endif
