"""Reproduce the gradient-midpoint experiment on your own machine.

Authors three opaque 321 by 121 PNGs from one endpoint pair, interpolating in
the encoded sRGB code values, in linear light and in Oklab. Writes the centre
pixel for each and the SHA-256 of every output.

Requires Python 3.13 and Pillow 12.3 (earlier Pillow versions also work).
"""
import hashlib
import json
import os

from PIL import Image

W, H = 321, 121
END_LEFT = (30, 90, 180)    # #1E5AB4
END_RIGHT = (242, 193, 78)  # #F2C14E


def srgb_to_linear(c):
    c = c / 255.0
    return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4


def linear_to_srgb(c):
    c = max(0.0, min(1.0, c))
    v = 12.92 * c if c <= 0.0031308 else 1.055 * (c ** (1 / 2.4)) - 0.055
    return int(round(max(0.0, min(1.0, v)) * 255))


def lin_rgb_to_oklab(r, g, b):
    l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b
    m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b
    s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b
    l_, m_, s_ = (v ** (1 / 3) if v >= 0 else -((-v) ** (1 / 3)) for v in (l, m, s))
    return (0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
            1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
            0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_)


def oklab_to_lin_rgb(L, a, b):
    l_ = L + 0.3963377774 * a + 0.2158037573 * b
    m_ = L - 0.1055613458 * a - 0.0638541728 * b
    s_ = L - 0.0894841775 * a - 1.2914855480 * b
    l, m, s = l_ ** 3, m_ ** 3, s_ ** 3
    return (+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
            -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
            -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s)


def hexs(rgb):
    return "#{:02X}{:02X}{:02X}".format(*rgb)


def pixel_at(t, method):
    if method == "srgb-code":
        return tuple(int(round(END_LEFT[i] + (END_RIGHT[i] - END_LEFT[i]) * t))
                     for i in range(3))
    if method == "linear-light":
        lin = [srgb_to_linear(END_LEFT[i]) + (srgb_to_linear(END_RIGHT[i]) - srgb_to_linear(END_LEFT[i])) * t
               for i in range(3)]
        return tuple(linear_to_srgb(v) for v in lin)
    if method == "oklab":
        o0 = lin_rgb_to_oklab(*(srgb_to_linear(v) for v in END_LEFT))
        o1 = lin_rgb_to_oklab(*(srgb_to_linear(v) for v in END_RIGHT))
        mix = [o0[i] + (o1[i] - o0[i]) * t for i in range(3)]
        return tuple(linear_to_srgb(v) for v in oklab_to_lin_rgb(*mix))
    raise ValueError(method)


def write(method, path):
    img = Image.new("RGB", (W, H))
    px = img.load()
    for x in range(W):
        t = x / (W - 1)
        c = pixel_at(t, method)
        for y in range(H):
            px[x, y] = c
    img.save(path, "PNG", optimize=True)
    with open(path, "rb") as f:
        return hashlib.sha256(f.read()).hexdigest(), os.path.getsize(path)


def main(out_dir):
    os.makedirs(out_dir, exist_ok=True)
    avg = tuple(int(round((END_LEFT[i] + END_RIGHT[i]) / 2 + 1e-9)) for i in range(3))
    report = {
        "endpoint_left": {"rgb": list(END_LEFT), "hex": hexs(END_LEFT)},
        "endpoint_right": {"rgb": list(END_RIGHT), "hex": hexs(END_RIGHT)},
        "endpoint_channel_average": {"rgb": list(avg), "hex": hexs(avg)},
        "fixtures": {},
    }
    for m in ("srgb-code", "linear-light", "oklab"):
        path = os.path.join(out_dir, f"gradient-{m}.png")
        sha, size = write(m, path)
        with Image.open(path) as im:
            centre = im.load()[(W - 1) // 2, H // 2]
        report["fixtures"][m] = {
            "file": f"gradient-{m}.png",
            "sha256": sha,
            "bytes": size,
            "measured_centre": {"rgb": list(centre), "hex": hexs(centre)},
        }
    with open(os.path.join(out_dir, "measurements.json"), "w", encoding="utf-8") as f:
        json.dump(report, f, indent=2)
    print(json.dumps(report, indent=2))


if __name__ == "__main__":
    import sys
    main(sys.argv[1] if len(sys.argv) > 1 else ".")