#ifndef ENDCRYSTAL_SAMPLER_DEFINE
    uniform isampler2D endcrystal_sampler;
#endif

const float healing_boundRadius = 6.0;
const float healing_ballRadius = 3.5;
const float healing_beamRadius = 0.6;
const float vortex_cylinderRadius = 3.0;
const float vortex_ballRadius = 5.0;
const float death_radius = 70.0;

#ifndef INCLUDE_ENDER_BEAMS
    #ifdef GBUFFERS_WATER
        float vlFactor = 0.5;
    #endif
#endif
vec3 beamPurple = normalize(endColorBeam * endColorBeam * endColorBeam) * (2.5 - 1.0 * vlFactor) * E_BEAM_I;

vec3 endDragonColM = sqrt(endOrangeCol);
vec3 beamColM = sqrt(beamPurple);

float GetBallRadius(float state) {
    return vortex_ballRadius * (1.0 + 4.0 * sqrt(1.0 - state));
}

float VortexWidth(float x, float ballRadius) {
    if (x > 0.5 * ballRadius) {
        float expScale = sqrt(0.75) * ballRadius - vortex_cylinderRadius;
        return vortex_cylinderRadius + expScale * exp( -sqrt(1.0/3.0) / expScale * (x - 0.5 * ballRadius));
    } else if (x > -ballRadius) {
        return sqrt(pow2(ballRadius) - pow2(x));
    }
    return 0.0;
}

vec4 SampleEndCrystalVortex(vec3 relPos, vec2 state, vec2 noiseOffset) {
    float thisBallRadius = GetBallRadius(state.x);

    float beamFactor = smoothstep(-thisBallRadius, thisBallRadius, relPos.y);
    float featureWidth = VortexWidth(relPos.y, thisBallRadius);
    vec2 horizontalScaledPos = featureWidth > 0.0 ? relPos.xz / featureWidth : vec2(2.0);
    float featureDist = length(horizontalScaledPos);
    if (length(relPos.xz) > featureWidth) {
        return vec4(0);
    }
    float beamStrength = 2.5 * beamFactor * (cos(featureDist * 3.1416) * 0.5 + 0.5) * pow2(max(0.0, 1 - pow2(0.005 / (0.9 * state.x + 0.1) / pow2(pow2(state.y)) * relPos.y))) * state.x;
    float spiralStrength = 200 * beamFactor * pow(featureDist, 7) * pow2(1.0 - featureDist) * pow2(max(0.0, 1 - pow2(0.02 / (0.6 * state.x * state.x + 0.4) / state.y * relPos.y)));
    float spiralAngle = (0.4 / vortex_cylinderRadius * relPos.y - 0.2 * pow2(min(0.0, -2.5 + relPos.y / thisBallRadius))) / (state.x + 0.2);
    vec2 spiralPos = mat2(cos(spiralAngle), -sin(spiralAngle), sin(spiralAngle), cos(spiralAngle)) * horizontalScaledPos;
    vec4 beamNoise = texture2DLod(noisetex, noiseOffset + 5.0 / noiseTextureResolution * horizontalScaledPos, 0.0);
    vec4 beamNoise2 = texture2DLod(noisetex, noiseOffset + 5.0 / noiseTextureResolution * vec2(relPos.y * 0.02 + 2.7 * beamNoise.gb - 3.6 * frameTimeCounter * 0.5), 0.0);
    vec4 spiralNoise = texture2DLod(noisetex, noiseOffset + 5.0 / noiseTextureResolution * spiralPos, 0.0);
    vec4 spiralNoise2 = texture2DLod(noisetex, noiseOffset + 20.0 / noiseTextureResolution * spiralPos, 0.0);
    return vec4(beamStrength * beamNoise.r * beamNoise2.r * endDragonColM + spiralStrength * pow2(spiralNoise.r) * (0.5 + spiralNoise2.r) * beamColM, beamStrength + spiralStrength) * 0.3;
}

vec4 SingleEndCrystalVortex(vec3 start, vec3 direction, vec3 center, vec2 state, float dither) {
    const float stepSize = 0.5;
    float invHorizontalDirLen = 1.0 / length(direction.xz);
    float thisBallRadius = GetBallRadius(state.x);
    float closestProgress = clamp(
        dot(center.xz - start.xz, direction.xz) * pow2(invHorizontalDirLen),
        -thisBallRadius * invHorizontalDirLen,
        1.0 + thisBallRadius * invHorizontalDirLen);
    vec3 closestPos = start + closestProgress * direction;
    float closestDist = length(closestPos.xz - center.xz);
    if (closestDist > thisBallRadius) {
        return vec4(0);
    }
    float startProgress = closestProgress - sqrt((thisBallRadius * thisBallRadius - closestDist * closestDist)) * invHorizontalDirLen;
    float endProgress = min(1.0, 2 * closestProgress - startProgress);
    startProgress = max(0.0, startProgress);
    vec2 noiseOffset = (center.xz + cameraPosition.xz + vec2(3.0, 1.6) * frameTimeCounter) * 0.005;
    vec4 colour = vec4(0);
    float dist = startProgress + dither * invHorizontalDirLen * stepSize;
    for (int k = 0; k < 100; k++) {
        if (dist > endProgress) break;
        colour += SampleEndCrystalVortex(start + dist * direction - center, state, noiseOffset);
        dist += invHorizontalDirLen * stepSize;
    }
    return colour * stepSize * smoothstep(0.0, 1.0, state.x);
}

float EndCrystalBeamWidth(float x, float len) {
    x = 0.5 * len - abs(x - 0.5 * len);
    if (x <= -healing_ballRadius) return 0.0;
    if (x < 0.5 * healing_ballRadius) return sqrt(pow2(healing_ballRadius) - pow2(x));
    float expScale = sqrt(0.75) * healing_ballRadius - healing_beamRadius;
    return healing_beamRadius + expScale * exp( -sqrt(1.0/3.0) / expScale * (x - 0.5 * healing_ballRadius));
}

vec4 SampleEndCrystalBeam(vec3 relPos, float len) {
    float beamWidth = EndCrystalBeamWidth(relPos.x, len);

    if (beamWidth > 0.0001) {
        float beamFactor = smoothstep(0.0, 2.0 * healing_ballRadius, 0.5 * len - abs(relPos.x - 0.5 * len));
        float noisyTime = frameTimeCounter + 0.4 * texture2DLod(noisetex, vec2(3.0 / noiseTextureResolution, frameTimeCounter / (0.45 * noiseTextureResolution)), 0.0).r;

        relPos.yz /= beamWidth;
        float strength = 0.0;
        vec3 healBeamColor = vec3(0);
        for (int k = 0; k < 3; k++) {
            vec2 noiseCoords = vec2(0.2 / noiseTextureResolution * relPos.x, 0 + vec2(k, 6 * k) / noiseTextureResolution);
            vec4 zapNoise0 = texture2DLod(noisetex, noiseCoords + floor(8.0 * noisyTime) / noiseTextureResolution, 0.0);
            vec4 zapNoise1 = texture2DLod(noisetex, 3.3 * noiseCoords + floor(8.0 * noisyTime) / noiseTextureResolution, 0.0);
            vec4 zapNoise2 = texture2DLod(noisetex, 6.8 * noiseCoords + (15.0 * frameTimeCounter) / noiseTextureResolution, 0.0);
            vec2 thisRelPos = relPos.yz + beamFactor / beamWidth * (6.0 * zapNoise0.rb + 1.6 * zapNoise1.rb + 1.2 * zapNoise2.rb - (3.0 + 0.8 + 0.6));
            vec4 sideNoise = texture2DLod(noisetex, (7.0 * thisRelPos.xy) / noiseTextureResolution, 0.0);
            vec3 colorNoise = texture2DLod(noisetex, 4.0 * noiseCoords + floor(12.0 * noisyTime) / noiseTextureResolution, 0.0).rgb;
            float centerDist0 = length(thisRelPos.xy);

            float centerDist = centerDist0 - 1.2;
            strength = max(strength, clamp( -centerDist, 0.0, 0.2) * pow2(max(0.0, 1.0 - pow2((centerDist0 - 1.0) * beamWidth * 0.5))) * mix(1.0, sideNoise.b, beamWidth / healing_ballRadius));
            healBeamColor = mix(clamp01(saturateColors(beamColM, 0.8) - sideNoise.rgb * 0.08), saturateColors(beamColM, 1.3) * 1.3, colorNoise);
        }
        return strength / beamWidth * vec4(healBeamColor * 0.5, 1.0) + 0.2 * beamFactor * exp(-6.0 * dot(relPos.yz, relPos.yz)) * vec4(endDragonColM * 2.2, 1.0);
    }
    return vec4(0.0);
}


vec4 EndCrystalBeam(vec3 start, vec3 direction, vec3 startPos, vec3 endPos, float dither) {
    vec3 startDiff = start - startPos;
    vec3 beamDirection = endPos - startPos;
    mat3 rotMat;
    rotMat[0] = normalize(beamDirection);
    rotMat[1] = normalize(cross(beamDirection, vec3(-2e-4, 1, 1e-5)));
    rotMat[2] = cross(rotMat[0], rotMat[1]);
    start *= rotMat;
    startPos *= rotMat;
    beamDirection *= rotMat;
    direction *= rotMat;
    const float stepSize = 0.5;
    float invHorizontalDirLen = 1.0 / length(direction.yz);
    float closestProgress = clamp(
        dot(startPos.yz - start.yz, direction.yz) * pow2(invHorizontalDirLen),
        -healing_boundRadius * invHorizontalDirLen,
        1.0 + healing_boundRadius * invHorizontalDirLen);
    vec3 closestPos = start + closestProgress * direction;
    float closestDist = length(closestPos.yz - startPos.yz);
    if (closestDist > healing_boundRadius) {
        return vec4(0);
    }
    float startProgress = closestProgress - sqrt((healing_boundRadius * healing_boundRadius - closestDist * closestDist)) * invHorizontalDirLen;
    float endProgress = min(1.0, 2 * closestProgress - startProgress);
    startProgress = max(0.0, startProgress);
    vec4 colour = vec4(0);
    float dist = startProgress + dither * invHorizontalDirLen * stepSize;
    for (int k = 0; k < 100; k++) {
        if (dist > endProgress) break;
        colour += SampleEndCrystalBeam(start + dist * direction - startPos, beamDirection.x);
        dist += invHorizontalDirLen * stepSize;
    }
    return 3.0 * log(length(colour) * stepSize + 1.0) * normalize(colour + 0.0000001);
}

float GetDragonDeathFactor(float dragonDeathTime) {
    return 0.02 * dragonDeathTime * exp(0.1 * dragonDeathTime);
}

vec4 SampleDeathBuildup(vec3 relPos, float dragonDeathTime) {
    float effectFactor = GetDragonDeathFactor(dragonDeathTime);
    float effectRadius = death_radius * effectFactor;
    float sizeNoiseFactor = 1.0 + 0.3 * texture2DLod(noisetex, vec2(0.2, dragonDeathTime * 5.0 / noiseTextureResolution), 0.0).r;
    float centerDist = length(relPos) / effectRadius;
    relPos *= sizeNoiseFactor;
    float angle = centerDist * 5.0 / log(dragonDeathTime * 0.6 + 1.0);
    mat2 rotMat = mat2(
        cos(angle), sin(angle),
       -sin(angle), cos(angle)
    );
    relPos.xz = rotMat * relPos.xz;
    vec2 val = pow(fract(hash23(floor(0.8 * relPos + 2.7 * sign(relPos) * exp(0.3 * dragonDeathTime)))), vec2(40.0 * pow2(centerDist))) * (1.0 - centerDist);
    return 0.1 * (vec4(beamColM, 1.0) * (val.x + 0.4 * exp(-8.0 * pow2(centerDist))) + vec4(endDragonColM, 1.0) * (val.y + 0.1 * exp(-3.0 * pow2(centerDist))));
}

vec4 DragonDeathAnimation(vec3 start, vec3 direction, vec3 dragonPos, float dragonDeathTime, float dragonDeathFactor, float dither) {
    float dirLen = length(direction);
    float closestProgress = dot(dragonPos - start, direction) / pow2(dirLen);
    vec4 colour = vec4(0);
    if (dragonDeathFactor >= 0.99) {
        float effectRadius = death_radius * GetDragonDeathFactor(dragonDeathTime);
        vec3 closestPos = start + closestProgress * direction;
        float closestDist = length(closestPos - dragonPos);
        if (closestDist >= effectRadius) return vec4(0.0);
        float stepSize = 0.5 / dirLen;
        float startProgress = closestProgress - sqrt(pow2(effectRadius) - pow2(closestDist)) / dirLen;
        float endProgress = min(1.0, 2.0 * closestProgress - startProgress);
        startProgress = max(0.0, startProgress);
        float dist = startProgress + stepSize * dither;
        for (int k = 0; k < 150; k++) {
            if (dist > endProgress) break;
            colour += SampleDeathBuildup(start + dist * direction - dragonPos, dragonDeathTime);
            dist += stepSize;
        }
        colour *= stepSize * dirLen;
    } else {
        vec3 closestPos = start + clamp(closestProgress, 0.0, 1.0) * direction;
        float closestDist = length(dragonPos - closestPos);
        colour = vec4(endDragonColM + 0.5 * beamColM, 1.0) * (0.4 * death_radius * (1.0 - exp(-dirLen/(4.0 * death_radius))) * exp(-10.0 * (1.0 - dragonDeathFactor) - closestDist * closestDist / (death_radius * death_radius)) * dragonDeathFactor);
    }
    return colour;
}

vec4 EndCrystalVortices(vec3 start, vec3 direction, float dither) {
    vec4 color = vec4(0);
    #if END_CRYSTAL_VORTEX_INTERNAL / 2 == 1 || DRAGON_DEATH_EFFECT_INTERNAL > 0
        ivec4 rawDragonPos = ivec4(
            texelFetch(endcrystal_sampler, ivec2(35, 5), 0).r,
            texelFetch(endcrystal_sampler, ivec2(35, 6), 0).r,
            texelFetch(endcrystal_sampler, ivec2(35, 7), 0).r,
            texelFetch(endcrystal_sampler, ivec2(35, 8), 0).r
        );
        vec3 dragonPos = rawDragonPos.xyz != ivec3(0) ? 0.0001 * rawDragonPos.xyz : vec3(0.5, 80.5, 0.5) - cameraPosition;
    #endif
    #if END_CRYSTAL_VORTEX_INTERNAL / 2 == 1
        vec3[15] healBeamEndPositions;
        int isTarget = 0;
        int healBeamCount = 15;
        for (int k = 0; k < 15; k++) {
            ivec4 rawPos = ivec4(
                texelFetch(endcrystal_sampler, ivec2(20 + k, 5), 0).r,
                texelFetch(endcrystal_sampler, ivec2(20 + k, 6), 0).r,
                texelFetch(endcrystal_sampler, ivec2(20 + k, 7), 0).r,
                texelFetch(endcrystal_sampler, ivec2(20 + k, 8), 0).r
            );
            if (rawPos.w == 0) {
                healBeamCount = k;
                break;
            }
            healBeamEndPositions[k] = vec3(rawPos.xyz) / rawPos.w;
            isTarget |= (length(healBeamEndPositions[k].xz + cameraPosition.xz - 0.5) < 4.5 || length(dragonPos - healBeamEndPositions[k]) < 5.0) ? 1 << k : 0;
        }
    #endif
    #if END_CRYSTAL_VORTEX_INTERNAL % 2 == 1
        for (int k = 0; k < 20; k++) {
            if (texelFetch(endcrystal_sampler, ivec2(k, 8), 0).r <= 0) continue;
            ivec4 rawPos = ivec4(
                texelFetch(endcrystal_sampler, ivec2(k, 5), 0).r,
                texelFetch(endcrystal_sampler, ivec2(k, 6), 0).r,
                texelFetch(endcrystal_sampler, ivec2(k, 7), 0).r,
                texelFetch(endcrystal_sampler, ivec2(k, 8), 0).r
            );
            if (rawPos.w <= 0) {
                continue;
            }
            int age = texelFetch(endcrystal_sampler, ivec2(k, 9), 0).r;
            vec3 pos = rawPos.xyz * 0.0001;
            #if END_CRYSTAL_VORTEX_INTERNAL / 2 == 1
                for (int i = 0; i < healBeamCount; i++) {
                    isTarget |= length(pos - healBeamEndPositions[i]) < 4.5 ? 1<<(i+15) : 0;
                }
            #endif
            vec2 state = vec2(clamp(rawPos.w / 15000.0, 0.0, 1.0), 1.00001 - exp(-0.0001 * age));
            if (length(pos) > min(shadowDistance, far) * 0.9 && state.x < 0.999) {
                state.y = state.x;
                state.x = 1.0;
            }
            vec4 thisVortexCol = pow2(SingleEndCrystalVortex(start, direction, pos, state, dither));
            color += thisVortexCol;
        }
    #endif
    #if END_CRYSTAL_VORTEX_INTERNAL / 2 == 1
        for (int k = 0; k < healBeamCount; k++) {
            for (int l = k+1; l < healBeamCount; l++) {
                if (
                    ((isTarget >> k & 1) == 0 ^^ (isTarget >> l & 1) == 0)
                    #if END_CRYSTAL_VORTEX_INTERNAL % 2 == 1
                        && ((isTarget >> k + 15 & 1) == 0 ^^ (isTarget >> l + 15 & 1) == 0)
                    #endif
                ) {
                    vec3 pos0 = healBeamEndPositions[k];
                    vec3 pos1 = healBeamEndPositions[l];
                    if (pos0.y > pos1.y) {
                        vec3 tmp = pos0;
                        pos0 = pos1;
                        pos1 = tmp;
                    }
                    color += pow2(EndCrystalBeam(start, direction, pos0, pos1, dither));
                }
            }
        }
    #endif
    #if DRAGON_DEATH_EFFECT_INTERNAL > 0
        int isDying = texelFetch(endcrystal_sampler, ivec2(35, 0), 0).r;
        float dragonDeathTime = 0.0001 * rawDragonPos.w;
        float dragonDeathFactor = 0.0001 * isDying;
        // dragonDeathTime = mod(frameTimeCounter, 22.0);
        // dragonDeathFactor = 2.2 - 0.1 * dragonDeathTime;
        // dragonPos = vec3(0, 80, 0) - cameraPosition;
        if (dragonDeathFactor > 0.001) {
            color += pow2(DragonDeathAnimation(start, direction, dragonPos, dragonDeathTime, dragonDeathFactor, dither));
        }
    #endif
    return sqrt(color) * (1.0 - maxBlindnessDarkness);
}