/********************************************************
    © 2020 Continuum Graphics LLC. All Rights Reserved
 ********************************************************/

#if !defined _SHADOWS_
#define _SHADOWS_

//Optimisation Idea: Calculate shadow matrix all in the vertex and pass it as a varying

vec2 CalculateShadowSoftness(inout vec2 blockerDepth, vec3 shadowPosition, float dither, float depthBias, float offsetBias, const int samples, const float sampleSize, const float texelSize, const float maxSpread, const float penumbraAngle) {
	blockerDepth = vec2(0.0);

	for(int i = 0; i < samples; ++i) {
		vec3 offset    = circlemapL((dither + float(i)) * sampleSize, 4096.0 * float(samples));
			offset.z *= maxSpread * texelSize;

		vec3 coord    = vec3(offset.xy, -offsetBias) * offset.z + shadowPosition;
             coord.xy = DistortShadowSpaceProj(coord.xy);
			 coord.z -= depthBias;

		blockerDepth = max(blockerDepth, vec2(coord.z) - vec2(textureLod(shadowtex1, coord.xy, 0).x, textureLod(shadowtex0, coord.xy, 0).x));
	}

	return min(blockerDepth * vec2(penumbraAngle), vec2(maxSpread));
}

vec3 CalculateHardShadow(vec3 shadowPosition, vec3 shadowSpaceFlatNormal, float depthBias, float shadowSurfaceDepth, bool isVeg, bool isLava) {
    shadowPosition.xy = DistortShadowSpaceProj(shadowPosition.xy);

	bool correctBiasMask = !(isVeg || isLava);
		
    vec4 shadowColor1 = texture2DLod(shadowcolor1, shadowPosition.xy, 0);
    vec3 shadowNormal = shadowColor1.rgb * 2.0 - 1.0;

    float surface0 = texture2DLod(shadowtex0, shadowPosition.xy, 0).x;
    float surface1 = texture2DLod(shadowtex1, shadowPosition.xy, 0).x;

    float correctedBias = (surface0 == surface1 && correctBiasMask) ? (dot(shadowNormal, shadowSpaceFlatNormal) > 0.1 ? depthBias : 0.0) : depthBias;

    float shadowSolid = fstep(shadowPosition.z - correctedBias, surface1);
    float shadowGlass = fstep(shadowPosition.z - correctedBias, surface0);

	float shadowWaterMask = shadowColor1.a * 2.0 - 1.0;

	float waterDepth = (surface0 * 8.0 - 4.0);
          waterDepth = waterDepth * shadowProjectionInverse[2].z + shadowProjectionInverse[3].z;
          waterDepth = (waterDepth - shadowSurfaceDepth);

          waterDepth = mix(0.0, waterDepth, shadowWaterMask * fstep(0.0, waterDepth));

	vec3 waterTransmit = exp2(-waterTransmitCoefficient * waterDepth * rLOG2);

    vec4 shadowColor 	 = textureLod(shadowcolor, shadowPosition.xy, 0);
         shadowColor.rgb = pow(shadowColor.rgb, vec3(2.2));

	#ifdef WATER_CAUSTICS
			float causticMask = clamp01(waterDepth - 0.25);

			float caustics = shadowColor.r * 100.0 * (shadowWaterMask * shadowWaterMask);
			shadowColor.rgb = mix(shadowColor.rgb, vec3(caustics * causticMask + (1.0 - causticMask)), shadowWaterMask);
	#endif

    return mix(vec3(shadowGlass), shadowColor.rgb, clamp01(shadowSolid - shadowGlass)) * waterTransmit;
}

vec3 CalculateSoftShadow(vec3 shadowPosition, vec3 shadowSpaceFlatNormal, vec3 normal, float dither, vec2 spread, float offsetBias, float depthBias, float shadowSurfaceDepth, bool isVeg, bool isLava, const int samples, const float sampleSize, const float texelSize, const float maxSpread) {
	spread *= 0.0009765625; //Pixel Size non-distort to a factor of 4.0

	vec3 shadow = vec3(0.0);

	bool correctBiasMask = !(isVeg || isLava);

	for(int i = 0; i < samples; ++i) {
		vec3 solidOffset = circlemapL((dither + float(i)) * sampleSize, 4096.0 * float(samples));
		vec3 transparentOffset = solidOffset;

		solidOffset.z *= spread.x;
		transparentOffset.z *= spread.y;

		vec3 solidCoord    = vec3(solidOffset.xy, -offsetBias) * solidOffset.z + shadowPosition;
			 solidCoord.xy = DistortShadowSpaceProj(solidCoord.xy);

		vec3 transparentCoord    = vec3(transparentOffset.xy, -offsetBias) * transparentOffset.z + shadowPosition;
			 transparentCoord.xy = DistortShadowSpaceProj(transparentCoord.xy);

		float surface0 = texture2DLod(shadowtex0, transparentCoord.xy, 0).x;
		float surface1 = texture2DLod(shadowtex1, solidCoord.xy, 0).x;

		vec4 shadowColor1 = texture2DLod(shadowcolor1, solidCoord.xy, 0);
		vec3 shadowNormal = shadowColor1.rgb * 2.0 - 1.0;

		float correctedBias = (surface0 == surface1 && correctBiasMask) ? (dot(shadowNormal, shadowSpaceFlatNormal) > 0.1 ? depthBias : 0.0) : depthBias;

		float shadowSolid = fstep(solidCoord.z - correctedBias, surface1);
		float shadowGlass = fstep(transparentCoord.z - correctedBias, surface0);

		float shadowWaterMask = shadowColor1.a * 2.0 - 1.0;

		vec4 shadowColor     = textureLod(shadowcolor, transparentCoord.xy, 0);
			 shadowColor.rgb = pow(shadowColor.rgb, vec3(2.2));

		float waterDepth = (surface0 * 8.0 - 4.0);
        	  waterDepth = waterDepth * shadowProjectionInverse[2].z + shadowProjectionInverse[3].z;
        	  waterDepth = (waterDepth - shadowSurfaceDepth);

        	  waterDepth = mix(0.0, waterDepth, shadowWaterMask * fstep(0.0, waterDepth));

		#ifdef WATER_CAUSTICS
			float causticMask = clamp01(waterDepth - 0.25);

			float caustics = shadowColor.r * 100.0 * (shadowWaterMask * shadowWaterMask);
			shadowColor.rgb = mix(shadowColor.rgb, vec3(caustics * causticMask + (1.0 - causticMask)), shadowWaterMask);
		#endif
    
    	vec3 waterTransmit = exp2(-waterTransmitCoefficient * waterDepth * rLOG2);

		shadow += mix(vec3(shadowGlass), shadowColor.rgb, clamp01(shadowSolid - shadowGlass)) * waterTransmit;
        //shadow += (shadowColor.rgb * shadowColor.a - shadowColor.a) * (-shadowSolid * shadowGlass + shadowSolid) + shadowSolid;
	}

	return max0(shadow * sampleSize);
}

vec3 CalculateEarlyOut(vec3 shadowPosition, float depthBias, float blockerDepth) {
	return vec3(fstep(shadowPosition.z - depthBias, textureLod(shadowtex1, shadowPosition.xy, 0).x));
}

vec3 CalculateShadows(vec3 shadowPosition, mat2x3 positions, vec3 surfaceNormal, vec3 flatWorldNormal, vec3 lightVector, float dither, bool isVeg, bool isLava) {
	float NdotL = isVeg ? 0.5 : dot(surfaceNormal, lightVector);
	if (NdotL <= 0.0) return vec3(0.0);
	vec3 distortedShadowPosition = vec3(DistortShadowSpaceProj(shadowPosition.xy), shadowPosition.z);
	if (any(greaterThanEqual(abs(distortedShadowPosition * 2.0 - 1.0), vec3(1.0)))) return vec3(1.0);

	// Constants. The user doesn't ever need to modify these through code, so these are passed in to the functions as constants.
	const int samples = SHADOW_QUALITY;
	const float sampleSize = 1.0 / samples;

	const float maxSpread = SHADOW_PENUMBRA_ANGLE;
	#if SHADOW_TYPE == SHADOW_PCSS
		const float penumbraAngle = SHADOW_PENUMBRA_ANGLE * 4.0;
	#else
		const float penumbraAngle = SHADOW_PENUMBRA_ANGLE * 0.015625;
	#endif

	const float texelSize = 1.0 / 8192.0;
	
	float distFactor = 1.0 + length(shadowPosition.xy * 2.0 - 1.0) * (length((shadowPosition.xy * 2.0 - 1.0) * 1.165) * SHADOW_DISTORTION_FACTOR + (1.0 - SHADOW_DISTORTION_FACTOR));
		
    NdotL = clamp(NdotL, 0.0, 1.0);
	float offsetBias = sqrt(1.0 - NdotL * NdotL) / NdotL;
		  offsetBias = min(offsetBias, 7.5) * texelSize * distFactor;

	vec3 shadowSpaceFlatNormal = mat3(shadowModelView) * flatWorldNormal;

	float shadowSurfaceDepth = transMAD(shadowModelViewCustom, positions[1]).z;

    #if SHADOW_TYPE == SHADOW_HARD
        return CalculateHardShadow(shadowPosition, shadowSpaceFlatNormal, offsetBias, shadowSurfaceDepth, isVeg, isLava);
    #endif

	// Shadow spread. This controls how soft shadows appear. Again, the user can override the built-in shadow spread function if they wish.
	#if SHADOW_TYPE == SHADOW_PCSS
		vec2 blockerDepth = vec2(0.0);
		vec2 penumbraSpread = CalculateShadowSoftness(blockerDepth, shadowPosition, dither, offsetBias, offsetBias, samples, sampleSize, texelSize, maxSpread, penumbraAngle);
	#else
		vec2 blockerDepth = vec2(1.0);
		vec2 penumbraSpread = vec2(penumbraAngle);
	#endif

    // Early out. Maybe have this on an option, so if the user doesn't want the artifacts that come from this, they can turn this off if they want. Again, the user has control over the value returned if they wish to.
    if(blockerDepth.y <= 0.0) return CalculateEarlyOut(distortedShadowPosition, offsetBias, blockerDepth.y);

    // And finally, the actual filter itself. Yet again, the user can override this if they wish.
    return CalculateSoftShadow(shadowPosition, shadowSpaceFlatNormal, surfaceNormal, dither, penumbraSpread, offsetBias, offsetBias, shadowSurfaceDepth, isVeg, isLava, samples, sampleSize, texelSize, maxSpread);
}

#endif
