#version 430 core

// general luminance threshold at which antialiasing will take place
const float contrastThreshold = 0.03;
// additional relative luminance threshold at which antialiasing will take place
const float relativeThreshold = 0.06;

// gamma value
const float epsilon = 1.5; // used for tone mapping and gamma correcting DoF bokeh blur

// distance from the camera's origin to focal point
const float focusDistance = 0.4;
// // range of focus.
const float focusRange = 0.5; // Lower values mean more sharper image

// contrast of fragment
const float contrast = 0.3;
// additional brightness of fragment
const float brightness = 0.05;

// output fragment color
out vec4 fragColor;

// screen resolution in pixels
uniform vec2 resolution;
// fbo texture sampler
uniform sampler2D fboTexture;	// RGB-components store pixel color, alpha holds linear world scene depth from the camera's origin

uniform int boolFilterDoF;	// enabled Depth of Field
uniform int boolFilterFXAA;	// enabled Fast Approximate Anti Aliasing
uniform int boolFilterGrain;// enable some graininess to add to every pixels

// normalized fragment coordinates
const vec2 fragCoords = gl_FragCoord.xy / resolution;
// texel size
const vec2 fboTexelSize = 1.0 / resolution;

#define true 1	// true is not natively defined

// FXAA implementation from: https://catlikecoding.com/unity/tutorials/advanced-rendering/fxaa/
// with some minor optimizations regarding branches and texture lookups
struct LuminanceData {
	// stores information about luminance
	float max, min, diff;	// maximum, minimum and luminance difference between max/min
	// fragment values and its neighboors
	float texels[3][3];
	// field names
	#define lumSouthWest lumData.texels[0][0]
	#define lumSouth     lumData.texels[1][0]
	#define lumSouthEast lumData.texels[2][0]
	#define lumWest      lumData.texels[0][1]
	#define lumMiddle    lumData.texels[1][1]
	#define lumEast      lumData.texels[2][1]
	#define lumNorthWest lumData.texels[0][2]
	#define lumNorth 	 lumData.texels[1][2]
	#define lumNorthEast lumData.texels[2][2]
};

float luminanceFromLinearRgb(in vec3 linearRgb)
{
	// relative luminance coefficent factors
	const vec3 luminanceFactors = vec3(0.2126729,  0.7151522, 0.0721750);
	// compute luminance
	return dot(linearRgb, luminanceFactors);
}

float texelLuminosity(in vec2 pixelOffset)
{
	vec3 linearRgb = texture2D(fboTexture, fragCoords + pixelOffset * fboTexelSize).rgb;

	return luminanceFromLinearRgb(linearRgb);
}

// clamp between [0, 1]
#define saturate(x) clamp(x, 0.0, 1.0)

// collect luminance data and fill struct
void collectLuminance(inout LuminanceData lumData)
{
	// collect data
	for (int x = -1; x < 2; x++) {
		for (int y = -1; y < 2; y++) {
			lumData.texels[x + 1][y + 1] = texelLuminosity(vec2(float(x), float(y) ) );
		}
	}
	// calculate highest and lowest + difference i.e the contrast
	lumData.max = max(max(max(max(lumEast, lumWest), lumSouth), lumNorth), lumMiddle);
	lumData.min = min(min(min(min(lumEast, lumWest), lumSouth), lumNorth), lumMiddle);
	// compute difference
	lumData.diff = lumData.max - lumData.min;
}

// apply a basic tent blur filter, to average the fragments luminance
float fragBlendFactor(in LuminanceData lumData)
{
	// these should matter more, so double their influence
	float fac = 2.0 * (lumNorth + lumSouth + lumWest + lumEast);
	fac += lumNorthWest + lumNorthEast + lumSouthWest + lumSouthEast;
	fac /= 12.0; // scale down
	
	fac = abs(fac - lumMiddle);
	fac = saturate(fac / lumData.diff);

	float blendFac = smoothstep(0.0, 1.0, fac);

	return blendFac * blendFac;
}

// determine the edge type
bool determineEdge(in LuminanceData lumData)
{
	// change in horizontal pixels
	float hori =
			abs(lumNorth + lumSouth - 2.0 * lumMiddle) * 2.0 +
			abs(lumNorthEast + lumSouthEast - 2.0 * lumEast) +
			abs(lumNorthWest + lumSouthWest - 2.0 * lumWest);
	// change in vertical pixels
	float veri =
			abs(lumEast  + lumWest  - 2.0 * lumMiddle) * 2.0 +
			abs(lumNorthEast + lumNorthWest - 2.0 * lumNorth) +
			abs(lumSouthEast + lumSouthWest - 2.0 * lumSouth);

	return hori >= veri;
}

vec3 filterFXAA()
{
	// collect luminance data off current fragment
	LuminanceData lumData;
	collectLuminance(lumData);

	vec2 st = fragCoords;
	if (lumData.diff > max(contrastThreshold, relativeThreshold * lumData.max) )
	{
		float fac = fragBlendFactor(lumData);
		bool edge = determineEdge(lumData);

		float pixelStep = edge ? fboTexelSize.y : fboTexelSize.x;

		float pLum = edge ? lumNorth : lumEast;
		float nLum = edge ? lumSouth : lumWest;

		float pGrad = abs(pLum - lumMiddle);
		float nGrad = abs(nLum - lumMiddle);

		pixelStep = pixelStep * sign(pGrad - nGrad);
	
		st += edge ? vec2(0, pixelStep * fac) : vec2(pixelStep * fac, 0);
	}
	return texture2D(fboTexture, st).rgb;
}

// apply gamma correctio to linear RGB color
vec3 gamma(in vec3 linearRgb)
{
	return pow(linearRgb, vec3(epsilon) );
}

// de-gamma linear RGB color
// degamma() is the inverse function of gamma()
vec3 degamma(in vec3 linearRgb)
{
	return pow(linearRgb, vec3(1.0 / epsilon) );
}

// Gold Noise ©2015 dcerisano@standard3d.com
// - based on the Golden Ratio
// - uniform normalized distribution
// - (fastest?) static noise generator function (also runs at low precision)


// generate some 1D noise
// no seed required
float goldNoise(in vec2 xy)
{
	const float phi  = 1.61803398874989484820459;  // Φ = Golden Ratio

	return fract(tan(distance(xy * phi, xy) ) * xy.x) * 2.0 - 1.0;
}

// gamma corrected bokeh down sampling filter as DoF blur
vec3 bokeh(in float bokehSize)
{

	// if sampling rate is low enough, clamp it to use FXAA instead of a single unfiltered texture lookup
	if (bokehSize < 2.0)
	{
		if (boolFilterFXAA == true)
		{
			// if filter FXAA
			return filterFXAA();
		}
		else
		{
			// if no DoF and no FXAA, do nearly nothing
			return texture2D(fboTexture, fragCoords).rgb;
		}
	}

	vec3 color = vec3(0);

	const int steps = 8;	// samples to take per axis {x,y}

	const int stepsHalf  = steps / 2;
	const float stepSize = bokehSize / steps;

	float samples = 0.0;	// samples actually taken

	for (int i = -stepsHalf; i < stepsHalf; i++) {
		float u = stepSize * float(i) * fboTexelSize.x;

		for (int j = -stepsHalf; j < stepsHalf; j++) {
			float v = stepSize * float(j) * fboTexelSize.y;

			// calculate sample effectiveness, as circle for perfect bokeh
			float shape = 1.0 - step(stepsHalf, length(vec2(i, j) ) );

			// if we have enough effectiveness, take a sample
			if (shape > 0.5) {
				// calculate offset coordinates for sample, and add some noise to break of grid patterns when using low amount of samples
				float noiseX = goldNoise(vec2(i, j) + gl_FragCoord.xy + 1.0);
				float noiseY = goldNoise(vec2(i, j) + gl_FragCoord.xy + 4.0);
				vec2  coords = vec2(noiseX, noiseY) * fboTexelSize + fragCoords + vec2(u, v);

				// gamma correct texture lookup and contribute
				color   += gamma(texture2D(fboTexture, coords).rgb * shape);
				samples += shape;	// we have taken a sample, so remember for normalization later
			}
		}
	}
	// de-gamma correct and normalize
	return degamma(color / samples);
}

vec3 filterDoF()
{
	// get scene depth
	float depth = texture2D(fboTexture, fragCoords).a;

	// Circle of Confusion
	float CoC = clamp( (depth - focusDistance) / focusRange, -1, 1) * 16.0;
	// compute bokeh blur
	return bokeh(abs(CoC) );
}

void filmGrain(inout vec3 color)
{
	float noiseRed   = goldNoise(gl_FragCoord.xy + 1.0);
	float noiseGreen = goldNoise(gl_FragCoord.xy + 16.0);
	float noiseBlue  = goldNoise(gl_FragCoord.xy + 32.0);

	vec3 noise = vec3(noiseRed, noiseGreen, noiseBlue);

	float intensity = 1.0 - sqrt(texelLuminosity(vec2(0) ) );

	color = color + color * noise * intensity * 0.2;
}

void sampleColorTexture(inout vec3 color)
{
	if (boolFilterDoF == true)
	{
		// apply DoF
		color = filterDoF();
	}
	else if (boolFilterFXAA == true)
	{
		// if no DoF, filter FXAA
		color = filterFXAA();
	}
	else
	{
		// if no DoF and no FXAA, do nearly nothing
		color = texture2D(fboTexture, fragCoords).rgb;
	}
}

void main()
{
	vec3 color;
	// get primary scene color
	sampleColorTexture(color);

	// adjust contrast and brightness
	color = (color - 0.5) * (1.0 + contrast) + 0.5 + brightness;
	// vignette
	// falloff
	float falloff = distance(fragCoords, vec2(0.5, 0.5) );
	// adjust falloff and darken
	color = color * (1.0 - falloff * falloff * falloff * 0.2);

	// tone map
	color = clamp(color, 0.0, 1.0);
	color = 1.0 - pow(1.0 - color, vec3(epsilon) );

	if (boolFilterGrain == true)
	{
		// add some film grain
		filmGrain(color);
	}

	fragColor = vec4(color, 1);
}
