vec3 clipAABB(vec3 prevColor, vec3 minColor, vec3 maxColor) {
    vec3 pClip = 0.5 * (maxColor + minColor); // Center
    vec3 eClip = 0.5 * (maxColor - minColor); // Size

    vec3 vClip = prevColor - pClip;
    vec3 aUnit = abs(vClip / eClip);
    float denom = max(aUnit.x, max(aUnit.y, aUnit.z));

    return denom > 1.0 ? pClip + vClip / denom : prevColor;
}

vec3 neighbourhoodClipping(sampler2D currTex, vec3 CurrentColor, vec3 prevColor, out vec3 maxColor, ivec2 FragCoord) {
    vec3 minColor = CurrentColor;
    maxColor = CurrentColor;

    for (int x = -1; x <= 1; x++) {
        for (int y = -1; y <= 1; y++) {
            if (x == y && x == 0) continue;
            vec3 color = texelFetch2D(currTex, ivec2(FragCoord + vec2(x, y)), 0).rgb;
            minColor = min(minColor, color);
            maxColor = max(maxColor, color);
        }
    }
    return clipAABB(prevColor, minColor, maxColor);
}

vec3 get_closest_depth(ivec2 FragCoord, vec2 Texcoord, out bool IsDH) {
    vec3 MinDepth = vec3(FragCoord, get_depth_solid(Texcoord, IsDH));
    for (int i = -2; i <= 2; i += 4) {
        for (int j = -2; j <= 2; j += 4) {
            ivec2 OffsetCoords = FragCoord + ivec2(i, j);
            float NewDepth = texelFetch(depthtex1, OffsetCoords, 0).x;
            if (NewDepth < MinDepth.z) MinDepth = vec3(OffsetCoords, NewDepth);
        }
    }
    MinDepth.xy /= resolution;
    return vec3(MinDepth);
}

vec3 TAA(vec3 Color, ivec2 FragCoord, vec2 Texcoord) {
    bool IsDH;
    vec3 ClosestSample = get_closest_depth(FragCoord, Texcoord, IsDH);
    vec2 PrevCoord = Texcoord + toPrevScreenPos(ClosestSample.xy, ClosestSample.z, IsDH).xy - ClosestSample.xy;

    if (clamp(PrevCoord, 0, 1) != PrevCoord)
        return Color;

    vec3 PrevColor = texture_catmullrom_fast(colortex4, PrevCoord).rgb;
    if (PrevColor == vec3(0)) return Color;

    vec3 ClippingMaxColor;
    vec3 ClampedColor = neighbourhoodClipping(colortex0, Color, PrevColor, ClippingMaxColor, FragCoord);

    #if AA_MODE != 2
    float blendFactor = 0.6 + 0.2 * exp(-length((PrevCoord - Texcoord) * resolution));
    #else
    float blendFactor = 0.9;
    #endif

    // Jessie's offcenter rejection (reduce ghosting)
    vec2 pixelOffset = 1.0 - abs(2.0 * fract(PrevCoord * resolution) - 1.0);
    float OffcenterRejection = sqrt(pixelOffset.x * pixelOffset.y) * 0.15 + 0.85;
    blendFactor *= OffcenterRejection;

    #if AA_MODE == 2
    // Flicker reduction
    blendFactor = clamp(blendFactor + pow2(get_luminance((PrevColor - Color) / ClippingMaxColor)) * 0.15, 0, 1);
    #endif

    Color = mix(Color, ClampedColor, blendFactor);
    return Color;
}

// Temporal component for SMAA T2x
vec3 T2x(vec3 Color, ivec2 FragCoord, vec2 Texcoord) {
    bool IsDH;
    float Depth = get_depth_solid(Texcoord, IsDH);
    vec2 PrevCoord = toPrevScreenPos(Texcoord, Depth, IsDH).xy;

    if (clamp(PrevCoord, 0, 1) != PrevCoord)
        return Color;

    vec3 PrevColor = textureNicest(colortex9, PrevCoord).rgb;
    if (PrevColor == vec3(0)) return Color;

    float blendFactor = 0.5 * exp(-length((PrevCoord - Texcoord) * resolution));

    Color = mix(Color, PrevColor, blendFactor);
    return Color;
}

vec4 temporal_upscale_clouds(vec3 ScreenPos, bool IsDH, ivec2 FragCoord, vec3 PlayerPos, sampler2D Sampler) {
    const int CLOUD_RES_SCALING_INV = int(1 / CLOUD_RES_SCALING);
    float DistToCloudCurrent = imageLoad(image1, ivec2(FragCoord * CLOUD_RES_SCALING)).r * farLod;

    // Prevent clouds from clipping in front of objects
    if(DistToCloudCurrent > length(PlayerPos) && ScreenPos.z < 1) return vec4(0, 0, 0, 1);

    vec2 PrevCoord = toPrevScreenPos(ScreenPos.xy, ScreenPos.z, IsDH).xy;
    ivec2 LastUpdatePos = FragCoord - FragCoord % CLOUD_RES_SCALING_INV + ivec2(frameCounter * CLOUD_RES_SCALING, frameCounter) % CLOUD_RES_SCALING_INV;

    // Sample last updated pos when there's no other data available 
    bool WasOffScreen = clamp(PrevCoord, 0, 1) != PrevCoord;
    float DepthPrev = min_component(textureGather(colortex8, PrevCoord));
    bool WasOccluded = DepthPrev < ScreenPos.z && ScreenPos.z == 1;
    if (WasOffScreen || WasOccluded) {
        vec4 Color = texture(image0Sampler, ScreenPos.xy * CLOUD_RES_SCALING);
        Color.a = 1 - Color.a;
        Color = max(Color, 0);
        return Color;
    }
    
    // Only update when there's new samples
    bool IsSampleNotCurrent = any(notEqual(FragCoord, LastUpdatePos));

    if(IsSampleNotCurrent) {
        vec4 PrevColor = texture_bicubic(Sampler, PrevCoord);
        return PrevColor;
    } else {
        vec4 Color = imageLoad(image0, ivec2(gl_FragCoord.xy * CLOUD_RES_SCALING));
        // Transmittance should default to 1
        Color.a = 1 - Color.a;
        Color = max(Color, 0);
        return Color;
    }
}