#ifndef ASTRAL_LIVING_WATER_GLSL
#define ASTRAL_LIVING_WATER_GLSL

uniform sampler2D astralWaterCycleAtlas;

float astral_water_mirror(float value) {
    float folded = fract(value * 0.5) * 2.0;
    return 1.0 - abs(folded - 1.0);
}

vec3 astral_water_atlas_sample(vec2 uv, float sliceCenter) {
    vec2 foldedUv = vec2(
        astral_water_mirror(uv.x),
        astral_water_mirror(uv.y));
    vec2 atlasUv = vec2(
        mix(0.012, 0.988, foldedUv.x),
        sliceCenter + (foldedUv.y - 0.5) * 0.476);
    return texture2D(astralWaterCycleAtlas, atlasUv).rgb;
}

float astral_water_night_amount() {
    float cycle = fract(float(worldTime) / 24000.0);
    float enterNight = smoothstep(0.515, 0.695, cycle);
    float leaveNight = 1.0 - smoothstep(0.865, 0.995, cycle);
    return enterNight * leaveNight;
}

float astral_water_twilight_amount() {
    float cycle = fract(float(worldTime) / 24000.0);
    float dusk = smoothstep(0.455, 0.535, cycle)
        * (1.0 - smoothstep(0.625, 0.705, cycle));
    float lateDawn = smoothstep(0.905, 0.975, cycle);
    float earlyDawn = 1.0 - smoothstep(0.010, 0.080, cycle);
    return clamp(max(dusk, max(lateDawn, earlyDawn)), 0.0, 1.0);
}

void astral_water_time_palette(
        out vec3 shallowColor,
        out vec3 deepColor,
        out vec3 crestColor) {
    float cycle = fract(float(worldTime) / 24000.0);
    float dawnToNoon = smoothstep(0.015, 0.245, cycle);
    float noonToDusk = smoothstep(0.285, 0.505, cycle);
    float duskToNight = smoothstep(0.515, 0.705, cycle);
    float nightToDawn = smoothstep(0.875, 0.998, cycle);

    // Restrained optical-density palettes.  They are intentionally darker
    // than the highlights so the water can keep real reflection headroom.
    const vec3 dawnShallow = vec3(0.055, 0.170, 0.235);
    const vec3 dawnDeep = vec3(0.020, 0.055, 0.125);
    const vec3 dawnCrest = vec3(0.72, 0.38, 0.22);
    const vec3 noonShallow = vec3(0.035, 0.225, 0.315);
    const vec3 noonDeep = vec3(0.014, 0.070, 0.165);
    const vec3 noonCrest = vec3(0.32, 0.67, 0.82);
    const vec3 duskShallow = vec3(0.050, 0.115, 0.175);
    const vec3 duskDeep = vec3(0.018, 0.035, 0.090);
    const vec3 duskCrest = vec3(0.62, 0.30, 0.36);
    const vec3 nightShallow = vec3(0.035, 0.085, 0.205);
    const vec3 nightDeep = vec3(0.015, 0.025, 0.095);
    const vec3 nightCrest = vec3(0.19, 0.43, 0.72);

    shallowColor = mix(dawnShallow, noonShallow, dawnToNoon);
    deepColor = mix(dawnDeep, noonDeep, dawnToNoon);
    crestColor = mix(dawnCrest, noonCrest, dawnToNoon);
    shallowColor = mix(shallowColor, duskShallow, noonToDusk);
    deepColor = mix(deepColor, duskDeep, noonToDusk);
    crestColor = mix(crestColor, duskCrest, noonToDusk);
    shallowColor = mix(shallowColor, nightShallow, duskToNight);
    deepColor = mix(deepColor, nightDeep, duskToNight);
    crestColor = mix(crestColor, nightCrest, duskToNight);
    shallowColor = mix(shallowColor, dawnShallow, nightToDawn);
    deepColor = mix(deepColor, dawnDeep, nightToDawn);
    crestColor = mix(crestColor, dawnCrest, nightToDawn);
}

void getAstralLivingWater(
        vec2 worldXZ,
        float waterDepth,
        out vec3 baseColor,
        out vec3 crestColor,
        out float materialPattern,
        out float materialEnergy,
        out float nightAmount) {
    #ifndef ASTRAL_LIVING_WATER
        baseColor = mix(
            vec3(0.06, 0.18, 0.28),
            vec3(0.05, 0.04, 0.18), waterDepth);
        crestColor = vec3(0.30, 0.65, 0.90);
        materialPattern = 1.0;
        materialEnergy = 0.0;
        nightAmount = astral_water_night_amount();
    #else
        float clock = frameTimeCounter * ASTRAL_WATER_MOTION;
        nightAmount = astral_water_night_amount();

        // Slow non-linear domain deformation.  The atlas never translates as
        // one rigid sheet: two independently bent currents cross and shear.
        float bendA = sin(worldXZ.y * 0.027 + clock * 0.052);
        float bendB = cos(worldXZ.x * 0.023 - clock * 0.041);
        float crossBend = sin(dot(worldXZ, vec2(0.017, -0.021))
            + clock * 0.028);
        vec2 domainWarp = vec2(
            bendA + crossBend * 0.48,
            bendB - crossBend * 0.42) * 0.72;
        vec2 warpedWorld = worldXZ + domainWarp;

        float directionalShear = sin(dot(warpedWorld,
            vec2(0.020, 0.014)) - clock * 0.023);
        vec2 primaryCurrent = warpedWorld * 0.028
            + vec2(clock * 0.0020, clock * 0.0011)
            + vec2(directionalShear, -directionalShear) * 0.026;
        vec2 counterCurrent = vec2(-warpedWorld.y, warpedWorld.x) * 0.067
            + vec2(-clock * 0.0031, clock * 0.0017)
            + domainWarp.yx * 0.020;

        vec3 dayPrimary = astral_water_atlas_sample(primaryCurrent, 0.25);
        vec3 daySecondary = astral_water_atlas_sample(counterCurrent, 0.25);
        vec3 nightPrimary = astral_water_atlas_sample(
            primaryCurrent * 0.91 + domainWarp * 0.014, 0.75);
        vec3 nightSecondary = astral_water_atlas_sample(
            -counterCurrent * 0.87 + domainWarp * 0.018, 0.75);

        vec3 dayField = mix(dayPrimary, daySecondary, 0.27);
        vec3 nightField = mix(nightPrimary, nightSecondary, 0.31);

        float dayLuma = dot(dayField, vec3(0.2126, 0.7152, 0.0722));
        float nightLuma = dot(nightField,
            vec3(0.2126, 0.7152, 0.0722));
        float dayContrast = abs(dot(dayPrimary - daySecondary,
            vec3(0.2126, 0.7152, 0.0722)));
        float nightContrast = abs(dot(nightPrimary - nightSecondary,
            vec3(0.2126, 0.7152, 0.0722)));
        float dayStructure = smoothstep(0.34, 0.80,
            dayLuma * 0.86 + dayContrast * 0.55);
        float nightChromatic = max(
            nightField.b - nightField.r * 0.58,
            nightField.r - nightField.g * 0.72);
        float nightStructure = smoothstep(0.20, 0.66,
            nightLuma * 0.46 + nightChromatic * 0.38
                + nightContrast * 0.62);
        float structure = mix(dayStructure, nightStructure, nightAmount);
        materialPattern = mix(0.90, 1.08, structure);

        // Energy is a sparse highlight mask, never the opacity of the entire
        // texture.  Only intersections between both currents can illuminate.
        float dayPearl = smoothstep(0.58, 0.91,
            dayLuma + dayContrast * 0.65);
        float cosmicVeins = smoothstep(0.38, 0.78,
            nightChromatic + nightContrast * 0.82);
        materialEnergy = mix(dayPearl * 0.32,
            cosmicVeins * 0.46, nightAmount);

        vec3 shallowColor;
        vec3 deepColor;
        astral_water_time_palette(
            shallowColor, deepColor, crestColor);
        baseColor = mix(shallowColor, deepColor,
            smoothstep(0.02, 0.96, waterDepth));

        vec3 fieldHue = mix(dayField, nightField, nightAmount);
        float fieldLuma = max(dot(
            fieldHue, vec3(0.2126, 0.7152, 0.0722)), 0.035);
        fieldHue = clamp(fieldHue / fieldLuma, vec3(0.45), vec3(1.65));
        baseColor *= mix(vec3(1.0), fieldHue, 0.07);
        baseColor *= ASTRAL_WATER_INTENSITY;
    #endif
}

// Reflection is deliberately composed before this layer.  This keeps the
// generated material visible as moving surface currents instead of allowing
// a strong sky reflection to erase it.
vec3 applyAstralLivingWaterSurface(
        vec3 composedWater,
        vec3 baseColor,
        vec3 crestColor,
        float materialPattern,
        float materialEnergy,
        float nightAmount,
        float visibleSky,
        float rainAmount,
        float viewDistance) {
    #ifndef ASTRAL_LIVING_WATER
        return composedWater;
    #else
        float structure = clamp(
            (materialPattern - 0.90) / 0.18, 0.0, 1.0);
        float twilightAmount = astral_water_twilight_amount();
        float dayAmount = 1.0 - nightAmount;
        float signedStructure = structure * 2.0 - 1.0;
        float softBody = smoothstep(0.16, 0.62, structure)
            * (1.0 - smoothstep(0.72, 0.97, structure));
        float trough = smoothstep(0.24, 0.86, 1.0 - structure);
        float filament = smoothstep(0.70, 0.96, structure)
            * smoothstep(0.08, 0.56, materialEnergy);

        float weatherVisibility = 1.0
            - clamp(rainAmount, 0.0, 1.0) * 0.48;
        float skyVisibility = mix(0.38, 1.0,
            clamp(visibleSky, 0.0, 1.0));
        // Suppress compressed high-frequency detail near the horizon.
        float distanceDetail = 1.0 - smoothstep(70.0, 250.0,
            max(viewDistance, 0.0));
        float layerVisibility = weatherVisibility * skyVisibility
            * mix(0.18, 1.0, distanceDetail)
            * clamp(ASTRAL_WATER_INTENSITY, 0.0, 1.3);

        // DAY / TWILIGHT: one natural-water hue correction and one pearl
        // relief.  The former greyscale twilight pass, body-tint layer and
        // separate trough layer were deliberately removed.
        float composedLuma = dot(composedWater,
            vec3(0.2126, 0.7152, 0.0722));
        float baseLuma = max(dot(baseColor,
            vec3(0.2126, 0.7152, 0.0722)), 0.018);
        vec3 naturalWaterHue = clamp(baseColor / baseLuma,
            vec3(0.38), vec3(2.15)) * composedLuma;
        float hueCorrection = mix(0.115, 0.155, twilightAmount)
            * weatherVisibility * skyVisibility;
        vec3 dayResult = mix(composedWater, naturalWaterHue,
            hueCorrection);

        float dayRelief = mix(0.046, 0.034, twilightAmount)
            * mix(0.58, 1.0, distanceDetail);
        dayResult *= 1.0 + signedStructure
            * dayRelief * layerVisibility;

        float daylightPearl = smoothstep(0.52, 0.88, structure)
            * smoothstep(0.025, 0.24, materialEnergy)
            * dayAmount;
        vec3 daylightPearlColor = mix(
            naturalWaterHue, crestColor, 0.38);
        dayResult += daylightPearlColor * daylightPearl
            * mix(0.028, 0.020, twilightAmount) * layerVisibility;

        // MIDNIGHT: preserve the already approved cosmic-water response in a
        // separate branch so simplifying daytime cannot flatten it.
        vec3 nightResult = composedWater
            * (1.0 + signedStructure * 0.040 * layerVisibility);
        vec3 nightBodyTint = mix(
            composedWater, baseColor * 1.24, 0.18);
        nightResult = mix(nightResult, nightBodyTint,
            softBody * 0.10 * layerVisibility);
        nightResult = mix(nightResult,
            nightResult * 0.965 + baseColor * 0.035,
            trough * 0.09 * layerVisibility);
        vec3 filamentColor = mix(baseColor * 1.18, crestColor, 0.58);
        nightResult += filamentColor * filament
            * 0.022 * layerVisibility;

        return mix(dayResult, nightResult,
            smoothstep(0.18, 0.88, nightAmount));
    #endif
}

vec2 getAstralWaterNormalFlow(vec2 worldXZ) {
    #ifndef ASTRAL_LIVING_WATER
        return vec2(0.0);
    #else
        float clock = frameTimeCounter * ASTRAL_WATER_MOTION;
        float nightAmount = astral_water_night_amount();
        float twilightAmount = astral_water_twilight_amount();
        vec2 normalWarp = vec2(
            sin(worldXZ.y * 0.031 + clock * 0.047),
            cos(worldXZ.x * 0.026 - clock * 0.039)) * 0.62;
        vec3 dayField = astral_water_atlas_sample(
            (worldXZ + normalWarp) * 0.039
                + vec2(clock * 0.0022, clock * 0.0013), 0.25);
        vec3 nightField = astral_water_atlas_sample(
            vec2(-(worldXZ.y + normalWarp.y),
                worldXZ.x + normalWarp.x) * 0.034
                + vec2(-clock * 0.0017, clock * 0.0024), 0.75);
        vec3 field = mix(dayField, nightField, nightAmount);
        float normalStrength = mix(0.045, 0.13, nightAmount)
            * mix(1.0, 0.78, twilightAmount);
        return vec2(field.r - field.g, field.g - field.b)
            * normalStrength;
    #endif
}

#endif // ASTRAL_LIVING_WATER_GLSL
