// ============================================================
//  voxy_common.glsl
//  Shared helpers for voxy_opaque.glsl / voxy_translucent.glsl
// ============================================================

#include "commonSettings.glsl"
#include "lib.glsl"

#define EMISSIVE_BASE_THRESHOLD 0.7
#define EMISSIVE_SATURATION_BOOST 0.5
#define EMISSIVE_WARM_COLOR_BOOST 0.05
#define EMISSIVE_COOL_COLOR_BOOST 0.4
#define EMISSIVE_GRAY_THRESHOLD 0.005814
#define EMISSIVE_GRAY_PENALTY 0.3
#define EMISSIVE_BOOST_STRENGTH 0.5

// Position reconstruction
vec3 voxy_viewPos() {
    vec2 uv = gl_FragCoord.xy / vec2(viewWidth, viewHeight);
    vec3 ndc = vec3(uv, gl_FragCoord.z) * 2.0 - 1.0;
    vec4 clip = vxProjInv * vec4(ndc, 1.0);
    return clip.xyz / clip.w;
}

vec3 voxy_worldPos(vec3 viewPos) {
    return (vxModelViewInv * vec4(viewPos, 1.0)).xyz;
}

// Face normal
// LOD terrain is axis-aligned, so we use a flat per-face normal
vec3 voxy_faceNormal(uint face) {
    return vec3(uint((face >> 1) == 2u), uint((face >> 1) == 0u), uint((face >> 1) == 1u))
         * (float(int(face) & 1) * 2.0 - 1.0);
}

// Paper gray-bias tone mapping (texture outline part of the
// style is skipped, it needs atlas + derivative access voxy
// fragments don't have)
vec3 voxy_applyGrayBias(vec3 inputColor, vec3 originalTexture, vec3 vertexColor) {
    float inputLuminance = dot(inputColor, vec3(0.299, 0.587, 0.114));
    float textureLuminance = dot(originalTexture, vec3(0.299, 0.587, 0.114));

    float inMidToneRange = smoothstep(GRAY_BIAS_RANGE_MIN, GRAY_BIAS_RANGE_MIN + 0.1, textureLuminance) *
                          (1.0 - smoothstep(GRAY_BIAS_RANGE_MAX - 0.1, GRAY_BIAS_RANGE_MAX, textureLuminance));

    float inBrightToneRange = smoothstep(GRAY_BIAS_RANGE_MAX - 0.1, GRAY_BIAS_RANGE_MAX + 0.05, textureLuminance) *
                             (1.0 - smoothstep(0.85, 0.95, textureLuminance));

    float totalBiasRange = inMidToneRange + inBrightToneRange * 0.4;

    float finalInRange = smoothstep(0.15, 0.25, inputLuminance) *
                        (1.0 - smoothstep(0.92, 0.98, inputLuminance));

    vec3 neutralVertex = vec3(1.0);
    float biomeStrength = length(vertexColor - neutralVertex);
    float isBiomeTinted = smoothstep(0.1, 0.3, biomeStrength);

    float baseBiasStrength = GRAY_BIAS_STRENGTH * totalBiasRange * finalInRange;

    float biomeBoost = GRAY_BIAS_STRENGTH * 0.4 * isBiomeTinted * finalInRange;
    biomeBoost *= (inMidToneRange + inBrightToneRange * 0.5);
    baseBiasStrength += biomeBoost;
    baseBiasStrength = min(baseBiasStrength, GRAY_BIAS_STRENGTH);

    float newLuminance = inputLuminance;
    if (baseBiasStrength > 0.0) {
        float biasTarget = TEXTURE_PAPER_WHITENESS;
        float luminanceBias = baseBiasStrength * (1.0 - inputLuminance) * (1.0 - inputLuminance);
        newLuminance = inputLuminance + (biasTarget - inputLuminance) * luminanceBias;
        newLuminance = clamp(newLuminance, inputLuminance, biasTarget);
    }

    vec3 result = inputColor;
    if (inputLuminance > 0.001 && newLuminance != inputLuminance) {
        float luminanceScale = newLuminance / inputLuminance;
        result = inputColor * luminanceScale;

        float grayness = 1.0 - length(inputColor - vec3(inputLuminance));
        grayness = smoothstep(0.8, 1.0, grayness);

        vec3 targetWhite = vec3(TEXTURE_PAPER_WHITENESS);
        if (isBiomeTinted > 0.5) {
            vec3 tintDirection = normalize(vertexColor);
            targetWhite = mix(targetWhite, targetWhite * tintDirection * 1.05, 0.1);
        }

        result = mix(result, targetWhite * (newLuminance / TEXTURE_PAPER_WHITENESS),
                    grayness * baseBiasStrength * 0.3);
    }

    if (baseBiasStrength > 0.0) {
        float brightnessMultiplier = GRAY_BIAS_BRIGHTNESS_LIFT * (1.0 - smoothstep(0.7, 0.9, newLuminance));
        float brightnessBias = baseBiasStrength * brightnessMultiplier * 0.5;
        result = result * (1.0 + brightnessBias);
    }

    return result;
}

// Fake shadow for LOD terrain
float voxy_softenSkyLight(float skyLight) {
    return min(skyLight, 0.92);
}

float voxy_pseudoAO(vec3 norm) {
    return mix(0.5, 1.0, norm.y * 0.5 + 0.5);
}

// Ambient compensation, scaled by time of day
// VOXY_AMBIENT_COMPENSATION exists because voxy has no real per-voxel occlusion
float voxy_ambientCompensation(float compensationDay, float compensationNight, float sunAngleIn) {
    return mix(compensationDay, compensationNight, getNightAmount(sunAngleIn));
}

float voxy_fakeShadow(float skyLight, vec3 norm) {
    return smoothstep(13.5 / 15.0, 14.5 / 15.0, skyLight) * voxy_pseudoAO(norm);
}

// Sun/moon twilight fade
float voxy_sunlightFade(float sunAngleIn) {
    float sunsetFade = 1.0 - smoothstep(0.49, 0.50, sunAngleIn) * (1.0 - smoothstep(0.507, 0.511, sunAngleIn));
    sunsetFade = clamp(sunsetFade, 0.0, 1.0);

    float wrappedAngle = sunAngleIn > 0.5 ? sunAngleIn - 1.0 : sunAngleIn;
    float sunriseFade = 1.0 - smoothstep(-0.03, 0.0, wrappedAngle) + smoothstep(0.0, 0.06, wrappedAngle);
    sunriseFade = clamp(sunriseFade, 0.0, 1.0);

    return min(sunsetFade, sunriseFade);
}

// Sky fog colour
vec3 voxy_skyFog(vec3 viewPos, float sunAngleIn, float rainStrengthIn, vec3 fogColorIn, vec3 skyColorIn, vec3 upPositionIn, int isEyeInWaterIn, float skyLightFactor) {
    float zSky = clamp(dot(normalize(viewPos), normalize(upPositionIn)), 0.0, 1.0);

    #if CUSTOM_SKY == 1
        vec3 skyFogCol;
        if (isEyeInWaterIn == 1) {
            skyFogCol = fogColorIn;
        } else {
            vec3 fogViewDir = normalize((vxModelViewInv * vec4(viewPos, 0.0)).xyz);
            vec3 horizonFog = customHorizonFogColor(sunAngleIn, fogViewDir, rainStrengthIn);
            skyFogCol = mix(horizonFog, skyColorIn, zSky);
        }
    #else
        vec3 skyFogCol = mix(fogColorIn, skyColorIn, zSky);
    #endif

    return mix(vec3(0.0), skyFogCol, skyLightFactor);
}

float voxy_fogFarDist() {
    return float(vxRenderDistance) * 16.0;
}

#define VOXY_WATER_FOG_DENSITY 0.1

vec3 voxy_applyFog(vec3 col, float rawDist, float farDist, int isEyeInWaterIn, float rainStrengthIn, vec3 skyFogCol, float nightVisionIn) {
    float fogDist = (isEyeInWaterIn == 1)
        ? 1.0 - exp(-rawDist * VOXY_WATER_FOG_DENSITY)
        : pow(clamp(rawDist / farDist, 0.0, 1.0), mix(3.0, 1.0, rainStrengthIn));

    vec3 nvSkyFog = skyFogCol;
    if (nightVisionIn > 0.0) {
        #define NV_BRIGHTNESS_LIFT    0.85
        #define NV_BRIGHTNESS_FLOOR   0.7
        #define NV_CONTRAST_BOOST     0.15
        #define NV_TINT_STRENGTH      0.5
        #define NV_TINT_R             0.55
        #define NV_TINT_G             0.75
        #define NV_TINT_B             0.60
        #define NV_TINT_BRIGHTNESS    1.4
        #define NV_DARK_CUTOFF        0.5

        float fogLuma = dot(skyFogCol, vec3(0.299, 0.587, 0.114));
        float darknessFactor = 1.0 - smoothstep(0.0, NV_DARK_CUTOFF, fogLuma);
        float strength = nightVisionIn * darknessFactor;
        nvSkyFog = mix(skyFogCol, vec3(max(fogLuma, NV_BRIGHTNESS_FLOOR)), strength * NV_BRIGHTNESS_LIFT);
        float nvLuma = dot(nvSkyFog, vec3(0.299, 0.587, 0.114));
        nvSkyFog = mix(vec3(nvLuma), nvSkyFog, 1.0 + NV_CONTRAST_BOOST * strength);
        nvSkyFog = mix(nvSkyFog, nvSkyFog * vec3(NV_TINT_R, NV_TINT_G, NV_TINT_B) * NV_TINT_BRIGHTNESS, strength * NV_TINT_STRENGTH);
        nvSkyFog = clamp(nvSkyFog, 0.0, 1.0);
    }

    return mix(col, nvSkyFog, fogDist);
}
