#ifndef ASTRAL_END_COSMOS_GLSL
#define ASTRAL_END_COSMOS_GLSL

// Two independently generated materials build The End. The sky material owns
// the entire animated galaxy; the former rift material is sampled only as
// microscopic spectral detail inside sparse embedded fractures.
uniform sampler2D astralEndCosmosMaterial;
uniform sampler2D astralEndSkyMaterial;

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

vec2 end_cosmos_mirror_uv(vec2 uv) {
    return vec2(end_cosmos_mirror(uv.x), end_cosmos_mirror(uv.y));
}

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

// Distance between the nearest two animated cellular sites. A value near
// zero lies on a natural Voronoi fracture, while larger values are intact
// galactic material. The sites move independently and very slowly, deforming
// the topology without translating a crack texture over the sky.
float end_cosmos_cellular_gap(
        vec2 p, float clock, float seed, out float identity) {
    vec2 baseCell = floor(p);
    vec2 localPosition = fract(p);
    float nearestDistance = 16.0;
    float secondDistance = 16.0;
    float nearestIdentity = 0.0;

    for (int y = -1; y <= 1; y++) {
        for (int x = -1; x <= 1; x++) {
            vec2 neighbor = vec2(float(x), float(y));
            vec2 cellId = baseCell + neighbor;
            vec2 randomPoint = rift_hash22(
                cellId + vec2(seed, seed * 1.71));
            float pointIdentity = rift_hash12(
                cellId + vec2(seed * 2.13, seed * 0.77));
            float pointClock = clock
                * (0.010 + pointIdentity * 0.006);
            randomPoint = 0.18 + randomPoint * 0.64;
            randomPoint += vec2(
                sin(pointClock + pointIdentity * 13.7),
                cos(pointClock * 0.83 + pointIdentity * 17.1))
                * 0.050;
            vec2 delta = neighbor + randomPoint - localPosition;
            float distanceSquared = dot(delta, delta);
            if (distanceSquared < nearestDistance) {
                secondDistance = nearestDistance;
                nearestDistance = distanceSquared;
                nearestIdentity = pointIdentity;
            } else if (distanceSquared < secondDistance) {
                secondDistance = distanceSquared;
            }
        }
    }

    identity = nearestIdentity;
    return max(sqrt(secondDistance) - sqrt(nearestDistance), 0.0);
}

// Animate the inherited End star field in direction space. This keeps stars
// attached to a celestial sphere while making the sphere precess, flex and
// drift; it never becomes a flat UV layer sliding across the screen.
vec3 getAstralEndAnimatedStarDirection(vec3 viewDirection) {
    vec3 direction = normalize(viewDirection);
    float starClock = astral_end_animation_time()
        * ASTRAL_END_COSMOS_MOTION;
    float yaw = starClock * 0.0068
        + sin(starClock * 0.011) * 0.075;
    direction.xz = end_cosmos_rotation(yaw) * direction.xz;
    float pitch = sin(starClock * 0.0083 + 1.4) * 0.052;
    direction.yz = end_cosmos_rotation(pitch) * direction.yz;
    vec3 stellarWarp = vec3(
        sin(direction.y * 7.1 + starClock * 0.041),
        cos(direction.z * 6.3 - starClock * 0.033),
        sin(direction.x * 8.4 + starClock * 0.037 + 2.1));
    return normalize(direction + stellarWarp * 0.0095);
}

// Calm material for the sky outside the dimensional tear.  Two differently
// stretched projections are domain-warped by the original rift FBM, so the
// generated artwork deforms and exchanges detail instead of translating as
// one rigid layer.
vec3 end_cosmos_background(vec3 viewDir, float textureClock) {
    vec2 skyUv = rift_octahedral_sky(viewDir);
    // The two flow fields evolve at unrelated rates and recursively bend one
    // another. This is the same living-volume principle used by the Overworld
    // rift, now applied to every pixel of the End rather than only its mouth.
    float warpA = rift_fbm(skyUv * 3.10
        + vec2(textureClock * 0.031, -textureClock * 0.023));
    float warpB = rift_fbm(skyUv.yx * 4.25 + vec2(19.7, 7.3)
        + vec2(-textureClock * 0.026, textureClock * 0.037)
        + vec2(warpA - 0.5) * 0.62);
    float warpC = rift_fbm(skyUv * 6.7 + vec2(43.1, 17.8)
        + vec2(textureClock * 0.044, textureClock * 0.019)
        + vec2(warpB - warpA, warpA + warpB - 1.0) * 0.55);
    vec2 warp = (vec2(warpA, warpB) - 0.5) * 0.125;

    float skyStretchX = 1.0 + sin(
        textureClock * 0.047 + warpB * 4.0) * 0.075;
    float skyStretchY = 1.0 + cos(
        textureClock * 0.039 + warpA * 4.7) * 0.065;

    vec2 skyUvA = end_cosmos_rotation(
        0.31 + textureClock * 0.0048)
        * (skyUv * vec2(0.79 * skyStretchX, 0.68 * skyStretchY)
            + warp)
        + vec2(0.50 + textureClock * 0.0038,
            0.50 - textureClock * 0.0027);
    vec2 skyUvB = end_cosmos_rotation(
        -0.83 - textureClock * 0.0037)
        * (skyUv.yx * vec2(0.56 * skyStretchY, 0.91 * skyStretchX)
            - warp.yx * 0.82)
        + vec2(0.50 - textureClock * 0.0031,
            0.50 + textureClock * 0.0042);
    vec2 skyUvC = end_cosmos_rotation(
        1.41 + sin(textureClock * 0.017) * 0.12)
        * (skyUv * vec2(1.07, 0.49)
            + vec2(warp.y, -warp.x) * 0.68)
        + vec2(0.50 + textureClock * 0.0019,
            0.50 + textureClock * 0.0024);
    vec3 skyLayerA = texture2D(astralEndSkyMaterial,
        end_cosmos_mirror_uv(skyUvA)).rgb;
    vec3 skyLayerB = texture2D(astralEndSkyMaterial,
        end_cosmos_mirror_uv(skyUvB)).rgb;
    vec3 skyLayerC = texture2D(astralEndSkyMaterial,
        end_cosmos_mirror_uv(skyUvC)).rgb;
    float exchange = 0.5 + 0.5 * sin(textureClock * 0.041
        + warpA * 4.3 - warpB * 3.1 + warpC * 2.2);
    vec3 skyMaterial = mix(skyLayerA, skyLayerB,
        0.24 + exchange * 0.30);
    skyMaterial = mix(skyMaterial, skyLayerC,
        0.10 + (1.0 - exchange) * 0.16);

    float luma = dot(skyMaterial, vec3(0.2126, 0.7152, 0.0722));
    float structure = smoothstep(0.055, 0.43, luma);
    float cyan = max(skyMaterial.b - skyMaterial.r * 0.62, 0.0);
    float violet = max(skyMaterial.r - skyMaterial.g * 0.78, 0.0);
    // A visible amethyst floor prevents empty regions of the generated image
    // from collapsing into display black after tone mapping.
    vec3 background = vec3(0.018, 0.013, 0.043);
    background += skyMaterial * (0.125 + structure * 0.095);
    background += vec3(0.015, 0.034, 0.064) * cyan * 0.075;
    background += vec3(0.058, 0.014, 0.070) * violet * 0.105;
    // Moving gas continents change the silhouette and local exposure of the
    // painted field. The texture therefore grows, thins and folds instead of
    // behaving like a rotating skybox.
    float gasBody = smoothstep(0.30, 0.71,
        warpA * 0.42 + warpB * 0.34 + warpC * 0.24);
    vec3 paletteAxis = normalize(vec3(
        cos(textureClock * 0.021), 0.31,
        sin(textureClock * 0.021)));
    float paletteTravel = 0.5 + 0.5 * dot(viewDir, paletteAxis);
    background += mix(vec3(0.015, 0.034, 0.068),
        vec3(0.070, 0.016, 0.076), paletteTravel)
        * gasBody * (0.046 + warpC * 0.036);
    background *= 0.92 + 0.08 * sin(
        textureClock * 0.031 + warpA * 5.0 + warpC * 2.3);

    // A broad, warm-violet horizon response separates terrain silhouettes
    // from the void without creating a visible horizontal fog stripe.
    float horizonLift = 1.0 - smoothstep(
        0.04, 0.62, abs(viewDir.y));
    background += vec3(0.020, 0.013, 0.030)
        * horizonLift * (0.55 + gasBody * 0.45);

    // Reduce the global cyan cast while retaining genuinely cyan nebular
    // filaments. This operates on the ambient field, not on bright stars.
    float backgroundLuma = dot(background,
        vec3(0.2126, 0.7152, 0.0722));
    vec3 neutralBackground = vec3(backgroundLuma)
        * vec3(1.04, 0.99, 1.03);
    background = mix(neutralBackground, background, 0.84);

    // Two decorrelated drifting particle fields make the star population
    // travel with the cosmic current. The generated material still supplies
    // its own tiny stars, producing several independent depth layers.
    float starA = rift_star_layer(
        skyUv * 96.0
            + vec2(textureClock * 0.034, -textureClock * 0.019)
            + warp * 7.0,
        0.990, textureClock * 1.9, 317.0);
    float starB = rift_star_layer(
        end_cosmos_rotation(0.72) * skyUv * 61.0
            + vec2(-textureClock * 0.021, textureClock * 0.029)
            - warp.yx * 5.0,
        0.986, textureClock * 1.3, 431.0);
    background += vec3(0.17, 0.48, 1.12) * starA * 0.38;
    background += vec3(0.66, 0.24, 0.96) * starB * 0.24;
    return background;
}

vec3 getAstralEndCosmos(vec3 viewDirection, vec3 baseSkyColor) {
    #ifndef ASTRAL_END_COSMOS
        return baseSkyColor;
    #else
        vec3 viewDir = normalize(viewDirection);
        float clock = astral_end_animation_time()
            * ASTRAL_END_COSMOS_MOTION;
        float textureClock = clock * 1.08;
        float fractureScale = max(
            ASTRAL_END_COSMOS_SCALE, 0.55);
        vec3 cosmos = end_cosmos_background(viewDir, textureClock);
        vec2 celestialUv = rift_octahedral_sky(viewDir);

        // Two moving domain fields make fractures flex with the existing
        // galaxy. Neither field defines a large opening or continuous ribbon.
        float fractureFlowA = rift_fbm(celestialUv * 3.7
            + vec2(textureClock * 0.016, -textureClock * 0.012));
        float fractureFlowB = rift_fbm(celestialUv.yx * 5.1
            + vec2(27.3, 61.9)
            + vec2(-textureClock * 0.013, textureClock * 0.018));
        vec2 fractureWarp = (vec2(
            fractureFlowA, fractureFlowB) - 0.5) * 0.19;

        float identityA;
        float identityB;
        vec2 fractureUvA = end_cosmos_rotation(
            0.28 + textureClock * 0.0012)
            * (celestialUv * (3.15 / fractureScale)
                + fractureWarp);
        vec2 fractureUvB = end_cosmos_rotation(
            -0.74 - textureClock * 0.0009)
            * (celestialUv * (6.4 / fractureScale)
                - fractureWarp.yx * 0.72)
            + vec2(13.7, 29.1);
        float gapA = end_cosmos_cellular_gap(
            fractureUvA, textureClock * 0.72, 521.0, identityA);
        float gapB = end_cosmos_cellular_gap(
            fractureUvB, textureClock * 0.91, 787.0, identityB);

        float aaA = max(fwidth(gapA), 0.0020);
        float aaB = max(fwidth(gapB), 0.0020);
        float hairlineA = 1.0 - smoothstep(
            0.006 + aaA, 0.025 + aaA, gapA);
        float softEdgeA = 1.0 - smoothstep(
            0.020 + aaA, 0.095 + aaA, gapA);
        float hairlineB = 1.0 - smoothstep(
            0.004 + aaB, 0.020 + aaB, gapB);
        float softEdgeB = 1.0 - smoothstep(
            0.016 + aaB, 0.066 + aaB, gapB);

        // Large coherent gates dissolve most cellular edges. The result is a
        // handful of disconnected fracture families embedded in nebulae, not
        // a wireframe spread over the whole celestial sphere.
        float regionA = rift_fbm(fractureUvA * 0.31
            + vec2(textureClock * 0.0041, -textureClock * 0.0033));
        float regionB = rift_fbm(fractureUvB * 0.22 + vec2(41.2, 8.6)
            + vec2(-textureClock * 0.0037, textureClock * 0.0029));
        float gateA = smoothstep(0.61, 0.79, regionA)
            * smoothstep(0.18, 0.66, fractureFlowB);
        float gateB = smoothstep(0.70, 0.86, regionB)
            * (0.28 + softEdgeA * 0.72);
        float horizonFade = smoothstep(0.035, 0.19, viewDir.y);
        float crackA = hairlineA * gateA * horizonFade;
        float crackB = hairlineB * gateB * horizonFade;
        float crackGlow = (softEdgeA * gateA * 0.68
            + softEdgeB * gateB * 0.34) * horizonFade;

        // The former opening texture now contributes only microscopic color
        // variation along the hairlines. It can no longer cover any broad sky
        // area or replace the generated galactic background.
        vec2 detailUvA = celestialUv * vec2(1.18, 0.83)
            + fractureWarp * 0.42
            + vec2(textureClock * 0.0021, -textureClock * 0.0017)
            + vec2(0.5);
        vec2 detailUvB = end_cosmos_rotation(1.13)
            * celestialUv.yx * vec2(0.72, 1.27)
            - fractureWarp.yx * 0.36
            + vec2(-textureClock * 0.0016,
                textureClock * 0.0023) + vec2(0.5);
        vec3 crackDetailA = texture2D(astralEndCosmosMaterial,
            end_cosmos_mirror_uv(detailUvA)).rgb;
        vec3 crackDetailB = texture2D(astralEndCosmosMaterial,
            end_cosmos_mirror_uv(detailUvB)).rgb;
        vec3 crackDetail = mix(crackDetailA, crackDetailB,
            0.38 + fractureFlowA * 0.24);
        float detailLuma = dot(crackDetail,
            vec3(0.2126, 0.7152, 0.0722));

        float cyanWeight = clamp(crackDetail.b
            - crackDetail.r * 0.45 + identityA * 0.24, 0.0, 1.0);
        vec3 fractureColor = mix(
            vec3(0.18, 0.055, 0.40),
            vec3(0.025, 0.28, 0.62), cyanWeight);
        float irregularPulse = 0.78
            + 0.12 * sin(textureClock * 0.071 + identityA * 17.0)
            + 0.10 * sin(textureClock * 0.037
                + identityB * 23.0 + fractureFlowA * 4.0);

        // A very thin dark incision gives depth. The low-energy colored veil
        // is deliberately weaker than the surrounding galaxy, keeping the
        // fractures readable without becoming the new focal object.
        float darkIncision = clamp(
            crackA * 0.23 + crackB * 0.13, 0.0, 0.28);
        cosmos *= 1.0 - darkIncision;
        cosmos += fractureColor * crackGlow
            * (0.018 + detailLuma * 0.026) * irregularPulse;
        cosmos += fractureColor * (crackA + crackB * 0.62)
            * (0.040 + detailLuma * 0.038) * irregularPulse;

        return cosmos * max(ASTRAL_END_COSMOS_INTENSITY, 0.0);
    #endif
}

#endif // ASTRAL_END_COSMOS_GLSL
