#ifndef UNITY_SAMPLING_INCLUDED
#define UNITY_SAMPLING_INCLUDED

//-----------------------------------------------------------------------------
// Sample generator
//-----------------------------------------------------------------------------

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Sampling/Fibonacci.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Sampling/Hammersley.hlsl"

//-----------------------------------------------------------------------------
// Coordinate system conversion
//-----------------------------------------------------------------------------

// Transforms the unit vector from the spherical to the Cartesian (right-handed, Z up) coordinate.
real3 SphericalToCartesian(real cosPhi, real sinPhi, real cosTheta)
{
    real sinTheta = SinFromCos(cosTheta);

    return real3(real2(cosPhi, sinPhi) * sinTheta, cosTheta);
}

real3 SphericalToCartesian(real phi, real cosTheta)
{
    real sinPhi, cosPhi;
    sincos(phi, sinPhi, cosPhi);

    return SphericalToCartesian(cosPhi, sinPhi, cosTheta);
}

// Converts Cartesian coordinates given in the right-handed coordinate system
// with Z pointing upwards (OpenGL style) to the coordinates in the left-handed
// coordinate system with Y pointing up and Z facing forward (DirectX style).
real3 TransformGLtoDX(real3 v)
{
    return v.xzy;
}

// Performs conversion from equiareal map coordinates to Cartesian (DirectX cubemap) ones.
real3 ConvertEquiarealToCubemap(real u, real v)
{
    real phi      = TWO_PI - TWO_PI * u;
    real cosTheta = 1.0 - 2.0 * v;

    return TransformGLtoDX(SphericalToCartesian(phi, cosTheta));
}

// Convert a texel position into normalized position [-1..1]x[-1..1]
real2 CubemapTexelToNVC(uint2 unPositionTXS, uint cubemapSize)
{
    return 2.0 * real2(unPositionTXS) / real(max(cubemapSize - 1, 1)) - 1.0;
}

// Map cubemap face to world vector basis
static const real3 CUBEMAP_FACE_BASIS_MAPPING[6][3] =
{
    //XPOS face
    {
        real3(0.0, 0.0, -1.0),
        real3(0.0, -1.0, 0.0),
        real3(1.0, 0.0, 0.0)
    },
    //XNEG face
    {
        real3(0.0, 0.0, 1.0),
        real3(0.0, -1.0, 0.0),
        real3(-1.0, 0.0, 0.0)
    },
    //YPOS face
    {
        real3(1.0, 0.0, 0.0),
        real3(0.0, 0.0, 1.0),
        real3(0.0, 1.0, 0.0)
    },
    //YNEG face
    {
        real3(1.0, 0.0, 0.0),
        real3(0.0, 0.0, -1.0),
        real3(0.0, -1.0, 0.0)
    },
    //ZPOS face
    {
        real3(1.0, 0.0, 0.0),
        real3(0.0, -1.0, 0.0),
        real3(0.0, 0.0, 1.0)
    },
    //ZNEG face
    {
        real3(-1.0, 0.0, 0.0),
        real3(0.0, -1.0, 0.0),
        real3(0.0, 0.0, -1.0)
    }
};

// Convert a normalized cubemap face position into a direction
real3 CubemapTexelToDirection(real2 positionNVC, uint faceId)
{
    real3 dir = CUBEMAP_FACE_BASIS_MAPPING[faceId][0] * positionNVC.x
               + CUBEMAP_FACE_BASIS_MAPPING[faceId][1] * positionNVC.y
               + CUBEMAP_FACE_BASIS_MAPPING[faceId][2];

    return normalize(dir);
}

//-----------------------------------------------------------------------------
// Sampling function
// Reference : http://www.cs.virginia.edu/~jdl/bib/globillum/mis/shirley96.pdf + PBRT
//-----------------------------------------------------------------------------

// Performs uniform sampling of the unit disk.
// Ref: PBRT v3, p. 777.
real2 SampleDiskUniform(real u1, real u2)
{
    real r   = sqrt(u1);
    real phi = TWO_PI * u2;

    real sinPhi, cosPhi;
    sincos(phi, sinPhi, cosPhi);

    return r * real2(cosPhi, sinPhi);
}

// Performs cubic sampling of the unit disk.
real2 SampleDiskCubic(real u1, real u2)
{
    real r   = u1;
    real phi = TWO_PI * u2;

    real sinPhi, cosPhi;
    sincos(phi, sinPhi, cosPhi);

    return r * real2(cosPhi, sinPhi);
}

real3 SampleConeUniform(real u1, real u2, real cos_theta)
{
    float r0 = cos_theta + u1 * (1.0f - cos_theta);
    float r = sqrt(max(0.0, 1.0 - r0 * r0));
    float phi = TWO_PI * u2;
    return float3(r * cos(phi), r * sin(phi), r0);
}

real3 SampleSphereUniform(real u1, real u2)
{
    real phi      = TWO_PI * u2;
    real cosTheta = 1.0 - 2.0 * u1;

    return SphericalToCartesian(phi, cosTheta);
}

// Performs cosine-weighted sampling of the hemisphere.
// Ref: PBRT v3, p. 780.
real3 SampleHemisphereCosine(real u1, real u2)
{
    real3 localL;

    // Since we don't really care about the area distortion,
    // we substitute uniform disk sampling for the concentric one.
    localL.xy = SampleDiskUniform(u1, u2);

    // Project the point from the disk onto the hemisphere.
    localL.z = sqrt(1.0 - u1);

    return localL;
}

// Cosine-weighted sampling without the tangent frame.
// Ref: http://www.amietia.com/lambertnotangent.html
real3 SampleHemisphereCosine(real u1, real u2, real3 normal)
{
    // This function needs to used safenormalize because there is a probability
    // that the generated direction is the exact opposite of the normal and that would lead
    // to a nan vector otheriwse.
    real3 pointOnSphere = SampleSphereUniform(u1, u2);
    return SafeNormalize(normal + pointOnSphere);
}

real3 SampleHemisphereUniform(real u1, real u2)
{
    real phi      = TWO_PI * u2;
    real cosTheta = 1.0 - u1;

    return SphericalToCartesian(phi, cosTheta);
}

void SampleSphere(real2   u,
                  real4x4 localToWorld,
                  real    radius,
              out real    lightPdf,
              out real3   P,
              out real3   Ns)
{
    real u1 = u.x;
    real u2 = u.y;

    Ns = SampleSphereUniform(u1, u2);

    // Transform from unit sphere to world space
    P = radius * Ns + localToWorld[3].xyz;

    // pdf is inverse of area
    lightPdf = 1.0 / (FOUR_PI * radius * radius);
}

void SampleHemisphere(real2   u,
                      real4x4 localToWorld,
                      real    radius,
                  out real    lightPdf,
                  out real3   P,
                  out real3   Ns)
{
    real u1 = u.x;
    real u2 = u.y;

    // Random point at hemisphere surface
    Ns = -SampleHemisphereUniform(u1, u2); // We want the y down hemisphere
    P = radius * Ns;

    // Transform to world space
    P = mul(real4(P, 1.0), localToWorld).xyz;
    Ns = mul(Ns, (real3x3)(localToWorld));

    // pdf is inverse of area
    lightPdf = 1.0 / (TWO_PI * radius * radius);
}

// Note: The cylinder has no end caps (i.e. no disk on the side)
void SampleCylinder(real2   u,
                    real4x4 localToWorld,
                    real    radius,
                    real    width,
                out real    lightPdf,
                out real3   P,
                out real3   Ns)
{
    real u1 = u.x;
    real u2 = u.y;

    // Random point at cylinder surface
    real t = (u1 - 0.5) * width;
    real theta = 2.0 * PI * u2;
    real cosTheta = cos(theta);
    real sinTheta = sin(theta);

    // Cylinder are align on the right axis
    P = real3(t, radius * cosTheta, radius * sinTheta);
    Ns = normalize(real3(0.0, cosTheta, sinTheta));

    // Transform to world space
    P = mul(real4(P, 1.0), localToWorld).xyz;
    Ns = mul(Ns, (real3x3)(localToWorld));

    // pdf is inverse of area
    lightPdf = 1.0 / (TWO_PI * radius * width);
}

void SampleRectangle(real2   u,
                     real4x4 localToWorld,
                     real    width,
                     real    height,
                 out real    lightPdf,
                 out real3   P,
                 out real3   Ns)
{
    // Random point at rectangle surface
    P = real3((u.x - 0.5) * width, (u.y - 0.5) * height, 0);
    Ns = real3(0, 0, -1); // Light down (-Z)

    // Transform to world space
    P = mul(real4(P, 1.0), localToWorld).xyz;
    Ns = mul(Ns, (real3x3)(localToWorld));

    // pdf is inverse of area
    lightPdf = 1.0 / (width * height);
}

void SampleDisk(real2   u,
                real4x4 localToWorld,
                real    radius,
            out real    lightPdf,
            out real3   P,
            out real3   Ns)
{
    // Random point at disk surface
    P  = real3(radius * SampleDiskUniform(u.x, u.y), 0);
    Ns = real3(0.0, 0.0, -1.0); // Light down (-Z)

    // Transform to world space
    P = mul(real4(P, 1.0), localToWorld).xyz;
    Ns = mul(Ns, (real3x3)(localToWorld));

    // pdf is inverse of area
    lightPdf = 1.0 / (PI * radius * radius);
}

// Solid angle cone sampling.
// Takes the cosine of the aperture as an input.
void SampleCone(real2 u, real cosHalfAngle,
                out real3 dir, out real rcpPdf)
{
    real cosTheta = lerp(1, cosHalfAngle, u.x);
    real phi      = TWO_PI * u.y;

    dir    = SphericalToCartesian(phi, cosTheta);
    rcpPdf = TWO_PI * (1 - cosHalfAngle);
}

#endif // UNITY_SAMPLING_INCLUDED
