/**
\author Michael Mara and Morgan McGuire, Casual Effects. 2015.
*/

#ifndef __SCREEN_SPACE_RAYTRACE__
#define __SCREEN_SPACE_RAYTRACE__

sampler2D_float _CameraDepthTexture;

float distanceSquared(float2 A, float2 B)
{
    A -= B;
    return dot(A, A);
}

float distanceSquared(float3 A, float3 B)
{
    A -= B;
    return dot(A, A);
}

void swap(inout float v0, inout float v1)
{
    float temp = v0;
    v0 = v1;
    v1 = temp;
}

bool isIntersecting(float rayZMin, float rayZMax, float sceneZ, float layerThickness)
{
    return (rayZMax >= sceneZ - layerThickness) && (rayZMin <= sceneZ);
}

void rayIterations(in bool traceBehindObjects, inout float2 P, inout float stepDirection, inout float end, inout int stepCount, inout int maxSteps, inout bool intersecting,
        inout float sceneZ, inout float2 dP, inout float3 Q, inout float3 dQ, inout float k, inout float dk,
        inout float rayZMin, inout float rayZMax, inout float prevZMaxEstimate, inout bool permute, inout float2 hitPixel,
        inout float2 invSize, inout float layerThickness)
{
    bool stop = intersecting;

    UNITY_LOOP
    for (; (P.x * stepDirection) <= end && stepCount < maxSteps && !stop; P += dP, Q.z += dQ.z, k += dk, stepCount += 1)
    {
        // The depth range that the ray covers within this loop iteration.
        // Assume that the ray is moving in increasing z and swap if backwards.
        rayZMin = prevZMaxEstimate;
        //rayZMin = (dQ.z * -0.5 + Q.z) / (dk * -0.5 + k);
        // Compute the value at 1/2 pixel into the future
        rayZMax = (dQ.z * 0.5 + Q.z) / (dk * 0.5 + k);
        prevZMaxEstimate = rayZMax;

        if (rayZMin > rayZMax)
        {
            swap(rayZMin, rayZMax);
        }

        // Undo the homogeneous operation to obtain the camera-space
        // Q at each point
        hitPixel = permute ? P.yx : P;

        sceneZ = tex2Dlod(_CameraDepthTexture, float4(hitPixel * invSize,0,0)).r;
        sceneZ = -LinearEyeDepth(sceneZ);

        bool isBehind = (rayZMin <= sceneZ);
        intersecting = isBehind && (rayZMax >= sceneZ - layerThickness);
        stop = traceBehindObjects ? intersecting : isBehind;

    } // pixel on ray

    P -= dP, Q.z -= dQ.z, k -= dk;
}

/**
  \param csOrigin must have z < -0.01, and project within the valid screen rectangle
  \param stepRate Set to 1.0 by default, higher to step faster
 */
bool castDenseScreenSpaceRay
   (float3          csOrigin,
    float3          csDirection,
    float4x4        projectToPixelMatrix,
    float2          csZBufferSize,
    float3          clipInfo,
    float           jitterFraction,
    int             maxSteps,
    float           layerThickness,
    float           maxRayTraceDistance,
    out float2      hitPixel,
    int             stepRate,
    bool            traceBehindObjects,
    out float3      csHitPoint,
    out float       stepCount) {

    float2 invSize = float2(1.0 / csZBufferSize.x, 1.0 / csZBufferSize.y);

    // Initialize to off screen
    hitPixel = float2(-1, -1);

    float nearPlaneZ = -0.01;
    // Clip ray to a near plane in 3D (doesn't have to be *the* near plane, although that would be a good idea)
    float rayLength = ((csOrigin.z + csDirection.z * maxRayTraceDistance) > nearPlaneZ) ?
                        ((nearPlaneZ - csOrigin.z) / csDirection.z) :
                        maxRayTraceDistance;

    float3 csEndPoint = csDirection * rayLength + csOrigin;

    // Project into screen space
    // This matrix has a lot of zeroes in it. We could expand
    // out these multiplies to avoid multiplying by zero
    // ...but 16 MADDs are not a big deal compared to what's ahead
    float4 H0 = mul(projectToPixelMatrix, float4(csOrigin, 1.0));
    float4 H1 = mul(projectToPixelMatrix, float4(csEndPoint, 1.0));

    // There are a lot of divisions by w that can be turned into multiplications
    // at some minor precision loss...and we need to interpolate these 1/w values
    // anyway.
    //
    // Because the caller was required to clip to the near plane,
    // this homogeneous division (projecting from 4D to 2D) is guaranteed
    // to succeed.
    float k0 = 1.0 / H0.w;
    float k1 = 1.0 / H1.w;

    // Screen-space endpoints
    float2 P0 = H0.xy * k0;
    float2 P1 = H1.xy * k1;

    // Switch the original points to values that interpolate linearly in 2D:
    float3 Q0 = csOrigin * k0;
    float3 Q1 = csEndPoint * k1;

#if 1 // Clipping to the screen coordinates. We could simply modify maxSteps instead
    float yMax = csZBufferSize.y - 0.5;
    float yMin = 0.5;
    float xMax = csZBufferSize.x - 0.5;
    float xMin = 0.5;

    // 2D interpolation parameter
    float alpha = 0.0;
    // P0 must be in bounds
    if (P1.y > yMax || P1.y < yMin) {
        float yClip = (P1.y > yMax) ? yMax : yMin;
        float yAlpha = (P1.y - yClip) / (P1.y - P0.y); // Denominator is not zero, since P0 != P1 (or P0 would have been clipped!)
        alpha = yAlpha;
    }

    // P0 must be in bounds
    if (P1.x > xMax || P1.x < xMin) {
        float xClip = (P1.x > xMax) ? xMax : xMin;
        float xAlpha = (P1.x - xClip) / (P1.x - P0.x); // Denominator is not zero, since P0 != P1 (or P0 would have been clipped!)
        alpha = max(alpha, xAlpha);
    }

    // These are all in homogeneous space, so they interpolate linearly
    P1 = lerp(P1, P0, alpha);
    k1 = lerp(k1, k0, alpha);
    Q1 = lerp(Q1, Q0, alpha);
#endif

    // We're doing this to avoid divide by zero (rays exactly parallel to an eye ray)
    P1 = (distanceSquared(P0, P1) < 0.0001) ? P0 + float2(0.01, 0.01) : P1;

    float2 delta = P1 - P0;

    // Assume horizontal
    bool permute = false;
    if (abs(delta.x) < abs(delta.y)) {
        // More-vertical line. Create a permutation that swaps x and y in the output
        permute = true;

        // Directly swizzle the inputs
        delta = delta.yx;
        P1 = P1.yx;
        P0 = P0.yx;
    }

    // From now on, "x" is the primary iteration direction and "y" is the secondary one

    float stepDirection = sign(delta.x);
    float invdx = stepDirection / delta.x;
    float2 dP = float2(stepDirection, invdx * delta.y);

    // Track the derivatives of Q and k
    float3 dQ = (Q1 - Q0) * invdx;
    float   dk = (k1 - k0) * invdx;

    dP *= stepRate;
    dQ *= stepRate;
    dk *= stepRate;

    P0 += dP * jitterFraction;
    Q0 += dQ * jitterFraction;
    k0 += dk * jitterFraction;

    // Slide P from P0 to P1, (now-homogeneous) Q from Q0 to Q1, and k from k0 to k1
    float3 Q = Q0;
    float  k = k0;

    // We track the ray depth at +/- 1/2 pixel to treat pixels as clip-space solid
    // voxels. Because the depth at -1/2 for a given pixel will be the same as at
    // +1/2 for the previous iteration, we actually only have to compute one value
    // per iteration.
    float prevZMaxEstimate = csOrigin.z;
    stepCount = 0.0;
    float rayZMax = prevZMaxEstimate, rayZMin = prevZMaxEstimate;
    float sceneZ = 100000;

    // P1.x is never modified after this point, so pre-scale it by
    // the step direction for a signed comparison
    float end = P1.x * stepDirection;

    bool intersecting = isIntersecting(rayZMin, rayZMax, sceneZ, layerThickness);
    // We only advance the z field of Q in the inner loop, since
    // Q.xy is never used until after the loop terminates

    //int rayIterations = min(maxSteps, stepsToGetOffscreen);


    float2 P = P0;

    int originalStepCount = 0;
    rayIterations(traceBehindObjects, P, stepDirection, end,  originalStepCount,  maxSteps, intersecting,
         sceneZ, dP, Q, dQ,  k,  dk,
         rayZMin,  rayZMax,  prevZMaxEstimate, permute, hitPixel,
         invSize,  layerThickness);


    stepCount = originalStepCount;

    // Loop only advanced the Z component. Now that we know where we are going
    // update xy
    Q.xy += dQ.xy * stepCount;
    // Q is a vector, so we are trying to get by with 1 division instead of 3.
    csHitPoint = Q * (1.0 / k);

    return intersecting;
}

#endif // __SCREEN_SPACE_RAYTRACE__
