/////////////////////////////////////
// Unbound Visual + PT v9.6 by EminGT //
/////////////////////////////////////

//Common//
#include "/lib/common.glsl"

#if LUT > 0
    #include "/lib/colors/lutColorGrading.glsl"
#endif

#include "/lib/colors/selectiveColorGrade.glsl"

//////////Fragment Shader//////////Fragment Shader//////////Fragment Shader//////////
#ifdef FRAGMENT_SHADER

noperspective in vec2 texCoord;

#if defined BLOOM_FOG || LENSFLARE_MODE > 0 && defined OVERWORLD || AUTO_EXPOSURE == 1 && defined OVERWORLD
    flat in vec3 upVec, sunVec;
#endif

#if SCENE_AUTO_EXPOSURE == 1
    #extension GL_ARB_shader_image_load_store : enable

    uniform sampler2D exposure_sampler;
    writeonly uniform image2D exposure_img;
#endif

#if DAY_JUMP_TRANSITION == 1 && defined OVERWORLD
    #extension GL_ARB_shader_image_load_store : enable
    uniform sampler2D day_jump_sampler;
    writeonly uniform image2D day_jump_img;
#endif

//Pipeline Constants//

//Common Variables//
float pw = 1.0 / viewWidth;
float ph = 1.0 / viewHeight;

vec2 view = vec2(viewWidth, viewHeight);

#if defined BLOOM_FOG || LENSFLARE_MODE > 0 && defined OVERWORLD
    float SdotU = dot(sunVec, upVec);
    float sunFactor = SdotU < 0.0 ? clamp(SdotU + 0.375, 0.0, 0.75) / 0.75 : clamp(SdotU + 0.03125, 0.0, 0.0625) / 0.0625;
#endif

#if AUTO_EXPOSURE == 1 && defined OVERWORLD
    // How directly the camera is looking at the sun (1.0 = dead center, -1.0 = sun behind camera).
    // sunVec is already in view space, so the camera's forward direction is simply (0,0,-1).
    float sunGazeDot = -sunVec.z;
    // Sun has to be above the horizon for glare to happen (fades out around sunrise/sunset/night).
    float sunAboveHorizon = smoothstep(-0.15, 0.15, dot(sunVec, upVec));
    // Glare ramps up smoothly only within a cone of roughly 18 degrees around the sun.
    float sunGlare = smoothstep(0.75, 0.999, sunGazeDot) * sunAboveHorizon * eyeBrightnessM;
    float autoExposureMult = 1.0 - sunGlare * AUTO_EXPOSURE_STRENGTH;
#endif

#if EYE_ADAPTATION == 1
    // Real "pupil" eye adaptation: eyeBrightnessM is Minecraft's own light-level uniform,
    // already smoothed over ~5s by Iris (see shaders.properties). eyeBrightness is the
    // instant target. When they diverge (e.g. walking from a cave into daylight), the
    // instant value jumps immediately while the smoothed one lags - we use that gap to
    // temporarily over/under-expose the image, then let it settle, just like a real pupil.
    float eyeInstantBrightness = eyeBrightness.y / 240.0;
    float eyeAdaptDiff = eyeInstantBrightness - eyeBrightnessM; // >0: got brighter suddenly, <0: got darker suddenly
    float eyeAdaptationMult = clamp(1.0 + eyeAdaptDiff * EYE_ADAPTATION_STRENGTH, 1.0 - EYE_ADAPTATION_STRENGTH, 1.0 + EYE_ADAPTATION_STRENGTH * 1.5);
#endif

// Unified auto brightness/gamma/contrast signal: one combined value that drives every
// automatic adaptation below, so brightness, contrast and shadow gamma all react together
// instead of as separate disconnected effects - like a real eye adjusting to a scene.
#if AUTO_EXPOSURE == 1 && defined OVERWORLD
    #if EYE_ADAPTATION == 1
        float autoAdaptMult = autoExposureMult * eyeAdaptationMult;
    #else
        float autoAdaptMult = autoExposureMult;
    #endif
#else
    #if EYE_ADAPTATION == 1
        float autoAdaptMult = eyeAdaptationMult;
    #else
        float autoAdaptMult = 1.0;
    #endif
#endif

//Common Functions//
void LinearToRGB(inout vec3 color) {
    const vec3 k = vec3(0.055);
    color = mix((vec3(1.0) + k) * pow(color, vec3(1.0 / 2.4)) - k, 12.92 * color, lessThan(color, vec3(0.0031308)));
}

vec3 ACESFilmicTonemap(vec3 x) {
    // Narkowicz 2015 fitted approximation of the ACES Reference Rendering Transform.
    // Industry-standard filmic curve (used in Unreal/Unity/many film pipelines).
    const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
    return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}

// BakingLab ACES Fitted (from Foxy Shader) - more accurate ACES with
// proper input/output color space matrices. Produces more natural
// highlights and shadows than the simple Narkowicz fit.
vec3 ACESFittedTonemap(vec3 color) {
    const mat3 acesInput = mat3(
        0.59719, 0.07600, 0.02840,
        0.35458, 0.90834, 0.13383,
        0.04823, 0.01566, 0.83777
    );
    const mat3 acesOutput = mat3(
         1.60475, -0.10208, -0.00327,
        -0.53108,  1.10813, -0.07276,
        -0.07367, -0.00605,  1.07602
    );
    vec3 x = acesInput * max(color * 2.0, vec3(0.0));
    vec3 numerator = x * (x + 0.0245786) - 0.000090537;
    vec3 denominator = x * (0.983729 * x + 0.4329510) + 0.238081;
    return clamp(acesOutput * (numerator / max(denominator, vec3(1.0e-6))), 0.0, 1.0);
}

// White balance (from Foxy Shader) - Tanner Helland kelvin-to-RGB
vec3 ColorTemperatureToRgb(const in float kelvin) {
    float t = clamp(kelvin, 1000.0, 40000.0) / 100.0;
    float r = t <= 66.0 ? 1.0 : clamp(1.292936186062745 * pow(max(t - 60.0, 1.0), -0.1332047592), 0.0, 1.0);
    float g = t <= 66.0 ? clamp(0.3900815787690196 * log(max(t, 1.0)) - 0.6318414437886275, 0.0, 1.0) : clamp(1.129890860895294 * pow(max(t - 60.0, 1.0), -0.0755148492), 0.0, 1.0);
    float b = t >= 66.0 ? 1.0 : (t <= 19.0 ? 0.0 : clamp(0.5432067891101961 * log(max(t - 10.0, 1.0)) - 1.19625408914, 0.0, 1.0));
    return vec3(r, g, b);
}

// Apply white balance: shift from chosen kelvin to D65 neutral
vec3 ApplyWhiteBalance(vec3 color) {
    #if WHITE_BALANCE == 6500
        return color;
    #else
        vec3 d65 = ColorTemperatureToRgb(6500.0);
        vec3 sourceWhite = max(ColorTemperatureToRgb(float(WHITE_BALANCE)), vec3(0.04));
        return max(color * (d65 / sourceWhite), vec3(0.0));
    #endif
}

// Contrast with pivot at 0.18 luma (from Foxy Shader)
// Shadows below 0.18 get darker, highlights above get brighter
// Very dark pixels (near-black) are protected from contrast crushing
vec3 ApplyContrastPivot(vec3 color, float strength) {
    float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
    float contrastScale = mix(1.0, 1.045, strength);
    float pivotLuma = max(0.18 + (luma - 0.18) * contrastScale, 0.0);
    float protectWeight = smoothstep(0.012, 0.22, luma);
    float newLuma = mix(luma, pivotLuma, protectWeight);
    return color * (newLuma / max(luma, 1.0e-6));
}

// Chroma boost (from Foxy Shader)
// Boosts saturation slightly, protects near-blacks from color noise
vec3 ApplyChromaBoost(vec3 color, float strength) {
    float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
    float chromaWeight = smoothstep(0.025, 0.22, luma);
    float factor = mix(1.0, 1.035, strength * chromaWeight);
    return mix(vec3(luma), color, factor);
}

// Vignette (from Foxy Shader) - softer falloff than standard
float ApplyVignette(vec2 uv, float strength) {
    vec2 p = uv * (vec2(1.0) - uv);
    float shape = pow(clamp(p.x * p.y * 16.0, 0.0, 1.0), 0.35);
    return mix(1.0, shape, clamp(strength, 0.0, 1.0));
}

// Classic Hable/"Uncharted2" filmic curve (Hable, GDC 2010 - a widely published,
// public-domain-style tonemapping formula, independent of any specific shaderpack).
// Unlike the single-knob ACES fit above, its raw/mid/toe/shoulder shape exposes
// separate highlight-rolloff and shadow-lift controls, which is the "look" people
// usually mean by a classic filmic curve. autoContrast reuses the same auto-adapted
// contrast signal as the rest of DoCompTonemap so all three curve options react to
// eye adaptation consistently.
vec3 FilmicHighlightShadowTonemap(vec3 x, float autoContrast) {
    float A = mix(0.15, 0.22, autoContrast * 0.5);       // shoulder strength (tied to auto contrast)
    float B = 0.50;                                       // linear strength
    float C = TM_FILMIC_SHADOWS;                          // toe strength - shadow lift/crush
    float D = 0.20;                                       // toe numerator offset
    float E = TM_FILMIC_HIGHLIGHTS * 0.10;                 // shoulder numerator offset - highlight rolloff
    float F = 0.30;                                        // shoulder denominator offset

    vec3 curved  = ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
    // Re-normalize against the curve's response to a fixed reference white so exposure
    // stays consistent as TM_FILMIC_HIGHLIGHTS/SHADOWS are tweaked.
    const float whiteRef = 11.2;
    float whiteScale = 1.0 / (((whiteRef * (A * whiteRef + C * B) + D * E) / (whiteRef * (A * whiteRef + B) + D * F)) - E / F);

    return clamp(curved * whiteScale, 0.0, 1.0);
}

// "Gran Turismo" tonemap - Hajime Uchimura, CEDEC 2017 "HDR Theory and
// Practice". This is a public, widely-republished formula (many open-source
// tonemapping collections include it verbatim, e.g. glsl-tone-map on GitHub)
// - independent of any one shaderpack, same category as the Hable curve
// above. Three explicit segments: a toe (power curve, dark end), a linear
// midsection, and an exponential shoulder that asymptotes to a hard display
// maximum P - which is what gives it that "GT" look of keeping midtones
// punchy while still softly rolling off very bright highlights instead of
// clipping them.
vec3 UchimuraTonemap(vec3 x) {
    const float P = 1.0;   // maximum display brightness
    const float a = 1.0;   // linear section contrast/slope
    const float m = 0.22;  // linear section start point
    const float l = 0.4;   // linear section length
    const float c = 1.33;  // toe strength
    const float b = 0.0;   // toe pedestal (black offset)

    float l0 = (P - m) * l / a;
    float S0 = m + l0;
    float S1 = m + a * l0;
    float C2 = a * P / (P - S1);
    float CP = -C2 / P;

    vec3 w0 = vec3(1.0) - smoothstep(vec3(0.0), vec3(m), x);
    vec3 w2 = vec3(greaterThanEqual(x, vec3(S0)));
    vec3 w1 = vec3(1.0) - w0 - w2;

    vec3 T = m * pow(max(x / m, vec3(0.0)), vec3(c)) + b;
    vec3 S = P - (P - S1) * exp(CP * (x - S0));
    vec3 L = m + a * (x - m);

    return clamp(T * w0 + L * w1 + S * w2, 0.0, 1.0);
}

// AgX view transform - Troy Sobotka's AgX, in the commonly-used "minimal"
// fitted-approximation form (inset/outset 3x3 matrices + a 6th-order
// polynomial standing in for the full reference OCIO LUT). Like the ACES fit
// above, this specific minimal approximation has been independently
// published and reused across many open-source engines/shaderpacks - it's
// not tied to any one project. Compared to the other operators here, AgX
// compresses/desaturates extreme highlights much more aggressively (closer
// to how an actual film negative clips), which tends to hold detail better
// in very high dynamic range shots (bright open sky next to sunlit snow,
// etc.) at the cost of a slightly flatter/desaturated look at the top end.
// NOTE: the fitted matrices/polynomial coefficients below are transcribed
// from memory of the public reference values, not re-derived here - if the
// resulting look seems off (too flat, wrong midtone balance) compared to a
// reference AgX render, that's the first place to double check.
//
// ("aktifkan Salicylic AgX full version" fix, v9.0): replaced the minimal
// AgX fit with the full Salicylic AgX Film Tonemap port from
// SalicylicShaders_v1 (MIT License). This version uses proper Rec.2020
// color space conversion, AgX log-domain encoding with 14 EV range,
// polynomial sigmoid contrast curve, and ASC-CDL look transform
// (power=1.15, saturation=1.25) for a cinematic, punchy film look.
// The old minimal fit had incorrect outset matrix values and no ASC-CDL.
vec3 AgXTonemap(vec3 color) {
    // --- Scene-referred boost (exposure pre-multiply) ---
    color *= 7.0;

    // --- Color space: sRGB linear -> Rec.2020 linear ---
    const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(
        0.6274, 0.0691, 0.0164,
        0.3293, 0.9195, 0.0880,
        0.0433, 0.0113, 0.8956
    );
    color = LINEAR_SRGB_TO_LINEAR_REC2020 * color;

    // --- AgX Inset (scene-referred -> AgX working space) ---
    // (fix: this matrix was transposed relative to Salicylic's original -
    // rows and columns were swapped, which silently skews the AgX working-
    // space conversion/hue. Restored to match the source exactly.)
    const mat3 AgXInsetMatrix = mat3(
        0.856627153315983, 0.137318972929847, 0.11189821299995,
        0.0951212405381588, 0.761241990602591, 0.0767994186031903,
        0.0482516061458583, 0.101439036467562, 0.811302368396859
    );
    color = AgXInsetMatrix * color;
    color = max(color, vec3(1e-10));

    // --- Log2 encoding (14 EV range centered on 0.18 mid-grey) ---
    const float AGX_EV = 14.0;
    const float hev = AGX_EV * 0.5;
    const float middle_grey = 0.18;
    color = clamp(log2(color / middle_grey), -hev, hev);
    color = (color + hev) / AGX_EV;
    color = clamp(color, 0.0, 1.0);

    // --- Polynomial sigmoid (fitted AgX contrast curve) ---
    vec3 x  = color;
    vec3 x2 = x * x;
    vec3 x4 = x2 * x2;
    vec3 x6 = x4 * x2;
    color = - 17.86  * x6 * x
            + 78.01  * x6
            - 126.7  * x4 * x
            + 92.06  * x4
            - 28.82  * x2 * x
            + 4.361  * x2
            - 0.1718 * x
            + 0.002857;

    // --- ASC-CDL Look Transform (Salicylic "punchy film" look) ---
    // slope=1.0, offset=0.0, power/saturation now tunable via
    // CDL_LOOK_POWER/CDL_LOOK_SATURATION in common.glsl ("film look tapi
    // jangan hitam putih banget" fix) instead of being hardcoded.
    {
        float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
        vec3 c = pow(color, vec3(CDL_LOOK_POWER));
        color = luma + CDL_LOOK_SATURATION * (c - luma);
    }

    // --- AgX Outset (AgX working space -> display) ---
    // (fix: transposed relative to Salicylic's original, same issue as
    // AgXInsetMatrix above - restored to match the source exactly.)
    const mat3 AgXOutsetMatrix = mat3(
        1.1271005818144368, -0.1413297634984383, -0.14132976349843826,
        -0.11060664309660323, 1.157823702216272, -0.11060664309660294,
        -0.016493938717834573, -0.016493938717834257, 1.2519364065950405
    );
    color = AgXOutsetMatrix * color;

    // --- Linearize (display gamma -> linear for further processing) ---
    color = pow(max(vec3(0.0), color), vec3(2.2));

    // --- Color space: Rec.2020 linear -> sRGB linear ---
    const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(
        1.6605, -0.1246, -0.0182,
       -0.5876,  1.1329, -0.1006,
       -0.0728, -0.0083,  1.1187
    );
    color = LINEAR_REC2020_TO_LINEAR_SRGB * color;
    color = clamp(color, 0.0, 1.0);

    return color;
}

/* End Salicylic AgX function - now active as the TONEMAP_STYLE==4 path */

void DoCompTonemap(inout vec3 color, float autoAdaptMult) {
    color = TM_EXPOSURE * color;

    float initialLuminance = GetLuminance(color);

    // Auto brightness + auto contrast + auto gamma, unified: when the eye dims the image
    // (autoAdaptMult < 1, e.g. looking straight at the sun), contrast is nudged up so the
    // dimmed picture still looks punchy and focused instead of flat/washed out. When the eye
    // boosts the image for a dark area (autoAdaptMult > 1), contrast is nudged down and shadow
    // detail is lifted more (see autoDarkLiftMix below), so dark corners stay readable and
    // natural instead of crushing to black - mimicking real-eye adaptation as one system.
    // ("auto jangan ngubah warna" fix): konstan, tidak digerakkan autoAdaptMult,
    // supaya exposure/gamma tidak menggeser cor/colour balance saat adaptasi mata.
    float autoContrast = TM_CONTRAST;

    #if TONEMAP_STYLE == 1
        // --- ACES Filmic ---
        vec3 colorOut = pow(color, vec3(autoContrast)); // TM_CONTRAST still acts as a contrast/gamma control pre-curve
        colorOut = ACESFilmicTonemap(colorOut);
        LinearToRGB(colorOut);
    #elif TONEMAP_STYLE == 2
        // --- Filmic Highlights/Shadows (Hable/Uncharted2-style) ---
        vec3 colorOut = FilmicHighlightShadowTonemap(color, autoContrast);
        LinearToRGB(colorOut);
    #elif TONEMAP_STYLE == 3
        // --- Uchimura / "GT" Tonemap ---
        vec3 colorOut = pow(color, vec3(autoContrast));
        colorOut = UchimuraTonemap(colorOut);
        LinearToRGB(colorOut);
    #elif TONEMAP_STYLE == 4 || TONEMAP_STYLE == 5
        // --- AgX / Salicylic AgX Film ---
        // (fix: TONEMAP_STYLE 5 is the default and is documented as this
        // exact Salicylic AgX path, but there was no "== 5" branch here at
        // all, so it silently fell through to the "#else" Lottes curve
        // below - the Salicylic tonemap was never actually active. Both
        // 4 and 5 point at the same AgXTonemap() since that function is
        // already the full Salicylic port, not the older minimal fit.)
        vec3 colorOut = pow(color, vec3(autoContrast));
        colorOut = AgXTonemap(colorOut);
        LinearToRGB(colorOut);
    #elif TONEMAP_STYLE == 6
        // --- ACES Fitted (BakingLab) - from Foxy Shader ---
        vec3 colorOut = pow(color, vec3(autoContrast));
        colorOut = ACESFittedTonemap(colorOut);
        LinearToRGB(colorOut);
    #else
        // Lottes tonemap modified for Unbound Visual + PT v9.6
        // Lottes 2016, "Advanced Techniques and Optimization of HDR Color Pipelines"
        // http://32ipi028l5q82yhj72224m8j.wpengine.netdna-cdn.com/wp-content/uploads/2016/03/GdcVdrLottes.pdf
        float colorMax = max(color.r, max(color.g, color.b));

        vec3 a      = vec3(autoContrast); // General Contrast (auto-adjusted)
        vec3 d      = vec3(1.0); // Roll-off control
        vec3 hdrMax = vec3(8.0); // Maximum input brightness
        vec3 midIn  = vec3(0.25); // Input middle gray
        vec3 midOut = vec3(0.25); // Output middle gray

        vec3 a_d = a * d;
        vec3 hdrMaxA = pow(hdrMax, a);
        vec3 hdrMaxAD = pow(hdrMax, a_d);
        vec3 midInA = pow(midIn, a);
        vec3 midInAD = pow(midIn, a_d);
        vec3 HM1 = hdrMaxA * midOut;
        vec3 HM2 = hdrMaxAD - midInAD;

        vec3 b = (-midInA + HM1) / (HM2 * midOut);
        vec3 c = (hdrMaxAD * midInA - HM1 * midInAD) / (HM2 * midOut);

        vec3 colorOut = pow(color, a) / (pow(color, a_d) * b + c);

        LinearToRGB(colorOut);
    #endif

    // v9.0: Improved dark area visibility for Survival/Adventure
    // Wider shadow lift range so mobs and terrain are more visible at night/in caves
    const float darkLiftStart = 0.25; // v9.0: increased from 0.20 for wider shadow recovery
    const float darkLiftMix = 0.75; // v9.0: increased from 0.70 for stronger shadow detail
    // Auto gamma: lift shadow detail further when the eye is compensating for a dark area
    // (autoAdaptMult > 1), less when it's dimming for a bright area (autoAdaptMult < 1).
    // ("auto jangan ngubah warna" fix): dibuat konstan agar tidak menggeser warna.
    float autoDarkLiftMix = clamp(darkLiftMix, 0.0, 1.0);
    float darkLift = smoothstep(darkLiftStart, 0.0, initialLuminance);
    vec3 smoothColor = pow(color, vec3(1.0 / 2.2));
    colorOut = mix(colorOut, smoothColor, darkLift * autoDarkLiftMix * max0(0.55 - abs(1.05 - autoContrast)) / 0.55);
    
    // Path to White
    const float wpInputCurveStart = 0.0;
    float wpInputCurveMax = TM_HDR_HEADROOM; // Increase this value to reduce the effect of white path (more HDR highlight headroom before clipping to white)
    float modifiedLuminance = pow(initialLuminance / wpInputCurveMax, 2.0 - TM_WHITE_PATH) * wpInputCurveMax;
    float whitePath = smoothstep(wpInputCurveStart, wpInputCurveMax, modifiedLuminance);
    colorOut = mix(colorOut, vec3(1.0), whitePath);

    // Desaturate dark colors
    const float dpInputCurveStart = 0.1;
    const float dpInputCurveMax = 0.0;
    float desaturatePath = smoothstep(dpInputCurveStart, dpInputCurveMax, initialLuminance);
    colorOut = mix(colorOut, vec3(GetLuminance(colorOut)), desaturatePath * TM_DARK_DESATURATION);

    colorOut = clamp01(colorOut);

    // Manual display gamma calibration (monitor-brightness knob), applied
    // last in display-referred space, after the tone curve's own
    // linear->display encode. Kept as a separate pass so it never
    // interacts with/duplicates the LinearToRGB encode above - it only
    // nudges the ALREADY-encoded image brighter/darker, exactly like a
    // TV/monitor gamma control, not a second color-science pass.
    colorOut = pow(colorOut, vec3(1.0 / MANUAL_GAMMA));

    color = colorOut;
}

void DoBSLColorSaturation(inout vec3 color) {
    float saturationFactor = T_SATURATION;

    float grayVibrance = (color.r + color.g + color.b) / 3.0;
    float graySaturation = grayVibrance;
    if (saturationFactor < 1.00) graySaturation = dot(color, vec3(0.299, 0.587, 0.114));

    float mn = min(color.r, min(color.g, color.b));
    float mx = max(color.r, max(color.g, color.b));
    float sat = (1.0 - (mx - mn)) * (1.0 - mx) * grayVibrance * 5.0;
    vec3 lightness = vec3((mn + mx) * 0.5);

    color = mix(color, mix(color, lightness, 1.0 - T_VIBRANCE), sat);
    color = mix(color, lightness, (1.0 - lightness) * (2.0 - T_VIBRANCE) / 2.0 * abs(T_VIBRANCE - 1.0));

    //Highlight saturation rolloff - tames the neon/overshoot look the plain saturation boost
    //used to cause on bright pixels, by fading saturationFactor back towards 1.0 as brightness climbs.
    float rolloffStrength = HIGHLIGHT_SATURATION_ROLLOFF * 0.01;
    float highlightMask = clamp(mx * 2.0 - 1.0, 0.0, 1.0); // ramps 0 -> 1 as max channel goes 0.5 -> 1.0
    highlightMask *= highlightMask;
    float rolloffSaturation = mix(saturationFactor, 1.0, highlightMask * rolloffStrength);

    color = color * rolloffSaturation - graySaturation * (rolloffSaturation - 1.0);

    //Safety clamp - the previous unclamped output could overshoot into clipped, neon-looking highlights
    color = clamp01(color);
}

//=== Custom Look: Warm Midtones + Dark Blue Shadows ===
// Tweak these 4 values to taste:
// ("terlalu coklat/kontras/gelap" fix): these 4 were stacked on top of
// TM_CONTRAST + the tonemap curve itself, applied to literally every pixel
// every frame - grass, dirt, wood, skin, all got the same +warm push, and
// every shadow got an extra multiply-darken ON TOP of the tonemapper's own
// ("cahaya tegas, vivid, warm, tajam - mirip shader Voyager" request):
// raised back up from the 0.03 "nearly off" value above, but only to 0.35
// (not back to the original strong value that was dialed back for reading
// "too brown"/too much shadow contrast) - a clearly visible warm push
// without swinging back to that earlier problem.
// ("sore oren/merah banget" fix): lowered from 0.35 so the warm midtone
            // push no longer over-amps the already-warm sunset cast.
// ("cahaya matahari vivid/tidak warm oranye" fix): diturunkan ke 0.05 supaya
// warna midtone tetap netral/putih bersih, bukan ada bias oranye di atasnya.
const float WARM_MIDTONE_STRENGTH = 0.05; // 0.0 = off, 1.0 = strong warm push in midtones
const float SHADOW_TINT_RANGE     = 0.12; // how far up the luminance range the shadow tint reaches
const float SHADOW_BLACK_STRENGTH = 0.02; // "layer 1": crush/density added into shadows (pure black)
const float SHADOW_BLUE_STRENGTH  = 0.03; // "layer 2": dark blue tint added on top of the black layer

void DoWarmMidtoneBlueShadowGrade(inout vec3 color) {
    float lum = GetLuminance(color);

    //--- 1) Warm Midtones ---
    // Only pushes warmth in the mid-luminance band, leaves deep shadows and bright
    // highlights untouched so the effect reads as "midtones", not a global white-balance shift.
    float midMask = smoothstep(0.05, 0.35, lum) * (1.0 - smoothstep(0.55, 0.85, lum));
    const vec3 warmMul = vec3(1.000, 1.000, 1.000); // neutralized - no warm push (was 1.030/1.012/0.970)
    color = mix(color, color * warmMul, midMask * WARM_MIDTONE_STRENGTH);

    //--- 2) Dark Blue Shadows: 2-layer overlay (Black then Dark Blue) ---
    float shadowMask = 1.0 - smoothstep(0.0, SHADOW_TINT_RANGE, lum);

    // Layer 1 - Black (multiply-style): adds weight/density to the shadows first,
    // like laying down a black solid under a "Multiply" blend mode.
    vec3 blackLayer = color * color;
    color = mix(color, blackLayer, shadowMask * SHADOW_BLACK_STRENGTH);

    // Layer 2 - Dark Blue (linear-add/"Screen"-style): a low-value, blue-leaning color
    // added on top. Kept very dark and low-saturation on purpose so shadows read as
    // "dark blue", not "blue" - it should never fight for attention against the black layer.
    const vec3 darkBlueLayer = vec3(0.018, 0.025, 0.045); // B > G > R, cool blue without brown
    color += darkBlueLayer * shadowMask * SHADOW_BLUE_STRENGTH;

    color = clamp01(color);
}

#if BLOOM_ENABLED == 1
    vec2 rescale = max(vec2(viewWidth, viewHeight) / vec2(1920.0, 1080.0), vec2(1.0));
    vec3 GetBloomTile(float lod, vec2 coord, vec2 offset) {
        float scale = exp2(lod);
        vec2 bloomCoord = coord / scale + offset;
        bloomCoord = clamp(bloomCoord, offset, 1.0 / scale + offset);

        vec3 bloom = texture2D(colortex3, bloomCoord / rescale).rgb;
        bloom *= bloom;
        bloom *= bloom;
        return bloom * 128.0;
    }

    void DoBloom(inout vec3 color, vec2 coord, float dither, float lViewPos) {
        vec3 blur1 = GetBloomTile(2.0, coord, vec2(0.0      , 0.0   ));
        vec3 blur2 = GetBloomTile(3.0, coord, vec2(0.0      , 0.26  ));
        vec3 blur3 = GetBloomTile(4.0, coord, vec2(0.135    , 0.26  ));

        vec3 blur = (blur1 * 3.0 + blur2 * 2.0 + blur3 * 1.0) * 0.16;

        float bloomStrength = BLOOM_STRENGTH;

        #if defined BLOOM_FOG && defined NETHER && defined BORDER_FOG
            float farM = min(renderDistance, NETHER_VIEW_LIMIT); // consistency9023HFUE85JG
            float netherBloom = lViewPos / clamp(farM, 96.0, 256.0);
            netherBloom *= netherBloom;
            netherBloom *= netherBloom;
            netherBloom = 1.0 - exp(-8.0 * netherBloom);
            netherBloom *= 1.0 - maxBlindnessDarkness;
            bloomStrength = mix(bloomStrength * 0.7, bloomStrength * 1.8, netherBloom);
        #endif

        color = mix(color, blur, bloomStrength);
        //color += blur * bloomStrength * (ditherFactor.x + ditherFactor.y);
    }
#endif

//Includes//
#ifdef BLOOM_FOG
    #include "/lib/atmospherics/fog/bloomFog.glsl"
#endif

#if BLOOM_ENABLED == 1
    #include "/lib/util/dither.glsl"
#endif

#if LENSFLARE_MODE > 0 && defined OVERWORLD
    #include "/lib/misc/lensFlare.glsl"
#endif

//Program//
void main() {
    vec3 color = texture2D(colortex0, texCoord).rgb;

    #if SCENE_AUTO_EXPOSURE == 1
        // Scene auto exposure: measures how bright the actual rendered frame
        // is (not just the player's light level), and gently nudges exposure
        // to compensate - looking at bright sky/windows dims down, looking
        // into a dark corner/cave brightens up.
        float sceneExposureMult = 1.0;
        {
            // The main HDR color buffer already has mipmaps generated for
            // other effects (DoF, reflection roughness) elsewhere in this
            // pack, so sampling a very downsampled mip level gives an
            // approximate box-filtered average of the whole visible frame
            // in a single texture fetch - no separate reduction pass needed.
            const float SCENE_AUTO_EXPOSURE_MIP = 8.0;
            const float SCENE_AUTO_EXPOSURE_TARGET = 0.30; // approximate "middle grey" reference in this engine's linear scale

            vec3 avgColor = textureLod(colortex0, vec2(0.5), SCENE_AUTO_EXPOSURE_MIP).rgb;
            float avgLuma = GetLuminance(avgColor);

            float rawMult = SCENE_AUTO_EXPOSURE_TARGET / max(avgLuma, 0.02);
            rawMult = clamp(rawMult, SCENE_AUTO_EXPOSURE_MIN, SCENE_AUTO_EXPOSURE_MAX);

            // "Not aggressive": only apply a fraction of the full
            // correction - 0 strength would leave the image untouched,
            // 1.0 would fully normalize to the target level every frame.
            float desiredMult = mix(1.0, rawMult, SCENE_AUTO_EXPOSURE_STRENGTH);

            // Ease toward the desired value instead of snapping to it, using
            // last frame's result from the persistent 1x1 buffer, so panning
            // across a bright window or dark doorway fades in/out smoothly.
            float prevMult = texelFetch(exposure_sampler, ivec2(0, 0), 0).r;
            if (prevMult < 0.01) prevMult = 1.0; // first-frame fallback, buffer starts at 0
            // ("auto exposure formula ala game AAA" request): adaptasi
            // dibuat ASIMETRIS, persis seperti mata/kamera sungguhan dan
            // seperti yang dipakai kebanyakan game AAA (Unreal/Frostbite,
            // dll) - MENGGELAP itu cepat (proteksi dari silau saat lihat
            // area terang), MENCERAHKAN itu lambat (adaptasi ke gelap makan
            // waktu). Sebelumnya cuma satu kecepatan (SCENE_AUTO_EXPOSURE_SPEED)
            // dipakai untuk dua arah, jadi ruangan gelap ikut "diangkat"
            // secepat proteksi silau - ini yang bikin ruangan gelap di
            // referensi foto kamu jadi ketarik naik alih-alih tetap gelap.
            float sceneSpeedDarken  = SCENE_AUTO_EXPOSURE_SPEED * 4.0; // gelapin: cepat
            float sceneSpeedBrighten = SCENE_AUTO_EXPOSURE_SPEED * 0.5; // terangin: lambat
            float sceneAdaptSpeed = desiredMult < prevMult ? sceneSpeedDarken : sceneSpeedBrighten;
            sceneExposureMult = mix(prevMult, desiredMult, clamp(sceneAdaptSpeed, 0.0, 1.0));

            // Only a single fragment invocation writes the persistent value
            // back, to avoid every pixel racing to write the same texel.
            if (gl_FragCoord.x < 1.0 && gl_FragCoord.y < 1.0) {
                imageStore(exposure_img, ivec2(0, 0), vec4(sceneExposureMult, 0.0, 0.0, 0.0));
            }
        }
        autoAdaptMult *= sceneExposureMult;
    #endif
    
    #if defined BLOOM_FOG || LENSFLARE_MODE > 0 && defined OVERWORLD
        float z0 = texture2D(depthtex0, texCoord).r;
        vec4 screenPos = vec4(texCoord, z0, 1.0);
        vec4 viewPos = gbufferProjectionInverse * (screenPos * 2.0 - 1.0);
        viewPos /= viewPos.w;
        float lViewPos = length(viewPos.xyz);

        #if defined DISTANT_HORIZONS || defined VOXY
            #ifdef DISTANT_HORIZONS
                float z0lod = texelFetch(dhDepthTex, texelCoord, 0).r;
                vec4 screenPosLod = vec4(texCoord, z0lod, 1.0);
                vec4 viewPosLod = dhProjectionInverse * (screenPosLod * 2.0 - 1.0);
            #elif defined VOXY
                float z0lod = texelFetch(vxDepthTexTrans, texelCoord, 0).r;
                vec4 screenPosLod = vec4(texCoord, z0lod, 1.0);
                vec4 viewPosLod = vxProjInv * (screenPosLod * 2.0 - 1.0);
            #endif
            viewPosLod /= viewPosLod.w;
            lViewPos = min(lViewPos, length(viewPosLod.xyz));
        #endif
    #else
        float lViewPos = 0.0;
    #endif

    float dither = texture2DLod(noisetex, texCoord * view / 128.0, 0.0).b;
    #ifdef TAA
        dither = fract(dither + goldenRatio * mod(float(frameCounter), 3600.0));
    #endif

    #ifdef BLOOM_FOG
        color /= GetBloomFog(lViewPos);
    #endif

    #if BLOOM_ENABLED == 1
        DoBloom(color, texCoord, dither, lViewPos);
    #endif

    #ifdef COLORGRADING
        color =
            pow(color.r, GR_RC) * vec3(GR_RR, GR_RG, GR_RB) +
            pow(color.g, GR_GC) * vec3(GR_GR, GR_GG, GR_GB) +
            pow(color.b, GR_BC) * vec3(GR_BR, GR_BG, GR_BB);
        color *= 0.01;
    #endif

    color *= autoAdaptMult;

    // White balance (from Foxy Shader) - before tonemap
    #if WHITE_BALANCE != 6500
        color = ApplyWhiteBalance(color);
    #endif

    // Vignette (from Foxy Shader) - softer falloff
    #if VIGNETTE == 1
        color *= ApplyVignette(texCoord, VIGNETTE_INTENSITY);
    #endif

    DoCompTonemap(color, autoAdaptMult);

    // Shadow/Highlight color grading (from Foxy Shader)
    // Cool shadows + warm highlights = cinematic look
    #if SHADOW_HIGHLIGHT_GRADE > 0.0
    {
        float luma = GetLuminance(color);
        float shadow = 1.0 - smoothstep(0.045, 0.340, luma);
        float highlight = smoothstep(0.360, 0.920, luma);
        // Shadow tint: push toward blue/cool
        color *= mix(vec3(1.0), vec3(0.965, 0.992, 1.045), shadow * SHADOW_HIGHLIGHT_GRADE);
        // Highlight tint: push toward warm/golden
        color *= mix(vec3(1.0), vec3(1.040, 1.010, 0.965), highlight * SHADOW_HIGHLIGHT_GRADE);
    }
    #endif

    // Purkinje shift (from Foxy Shader)
    // Simulates night vision: increased blue sensitivity in low light
    #if PURKINJE_SHIFT > 0.0
    {
        float luma = GetLuminance(color);
        float scotopicLuma = pow(luma, 0.43);
        float purkinjeFactor = 1.0 - smoothstep(0.02, 0.12, luma);
        vec3 purkinjeTint = vec3(0.82, 0.96, 1.10);
        color = mix(color, color * purkinjeTint * (scotopicLuma / max(luma, 0.001)), purkinjeFactor * PURKINJE_SHIFT);
    }
    #endif

    // Contrast pivot at 0.18 luma (from Foxy Shader)
    // Shadows below 0.18 get darker, highlights above get brighter
    #if CONTRAST_PIVOT_STRENGTH > 0.0
        color = ApplyContrastPivot(color, CONTRAST_PIVOT_STRENGTH);
    #endif

    // Chroma boost (from Foxy Shader)
    // Subtle saturation push, protects near-blacks
    #if CHROMA_BOOST > 0.0
        color = ApplyChromaBoost(color, CHROMA_BOOST);
    #endif

    #if defined GREEN_SCREEN_LIME || SELECT_OUTLINE == 4
        int materialMaskInt = int(texelFetch(colortex6, texelCoord, 0).g * 255.1);
    #endif

    #ifdef GREEN_SCREEN_LIME
        if (materialMaskInt == 240) { // Green Screen Lime Blocks
            color = vec3(0.0, 1.0, 0.0);
        }
    #endif

    #if SELECT_OUTLINE == 4
        if (materialMaskInt == 252) { // Versatile Selection Outline
            float colorMF = 1.0 - dot(color, vec3(0.25, 0.45, 0.1));
            colorMF = smoothstep1(smoothstep1(smoothstep1(smoothstep1(smoothstep1(colorMF)))));
            color = mix(color, 3.0 * (color + 0.2) * vec3(colorMF * SELECT_OUTLINE_I), 0.3);
        }
    #endif

    #if LENSFLARE_MODE > 0 && defined OVERWORLD
        DoLensFlare(color, viewPos.xyz, dither);
    #endif

    DoBSLColorSaturation(color);

    DoWarmMidtoneBlueShadowGrade(color);

    #if SELECTIVE_COLOR_GRADE == 1
        DoSelectiveColorGrade(color);
    #endif

    #if LUT > 0
        ApplyLUT(color);
    #endif

    #if DAY_JUMP_TRANSITION == 1 && defined OVERWORLD
        // Persist previous world time + replay progress in a 1x1 RGBA16F image.
        // This stage runs after composite1, so composite1 always sees the last
        // frame's values and can detect a real discontinuity.
        vec4 dayJumpPrev = texelFetch(day_jump_sampler, ivec2(0, 0), 0);
        float prevWorldTime = dayJumpPrev.r;
        float prevProgress = clamp(dayJumpPrev.g, 0.0, 1.0);
        bool historyInitialized = dayJumpPrev.b > 0.5;
        float currentWorldTime = mod(float(worldTime), 24000.0);
        float forwardDelta = mod(currentWorldTime - prevWorldTime + 24000.0, 24000.0);
        bool wasNight = (prevWorldTime >= 13000.0 || prevWorldTime <= 1200.0);
        bool isDayNow = (currentWorldTime > 900.0 && currentWorldTime < 11500.0);
        bool largeTimeJump = forwardDelta > 24000.0 * clamp(DAY_JUMP_TRANSITION_SENSITIVITY, 0.02, 0.35);
        bool startReplay = historyInitialized && isDayNow && wasNight && largeTimeJump;

        float duration = max(DAY_JUMP_TRANSITION_DURATION, 0.5);
        float progress = max(prevProgress - frameTime / duration, 0.0);
        if (startReplay) progress = 1.0;

        // One invocation is enough; all pixels read the same persistent 1x1 value.
        if (gl_FragCoord.x < 1.0 && gl_FragCoord.y < 1.0) {
            imageStore(day_jump_img, ivec2(0, 0), vec4(currentWorldTime, progress, 1.0, 0.0));
        }
    #endif

    /* DRAWBUFFERS:3 */
    gl_FragData[0] = vec4(color, 1.0);
}

#endif

//////////Vertex Shader//////////Vertex Shader//////////Vertex Shader//////////
#ifdef VERTEX_SHADER

noperspective out vec2 texCoord;

#if defined BLOOM_FOG || LENSFLARE_MODE > 0 && defined OVERWORLD || AUTO_EXPOSURE == 1 && defined OVERWORLD
    flat out vec3 upVec, sunVec;
#endif

//Attributes//

//Common Variables//

//Common Functions//

//Includes//

//Program//
void main() {
    gl_Position = ftransform();

    texCoord = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy;

    #if defined BLOOM_FOG || LENSFLARE_MODE > 0 && defined OVERWORLD || AUTO_EXPOSURE == 1 && defined OVERWORLD
        upVec = normalize(gbufferModelView[1].xyz);
        sunVec = GetSunVector();
    #endif
}

#endif
