/*
====================================================================================================

    Copyright (C) 2020 RRe36

    All Rights Reserved unless otherwise explicitly stated.


    By downloading this you have agreed to the license and terms of use.
    These can be found inside the included license-file
    or here: https://rre36.com/copyright-license

    Violating these terms may be penalized with actions according to the Digital Millennium
    Copyright Act (DMCA), the Information Society Directive and/or similar laws
    depending on your country.

====================================================================================================
*/

#include "/lib/util/poisson.glsl"

struct shadowData {
    float shadow;
    vec3 color;
    vec3 subsurfaceScatter;
};

float vectorAverage(vec4 x) {
    return dot(x, vec4(0.25));
}

const ivec2 shadowmapSize   = ivec2(shadowMapResolution * MC_SHADOW_QUALITY);
const vec2 shadowmapPixel   = 1.0 / vec2(shadowmapSize);
const float shadowmapDepthScale = (2.0 * 256.0) / 0.2;
const float shadowmapPixelZ = shadowmapDepthScale / shadowmapSize.x;

float textureShadowPCF(sampler2D tex, vec3 pos) {
    //vec2 size       = textureSize(tex, 0);
    vec2 coord      = fract(pos.xy - 0.5 * shadowmapPixel);
    ivec2 location  = ivec2(coord * shadowmapSize);

    vec4 samples    = vec4(
        texelFetch(tex, location, 0).x,                texelFetch(tex, location + ivec2(1, 0), 0).x,
        texelFetch(tex, location +  ivec2(0, 1), 0).x, texelFetch(tex, location + ivec2(1, 1), 0).x
    );
        samples     = step(vec4(pos.z), samples);

    vec2 weights    = fract(coord * shadowmapSize);


    return mix(
        mix(samples.x, samples.y, weights.x),
        mix(samples.z, samples.w, weights.x), weights.y
    );
}

#include "/lib/atmos/waterConst.glsl"

vec3 getShadowcol(sampler2D tex, vec2 coord, float surfaceDist) {
    vec4 color  = textureLod(tex, coord, 0);

    if (color.a < 0.97 && color.a > 0.93) {
        float caustic     = (2.5 * color.z * color.z);

        vec3 absorb     = expf(-max0(surfaceDist * waterDensity) * waterAbsorbCoeff);
            absorb     *= mix(caustic, 1.0, expf(-max0(surfaceDist) * 0.2));

        bool ice        = color.a < 0.95;
        if (ice) absorb *= expf(-surfaceDist * 0.15) * 0.8 + 0.1;
            
        color.rgb   = absorb;
        color.a     = 1.0;
    } else {
        color.rgb = mix(vec3(1.0), sqr(color.rgb), sqrt(color.a));
    }

    return color.rgb;
}

vec3 getShadowcol(sampler2D tex, vec2 coord) {
    vec4 x  = textureLod(tex, coord, 0);
        x.rgb = sqr(x.rgb);

    if (x.a < 0.97 && x.a > 0.93) {
        x.rgb = vec3(0.1, 0.2, 1.0);
    }

    return mix(vec3(1.0), x.rgb, x.a);
}

#ifndef SHADOW_PREPASS

float getShadowSoft(sampler2D tex, vec3 pos) {
    float step  = rcp(float(shadowMapResolution));
    float n     = ditherBluenoise()*pi;
    vec2 noise  = vec2(cos(n), sin(n));
    vec3 offset = vec3(noise, 0.0)*step;

    float s0    = textureShadowPCF(tex, pos)*0.5;
    float s1    = textureShadowPCF(tex, pos+offset);
    float s2    = textureShadowPCF(tex, pos-offset);

    return saturate((s0 + s1 + s2)*0.4);
}

float getShadowSoftHQ(sampler2D tex, vec3 pos) {
    float step  = rcp(float(shadowMapResolution));
    float n     = ditherBluenoise()*pi;
    vec2 noise  = vec2(cos(n), sin(n));
    vec3 offset = vec3(noise, 0.0)*step;

    float s0    = textureShadowPCF(tex, pos);
    float s1    = textureShadowPCF(tex, pos+offset);
    float s2    = textureShadowPCF(tex, pos-offset);
    float s3    = textureShadowPCF(tex, pos+offset*2.0)*0.5;
    float s4    = textureShadowPCF(tex, pos-offset*2.0)*0.5;

    return (s0 + s1 + s2 + s3 + s4)/4.0;
}
/*
    This offset thing is from spectrum as well, and zombye has it from here:
    http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/
*/

const float phi2 = 1.32471795724474602596090885447809; // = plastic constant, plastic ratio, etc

vec2 R2(float n) {
	const float s0 = 0.5;
	const vec2 alpha = 1.0 / vec2(phi2, phi2 * phi2);
	return fract(s0 + n * alpha);
}

vec4 textureShadowColored(sampler2D tex0, sampler2D tex1, sampler2D color, vec3 pos, bool water, out bool transparent) {
   vec4 samples0    = textureGather(tex0, pos.xy, 0);

   vec4 depth0      = samples0;

        samples0    = step(vec4(pos.z), samples0);

   vec4 samples1    = textureGather(tex1, pos.xy, 0);

   vec4 depth1      = samples1;

        samples1    = step(vec4(pos.z), samples1);

    vec2 weights    = cubeSmooth(fract(pos.xy * shadowmapSize - 0.5));

    //ivec2 location  = ivec2(pos.xy * shadowmapSize);

        depth1      = clamp((depth1 - pos.z) * shadowmapDepthScale + shadowmapPixelZ / 2.0, 0.0, shadowmapPixelZ);

    float shadow    = mix(
            mix(depth1.w, depth1.z, weights.x),
            mix(depth1.x, depth1.y, weights.x), weights.y
        );
        shadow      = saturate(shadow / shadowmapPixelZ);

    transparent     = all(equal(samples1, samples0));

    if (transparent || water) return vec4(vec3(1.0), shadow);

        depth0 = (depth0 - vec4(pos.z)) * shadowmapDepthScale;
        depth0 = max(-depth0, 0.0);

    float transparentDist = mix(
        mix(depth0.w, depth0.z, weights.x),
        mix(depth0.x, depth0.y, weights.x), weights.y
    );

    return vec4(getShadowcol(color, pos.xy, transparentDist), shadow);
}

vec4 shadowFiltered(vec3 pos, float sigma) {
    float stepsize = rcp(float(shadowMapResolution));
    float dither   = ditherBluenoise();

    vec4 sum = vec4(0.0);

    const float minSoftSigma = shadowmapPixel.x * 2.0;

    float softSigma = max(sigma, minSoftSigma);

    float sharpenLerp = saturate(sigma / minSoftSigma);

    vec2 noise  = vec2(cos(dither * pi), sin(dither * pi)) * stepsize;

    vec4 color      = texture(shadowcolor0, pos.xy);

    bool water      = color.a < 0.97 && color.a > 0.95;

    bool anyTransparent = false;

    for (uint i = 0; i < shadowFilterIterations; ++i) {
        vec2 offset     = R2((i + dither) * 64.0);
            offset      = vec2(cos(offset.x * tau), sin(offset.x * tau)) * sqrt(offset.y);
        bool transparent;
        sum   += textureShadowColored(shadowtex0, shadowtex1, shadowcolor0, pos + vec3(offset * softSigma, 0.0), water, transparent);

        anyTransparent = (anyTransparent || !transparent);
    }
    sum /= float(shadowFilterIterations);

    vec2 sharpenBorders = mix(vec2(0.4, 0.6), vec2(0.0, 1.0), sharpenLerp);

    sum.w   = linStep(sum.w, sharpenBorders.x, sharpenBorders.y);

    if (anyTransparent && !water) {
        sum.w   = min(sum.w, textureShadowPCF(shadowtex1, pos));
    }
    
    if (water) {
        float s0depth = texture(shadowtex0, pos.xy).x;
        float surfaceDist  = distance(s0depth, pos.z) * shadowmapDepthScale;

        float caustic     = (2.5 * color.z * color.z);

        vec3 absorb     = expf(-max0(surfaceDist * waterDensity) * waterAbsorbCoeff);
            absorb     *= mix(caustic, 1.0, expf(-max0(surfaceDist) * 0.2));
            
        sum.rgb = absorb;
    }

    return sum;
}
#endif

#ifdef SOLIDPASS
vec3 getShadowmapPos(vec3 scenePos, const float bias, out float warp, out bool clipped) {
    vec3 pos    = scenePos + vec3(bias) * lightvec;
    float a     = length(pos);
        pos     = viewMAD(shadowModelView, pos);
        pos     = projMAD(shadowProjection, pos);
        pos.z  *= 0.2;
        clipped = pos.z > 1.0;
        pos.z  -= 0.0012*(saturate(a/256.0));

        warp    = 1.0;
        pos.xy  = shadowmapWarp(pos.xy, warp);

    return pos*0.5+0.5;
}

#ifndef SHADOW_PREPASS
float shadowVPSSigma(sampler2D tex, vec3 pos, vec2 posUnwarped, float warp) {
    const uint iterations = 6;

    const float penumbraScale = tan(radians(0.3));

    float penumbraMax   = shadowmapDepthScale * penumbraScale * shadowProjection[0].x;
    float searchRad     = min(0.5 * shadowProjection[0].x, penumbraMax / tau);

    float maxRad        = max(searchRad, 2.0 / shadowmapSize.x / warp);

    float minDepth      = pos.z - maxRad / penumbraMax / tau;
    float maxDepth      = pos.z;

    const uint itSquare = iterations * iterations;
    const float w = 1.0 / float(itSquare);

    float weightSum = 0.0;
    float depthSum  = 0.0;

    float dither    = ditherGradNoiseTemporal();

    for (uint i = 0; i<iterations; ++i) {
        vec2 offset     = R2((i + dither) * 64.0);
            offset      = vec2(cos(offset.x * tau), sin(offset.x * tau)) * sqrt(offset.y);            

        vec2 searchPos  = posUnwarped + offset * searchRad;
            searchPos   = shadowmapWarp(searchPos) * 0.5 + 0.5;

        float depth     = texelFetch(tex, ivec2(searchPos * shadowmapSize), 0).x;
        float weight    = step(depth, pos.z);

            depthSum   += weight * clamp(depth, minDepth, maxDepth);
            weightSum  += weight;
    }
    depthSum   /= weightSum > 0.0 ? weightSum : 1.0;

    float sigma     = weightSum > 0.0 ? (pos.z - depthSum) * penumbraMax : 0.0;

    return max0(sigma) * rcp(warp);
}

shadowData getShadowRegular(bool diffLit, vec3 scenePos) {
    const float bias    = 0.08*(2048.0/shadowMapResolution);

    shadowData data     = shadowData(1.0, vec3(1.0), vec3(0.0));

    if (diffLit) {     
        #ifdef shadowVPSEnabled
            vec3 pos        = scenePos + vec3(bias) * lightvec;
            float a         = length(pos);
                pos         = viewMAD(shadowModelView, pos);
                pos         = projMAD(shadowProjection, pos);

                pos.z      *= 0.2;

            if (pos.z > 1.0) return data;

                pos.z      -= 0.0012*(saturate(a/256.0));

            vec2 posUnwarped = pos.xy;

            float warp      = 1.0;
                pos.xy      = shadowmapWarp(pos.xy, warp);
                pos         = pos * 0.5 + 0.5;

            float sigma     = shadowVPSSigma(shadowtex0, pos, posUnwarped, warp);

                pos.z      -= (sigma * warp);

            vec4 shadow     = shadowFiltered(pos, sigma);

            data.shadow     = shadow.w;
            data.color      = shadow.rgb;
        #else
            float warp      = 1.0;
            bool clipped    = false;
            vec3 pos        = getShadowmapPos(scenePos, bias, warp, clipped);

            if (clipped) return data;

            vec4 shadow     = shadowFiltered(pos, 0.005 * warp);

            data.shadow     = shadow.w;
            data.color      = shadow.rgb;
        #endif
    }

    return data;
}
#endif

#else
shadowData getShadowRegular(bool diffLit) {
    shadowData data     = shadowData(1.0, vec3(1.0), vec3(0.0));

    if (diffLit) {
        vec3 pos        = shadowmapPos;
        #ifdef shadowFilterHQ
            float s0        = getShadowSoftHQ(shadowtex0, pos);
            float s1        = getShadowSoftHQ(shadowtex1, pos);
        #else
            float s0        = getShadowSoft(shadowtex0, pos);
            float s1        = getShadowSoft(shadowtex1, pos);
        #endif

        bool translucent = distance(s0, s1)>0.1;

        data.shadow   = s1;

        if (translucent) {
            float s0depth = textureLod(shadowtex0, pos.xy, 0).x;
            float dist  = distance(s0depth, pos.z) * 2.0 * 256.0 * rcp(0.2);
            data.color  = getShadowcol(shadowcolor0, pos.xy, dist);
        }
    }

    return data;
}
#endif