// The Academy Color Encoding System (ACES)
// https://github.com/ampas/aces-core/tree/v1.3.1

//!HOOK OUTPUT
//!BIND HOOKED
//!DESC tone mapping (aces)

const float TINY         = 1e-5;
const float HALF_POS_INF = 31744.0;

// Gamut Compress Parameters
const float LIM_CYAN    = 1.147;
const float LIM_MAGENTA = 1.264;
const float LIM_YELLOW  = 1.312;

const float THR_CYAN    = 0.815;
const float THR_MAGENTA = 0.803;
const float THR_YELLOW  = 0.880;

const float PWR         = 1.2;

// "Glow" module constants
const float RRT_GLOW_GAIN = 0.05;
const float RRT_GLOW_MID  = 0.08;

// Red modifier constants
const float RRT_RED_SCALE = 0.82;
const float RRT_RED_PIVOT = 0.03;
const float RRT_RED_HUE   = 0.0;
const float RRT_RED_WIDTH = 135.0;

// Desaturation constants
const float RRT_SAT_FACTOR = 0.96;
const float ODT_SAT_FACTOR = 0.93;

// Gamma compensation factor
const float DIM_SURROUND_GAMMA = 0.9811;


const mat3 RGB_to_XYZ = mat3(
    0.6369580483012914, 0.14461690358620832,  0.1688809751641721,
    0.2627002120112671, 0.6779980715188708,   0.05930171646986196,
    0.000000000000000,  0.028072693049087428, 1.060985057710791
);

const mat3 XYZ_to_RGB = mat3(
     1.716651187971268,  -0.355670783776392, -0.253366281373660,
    -0.666684351832489,   1.616481236634939,  0.0157685458139111,
     0.017639857445311,  -0.042770613257809,  0.942103121235474
);

const mat3 AP0_to_XYZ = mat3(
    0.9525523959, 0.0000000000,  0.0000936786,
    0.3439664498, 0.7281660966, -0.0721325464,
    0.0000000000, 0.0000000000,  1.0088251844
);

const mat3 XYZ_to_AP0 = mat3(
     1.0498110175, 0.0000000000, -0.0000974845,
    -0.4959030231, 1.3733130458,  0.0982400361,
     0.0000000000, 0.0000000000,  0.9912520182
);

const mat3 AP1_to_XYZ = mat3(
     0.6624541811, 0.1340042065, 0.1561876870,
     0.2722287168, 0.6740817658, 0.0536895174,
    -0.0055746495, 0.0040607335, 1.0103391003
);

const mat3 XYZ_to_AP1 = mat3(
     1.6410233797, -0.3248032942, -0.2364246952,
    -0.6636628587,  1.6153315917,  0.0167563477,
     0.0117218943, -0.0082844420,  0.9883948585
);

const vec3 LUMINANCE_AP1 = vec3(0.2722287168, 0.6740817658, 0.0536895174);

const mat3 D60_to_D65_CAT = mat3(
     0.987224,   -0.00611327, 0.0159533,
    -0.00759836,  1.00186,    0.00533002,
     0.00307257, -0.00509595, 1.08168
);

const mat3 D65_to_D60_CAT = mat3(
     1.0130349238541335252,    0.0061053088545854651618, -0.014970963195236360098,
     0.0076982295895192892886, 0.9981648317745535941,    -0.0050320341346474782061,
    -0.0028413125165573776196, 0.0046851555780399034147,  0.92450665292696206889
);

// Power compression function
// https://www.desmos.com/calculator/iwcyjg6av0
float compress(float dist, float lim, float thr, float pwr) {
    float scl = (lim - thr) / pow(pow((1.0 - thr) / (lim - thr), -pwr) - 1.0, 1.0 / pwr);
    float c = thr + (dist - thr) / (pow(1.0 + pow((dist - thr) / scl, pwr), 1.0 / pwr));
    return (dist < thr ? dist : c);
}

vec3 reference_gamut_compress(vec3 rgb) {
    // Achromatic axis
    float ac = max(max(rgb.r, rgb.g), rgb.b);

    // Inverse RGB Ratios: distance from achromatic axis
    vec3 d = ac == 0.0 ? vec3(0.0) : (ac - rgb) / abs(ac);

    // Compressed distance
    vec3 cd = vec3(
        compress(d.x, LIM_CYAN,    THR_CYAN,    PWR),
        compress(d.y, LIM_MAGENTA, THR_MAGENTA, PWR),
        compress(d.z, LIM_YELLOW,  THR_YELLOW,  PWR)
    );

    // Inverse RGB Ratios to RGB
    vec3 crgb = ac - cd * abs(ac);

    return crgb;
}

float rgb_to_saturation(vec3 rgb) {
    float mi = min(min(rgb.r, rgb.g), rgb.b);
    float ma = max(max(rgb.r, rgb.g), rgb.b);
    return (max(ma, TINY) - max(mi, TINY)) / max(ma, 1e-2);
}

// Converts RGB to a luminance proxy, here called YC
// YC is ~ Y + K * Chroma
// Constant YC is a cone-shaped surface in RGB space, with the tip on the
// neutral axis, towards white.
// YC is normalized: RGB 1 1 1 maps to YC = 1
//
// ycRadiusWeight defaults to 1.75, although can be overridden in function
// call to rgb_to_yc
// ycRadiusWeight = 1 -> YC for pure cyan, magenta, yellow == YC for neutral
// of same value
// ycRadiusWeight = 2 -> YC for pure red, green, blue  == YC for  neutral of
// same value.
float rgb_to_yc(vec3 rgb) {
    float ycRadiusWeight = 1.75;

    float r = rgb.r;
    float g = rgb.g;
    float b = rgb.b;

    float chroma = sqrt(b * (b - g) + g * (g - r) + r * (r - b));

    return (b + g + r + ycRadiusWeight * chroma) / 3.0;
}

// Sigmoid function in the range 0 to 1 spanning -2 to +2.
float sigmoid_shaper(float x) {
    float t = max(1.0 - abs(x / 2.0), 0.0);
    float y = 1.0 + sign(x) * (1.0 - t * t);
    return y / 2.0;
}

float glow_fwd(float ycIn, float glowGainIn, float glowMid) {
    float glowGainOut;

    if (ycIn <= 2.0 / 3.0 * glowMid) {
        glowGainOut = glowGainIn;
    } else if ( ycIn >= 2.0 * glowMid) {
        glowGainOut = 0.0;
    } else {
        glowGainOut = glowGainIn * (glowMid / ycIn - 1.0 / 2.0);
    }

    return glowGainOut;
}

// Returns a geometric hue angle in degrees (0-360) based on RGB values.
// For neutral colors, hue is undefined and the function will return a quiet NaN value.
float rgb_to_hue(vec3 rgb) {
    // RGB triplets where RGB are equal have an undefined hue
    float hue = 0.0;

    if (!(rgb.x == rgb.y && rgb.y == rgb.z)) {
        float x = sqrt(3.0) * (rgb.y - rgb.z);
        float y = 2.0 * rgb.x - rgb.y - rgb.z;
        hue = degrees(atan(y, x));
    }

    return (hue < 0.0) ? hue + 360.0 : hue;
}

float center_hue(float hue, float centerH) {
    float hueCentered = hue - centerH;
    if (hueCentered < -180.0) {
        hueCentered = hueCentered + 360.0;
    } else if (hueCentered > 180.0) {
        hueCentered = hueCentered - 360.0;
    }
    return hueCentered;
}

// Fitting of RRT + ODT (RGB monitor 100 nits dim) from:
// https://github.com/colour-science/colour-unity/blob/master/Assets/Colour/Notebooks/CIECAM02_Unity.ipynb
// RMSE: 0.0012846272106
vec3 tonescale(vec3 ap1) {
    float a = 2.785085;
    float b = 0.107772;
    float c = 2.936045;
    float d = 0.887122;
    float e = 0.806889;
    return (ap1 * (a * ap1 + b)) / (ap1 * (c * ap1 + d) + e);
}

vec3 XYZ_to_xyY(vec3 XYZ) {
    float X = XYZ.x;
    float Y = XYZ.y;
    float Z = XYZ.z;

    float divisor = X + Y + Z;
    if (divisor == 0.0) divisor = 1e-6;

    float x = X / divisor;
    float y = Y / divisor;

    return vec3(x, y, Y);
}

vec3 xyY_to_XYZ(vec3 xyY) {
    float x = xyY.x;
    float y = xyY.y;
    float Y = xyY.z;

    float multiplo = Y / max(y, 1e-6);

    float z = 1.0 - x - y;
    float X = x * multiplo;
    float Z = z * multiplo;

    return vec3(X, Y, Z);
}

vec3 darkSurround_to_dimSurround(vec3 linearCV) {
    vec3 XYZ = linearCV * AP1_to_XYZ;
    vec3 xyY = XYZ_to_xyY(XYZ);

    xyY.z = clamp(xyY.z, 0.0, HALF_POS_INF);
    xyY.z = pow(xyY.z, DIM_SURROUND_GAMMA);

    XYZ = xyY_to_XYZ(xyY);
    return XYZ * XYZ_to_AP1;
}

vec3 ACES(vec3 color) {
    vec3 ap0;
    vec3 ap1;
    vec3 cv;

    // Look Modification Transforms (LMTs)
    ap1 = color * RGB_to_XYZ * D65_to_D60_CAT * XYZ_to_AP1;

    ap1 = reference_gamut_compress(ap1);

    ap0 = ap1 * AP1_to_XYZ * XYZ_to_AP0;


    // Reference Rendering Transform (RRT)

    // Glow module
    float saturation = rgb_to_saturation(ap0);
    float ycIn = rgb_to_yc(ap0);
    float s = sigmoid_shaper((saturation - 0.4) / 0.2);
    float addedGlow = 1.0 + glow_fwd(ycIn, RRT_GLOW_GAIN * s, RRT_GLOW_MID);
    ap0 *= addedGlow;

    // Red modifier
    float hue = rgb_to_hue(ap0);
    float centeredHue = center_hue(hue, RRT_RED_HUE);
    // hueWeight = cubic_basis_shaper(centeredHue, RRT_RED_WIDTH);
    float hueWeight = smoothstep(0.0, 1.0, 1.0 - abs(2.0 * centeredHue / RRT_RED_WIDTH));
    hueWeight *= hueWeight;

    ap0.r += hueWeight * saturation * (RRT_RED_PIVOT - ap0.r) * (1.0 - RRT_RED_SCALE);

    // ACES to RGB rendering space
    ap1 = ap0 * AP0_to_XYZ * XYZ_to_AP1;

    // avoids saturated negative colors from becoming positive in the matrix
    ap1 = clamp(ap1, 0.0, HALF_POS_INF);

    // Global desaturation
    ap1 = mix(vec3(dot(ap1, LUMINANCE_AP1)), ap1, RRT_SAT_FACTOR);


    // Output Device Transform (ODT)

    // Apply the tonescale independently in rendering-space RGB
    ap1 = tonescale(ap1);

    // Apply gamma adjustment to compensate for dim surround
    cv = darkSurround_to_dimSurround(ap1);

    // Apply desaturation to compensate for luminance difference
    cv = mix(vec3(dot(cv, LUMINANCE_AP1)), cv, ODT_SAT_FACTOR);

    // Convert to display primary encoding
    cv = cv * AP1_to_XYZ * D60_to_D65_CAT * XYZ_to_RGB;

    return cv;
}

vec4 hook() {
    vec4 color = HOOKED_tex(HOOKED_pos);

    color.rgb = ACES(color.rgb);

    return color;
}

//!HOOK OUTPUT
//!BIND HOOKED
//!DESC black point compensation

// https://www.color.org/WP40-Black_Point_Compensation_2010-07-27.pdf

vec3 RGB_to_XYZ(vec3 RGB) {
    return RGB * mat3(
        0.6369580483012914, 0.14461690358620832,  0.1688809751641721,
        0.2627002120112671, 0.6779980715188708,   0.05930171646986196,
        0.000000000000000,  0.028072693049087428, 1.060985057710791
    );
}

vec3 XYZ_to_RGB(vec3 XYZ) {
    return XYZ * mat3(
         1.716651187971268,  -0.355670783776392, -0.253366281373660,
        -0.666684351832489,   1.616481236634939,  0.0157685458139111,
         0.017639857445311,  -0.042770613257809,  0.942103121235474
    );
}

vec3 black_point_compensation(vec3 XYZ, float s, float d) {
    float r = (1.0 - d) / (1.0 - s);
    return r * XYZ + (1.0 - r) * RGB_to_XYZ(vec3(1.0));
}

vec4 hook() {
    vec4 color = HOOKED_tex(HOOKED_pos);

    color.rgb = RGB_to_XYZ(color.rgb);
    color.rgb = black_point_compensation(color.rgb, 0.0, 0.001);
    color.rgb = XYZ_to_RGB(color.rgb);

    return color;
}
