
#include <shaders/materials/commons_sampling.glsl>
#include <shaders/commons_hlsl.glsl>

float sampleShadowPCF(in sampler2DShadow sampler, in vec4 coords, out float in_frustum, int samples, float sampling_range)
{
	float fact = 0.0;
	float x, y;
	
	in_frustum = 0.0;

	// no shading if fragment behind
	if (coords.w <= 0.0)
		return 0.0;

	// no shading if fragment outside of light frustum
	if (coords.x < -coords.w || coords.x > coords.w || coords.y < -coords.w || coords.y > coords.w)
		return 1.0;

	in_frustum = 1.0;

	// NOTE: we need to shift after projection. this works here but we dont do it in directional. most likely because projection matrix is simple so it works...
	//       but this shift in directional might actualy introduce tiny inaccuracies
	// NOTE: can we factor that into projection matrix? most likely we can!
	coords.xy = coords.xy * vec2(0.5) + vec2(0.5) * coords.w;
	vec3 samp = coords.xyz / coords.w;

#if 1
	for(x=-1.0; x<1.0; x+=2.0/samples)
	{
		for(y=-1.0; y<1.0; y+=2.0/samples)
		{
			float shadow = textureProj(sampler, coords + vec4(x * sampling_range * coords.w, y * sampling_range * coords.w, 0.0, 0.0));
			fact += shadow;
		}
	}

	return fact * (1.0 / (samples * samples));
#else

	int h_samples = samples * samples / 2;
	for(int n = 0; n < h_samples; n++)
	{
		vec2 xy = (HammersleySample2D(n, h_samples) - 0.5) * 2.0;
		float shadow = textureProj(sampler, coords + vec4(xy.x * sampling_range * coords.w, xy.y * sampling_range * coords.w, 0.0, 0.0));
		fact += shadow;
	}
	return fact * (1.0 / h_samples);
#endif
}

// Occluder search

float AvgBlockersDepthToPenumbra(float z_shadowMapView, float avgBlockersDepth)
{
  float penumbra = (z_shadowMapView - avgBlockersDepth) / avgBlockersDepth;
  //penumbra *= penumbra;
  penumbra = abs(penumbra);
  return clamp(1000.0 * penumbra, 0.0, 1.0);
  //return clamp(1000000.0 * penumbra, 0.0, 1.0);
}

float PenumbraFromOccluderSearch(in sampler2D sampler, float gradientNoise, vec4 coords, float sampling_range, int samplesCount, float shadowmap_bias)
{
  float avgBlockersDepth = 0.0f;
  float blockersCount = 0.0f;
  const float penumbraFilterMaxSize = sampling_range;

  if (coords.x < -coords.w || coords.x > coords.w || coords.y < -coords.w || coords.y > coords.w)
	return 0.0;

  vec2 shadowMapUV = coords.xy / coords.w * 0.5 + 0.5;
  float z_shadowMapView = coords.z / coords.w;

  for(int i = 0; i < samplesCount; i++)
  {
    vec2 sampleUV = VogelDiskSample(i, samplesCount, gradientNoise) * 1.25; // to match sampling in PCF
    sampleUV = shadowMapUV + penumbraFilterMaxSize * sampleUV;

    float sampleDepth = texture(sampler, sampleUV).r;

    if(sampleDepth < z_shadowMapView)
    {
      avgBlockersDepth += sampleDepth;
      blockersCount += 1.0f;
    }
  }
  
  if(blockersCount > 0.0f)
  {
    avgBlockersDepth /= blockersCount;
    return AvgBlockersDepthToPenumbra(z_shadowMapView, avgBlockersDepth);
  }
  else
  {
    return 1.0f;
  }
}

float sampleShadowPCFNoiseOffset(in sampler2DShadow sampler, in vec4 coords, ivec2 noise_offset, out float in_frustum, int samples, float sampling_range, float shadowmap_bias)
{
	float fact = 0.0;
	float x, y, r;
	
	in_frustum = 0.0;

	// no shading if fragment behind
	if (coords.w <= 0.0)
		return 1.0;

	// no shading if fragment outside of light frustum
	if (coords.x < -coords.w || coords.x > coords.w || coords.y < -coords.w || coords.y > coords.w || coords.z < 0.0)
		return 1.0;

	in_frustum = 1.0;

	// NOTE: we need to shift after projection. this works here but we dont do it in directional. most likely because projection matrix is simple so it works...
	//       but this shift in directional might actualy introduce tiny inaccuracies
	// NOTE: can we factor that into projection matrix? most likely we can!
	coords.xy = coords.xy * vec2(0.5) + vec2(0.5) * coords.w;
	vec3 samp = coords.xyz / coords.w;

#if 1
	uint h_samples = 0;
	vec3 hash = (texelFetch(s_BlueNoise, ivec3(noise_offset.xy & ivec2(127), 0), 0).rgb - 0.5) * 0.25;
	float bias = shadowmap_bias; //-0.0001;
	float bias2 = 0.3;

	// coarse
	r = 1.0;
	for(x = -r; x <= r; x += r * 2.0 / (samples / 6.0))
	{
		for(y = -r; y <= r; y += r * 2.0 / (samples / 6.0))
		{
			vec2 xy = vec2(x + hash.x, y + hash.y);

			float shadow = textureProj(sampler, coords + vec4(xy.x * sampling_range * coords.w, xy.y * sampling_range * coords.w, bias, 0.0), bias2);
			fact += shadow;
			h_samples += 1;
		}
	}

	if (fact == 0.0)
		return fact;

	if (fact * (1.0 / float(h_samples)) >= 0.9995)
		return fact * (1.0 / float(h_samples));

	// fine
	r = 0.9;
	for(x = -r; x <= r; x += r * 2.0 / (samples / 2.0))
	{
		for(y = -r; y <= r; y += r * 2.0 / (samples / 2.0))
		{
			vec2 xy = vec2(x + hash.x, y + hash.y);

			float shadow = textureProj(sampler, coords + vec4(xy.x * sampling_range * coords.w, xy.y * sampling_range * coords.w, bias, 0.0), bias2);
			fact += shadow;
			h_samples += 1;
		}
	}

	return fact * (1.0 / float(h_samples));
#else

	int h_samples = samples * samples;
	vec3 hash = texelFetch(s_BlueNoise, ivec3(noise_offset.xy & ivec2(127), 0), 0).rgb - vec3(0.5, 0.5, 0.0);
	//hash.rg = vec2(0.0);

	for(int n = 0; n < h_samples; n++)
	{
		//vec2 xy = (HammersleySample2D(uint(n + hash.b * 31) & 31, 31) - 0.5) * 2.0 + hash.rg * 0.25;
		vec2 xy = (HammersleySample2D(n, h_samples - 1) - 0.5) * 2.0 + hash.rg * 0.25;

		float shadow = textureProj(sampler, coords + vec4(xy.x * sampling_range * coords.w, xy.y * sampling_range * coords.w, 0.0, 0.0));
		fact += shadow;
	}
	return fact * (1.0 / float(h_samples));
#endif
}

float sampleShadow(in sampler2DShadow sampler, in vec4 coords, out float in_frustum, out vec4 projector_color)
{
	in_frustum = 0.0;

	// no shading if fragment behind
	if (coords.w <= 0.0)
		return 0.0;

	// no shading if fragment outside of light frustum
	if (coords.x < -coords.w || coords.x > coords.w || coords.y < -coords.w || coords.y > coords.w)
		return 1.0;

	in_frustum = 1.0;

	// NOTE: we need to shift after projection. this works here but we dont do it in directional. most likely because projection matrix is simple so it works...
	//       but this shift in directional might actualy introduce tiny inaccuracies
	// NOTE: can we factor that into projection matrix? most likely we can!
	coords.xy = coords.xy * vec2(0.5) + vec2(0.5) * coords.w;
	vec3 samp = coords.xyz / coords.w;

	float shadow = textureProj(sampler, coords);
	// TODO: Reimplement
	projector_color = vec4(0.0);// texture(LightProjectorSamplers[light.projector_sampler], coords.xy / coords.w);// * 10.0;
	return shadow;
}

float light_calculate_spot_attenuation(in LightProperties light, in vec3 pos)
{
	float cutoff = light.cutoff;
	float light_distance = length(light.position.xyz - pos.xyz);
	float falloff = dot(light.direction.xyz, (pos.xyz - light.position.xyz) / light_distance);
	float attenuation = 0.0;

	if (falloff > cutoff && light_distance < light.range)
	{
		attenuation = 1.0 - (1.0 - falloff) / (1.0 - cutoff);
		attenuation *= (1.0 - clamp(light_distance / light.range, 0.0, 1.0));
		attenuation = pow(attenuation, 2.0);
	}
	
	return attenuation;
}

float light_calculate_area_attenuation(in LightProperties light, in vec3 pos)
{
	float light_distance = length(light.position.xyz - pos.xyz);
	float attenuation = 0.0;

	if (light_distance < light.range)
	{
		attenuation = (1.0 - clamp(light_distance / light.range, 0.0, 1.0));
		attenuation = pow(attenuation, 2.0);
	}
	
	return attenuation;
}

const float c_MinReflectance = 0.04;

struct MaterialInfo
{
    float perceptualRoughness; // roughness value, as authored by the model creator (input to shader)
    vec3 reflectance0;         // full reflectance color (normal incidence angle)

    float alphaRoughness;      // roughness mapped to a more linear change in the roughness (proposed by [2])
    vec3 diffuseColor;         // color contribution from diffuse lighting

    vec3 reflectance90;        // reflectance color at grazing angle
    vec3 specularColor;        // color contribution from specular lighting
};

struct AngularInfo
{
    float NdotL;                  // cos angle between normal and light direction
    float NdotV;                  // cos angle between normal and view direction
    float NdotH;                  // cos angle between normal and half vector
    float LdotH;                  // cos angle between light direction and half vector
    float VdotH;                  // cos angle between view direction and half vector
};

AngularInfo getAngularInfo(vec3 pointToLight, vec3 normal, vec3 view)
{
    // Standard one-letter names
    vec3 n = normalize(normal);           // Outward direction of surface point
    vec3 v = normalize(view);             // Direction from surface point to view
    vec3 l = normalize(pointToLight);     // Direction from surface point to light
    vec3 h = normalize(l + v);            // Direction of the vector between l and v

    //float NdotL = clamp(dot(n, l), 0.0, 1.0);
    float NdotL = dot(n, l);
    //float NdotV = clamp(dot(n, v), 0.0, 1.0);
    float NdotV = dot(n, v);
    float NdotH = clamp(dot(n, h), 0.0, 1.0);
    float LdotH = clamp(dot(l, h), 0.0, 1.0);
    float VdotH = clamp(dot(v, h), 0.0, 1.0);

    AngularInfo angularInfo = {
        NdotL,
        NdotV,
        NdotH,
        LdotH,
        VdotH
	};

    return angularInfo;
}

float getPerceivedBrightness(vec3 vec)
{
    return sqrt(0.299 * vec.r * vec.r + 0.587 * vec.g * vec.g + 0.114 * vec.b * vec.b);
}

// https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness/examples/convert-between-workflows/js/three.pbrUtilities.js#L34
float solveMetallic(vec3 diffuse, vec3 specular, float oneMinusSpecularStrength)
{
    float specularBrightness = getPerceivedBrightness(specular);

    if (specularBrightness < c_MinReflectance)
	{
		return 0.0;
    }

    float diffuseBrightness = getPerceivedBrightness(diffuse);

    float a = c_MinReflectance;
    float b = diffuseBrightness * oneMinusSpecularStrength / (1.0 - c_MinReflectance) + specularBrightness - 2.0 * c_MinReflectance;
    float c = c_MinReflectance - specularBrightness;
    float D = b * b - 4.0 * a * c;

    return clamp((-b + sqrt(D)) / (2.0 * a), 0.0, 1.0);
}

// Lambert lighting
// see https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/
vec3 diffuse(MaterialInfo materialInfo)
{
    return materialInfo.diffuseColor / M_PI;
}

// The following equation models the Fresnel reflectance term of the spec equation (aka F())
// Implementation of fresnel from [4], Equation 15
vec3 specularReflection(MaterialInfo materialInfo, AngularInfo angularInfo)
{
    return materialInfo.reflectance0 + (materialInfo.reflectance90 - materialInfo.reflectance0) * pow(clamp(1.0 - angularInfo.VdotH, 0.0, 1.0), 5.0);
}

// Smith Joint GGX
// Note: Vis = G / (4 * NdotL * NdotV)
// see Eric Heitz. 2014. Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs. Journal of Computer Graphics Techniques, 3
// see Real-Time Rendering. Page 331 to 336.
// see https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf/geometricshadowing(specularg)
float visibilityOcclusion(MaterialInfo materialInfo, AngularInfo angularInfo)
{
    float NdotL = clamp(angularInfo.NdotL, 0.0, 1.0);
    float NdotV = clamp(angularInfo.NdotV, 0.0, 1.0);
    float alphaRoughnessSq = materialInfo.alphaRoughness * materialInfo.alphaRoughness;

    float GGXV = NdotL * sqrt(NdotV * NdotV * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);
    float GGXL = NdotV * sqrt(NdotL * NdotL * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);

    float GGX = GGXV + GGXL;
    if (GGX > 0.0)
    {
        return 0.5 / GGX;
    }
    return 0.0;
}

// The following equation(s) model the distribution of microfacet normals across the area being drawn (aka D())
// Implementation from "Average Irregularity Representation of a Roughened Surface for Ray Reflection" by T. S. Trowbridge, and K. P. Reitz
// Follows the distribution function recommended in the SIGGRAPH 2013 course notes from EPIC Games [1], Equation 3.
float microfacetDistribution(MaterialInfo materialInfo, AngularInfo angularInfo)
{
    float alphaRoughnessSq = materialInfo.alphaRoughness * materialInfo.alphaRoughness;
    float f = (angularInfo.NdotH * alphaRoughnessSq - angularInfo.NdotH) * angularInfo.NdotH + 1.0;
    return alphaRoughnessSq / (M_PI * f * f + 0.000001f);
}

vec3 fakeSkinBRDF(float NdotL)
{
	float curvature = 0.195;

	NdotL = mad(NdotL, 0.5, 0.5); // map to 0 to 1 range
	float curva = (1.0/mad(curvature, 0.5 - 0.0625, 0.0625) - 2.0) / (16.0 - 2.0); // curvature is within [0, 1] remap to normalized r from 2 to 16
	float oneMinusCurva = 1.0 - curva;
	float3 curve0;
	{
		float3 rangeMin = float3(0.0, 0.3, 0.3);
		float3 rangeMax = float3(1.0, 0.7, 0.7);
		float3 offset = float3(0.0, 0.06, 0.06);
		float3 t = saturate( mad(float3(NdotL), vec3(1.0) / (rangeMax - rangeMin), (offset + rangeMin) / (rangeMin - rangeMax)  ) );
		float3 lowerLine = (t * t) * float3(0.65, 0.5, 0.9);
		lowerLine.r += 0.045;
		lowerLine.b *= t.b;
		float3 m = float3(1.75, 2.0, 1.97);
		float3 upperLine = mad(float3(NdotL), m, float3(0.99, 0.99, 0.99) -m );
		upperLine = saturate(upperLine);
		float3 lerpMin = float3(0.0, 0.35, 0.35);
		float3 lerpMax = float3(1.0, 0.7 , 0.6 );
		float3 lerpT = saturate( mad(float3(NdotL), float3(1.0)/(lerpMax-lerpMin), lerpMin/ (lerpMin - lerpMax) ));
		curve0 = lerp(lowerLine, upperLine, lerpT * lerpT);
	}
	float3 curve1;
	{
		float3 m = float3(1.95, 2.0, 2.0);
		float3 upperLine = mad( float3(NdotL), m, float3(0.99, 0.99, 1.0) - m);
		curve1 = saturate(upperLine);
	}
	float oneMinusCurva2 = oneMinusCurva * oneMinusCurva;
	float3 brdf = lerp(curve0, curve1, mad(oneMinusCurva2, -1.0 * oneMinusCurva2, 1.0) );

	return brdf;
}

vec3 getPointShade(vec3 pointToLight, MaterialInfo materialInfo, vec3 normal, vec3 view)
{
    AngularInfo angularInfo = getAngularInfo(pointToLight, normal, view);

    if (angularInfo.NdotL > 0.0 || angularInfo.NdotV > 0.0)
    {
        // Calculate the shading terms for the microfacet specular shading model
        vec3 F = specularReflection(materialInfo, angularInfo);
        float Vis = visibilityOcclusion(materialInfo, angularInfo);
        float D = microfacetDistribution(materialInfo, angularInfo);

        // Calculation of analytical lighting contribution
        vec3 diffuseContrib = (1.0 - F) * diffuse(materialInfo);
        vec3 specContrib = F * Vis * D;

        // Obtain final intensity as reflectance (BRDF) scaled by the energy of the light (cosine law)
        return clamp(angularInfo.NdotL, 0.0, 1.0) * (diffuseContrib + specContrib);
        //return fakeSkinBRDF(angularInfo.NdotL) * (diffuseContrib + specContrib);
    }

    return vec3(0.0, 0.0, 0.0);
}