#ifndef ASTRAL_DAYLIGHT_HALO_GLSL
#define ASTRAL_DAYLIGHT_HALO_GLSL

uniform sampler2D astralDayHaloTexture;

/* Astral Halo
 *
 * A daylight counterpart to the night rift, but not a second opening. The
 * effect is a sun-anchored optical fossil: broken orbital caustics, opaline
 * plasma cells and diffraction threads suspended behind the cloud layer.
 *
 * The generated artwork supplies only local structure. Geometry, motion,
 * color and visibility are rebuilt from the solar direction in shader space,
 * so the texture never appears as a square sky billboard.
 */

vec2 day_halo_mirror_uv(vec2 uv) {
    vec2 folded = fract(uv * 0.5) * 2.0;
    return 1.0 - abs(folded - 1.0);
}

mat2 day_halo_rotation(float angle) {
    float c = cos(angle);
    float s = sin(angle);
    return mat2(c, -s, s, c);
}

float day_halo_visibility(vec3 sunDirection) {
    float daylight = day_blend_float(0.0, 1.0, 0.0);
    daylight = smoothstep(0.08, 0.72, daylight);
    float sunAboveHorizon = smoothstep(-0.075, 0.16, sunDirection.y);
    float weatherVeil = 1.0 - smoothstep(
        0.008, 0.085, clamp(rainStrength, 0.0, 1.0));
    return daylight * sunAboveHorizon * weatherVeil;
}

vec4 getAstralDayHalo(
        vec3 viewDirection,
        vec3 sunWorldDirection,
        vec3 baseSkyColor) {
    #ifndef ASTRAL_DAY_HALO
        return vec4(0.0);
    #else
        vec3 viewDir = normalize(viewDirection);
        vec3 sunDir = normalize(sunWorldDirection);
        float visibility = day_halo_visibility(sunDir);
        if (visibility < 0.002 || viewDir.y < -0.035)
            return vec4(0.0);

        // A stable tangent frame makes the phenomenon follow the real sun
        // while remaining independent of screen orientation and camera FOV.
        vec3 frameUp = abs(sunDir.y) > 0.94
            ? vec3(1.0, 0.0, 0.0)
            : vec3(0.0, 1.0, 0.0);
        vec3 solarX = normalize(cross(frameUp, sunDir));
        vec3 solarY = normalize(cross(sunDir, solarX));
        vec2 solarPlane = vec2(
            dot(viewDir, solarX), dot(viewDir, solarY));
        float solarDot = clamp(dot(viewDir, sunDir), -1.0, 1.0);

        // Chord distance is continuous at the sun and avoids acos. It also
        // keeps rings circular on the celestial sphere at very wide FOVs.
        float radial = sqrt(max(2.0 - 2.0 * solarDot, 0.0));
        float angle = atan(solarPlane.y, solarPlane.x);
        float scale = max(ASTRAL_DAY_HALO_SCALE, 0.25);
        float clock = frameTimeCounter * ASTRAL_DAY_HALO_MOTION;

        // Two counter-rotating, differently stretched samples extract living
        // structure from the generated texture without translating the image
        // as one recognizable sheet. Mirrored addressing removes edge seams.
        vec2 rotatedA = day_halo_rotation(clock * 0.0065) * solarPlane;
        vec2 rotatedB = day_halo_rotation(-0.73 - clock * 0.0041)
            * solarPlane.yx;
        vec2 driftA = vec2(
            sin(clock * 0.021), cos(clock * 0.017)) * 0.026;
        vec2 driftB = vec2(
            cos(clock * 0.013), sin(clock * 0.019)) * 0.031;
        vec2 textureUvA = rotatedA * vec2(1.34, 1.08) / scale
            + vec2(0.5) + driftA;
        vec3 textureA = texture2D(
            astralDayHaloTexture,
            day_halo_mirror_uv(textureUvA)).rgb;

        #if PROFILE_QUALITY == 1
            vec3 textureField = textureA;
        #else
            vec2 textureUvB = rotatedB * vec2(1.03, 1.47) / scale
                + vec2(0.5) + driftB;
            vec3 textureB = texture2D(
                astralDayHaloTexture,
                day_halo_mirror_uv(textureUvB)).rgb;
            vec3 textureField = mix(textureA, textureB, 0.43);
        #endif

        float textureLuma = dot(
            textureField, vec3(0.2126, 0.7152, 0.0722));
        float textureChroma = max(textureField.r,
            max(textureField.g, textureField.b))
            - min(textureField.r, min(textureField.g, textureField.b));
        float cellField = smoothstep(0.58, 0.92, textureLuma);
        float spectralField = smoothstep(0.035, 0.19, textureChroma);
        float filamentField = smoothstep(
            0.75, 0.985, textureLuma + textureChroma * 0.62);

        // Three incomplete orbital caustics form the large-scale silhouette.
        // Their centers breathe independently, while texture and angular
        // gates break every ring into asymmetric celestial remnants.
        float breathA = sin(clock * 0.013 + angle * 2.0) * 0.018;
        float breathB = sin(clock * 0.009 - angle * 3.0 + 1.7) * 0.026;
        float ringA = exp(-pow(
            (radial - (0.22 * scale + breathA * 0.55))
                / max(0.032 * scale, 0.012), 2.0));
        float ringB = exp(-pow(
            (radial - (0.47 * scale + breathB * 0.72))
                / max(0.052 * scale, 0.018), 2.0));
        float ringC = exp(-pow(
            (radial - (0.78 * scale - breathA * 0.48))
                / max(0.080 * scale, 0.026), 2.0));

        float angularGateA = smoothstep(0.18, 0.82,
            0.5 + 0.5 * sin(angle * 3.0 + clock * 0.018
                + textureLuma * 5.0));
        float angularGateB = smoothstep(0.12, 0.88,
            0.5 + 0.5 * cos(angle * 5.0 - clock * 0.011
                + spectralField * 3.7));
        float orbitalField = ringA
            * (0.26 + angularGateA * 0.74)
            + ringB * (0.18 + angularGateB * 0.82)
            + ringC * (0.13 + angularGateA * angularGateB * 0.87);
        orbitalField *= 0.30 + cellField * 0.43
            + filamentField * 0.27;

        // A broad, texture-shaped plasma lens connects the rings without
        // filling their center or resembling a galaxy body.
        float solarExclusion = smoothstep(0.045, 0.105, radial);
        float farFade = 1.0 - smoothstep(
            0.94 * scale, 1.36 * scale, radial);
        float plasmaEnvelope = smoothstep(
            0.075 * scale, 0.18 * scale, radial)
            * (1.0 - smoothstep(
                0.68 * scale, 1.02 * scale, radial));
        float plasmaCells = plasmaEnvelope
            * (cellField * 0.42 + spectralField * 0.24)
            * (0.58 + 0.42 * sin(
                clock * 0.024 + textureLuma * 5.8 + angle * 2.0));
        plasmaCells = max(plasmaCells, 0.0);

        // Fine radial diffraction appears only inside existing generated
        // filaments. It is optical texture, not a star field or lens flare.
        float rayPhase = 0.5 + 0.5 * sin(
            angle * 29.0 + radial * 11.0 - clock * 0.031);
        float diffraction = pow(max(rayPhase, 0.0), 9.0)
            * filamentField * plasmaEnvelope * 0.34;

        float horizonFade = smoothstep(-0.015, 0.16, viewDir.y);
        float backHemisphereFade = smoothstep(-0.30, -0.02, solarDot);
        float geometry = (orbitalField + plasmaCells + diffraction)
            * solarExclusion * farFade * horizonFade
            * backHemisphereFade;
        if (geometry < 0.0004) return vec4(0.0);

        float spectralAxis = clamp(
            textureField.b - textureField.r + 0.5, 0.0, 1.0);
        vec3 iceColor = vec3(0.23, 0.61, 1.05);
        vec3 opalColor = vec3(0.67, 0.49, 0.98);
        vec3 champagneColor = vec3(1.08, 0.72, 0.31);
        vec3 haloColor = mix(iceColor, opalColor, spectralAxis);
        haloColor = mix(haloColor, champagneColor,
            filamentField * (0.22 + angularGateB * 0.20));

        // Use the live sky as the translucent body and reserve colored energy
        // for the caustic filaments. This keeps block and sky exposure intact.
        float skyLuma = dot(max(baseSkyColor, vec3(0.0)),
            vec3(0.2126, 0.7152, 0.0722));
        // The old hard HDR gate reached zero in Astral Cinematic's authored
        // noon sky and erased the entire effect despite successful shader
        // compilation. A reciprocal guard retains contrast without ever
        // making the daylight signature mathematically invisible.
        float exposureGuard = clamp(
            1.0 / (1.0 + skyLuma * 0.22), 0.56, 1.0);
        float alpha = clamp(geometry
            * (0.085 + cellField * 0.070 + orbitalField * 0.075)
            * visibility * exposureGuard * ASTRAL_DAY_HALO_INTENSITY,
            0.0, 0.26);
        vec3 transmittedSky = max(baseSkyColor, vec3(0.0))
            * (0.88 + textureLuma * 0.12);
        vec3 radiance = transmittedSky * alpha;
        radiance += haloColor * geometry
            * (0.075 + filamentField * 0.110
                + orbitalField * 0.030)
            * visibility * exposureGuard * ASTRAL_DAY_HALO_INTENSITY;

        return vec4(max(radiance, vec3(0.0)), alpha);
    #endif
}

#endif // ASTRAL_DAYLIGHT_HALO_GLSL
