#include "common.h"

// Tunables
#define CFG_TEXTURE_TILING 0.2
#define CFG_TEXTURE_DETILING 0.1

#define CFG_SPECULAR 2
#define CFG_GLOSS 900

#define CFG_NORMAL_STRENGTH 0.25

#define CFG_REFRACTION_STRENGTH 0.05

#define CFG_FRESNEL_OFFSET 0.3

#define CFG_SSR_STEPS 8
#define CFG_SSR_START_DISTANCE 1
#define CFG_SSR_STEP_CLAMP 0.2
#define CFG_SSR_DEPTH_CUTOFF 10

// Shader code
struct Appdata
{
#ifndef PIN_PACKED
    float4 Position	    : POSITION;
#else
	ATTR_INT4 Position	: POSITION;
#endif

    ATTR_INT4 Normal	: NORMAL;

    ATTR_INT4 Material0	: TEXCOORD0;
    ATTR_INT4 Material1	: TEXCOORD1;

#ifndef PIN_PACKED
    ATTR_INT4 Material2	: TEXCOORD2;
#endif
};

struct VertexOutput
{
    float4 HPosition    : POSITION;

    float4 Weights_Wave: COLOR0;
    float3 Tangents: COLOR1;

    float2 Uv0: TEXCOORD0;
    float2 Uv1: TEXCOORD1;
    float2 Uv2: TEXCOORD2;

    float4 LightPosition_Fog : TEXCOORD3;

	float3 Normal: TEXCOORD4;
    float4 View_Depth: TEXCOORD5;

#ifdef PIN_HQ
	float4 PositionScreen: TEXCOORD6;
#endif
};

WORLD_MATRIX_ARRAY(WorldMatrixArray, 72);

uniform float4 WaveParams; // .x = frequency  .y = phase  .z = height  .w = lerp
uniform float4 WaterColor; // deep water color
uniform float4 WaterParams; // .x = refraction depth scale, .y = refraction depth offset

float3 displacePosition(float3 position, float waveFactor)
{
	float x = sin((position.z - position.x) * WaveParams.x - WaveParams.y);
	float z = sin((position.z + position.x) * WaveParams.x + WaveParams.y);
	float p = (x + z) * WaveParams.z;

	float3 result = position;
	
	result.y += p * waveFactor;

	return result;
}

float4 clipToScreen(float4 pos)
{
#ifdef GLSL
	pos.xy = pos.xy * 0.5 + 0.5 * pos.w;
#else
	pos.xy = pos.xy * float2(0.5, -0.5) + 0.5 * pos.w;
#endif
	return pos;
}

#ifdef PIN_PACKED
float2 getUV(float3 position, ATTR_INT projection, float seed)
{
	float3 u = WorldMatrixArray[1 + int(projection)].xyz;
	float3 v = WorldMatrixArray[19 + int(projection)].xyz;

    float2 uv = float2(dot(position, u), dot(position, v)) * (0.25 * CFG_TEXTURE_TILING) + CFG_TEXTURE_DETILING * float2(seed, floor(seed * 2.6651441f));

    return uv;
}

VertexOutput WaterVS(Appdata IN)
{
    VertexOutput OUT = (VertexOutput)0;
    
	float3 posWorld = IN.Position.xyz * WorldMatrixArray[0].w + WorldMatrixArray[0].xyz;
    float3 normalWorld = IN.Normal.xyz * (1.0 / 127.0) - 1.0;

    float3 weights = IN.Position.www == float3(0, 1, 2);

    float waveFactor = dot(weights, IN.Material0.xyz) * (1.0 / 255.0);

#ifdef PIN_HQ
	float fade = saturate0(1 - dot(posWorld - G(CameraPosition), -G(ViewDir).xyz) * G(FadeDistance_GlowFactor).y);

	posWorld = displacePosition(posWorld, waveFactor * fade);
#endif

	OUT.HPosition = mul(G(ViewProjection), float4(posWorld, 1));

    OUT.LightPosition_Fog = float4(lgridPrepareSample(lgridOffset(posWorld, normalWorld)), (G(FogParams).z - OUT.HPosition.w) * G(FogParams).w);

    OUT.Uv0 = getUV(posWorld, IN.Material1.x, IN.Normal.w);
    OUT.Uv1 = getUV(posWorld, IN.Material1.y, IN.Material0.w);
    OUT.Uv2 = getUV(posWorld, IN.Material1.z, IN.Material1.w);

    OUT.Weights_Wave.xyz = weights;
    OUT.Weights_Wave.w = waveFactor;

	OUT.Normal = normalWorld;
    OUT.View_Depth = float4(G(CameraPosition) - posWorld, OUT.HPosition.w);
	OUT.Tangents = float3(IN.Material1.xyz) > 7.5; // side vs top

#ifdef PIN_HQ
	OUT.PositionScreen = clipToScreen(OUT.HPosition);
#endif

	return OUT;
}
#else
float2 getUV(float3 position, ATTR_INT4 material)
{
	float3 u = WorldMatrixArray[1 + int(material.y)].xyz;
	float3 v = WorldMatrixArray[19 + int(material.y)].xyz;

    float2 uv = float2(dot(position, u), dot(position, v)) * CFG_TEXTURE_TILING + CFG_TEXTURE_DETILING * material.zw;

    return uv;
}

VertexOutput WaterVS(Appdata IN)
{
    VertexOutput OUT = (VertexOutput)0;
    
	float3 posWorld = IN.Position.xyz * 4;
    float3 normalWorld = IN.Normal.xyz * (1.0 / 127.0) - 1.0;

    float3 weights = abs(IN.Normal.www - float3(0, 1, 2)) < 0.1;

    float layer = weights.x * IN.Material0.x + weights.y * IN.Material1.x + weights.z * IN.Material2.x;
    float waveFactor = layer / 255;

#ifdef PIN_HQ
	float fade = saturate0(1 - dot(posWorld - G(CameraPosition), -G(ViewDir).xyz) * G(FadeDistance_GlowFactor).y);

	posWorld = displacePosition(posWorld, waveFactor * fade);
#endif

	OUT.HPosition = mul(G(ViewProjection), float4(posWorld, 1));

    OUT.LightPosition_Fog = float4(lgridPrepareSample(lgridOffset(posWorld, normalWorld)), (G(FogParams).z - OUT.HPosition.w) * G(FogParams).w);

    OUT.Uv0 = getUV(IN.Position.xyz, IN.Material0);
    OUT.Uv1 = getUV(IN.Position.xyz, IN.Material1);
    OUT.Uv2 = getUV(IN.Position.xyz, IN.Material2);

    OUT.Weights_Wave.xyz = weights;
    OUT.Weights_Wave.w = waveFactor;

	OUT.Normal = normalWorld;
    OUT.View_Depth = float4(G(CameraPosition) - posWorld, OUT.HPosition.w);
	OUT.Tangents = float3(IN.Material0.y, IN.Material1.y, IN.Material2.y) > 7.5; // side vs top

#ifdef PIN_HQ
	OUT.PositionScreen = clipToScreen(OUT.HPosition);
#endif

	return OUT;
}
#endif

TEX_DECLARE2D(NormalMap1, 0);
TEX_DECLARE2D(NormalMap2, 1);
TEX_DECLARECUBE(EnvMap, 2);
LGRID_SAMPLER(LightMap, 3);
TEX_DECLARE2D(LightMapLookup, 4);
TEX_DECLARE2D(GBufferColor, 5);
TEX_DECLARE2D(GBufferDepth, 6);

float fresnel(float ndotv)
{
	return saturate(0.78 - 2.5 * abs(ndotv)) + CFG_FRESNEL_OFFSET;
}

float4 sampleMix(float2 uv)
{
#ifdef PIN_HQ
	return lerp(tex2D(NormalMap1, uv), tex2D(NormalMap2, uv), WaveParams.w);
#else
	return tex2D(NormalMap1, uv);
#endif
}

float3 sampleNormal(float2 uv0, float2 uv1, float2 uv2, float3 w, float3 normal, float3 tsel)
{
	return terrainNormal(sampleMix(uv0), sampleMix(uv1), sampleMix(uv2), w, normal, tsel);
}

float3 sampleNormalSimple(float2 uv0, float2 uv1, float2 uv2, float3 w)
{
    float4 data = sampleMix(uv0) * w.x + sampleMix(uv1) * w.y + sampleMix(uv2) * w.z;

    return nmapUnpack(data).xzy;
}

float unpackDepth(float2 uv)
{
	float4 geomTex = tex2D(GBufferDepth, uv);
	float d = geomTex.z * (1.0f/256.0f) + geomTex.w;
	return d * GBUFFER_MAX_DEPTH;
}

float3 getRefractedColor(float4 cpos, float3 N, float3 waterColor)
{
    float2 refruv0 = cpos.xy / cpos.w;
    float2 refruv1 = refruv0 + N.xz * CFG_REFRACTION_STRENGTH;

    float4 refr0 = tex2D(GBufferColor, refruv0);
	refr0.w = unpackDepth(refruv0);

    float4 refr1 = tex2D(GBufferColor, refruv1);
	refr1.w = unpackDepth(refruv1);

	float4 result = lerp(refr0, refr1, saturate(refr1.w - cpos.w));

	// Estimate water absorption by a scaled depth difference
	float depthfade = saturate((result.w - cpos.w) * WaterParams.x + WaterParams.y);

	// Since GBuffer depth is clamped we tone the refraction down after half of the range for a smooth fadeout
	float gbuffade = saturate(cpos.w * (2.f / GBUFFER_MAX_DEPTH) - 1);

	float fade = saturate(depthfade + gbuffade);

    return lerp(result.rgb, waterColor, fade);
}

float3 getReflectedColor(float4 cpos, float3 wpos, float3 R)
{
    float3 result = 0;
    float inside = 0;

    float distance = CFG_SSR_START_DISTANCE;
    float diff = 0;
	float diffclamp = cpos.w * CFG_SSR_STEP_CLAMP;

	float4 Pproj = cpos;
	float4 Rproj = clipToScreen(mul(G(ViewProjection), float4(R, 0)));

#ifndef GLSL
	[unroll]
#endif
    for (int i = 0; i < CFG_SSR_STEPS; ++i)
    {
        distance += clamp(diff, -diffclamp, diffclamp);

        float4 cposi = Pproj + Rproj * distance;
        float2 uv = cposi.xy / cposi.w;
        float depth = unpackDepth(uv);

        diff = depth - cposi.w;
    }

    float4 cposi = Pproj + Rproj * distance;
    float2 uv = cposi.xy / cposi.w;

	// Ray hit has to be inside the screen bounds
    float ufade = abs(uv.x - 0.5) < 0.5;
    float vfade = abs(uv.y - 0.5) < 0.5;

	// Fade reflections out with distance; use max(ray hit, original depth) to discard hits that are too far
	// 4 - depth * 4 would give us fade out from 0.75 to 1; 3.9 makes sure reflections go to 0 slightly before GBUFFER_MAX_DEPTH
    float wfade = saturate((4 - 0.1) - max(cpos.w, cposi.w) * (4.f / GBUFFER_MAX_DEPTH));

	// Ray hit has to be reasonably close to where we started
    float dfade = abs(diff) < CFG_SSR_DEPTH_CUTOFF;

	// Avoid back-projection 
    float Vfade = Rproj.w > 0;

    float fade = ufade * vfade * wfade * dfade * Vfade;

    return lerp(texCUBE(EnvMap, R).rgb, tex2D(GBufferColor, uv).rgb, fade);
}

float4 WaterPS(VertexOutput IN): COLOR0
{
    float4 light = lgridSample(TEXTURE(LightMap), TEXTURE(LightMapLookup), IN.LightPosition_Fog.xyz);
	float shadow = light.a;

	float3 w = IN.Weights_Wave.xyz;

	// Use simplified normal reconstruction for LQ mobile (assumes flat water surface)
#if defined(GLSLES) && !defined(PIN_HQ)
	float3 normal = sampleNormalSimple(IN.Uv0, IN.Uv1, IN.Uv2, w);
#else
	float3 normal = sampleNormal(IN.Uv0, IN.Uv1, IN.Uv2, w, IN.Normal, IN.Tangents);
#endif

	// Flatten the normal for Fresnel and for reflections to make them less chaotic
	float3 flatNormal = lerp(IN.Normal, normal, CFG_NORMAL_STRENGTH);

	float3 waterColor = WaterColor.rgb;

#ifdef PIN_HQ
	float fade = saturate0(1 - IN.View_Depth.w * G(FadeDistance_GlowFactor).y);

	float3 view = normalize(IN.View_Depth.xyz);

	float fre = fresnel(dot(flatNormal, view)) * IN.Weights_Wave.w;

	float3 position = G(CameraPosition) - IN.View_Depth.xyz;

#ifdef PIN_GBUFFER
    float3 refr = getRefractedColor(IN.PositionScreen, normal, waterColor);
	float3 refl = getReflectedColor(IN.PositionScreen, position, reflect(-view, flatNormal));
#else
    float3 refr = waterColor;
	float3 refl = texCUBE(EnvMap, reflect(-view, flatNormal)).rgb;
#endif

    float specularIntensity = CFG_SPECULAR * fade;
    float specularPower = CFG_GLOSS;

	float3 specular = G(Lamp0Color) * (specularIntensity * shadow * (float)(half)pow(saturate(dot(normal, normalize(-G(Lamp0Dir) + view))), specularPower));
#else
	float3 view = normalize(IN.View_Depth.xyz);

	float fre = fresnel(dot(flatNormal, view));

    float3 refr = waterColor;

	float3 refl = texCUBE(EnvMap, reflect(-IN.View_Depth.xyz, flatNormal)).rgb;

	float3 specular = 0;
#endif

    // Combine
	float4 result;
    result.rgb = lerp(refr, refl, fre) * (G(AmbientColor).rgb + G(Lamp0Color).rgb * shadow + light.rgb) + specular;
    result.a = 1;

    float fogAlpha = saturate(IN.LightPosition_Fog.w);

    result.rgb = lerp(G(FogColor), result.rgb, fogAlpha);

	return result;
}
