/*
 * colors.c
 * Copyright (C) 2016 Kovid Goyal <kovid at kovidgoyal.net>
 *
 * Distributed under terms of the GPL3 license.
 */

#include "state.h"
#include <structmember.h>
#include "colors.h"
#include "color-names.h"
#if defined(__APPLE__) || defined(__FreeBSD__)
// Needed for strtod_l
#include <xlocale.h>
#endif

static const color_type NULL_COLOR_VALUE = 0xffffffff;
static const color_type GENERATED_COLOR_MASK = 0xff000000;

static uint32_t FG_BG_256[256] = {
    0x000000,  // 0
    0xcd0000,  // 1
    0x00cd00,  // 2
    0xcdcd00,  // 3
    0x0000ee,  // 4
    0xcd00cd,  // 5
    0x00cdcd,  // 6
    0xe5e5e5,  // 7
    0x7f7f7f,  // 8
    0xff0000,  // 9
    0x00ff00,  // 10
    0xffff00,  // 11
    0x5c5cff,  // 12
    0xff00ff,  // 13
    0x00ffff,  // 14
    0xffffff,  // 15
};

static void
init_FG_BG_table(void) {
    if (UNLIKELY(FG_BG_256[255] == 0)) {
        // colors 16..232: the 6x6x6 color cube
        const uint8_t valuerange[6] = {0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff};
        uint8_t i, j=16;
        for(i = 0; i < 216; i++, j++) {
            uint8_t r = valuerange[(i / 36) % 6], g = valuerange[(i / 6) % 6], b = valuerange[i % 6];
            FG_BG_256[j] = (r << 16) | (g << 8) | b;
        }
        // colors 232..255: grayscale
        for(i = 0; i < 24; i++, j++) {
            uint8_t v = 8 + i * 10;
            FG_BG_256[j] = (v << 16) | (v << 8) | v;
        }
    }
}

static PyObject*
create_256_color_table(void) {
    init_FG_BG_table();
    PyObject *ans = PyTuple_New(arraysz(FG_BG_256));
    if (ans == NULL) return PyErr_NoMemory();
    for (size_t i=0; i < arraysz(FG_BG_256); i++) {
        PyObject *temp = PyLong_FromUnsignedLong(FG_BG_256[i]);
        if (temp == NULL) { Py_CLEAR(ans); return NULL; }
        PyTuple_SET_ITEM(ans, i, temp);
    }
    return ans;
}

static void
set_transparent_background_colors(TransparentDynamicColor *dest, PyObject *src) {
    memset(dest, 0, sizeof(((ColorProfile*)0)->configured_transparent_colors));
    for (Py_ssize_t i = 0; i < MIN(PyTuple_GET_SIZE(src), (Py_ssize_t)arraysz(((ColorProfile*)0)->configured_transparent_colors)); i++) {
        PyObject *e = PyTuple_GET_ITEM(src, i);
        dest[i].color = ((Color*)(PyTuple_GET_ITEM(e, 0)))->color.val & 0xffffff;
        dest[i].opacity = (float)PyFloat_AsDouble(PyTuple_GET_ITEM(e, 1));
        dest[i].is_set = true;
    }
}

static bool
set_configured_colors(ColorProfile *self, PyObject *opts) {
#define n(which, attr) { \
    RAII_PyObject(t, PyObject_GetAttrString(opts, #attr)); \
    if (t == NULL) return false; \
    if (t == Py_None) { self->configured.which.rgb = 0; self->configured.which.type = COLOR_IS_SPECIAL; } \
    else if (PyLong_Check(t)) { \
        unsigned int x = PyLong_AsUnsignedLong(t); \
        self->configured.which.rgb = x & 0xffffff; \
        self->configured.which.type = COLOR_IS_RGB; \
    } else if (PyObject_TypeCheck(t, &Color_Type)) { \
        Color *c = (Color*)t; \
        self->configured.which.rgb = c->color.rgb; \
        self->configured.which.type = COLOR_IS_RGB; \
    } else { PyErr_SetString(PyExc_TypeError, "colors must be integers or Color objects"); return false; } \
}

    n(default_fg, foreground); n(default_bg, background);
    n(cursor_color, cursor); n(cursor_text_color, cursor_text_color);
    n(highlight_fg, selection_foreground); n(highlight_bg, selection_background);
    n(visual_bell_color, visual_bell_color);
#undef n
    RAII_PyObject(src, PyObject_GetAttrString(opts, "transparent_background_colors"));
    if (!src) { PyErr_SetString(PyExc_TypeError, "No transparent_background_colors on opts object"); return false; }
    set_transparent_background_colors(self->configured_transparent_colors, src);
    return PyErr_Occurred() ? false : true;
}

static bool
set_mark_colors(ColorProfile *self, PyObject *opts) {
    char fgattr[] = "mark?_foreground", bgattr[] = "mark?_background";
#define n(i, attr, which) { \
    attr[4] = '1' + i; \
    RAII_PyObject(t, PyObject_GetAttrString(opts, attr)); \
    if (t == NULL) return false; \
    if (!PyObject_TypeCheck(t, &Color_Type)) { PyErr_SetString(PyExc_TypeError, "mark color is not Color object"); return false; } \
    Color *c = (Color*)t; self->which[i] = c->color.rgb; \
}
#define m(i) n(i, fgattr, mark_foregrounds); n(i, bgattr, mark_backgrounds);
    m(0); m(1); m(2);
#undef m
#undef n
    return true;
}

// Generating 256 color palette {{{
// For more information, see
// https://gist.github.com/jake-stewart/0a8ea46159a7da2c808e5be2177e1783

static void
color_type_to_lab(color_type color, float *lab) {
    float r = ((color >> 16) & 0xff) / 255.0f;
    float g = ((color >> 8) & 0xff) / 255.0f;
    float b = (color & 0xff) / 255.0f;

    r = r > 0.04045f ? powf((r + 0.055f) / 1.055f, 2.4f) : r / 12.92f;
    g = g > 0.04045f ? powf((g + 0.055f) / 1.055f, 2.4f) : g / 12.92f;
    b = b > 0.04045f ? powf((b + 0.055f) / 1.055f, 2.4f) : b / 12.92f;

    float x = (r * 0.4124564f + g * 0.3575761f + b * 0.1804375f) / 0.95047f;
    float y = (r * 0.2126729f + g * 0.7151522f + b * 0.0721750f);
    float z = (r * 0.0193339f + g * 0.1191920f + b * 0.9503041f) / 1.08883f;

    x = x > 0.008856f ? cbrtf(x) : 7.787f * x + 16.0f / 116.0f;
    y = y > 0.008856f ? cbrtf(y) : 7.787f * y + 16.0f / 116.0f;
    z = z > 0.008856f ? cbrtf(z) : 7.787f * z + 16.0f / 116.0f;

    lab[0] = 116.0f * y - 16.0f;
    lab[1] = 500.0f * (x - y);
    lab[2] = 200.0f * (y - z);
}

static color_type
lab_to_color_type(float *lab) {
    float y = (lab[0] + 16.0f) / 116.0f;
    float x = lab[1] / 500.0f + y;
    float z = y - lab[2] / 200.0f;

    float x3 = x * x * x, y3 = y * y * y, z3 = z * z * z;
    x = (x3 > 0.008856f ? x3 : (x - 16.0f / 116.0f) / 7.787f) * 0.95047f;
    y = y3 > 0.008856f ? y3 : (y - 16.0f / 116.0f) / 7.787f;
    z = (z3 > 0.008856f ? z3 : (z - 16.0f / 116.0f) / 7.787f) * 1.08883f;

    float r = x * 3.2404542f - y * 1.5371385f - z * 0.4985314f;
    float g = -x * 0.9692660f + y * 1.8760108f + z * 0.0415560f;
    float b = x * 0.0556434f - y * 0.2040259f + z * 1.0572252f;

    r = r > 0.0031308f ? 1.055f * powf(r, 1.0f / 2.4f) - 0.055f : 12.92f * r;
    g = g > 0.0031308f ? 1.055f * powf(g, 1.0f / 2.4f) - 0.055f : 12.92f * g;
    b = b > 0.0031308f ? 1.055f * powf(b, 1.0f / 2.4f) - 0.055f : 12.92f * b;

    uint8_t rb = (uint8_t)(fminf(fmaxf(r, 0.0f), 1.0f) * 255.0f + 0.5f);
    uint8_t gb = (uint8_t)(fminf(fmaxf(g, 0.0f), 1.0f) * 255.0f + 0.5f);
    uint8_t bb = (uint8_t)(fminf(fmaxf(b, 0.0f), 1.0f) * 255.0f + 0.5f);

    return GENERATED_COLOR_MASK | (rb << 16) | (gb << 8) | bb;
}

static void
lerp_lab(float t, float *a, float *b, float *out) {
    out[0] = a[0] + t * (b[0] - a[0]);
    out[1] = a[1] + t * (b[1] - a[1]);
    out[2] = a[2] + t * (b[2] - a[2]);
}

static void
setup_256_palette_lab(ColorProfile *self, color_type *color_table, bool semantic, float base8_lab[8][3]) {
    const color_type bg = colorprofile_to_color(self, self->overridden.default_bg, self->configured.default_bg).rgb;
    const color_type fg = colorprofile_to_color(self, self->overridden.default_fg, self->configured.default_fg).rgb;
    color_type_to_lab(bg, base8_lab[0]);
    color_type_to_lab(fg, base8_lab[7]);
    for (int i = 1; i < 7; i++) color_type_to_lab(color_table[i], base8_lab[i]);

    bool is_light_theme = base8_lab[7][0] < base8_lab[0][0];
    bool invert = is_light_theme && !semantic;
    if (invert) {
        float tmp[3];
        memcpy(tmp, base8_lab[0], sizeof(tmp));
        memcpy(base8_lab[0], base8_lab[7], sizeof(tmp));
        memcpy(base8_lab[7], tmp, sizeof(tmp));
    }
}

static bool
is_generated_color(const color_type c) { return (c & GENERATED_COLOR_MASK) == GENERATED_COLOR_MASK; }

static void
generate_256_palette(ColorProfile *self, color_type *color_table, bool semantic) {
    float base8_lab[8][3];
    setup_256_palette_lab(self, color_table, semantic, base8_lab);

    int idx = 16;
    for (int r = 0; r < 6; r++) {
        float c0[3], c1[3], c2[3], c3[3];
        float tr = r / 5.0f;
        lerp_lab(tr, base8_lab[0], base8_lab[1], c0);
        lerp_lab(tr, base8_lab[2], base8_lab[3], c1);
        lerp_lab(tr, base8_lab[4], base8_lab[5], c2);
        lerp_lab(tr, base8_lab[6], base8_lab[7], c3);
        for (int g = 0; g < 6; g++) {
            float c4[3], c5[3];
            float tg = g / 5.0f;
            lerp_lab(tg, c0, c1, c4);
            lerp_lab(tg, c2, c3, c5);
            for (int b = 0; b < 6; b++) {
                if (is_generated_color(color_table[idx])) {
                    float c6[3];
                    lerp_lab(b / 5.0f, c4, c5, c6);
                    color_table[idx] = lab_to_color_type(c6);
                }
                idx++;
            }
        }
    }

    for (int i = 0; i < 24; i++) {
        float t = (i + 1) / 25.0f;
        float lab[3];
        lerp_lab(t, base8_lab[0], base8_lab[7], lab);
        if (is_generated_color(color_table[idx])) color_table[idx] = lab_to_color_type(lab);
        idx++;
    }
}

static color_type
generate_256_palette_color(ColorProfile *self, color_type *color_table, unsigned int idx, bool semantic) {
    float base8_lab[8][3];
    setup_256_palette_lab(self, color_table, semantic, base8_lab);

    if (idx < 232) {
        unsigned int i = idx - 16;
        int r = i / 36, g = (i % 36) / 6, b = i % 6;
        float c0[3], c1[3], c2[3], c3[3], c4[3], c5[3], c6[3];
        lerp_lab(r / 5.0f, base8_lab[0], base8_lab[1], c0);
        lerp_lab(r / 5.0f, base8_lab[2], base8_lab[3], c1);
        lerp_lab(r / 5.0f, base8_lab[4], base8_lab[5], c2);
        lerp_lab(r / 5.0f, base8_lab[6], base8_lab[7], c3);
        lerp_lab(g / 5.0f, c0, c1, c4);
        lerp_lab(g / 5.0f, c2, c3, c5);
        lerp_lab(b / 5.0f, c4, c5, c6);
        return lab_to_color_type(c6);
    } else {
        int i = idx - 232;
        float lab[3];
        lerp_lab((i + 1) / 25.0f, base8_lab[0], base8_lab[7], lab);
        return lab_to_color_type(lab);
    }
}

static void
fixed_color_palette(color_type *color_table) {
    init_FG_BG_table();
    for (unsigned i = 16; i < arraysz(FG_BG_256); i++) {
        if (is_generated_color(color_table[i])) color_table[i] = GENERATED_COLOR_MASK | FG_BG_256[i];
    }
}

static bool
palette_generation_is_dynamic(PyObject *opts, bool *semantic) {
    bool ans = false; *semantic = false;
    if (opts) {
        RAII_PyObject(policy, PyObject_GetAttrString(opts, "palette_generate")); if (!policy) return ans;
        switch(PyUnicode_AsUTF8(policy)[0]) {
            case 'f': break;
            case 's': *semantic = true; /* fallthrough */
            default: ans = true;
        }
    }
    return ans;
}
// }}}

static bool
set_colortable(ColorProfile *self, PyObject *opts) {
    RAII_PyObject(ct, PyObject_GetAttrString(opts, "color_table")); if (!ct) return false;
    RAII_PyObject(ret, PyObject_CallMethod(ct, "buffer_info", NULL)); if (!ret) return false;
    bool semantic_generation, dynamic_palette = palette_generation_is_dynamic(opts, &semantic_generation);
    unsigned long *color_table = PyLong_AsVoidPtr(PyTuple_GET_ITEM(ret, 0));
    size_t count = PyLong_AsSize_t(PyTuple_GET_ITEM(ret, 1));
    if (!color_table || count != arraysz(FG_BG_256)) { PyErr_SetString(PyExc_TypeError, "color_table has incorrect length"); return false; }
    RAII_PyObject(r2, PyObject_GetAttrString(ct, "itemsize")); if (!r2) return false;
    size_t itemsize = PyLong_AsSize_t(r2);
    if (itemsize != sizeof(unsigned long)) { PyErr_Format(PyExc_TypeError, "color_table has incorrect itemsize: %zu", itemsize); return false; }
    for (size_t i = 0; i < arraysz(FG_BG_256); i++) self->color_table[i] = color_table[i];
    if (dynamic_palette) generate_256_palette(self, self->color_table, semantic_generation);
    else fixed_color_palette(self->color_table);
    memcpy(self->orig_color_table, self->color_table, sizeof(self->color_table));
    return true;
}


static PyObject*
new_cp(PyTypeObject *type, PyObject *args, PyObject *kwds) {
    PyObject *opts = global_state.options_object;
    ColorProfile *self;
    static const char* kw[] = {"opts", NULL};
    if (args && !PyArg_ParseTupleAndKeywords(args, kwds, "|O", (char**)kw, &opts)) return NULL;
    self = (ColorProfile *)type->tp_alloc(type, 0);
    RAII_PyObject(ans, (PyObject*)self);
    if (self != NULL) {
        init_FG_BG_table();
        if (opts) {
            if (!set_configured_colors(self, opts)) return NULL;
            if (!set_mark_colors(self, opts)) return NULL;
            if (!set_colortable(self, opts)) return NULL;
        } else {
            memcpy(self->color_table, FG_BG_256, sizeof(FG_BG_256));
            memcpy(self->orig_color_table, self->color_table, sizeof(FG_BG_256));
        }
        self->dirty = true;
        Py_INCREF(ans);
    }
    return ans;
}

static void
dealloc_cp(ColorProfile* self) {
    if (self->color_stack) free(self->color_stack);
    Py_TYPE(self)->tp_free((PyObject*)self);
}

ColorProfile*
alloc_color_profile(void) {
    return (ColorProfile*)new_cp(&ColorProfile_Type, NULL, NULL);
}


void
copy_color_profile(ColorProfile *dest, ColorProfile *src) {
    memcpy(dest->color_table, src->color_table, sizeof(dest->color_table));
    memcpy(dest->orig_color_table, src->orig_color_table, sizeof(dest->color_table));
    memcpy(&dest->configured, &src->configured, sizeof(dest->configured));
    memcpy(&dest->overridden, &src->overridden, sizeof(dest->overridden));
    memcpy(dest->overriden_transparent_colors, src->overriden_transparent_colors, sizeof(dest->overriden_transparent_colors));
    memcpy(dest->configured_transparent_colors, src->configured_transparent_colors, sizeof(dest->configured_transparent_colors));
    dest->dirty = true;
}

static void
patch_color_table(const char *key, PyObject *profiles, PyObject *spec, size_t which, int change_configured, bool *has_null_values) {
    PyObject *v = PyDict_GetItemString(spec, key);
    if (v) {
        color_type color = NULL_COLOR_VALUE;
        if (PyLong_Check(v)) color = PyLong_AsUnsignedLong(v);
        else *has_null_values = true;
        for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) {
            ColorProfile *self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j);
            self->color_table[which] = color;
            if (change_configured) self->orig_color_table[which] = color;
            self->dirty = true;
        }
    }

}

#define patch_mark_color(key, profiles, spec, array, i) { \
    PyObject *v = PyDict_GetItemString(spec, key); \
    if (v && PyLong_Check(v)) { \
        color_type color = PyLong_AsUnsignedLong(v); \
        for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) { \
            ColorProfile *self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j); \
            self->array[i] = color; \
            self->dirty = true; \
} } }


static PyObject*
patch_color_profiles(PyObject *module UNUSED, PyObject *args) {
    PyObject *spec, *transparent_background_colors, *profiles, *v; ColorProfile *self; int change_configured;
    if (!PyArg_ParseTuple(args, "O!O!O!p", &PyDict_Type, &spec, &PyTuple_Type, &transparent_background_colors, &PyTuple_Type, &profiles, &change_configured)) return NULL;
    char key[32] = {0};
    bool has_null_values = false;
    for (size_t i = 0; i < arraysz(FG_BG_256); i++) {
        snprintf(key, sizeof(key) - 1, "color%zu", i);
        patch_color_table(key, profiles, spec, i, change_configured, &has_null_values);
    }
    for (size_t i = 1; i <= MARK_MASK; i++) {
#define S(which, i) snprintf(key, sizeof(key) - 1, "mark%zu_" #which, i); patch_mark_color(key, profiles, spec, mark_##which##s, i)
    S(background, i); S(foreground, i);
#undef S
    }
#define SI(profile_name) \
    DynamicColor color; \
    if (PyLong_Check(v)) { \
        color.rgb = PyLong_AsUnsignedLong(v);  color.type = COLOR_IS_RGB; \
    } else { color.rgb = 0; color.type = COLOR_IS_SPECIAL; }\
    self->overridden.profile_name = color; \
    if (change_configured) self->configured.profile_name = color; \
    self->dirty = true;

#define S(config_name, profile_name) { \
    v = PyDict_GetItemString(spec, #config_name); \
    if (v) { \
        for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) { \
            self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i); \
            SI(profile_name); \
        } \
    } \
}
        S(foreground, default_fg); S(background, default_bg); S(cursor, cursor_color);
        S(selection_foreground, highlight_fg); S(selection_background, highlight_bg);
        S(cursor_text_color, cursor_text_color); S(visual_bell_color, visual_bell_color);
#undef SI
#undef S
    for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) {
        self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i);
        set_transparent_background_colors(self->overriden_transparent_colors, transparent_background_colors);
        if (change_configured) set_transparent_background_colors(self->configured_transparent_colors, transparent_background_colors);
    }

    if (has_null_values) {
        bool semantic, dynamic = palette_generation_is_dynamic(global_state.options_object, &semantic);
        for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) {
            ColorProfile *self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j);
            if (dynamic) {
                generate_256_palette(self, self->color_table, semantic);
                if (change_configured) generate_256_palette(self, self->orig_color_table, semantic);
            } else {
                fixed_color_palette(self->color_table);
                if (change_configured) fixed_color_palette(self->orig_color_table);
            }
        }
    }
    if (PyErr_Occurred()) return NULL;
    Py_RETURN_NONE;
}

bool
colorprofile_to_transparent_color(const ColorProfile *self, unsigned index, color_type *color, float *opacity) {
    *color = UINT32_MAX; *opacity = 1.0;
    if (index < arraysz(self->configured_transparent_colors)) {
        if (self->overriden_transparent_colors[index].is_set) {
            *color = self->overriden_transparent_colors[index].color; *opacity = self->overriden_transparent_colors[index].opacity;
            if (*opacity < 0) *opacity = OPT(background_opacity);
            return true;
        }
        if (self->configured_transparent_colors[index].is_set) {
            *color = self->configured_transparent_colors[index].color; *opacity = self->configured_transparent_colors[index].opacity;
            if (*opacity < 0) *opacity = OPT(background_opacity);
            return true;
        }
    }
    return false;
}

DynamicColor
colorprofile_to_color(const ColorProfile *self, DynamicColor entry, DynamicColor defval) {
    switch(entry.type) {
        case COLOR_NOT_SET:
            return defval;
        case COLOR_IS_INDEX: {
            DynamicColor ans;
            ans.rgb = self->color_table[entry.rgb & 0xff] & (~GENERATED_COLOR_MASK);
            ans.type = COLOR_IS_RGB;
            return ans;
        }
        case COLOR_IS_RGB:
        case COLOR_IS_SPECIAL:
            return entry;
    }
    return entry;
}

color_type
colorprofile_to_color_with_fallback(ColorProfile *self, DynamicColor entry, DynamicColor defval, DynamicColor fallback, DynamicColor fallback_defval) {
    switch(entry.type) {
        case COLOR_NOT_SET:
        case COLOR_IS_SPECIAL:
            if (defval.type == COLOR_IS_SPECIAL) return colorprofile_to_color(self, fallback, fallback_defval).rgb;
            return defval.rgb;
        case COLOR_IS_RGB:
            return entry.rgb;
        case COLOR_IS_INDEX:
            return self->color_table[entry.rgb & 0xff] & (~GENERATED_COLOR_MASK);
    }
    return entry.rgb;
}
static Color* alloc_color(unsigned char r, unsigned char g, unsigned char b, unsigned a);

static bool
colortable_colors_into_dict(ColorProfile *self, unsigned start, unsigned limit, PyObject *ans) {
    static char buf[32] = {'c', 'o', 'l', 'o', 'r', 0};
    for (unsigned i = start; i < limit; i++) {
        snprintf(buf + 5, sizeof(buf) - 6, "%u", i);
        PyObject *val = PyLong_FromUnsignedLong(self->color_table[i] &(~GENERATED_COLOR_MASK));
        if (!val) return false;
        int ret = PyDict_SetItemString(ans, buf, val);
        Py_DECREF(val);
        if (ret != 0) return false;
    }
    return true;
}

static PyObject*
basic_colors(ColorProfile *self, PyObject *args UNUSED) {
#define basic_colors_doc "Return the basic colors as a dictionary of color_name to integer or None (names are the same as used in kitty.conf)"
    RAII_PyObject(ans, PyDict_New()); if (ans == NULL) return NULL;
    if (!colortable_colors_into_dict(self, 0, 16, ans)) return NULL;

#define D(attr, name) { \
    unsigned long c = colorprofile_to_color(self, self->overridden.attr, self->configured.attr).rgb; \
    PyObject *val = PyLong_FromUnsignedLong(c); if (!val) return NULL; \
    int ret = PyDict_SetItemString(ans, #name, val); Py_DECREF(val); \
    if (ret != 0) return NULL; \
}

    D(default_fg, foreground); D(default_bg, background);
#undef D
    return Py_NewRef(ans);
}

static PyObject*
as_dict(ColorProfile *self, PyObject *args UNUSED) {
#define as_dict_doc "Return all colors as a dictionary of color_name to integer or None (names are the same as used in kitty.conf)"
    RAII_PyObject(ans, PyDict_New()); if (ans == NULL) return NULL;
    if (!colortable_colors_into_dict(self, 0, arraysz(self->color_table), ans)) return NULL;
#define D(attr, name) { \
    if (self->overridden.attr.type != COLOR_NOT_SET) { \
        int ret; PyObject *val; \
        if (self->overridden.attr.type == COLOR_IS_SPECIAL) { \
            val = Py_NewRef(Py_None); \
        } else { \
            unsigned long c = colorprofile_to_color(self, self->overridden.attr, self->configured.attr).rgb; \
            val = PyLong_FromUnsignedLong(c); \
        } \
        if (!val) { return NULL; } \
        ret = PyDict_SetItemString(ans, #name, val); \
        Py_DECREF(val); \
        if (ret != 0) { return NULL; } \
    }}
    D(default_fg, foreground); D(default_bg, background);
    D(cursor_color, cursor); D(cursor_text_color, cursor_text); D(highlight_fg, selection_foreground);
    D(highlight_bg, selection_background); D(visual_bell_color, visual_bell_color);
    RAII_PyObject(transparent_background_colors, PyList_New(0));
    if (!transparent_background_colors) return NULL;
    for (size_t i = 0; i < arraysz(self->overriden_transparent_colors); i++) {
        TransparentDynamicColor *c = NULL;
        if (self->overriden_transparent_colors[i].is_set) c = self->overriden_transparent_colors + i;
        else if (self->configured_transparent_colors[i].is_set) c = self->configured_transparent_colors + i;
        if (c) {
            RAII_PyObject(t, Py_BuildValue("Nf", alloc_color((c->color >> 16) & 0xff, (c->color >> 8) & 0xff, c->color & 0xff, 0), c->opacity));
            if (!t) return NULL;
            if (PyList_Append(transparent_background_colors, t) != 0) return NULL;
        }
    }
    if (PyList_GET_SIZE(transparent_background_colors)) {
        RAII_PyObject(t, PyList_AsTuple(transparent_background_colors));
        if (!t) return NULL;
        if (PyDict_SetItemString(ans, "transparent_background_colors", t) != 0) return NULL;
    }
#undef D
    return Py_NewRef(ans);
}

static PyObject*
as_color(ColorProfile *self, PyObject *val) {
#define as_color_doc "Convert the specified terminal color into an (r, g, b) tuple based on the current profile values"
    if (!PyLong_Check(val)) { PyErr_SetString(PyExc_TypeError, "val must be an int"); return NULL; }
    unsigned long entry = PyLong_AsUnsignedLong(val);
    unsigned int t = entry & 0xFF;
    uint8_t r;
    uint32_t col = 0;
    switch(t) {
        case 1:
            r = (entry >> 8) & 0xff;
            col = self->color_table[r] & (~GENERATED_COLOR_MASK);
            break;
        case 2:
            col = entry >> 8;
            break;
        default:
            Py_RETURN_NONE;
    }
    Color *ans = PyObject_New(Color, &Color_Type);
    if (ans) {
        ans->color.val = 0;
        ans->color.rgb = col;
    }
    return (PyObject*)ans;
}

static PyObject*
reset_color_table(ColorProfile *self, PyObject *a UNUSED) {
#define reset_color_table_doc "Reset all customized colors back to defaults"
    memcpy(self->color_table, self->orig_color_table, sizeof(FG_BG_256));
    self->dirty = true;
    Py_RETURN_NONE;
}

static PyObject*
reset_color(ColorProfile *self, PyObject *val) {
#define reset_color_doc "Reset the specified color"
    uint8_t i = PyLong_AsUnsignedLong(val) & 0xff;
    self->color_table[i] = self->orig_color_table[i];
    self->dirty = true;
    Py_RETURN_NONE;
}

static PyObject*
set_color(ColorProfile *self, PyObject *args) {
#define set_color_doc "Set the specified color"
    unsigned char i;
    unsigned long val;
    if (!PyArg_ParseTuple(args, "Bk", &i, &val)) return NULL;
    self->dirty = true;
    if (val == NULL_COLOR_VALUE && i >= 16) {
        bool semantic, dynamic = palette_generation_is_dynamic(global_state.options_object, &semantic);
        self->color_table[i] = dynamic ? generate_256_palette_color(self, self->color_table, i, semantic) : GENERATED_COLOR_MASK | FG_BG_256[i];
    } else self->color_table[i] = val;
    Py_RETURN_NONE;
}

void
copy_color_table_to_buffer(ColorProfile *self, color_type *buf, int offset, size_t stride) {
    size_t i;
    stride = MAX(1u, stride);
    for (i = 0, buf = buf + offset; i < arraysz(self->color_table); i++, buf += stride) *buf = self->color_table[i] & (~GENERATED_COLOR_MASK);
    // Copy the mark colors
    for (i = 0; i < arraysz(self->mark_backgrounds); i++) {
        *buf = self->mark_backgrounds[i]; buf += stride;
    }
    for (i = 0; i < arraysz(self->mark_foregrounds); i++) {
        *buf = self->mark_foregrounds[i]; buf += stride;
    }
    self->dirty = false;
}

static void
push_onto_color_stack_at(ColorProfile *self, unsigned int i) {
    self->color_stack[i].dynamic_colors = self->overridden;
    memcpy(self->color_stack[i].transparent_colors, self->overriden_transparent_colors, sizeof(self->overriden_transparent_colors));
    self->color_stack[i].dynamic_colors = self->overridden;
    memcpy(self->color_stack[i].color_table, self->color_table, sizeof(self->color_stack->color_table));
}

static void
copy_from_color_stack_at(ColorProfile *self, unsigned int i) {
    self->overridden = self->color_stack[i].dynamic_colors;
    memcpy(self->color_table, self->color_stack[i].color_table, sizeof(self->color_table));
    memcpy(self->overriden_transparent_colors, self->color_stack[i].transparent_colors, sizeof(self->overriden_transparent_colors));
}

bool
colorprofile_push_colors(ColorProfile *self, unsigned int idx) {
    if (idx > 10) return false;
    size_t sz = idx ? idx : self->color_stack_idx + 1;
    sz = MIN(10u, sz);
    if (self->color_stack_sz < sz) {
        self->color_stack = realloc(self->color_stack, sz * sizeof(self->color_stack[0]));
        if (self->color_stack == NULL) fatal("Out of memory while ensuring space for %zu elements in color stack", sz);
        memset(self->color_stack + self->color_stack_sz, 0, (sz - self->color_stack_sz) * sizeof(self->color_stack[0]));
        self->color_stack_sz = sz;
    }
    if (idx == 0) {
        if (self->color_stack_idx >= self->color_stack_sz) {
            memmove(self->color_stack, self->color_stack + 1, (self->color_stack_sz - 1) * sizeof(self->color_stack[0]));
            idx = self->color_stack_sz - 1;
        } else idx = self->color_stack_idx++;
        push_onto_color_stack_at(self, idx);
        return true;
    }
    idx -= 1;
    if (idx < self->color_stack_sz) {
        push_onto_color_stack_at(self, idx);
        return true;
    }
    return false;
}

void
colorprofile_reset(ColorProfile *self) {
    memcpy(self->color_table, self->orig_color_table, sizeof(FG_BG_256));
    self->dirty = true;
    self->color_stack_idx = 0;
    zero_at_ptr(&self->overridden);
    for (unsigned i = 0; i < arraysz(self->overriden_transparent_colors); i++) {
        zero_at_ptr(self->overriden_transparent_colors + i);
    }
    for (unsigned i = 0; i < self->color_stack_sz; i++) {
        zero_at_ptr(self->color_stack + i);
    }
}

bool
colorprofile_pop_colors(ColorProfile *self, unsigned int idx) {
    if (idx == 0) {
        if (!self->color_stack_idx) return false;
        copy_from_color_stack_at(self, --self->color_stack_idx);
        memset(self->color_stack + self->color_stack_idx, 0, sizeof(self->color_stack[0]));
        return true;
    }
    idx -= 1;
    if (idx < self->color_stack_sz) {
        copy_from_color_stack_at(self, idx);
        return true;
    }
    return false;
}

void
colorprofile_report_stack(ColorProfile *self, unsigned int *idx, unsigned int *count) {
    *count = self->color_stack_idx;
    *idx = self->color_stack_idx ? self->color_stack_idx - 1 : 0;
}

static PyObject*
color_table_address(ColorProfile *self, PyObject *a UNUSED) {
#define color_table_address_doc "Pointer address to start of color table"
    return PyLong_FromVoidPtr((void*)self->color_table);
}

static PyObject*
default_color_table(PyObject *self UNUSED, PyObject *args UNUSED) {
    return create_256_color_table();
}

// Boilerplate {{{

#define CGETSET(name, nullable) \
    static PyObject* name##_get(ColorProfile *self, void UNUSED *closure) {  \
        DynamicColor ans = colorprofile_to_color(self, self->overridden.name, self->configured.name);  \
        if (ans.type == COLOR_IS_SPECIAL) { \
            if (nullable) Py_RETURN_NONE; \
            return (PyObject*)alloc_color(0, 0, 0, 0); \
        } \
        return (PyObject*)alloc_color((ans.rgb >> 16) & 0xff, (ans.rgb >> 8) & 0xff, ans.rgb & 0xff, 0); \
    } \
    static int name##_set(ColorProfile *self, PyObject *v, void UNUSED *closure) { \
        if (v == NULL) { self->overridden.name.val = 0; return 0; } \
        if (PyLong_Check(v)) { \
            unsigned long val = PyLong_AsUnsignedLong(v); \
            self->overridden.name.rgb = val & 0xffffff; \
            self->overridden.name.type = COLOR_IS_RGB; \
        } else if (PyObject_TypeCheck(v, &Color_Type)) { \
            Color *c = (Color*)v; self->overridden.name.rgb = c->color.rgb; self->overridden.name.type = COLOR_IS_RGB; \
        } else if (v == Py_None) { \
            if (!nullable) { PyErr_SetString(PyExc_TypeError, #name " cannot be set to None"); return -1; } \
            self->overridden.name.type = COLOR_IS_SPECIAL; self->overridden.name.rgb = 0; \
        } \
        self->dirty = true; return 0; \
    }

CGETSET(default_fg, false)
CGETSET(default_bg, false)
CGETSET(cursor_color, true)
CGETSET(cursor_text_color, true)
CGETSET(highlight_fg, true)
CGETSET(highlight_bg, true)
CGETSET(visual_bell_color, true)
#undef CGETSET

static PyGetSetDef cp_getsetters[] = {
    GETSET(default_fg)
    GETSET(default_bg)
    GETSET(cursor_color)
    GETSET(cursor_text_color)
    GETSET(highlight_fg)
    GETSET(highlight_bg)
    GETSET(visual_bell_color)
    {NULL}  /* Sentinel */
};


static PyMemberDef cp_members[] = {
    {NULL}
};

static PyObject*
palette_color_is_generated(ColorProfile *self, PyObject *x) {
    if (!PyLong_Check(x)) { PyErr_SetString(PyExc_TypeError, "idx must be an integer"); return NULL; }
    size_t idx = PyLong_AsUnsignedLong(x);
    if (idx >= arraysz(FG_BG_256)) { PyErr_SetString(PyExc_IndexError, "idx out of bounds"); return NULL; }
    return Py_NewRef(is_generated_color(self->color_table[idx]) ? Py_True : Py_False);
}

static PyObject*
reload_from_opts(ColorProfile *self, PyObject *args, PyObject *kw) {
    PyObject *opts = NULL;
    int reset_overriden_colors = 1;
    static const char* kwlist[] = {"opts", "reset_overriden_colors", NULL};
    if (!PyArg_ParseTupleAndKeywords(args, kw, "|Op", (char**)kwlist, &opts, &reset_overriden_colors)) return NULL;
    if (opts == NULL) opts = global_state.options_object;
    self->dirty = true;
    if (!set_configured_colors(self, opts)) return NULL;
    if (reset_overriden_colors) {
        zero_at_ptr(&self->overridden);
        memset(self->overriden_transparent_colors, 0, sizeof(self->overriden_transparent_colors));
    }
    if (!set_mark_colors(self, opts)) return NULL;
    if (!set_colortable(self, opts)) return NULL;
    Py_RETURN_NONE;
}

static PyObject*
get_transparent_background_color(ColorProfile *self, PyObject *index) {
    if (!PyLong_Check(index)) { PyErr_SetString(PyExc_TypeError, "index must be an int"); return NULL; }
    unsigned long idx = PyLong_AsUnsignedLong(index);
    if (PyErr_Occurred()) return NULL;
    if (idx >= arraysz(self->configured_transparent_colors)) Py_RETURN_NONE;
    TransparentDynamicColor *c = self->overriden_transparent_colors[idx].is_set ? self->overriden_transparent_colors + idx : self->configured_transparent_colors + idx;
    if (!c->is_set) Py_RETURN_NONE;
    float opacity = c->opacity >= 0 ? c->opacity : OPT(background_opacity);
    return (PyObject*)alloc_color((c->color >> 16) & 0xff, (c->color >> 8) & 0xff, c->color & 0xff, (unsigned)(255.f * opacity));
}

static PyObject*
set_transparent_background_color(ColorProfile *self, PyObject *const *args, Py_ssize_t nargs) {
    if (nargs < 1) { PyErr_SetString(PyExc_TypeError, "must specify index"); return NULL; }
    if (!PyLong_Check(args[0])) { PyErr_SetString(PyExc_TypeError, "index must be an int"); return NULL; }
    unsigned long idx = PyLong_AsUnsignedLong(args[0]);
    if (PyErr_Occurred()) return NULL;
    if (idx >= arraysz(self->configured_transparent_colors)) Py_RETURN_NONE;
    if (nargs < 2) { self->overriden_transparent_colors[idx].is_set = false; Py_RETURN_NONE; }
    if (!PyObject_TypeCheck(args[1], &Color_Type)) { PyErr_SetString(PyExc_TypeError, "color must be Color object"); return NULL; }
    Color *c = (Color*)args[1];
    float opacity = (float)(c->color.alpha) / 255.f;
    if (nargs > 2 && PyFloat_Check(args[2])) opacity = (float)PyFloat_AsDouble(args[2]);
    self->overriden_transparent_colors[idx].is_set = true;
    self->overriden_transparent_colors[idx].color = c->color.rgb;
    self->overriden_transparent_colors[idx].opacity = MAX(-1.f, MIN(opacity, 1.f));
    Py_RETURN_NONE;
}

static PyMethodDef cp_methods[] = {
    METHOD(reset_color_table, METH_NOARGS)
    METHOD(as_dict, METH_NOARGS)
    METHOD(basic_colors, METH_NOARGS)
    METHOD(color_table_address, METH_NOARGS)
    METHOD(as_color, METH_O)
    METHOD(reset_color, METH_O)
    METHOD(set_color, METH_VARARGS)
    METHODB(palette_color_is_generated, METH_O),
    METHODB(get_transparent_background_color, METH_O),
    {"reload_from_opts", (PyCFunction)(void (*) (void))(reload_from_opts), METH_VARARGS | METH_KEYWORDS, NULL},
    {"set_transparent_background_color", (PyCFunction)(void(*)(void))set_transparent_background_color, METH_FASTCALL, ""},
    {NULL}  /* Sentinel */
};


PyTypeObject ColorProfile_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "fast_data_types.ColorProfile",
    .tp_basicsize = sizeof(ColorProfile),
    .tp_dealloc = (destructor)dealloc_cp,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_doc = "ColorProfile",
    .tp_members = cp_members,
    .tp_methods = cp_methods,
    .tp_getset = cp_getsetters,
    .tp_new = new_cp,
};
// }}}

static Color*
alloc_color(unsigned char r, unsigned char g, unsigned char b, unsigned a) {
    Color *self = (Color *)(&Color_Type)->tp_alloc(&Color_Type, 0);
    if (self != NULL) {
        self->color.r = r; self->color.g = g; self->color.b = b; self->color.a = a;
    }
    return self;
}

static PyObject *
new_color(PyTypeObject *type UNUSED, PyObject *args, PyObject *kwds) {
    static const char* kwlist[] = {"red", "green", "blue", "alpha", NULL};
    unsigned char r = 0, g = 0, b = 0, a = 0;
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|BBBB", (char**)kwlist, &r, &g, &b, &a)) return NULL;
    return (PyObject*) alloc_color(r, g, b, a);
}

static PyObject *
color_vectorcall(PyObject *type UNUSED, PyObject *const *args, size_t nargsf, PyObject *kwnames) {
    const Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
    if (nargs > 4) {
        PyErr_SetString(PyExc_TypeError, "Color() takes at most 4 arguments");
        return NULL;
    }
    unsigned char rgba[4] = {0, 0, 0, 0};
    for (Py_ssize_t i = 0; i < nargs; i++) {
        unsigned long val = PyLong_AsUnsignedLongMask(args[i]);
        if (val == (unsigned long)-1 && PyErr_Occurred()) return NULL;
        rgba[i] = (unsigned char)val;
    }
    if (kwnames) {
        const unsigned num = PyTuple_GET_SIZE(kwnames);
        for (unsigned i = 0; i < num; i++) {
            const char *name = PyUnicode_AsUTF8(PyTuple_GET_ITEM(kwnames, i));
            if (!name) return NULL;
            int idx;
#define C(ch, i, expected) case ch: idx = i; if (strcmp(name, expected) != 0) { \
            PyErr_Format(PyExc_TypeError, "Color() got an unexpected keyword argument '%s'", name); return NULL; }; break;
            switch(name[0]) {
                C('r', 0, "red"); C('g', 1, "green"); C('b', 2, "blue"); C('a', 3, "alpha");
                default:
                PyErr_Format(PyExc_TypeError, "Color() got an unexpected keyword argument '%s'", name); return NULL;
            }
#undef C
            if (idx < nargs) {
                PyErr_Format(PyExc_TypeError, "Color() got multiple values for argument '%s'", name);
                return NULL;
            }
            unsigned long val = PyLong_AsUnsignedLongMask(args[nargs + i]);
            if (val == (unsigned long)-1 && PyErr_Occurred()) return NULL;
            rgba[idx] = (unsigned char)val;
        }
    }
    return (PyObject*)alloc_color(rgba[0], rgba[1], rgba[2], rgba[3]);
}

static PyObject*
Color_as_int(Color *self) {
    return PyLong_FromUnsignedLong(self->color.val);
}

static PyObject*
color_truediv(Color *self, PyObject *divisor) {
    RAII_PyObject(o, PyNumber_Float(divisor));
    if (o == NULL) return NULL;
    double r = self->color.r, g = self->color.g, b = self->color.b, a = self->color.a;
    double d = PyFloat_AS_DOUBLE(o) * 255.;
    return Py_BuildValue("dddd", r/d, g/d, b/d, a/d);
}

static PyNumberMethods color_number_methods = {
    .nb_int = (unaryfunc)Color_as_int,
    .nb_true_divide = (binaryfunc)color_truediv,
};

#define CGETSET(name) \
    static PyObject* name##_get(Color *self, void UNUSED *closure) { return PyLong_FromUnsignedLong(self->color.name);  }
CGETSET(red)
CGETSET(green)
CGETSET(blue)
CGETSET(alpha)
#undef CGETSET

static PyObject*
rgb_get(Color *self, void *closure UNUSED) {
    return PyLong_FromUnsignedLong(self->color.rgb);
}

static PyObject*
luminance_get(Color *self, void *closure UNUSED) {
    return PyFloat_FromDouble(rgb_luminance(self->color) / 255.0);
}

static PyObject*
is_dark_get(Color *self, void *closure UNUSED) {
    if (rgb_luminance(self->color) / 255.0 < 0.5) Py_RETURN_TRUE;
    Py_RETURN_FALSE;
}

static PyObject*
sgr_get(Color* self, void *closure UNUSED) {
    char buf[32];
    int sz = snprintf(buf, sizeof(buf), ":2:%u:%u:%u", self->color.r, self->color.g, self->color.b);
    return PyUnicode_FromStringAndSize(buf, sz);
}

static PyObject*
sharp_get(Color* self, void *closure UNUSED) {
    char buf[32];
    int sz;
    if (self->color.alpha) sz = snprintf(buf, sizeof(buf), "#%02x%02x%02x%02x", self->color.a, self->color.r, self->color.g, self->color.b);
    else sz = snprintf(buf, sizeof(buf), "#%02x%02x%02x", self->color.r, self->color.g, self->color.b);
    return PyUnicode_FromStringAndSize(buf, sz);
}

static PyObject*
color_cmp(PyObject *self, PyObject *other, int op) {
    if (op != Py_EQ && op != Py_NE) return Py_NotImplemented;
    if (!PyObject_TypeCheck(other, &Color_Type)) {
        if (op == Py_EQ) Py_RETURN_FALSE;
        Py_RETURN_TRUE;
    }
    Color *a = (Color*)self, *b = (Color*)other;
    switch (op) {
        case Py_EQ: { if (a->color.val == b->color.val) { Py_RETURN_TRUE; } Py_RETURN_FALSE; }
        case Py_NE: { if (a->color.val != b->color.val) { Py_RETURN_TRUE; } Py_RETURN_FALSE; }
        default:
            return Py_NotImplemented;
    }
}

static PyGetSetDef color_getsetters[] = {
    {"rgb", (getter) rgb_get, NULL, "rgb", NULL},
    {"red", (getter) red_get, NULL, "red", NULL},
    {"green", (getter) green_get, NULL, "green", NULL},
    {"blue", (getter) blue_get, NULL, "blue", NULL},
    {"alpha", (getter) alpha_get, NULL, "alpha", NULL},
    {"r", (getter) red_get, NULL, "red", NULL},
    {"g", (getter) green_get, NULL, "green", NULL},
    {"b", (getter) blue_get, NULL, "blue", NULL},
    {"a", (getter) alpha_get, NULL, "alpha", NULL},
    {"luminance", (getter) luminance_get, NULL, "luminance", NULL},
    {"as_sgr", (getter) sgr_get, NULL, "as_sgr", NULL},
    {"as_sharp", (getter) sharp_get, NULL, "as_sharp", NULL},
    {"is_dark", (getter) is_dark_get, NULL, "is_dark", NULL},
    {NULL}  /* Sentinel */
};

static PyObject*
contrast(Color* self, PyObject *o) {
    if (!PyObject_TypeCheck(o, &Color_Type)) { PyErr_SetString(PyExc_TypeError, "Not a Color"); return NULL; }
    Color *other = (Color*) o;
    return PyFloat_FromDouble(rgb_contrast(self->color, other->color));
}

static int
hexchar_to_int(char c) {
    switch (c) {
        START_ALLOW_CASE_RANGE
        case '0' ... '9': return c - '0';
        case 'a' ... 'f': return c - 'a' + 10;
        case 'A' ... 'F': return c - 'A' + 10;
        END_ALLOW_CASE_RANGE
    }
    return -1;
}

static bool
parse_base16_uchar(const char *hex, unsigned char *out) {
    const int hi = hexchar_to_int(hex[0]);
    const int lo = hexchar_to_int(hex[1]);
    if (hi < 0 || lo < 0) return false;
    *out = (unsigned char)((hi << 4) | lo);
    return true;
}

static bool
parse_double(const char *src, double *out) {
    char *endptr;
    errno = 0;
    *out = strtod_l(src, &endptr, get_c_locale());
    return endptr != src && *endptr == 0 && errno == 0;
}

static bool
parse_single_color(const char *c, size_t len, unsigned char *out) {
    char buf[2];
    if (len == 1) { buf[0] = c[0]; buf[1] = c[0]; c = buf; }
    return parse_base16_uchar(c, out);
}

static PyObject*
parse_sharp(const char *spec, size_t len) {
    unsigned char r, g, b;
    switch(len) {
        case 3:
            if (!parse_single_color(spec, 1, &r) || !parse_single_color(spec + 1, 1, &g) || !parse_single_color(spec + 2, 1, &b)) Py_RETURN_NONE;
            break;
        case 6: case 9: case 12:
            if (!parse_single_color(spec, 2, &r) || !parse_single_color(spec + len/3, 2, &g) || !parse_single_color(spec + 2 * len / 3, 2, &b)) Py_RETURN_NONE;
            break;
        default:
            Py_RETURN_NONE;
    }
    return (PyObject*)alloc_color(r, g, b, 0);
}

static PyObject*
parse_rgb(const char *spec, size_t len) {
    char buf[32];
    if (len >= sizeof(buf)) Py_RETURN_NONE;
    memcpy(buf, spec, len); buf[len] = 0;
    unsigned char r, g, b; char *tok;
#define p(buf, out) if (!(tok = strtok(buf, "/")) || !parse_single_color(tok, strlen(tok), &out)) Py_RETURN_NONE;
    p(buf, r); p(NULL, g); p(NULL, b);
#undef p
    return (PyObject*)alloc_color(r, g, b, 0);
}

static unsigned char as8bit(double f) { return (unsigned char)(round(f * 255.)); }

static bool
parse_single_intensity(const char *s, unsigned char *out) {
    double f; if (!parse_double(s, &f)) return false;
    *out = as8bit(f);
    return true;
}

static PyObject*
parse_rgbi(const char *spec, size_t len) {
    char buf[256];
    if (len >= sizeof(buf)) Py_RETURN_NONE;
    memcpy(buf, spec, len); buf[len] = 0;
    unsigned char r, g, b; char *tok;
#define p(buf, out) if (!(tok = strtok(buf, "/")) || !parse_single_intensity(tok, &out)) Py_RETURN_NONE;
    p(buf, r); p(NULL, g); p(NULL, b);
#undef p
    return (PyObject*)alloc_color(r, g, b, 0);
}

static bool
parse_double_intensity(char *s, double *out, double percentage_divider) {
    size_t l = strlen(s);
    if (l == 0) return false;
    double divisor = 1;
    if (s[l-1] == '%') { s[l-1] = 0; divisor = percentage_divider; }
    if (!parse_double(s, out)) return false;
    *out /= divisor;
    return true;
}

static double clamp(const double f) { return MAX(0, MIN(f, 1)); }

static double
linear_to_srgb(double c) { return c <= 0.0031308 ? c * 12.92 : (1.055 * pow(c, (1 / 2.4)) - 0.055); }

static double degrees_to_radians(double degrees) { return degrees * (M_PI / 180); }
static double radians_to_degrees(double radians) { return 180 * radians / M_PI; }

static void
oklch_to_srgb(double l, double c, double h, double *r, double *g, double *b) {
    // Convert OKLCH to OKLab
    const double h_rad = degrees_to_radians(h);
    const double a = c * cos(h_rad);
    const double lb = c * sin(h_rad);
    // Convert OKLab to Linear sRGB
    // Using the OKLab to Linear sRGB transformation
    const double l_ = l + 0.3963377774 * a + 0.2158037573 * lb;
    const double m_ = l - 0.1055613458 * a - 0.0638541728 * lb;
    const double s_ = l - 0.0894841775 * a - 1.2914855480 * lb;

    const double l_lin = l_ * l_ * l_;
    const double m_lin = m_ * m_ * m_;
    const double s_lin = s_ * s_ * s_;

    const double r_lin = +4.0767416621 * l_lin - 3.3077115913 * m_lin + 0.2309699292 * s_lin;
    const double g_lin = -1.2684380046 * l_lin + 2.6097574011 * m_lin - 0.3413193965 * s_lin;
    const double b_lin = -0.0041960863 * l_lin - 0.7034186147 * m_lin + 1.7076147010 * s_lin;

    *r = linear_to_srgb(clamp(r_lin)); *g = linear_to_srgb(clamp(g_lin)); *b = linear_to_srgb(clamp(b_lin));
}

static double srgb_to_linear(double c) { return c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4); }


static void
srgb_to_oklab(double r, double g, double b, double *l, double *a, double *lb) {
    // Convert sRGB to linear sRGB
    const double r_lin = srgb_to_linear(r);
    const double g_lin = srgb_to_linear(g);
    const double b_lin = srgb_to_linear(b);

    // Convert Linear sRGB to OKLab (inverse of oklch_to_srgb)
    const double l_lin = 0.4122214708 * r_lin + 0.5363325363 * g_lin + 0.0514459929 * b_lin;
    const double m_lin = 0.2119034982 * r_lin + 0.6806995451 * g_lin + 0.1073969566 * b_lin;
    const double s_lin = 0.0883024619 * r_lin + 0.2817188376 * g_lin + 0.6299787005 * b_lin;

    const double l_ = l_lin != 0 ? copysign(pow(fabs(l_lin), 1./3.), l_lin) : 0;
    const double m_ = m_lin != 0 ? copysign(pow(fabs(m_lin), 1./3.), m_lin) : 0;
    const double s_ = s_lin != 0 ? copysign(pow(fabs(s_lin), 1./3.), s_lin) : 0;

    // OKLab coordinates
    *l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
    *a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
    *lb = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
}

static double
distance(double x_l, double x_a, double x_b, double y_l, double y_a, double y_b) {
    return sqrt((x_l - y_l)*(x_l - y_l) + (x_a - y_a)*(x_a - y_a) + (x_b - y_b)*(x_b - y_b));
}

static void
oklch_to_srgb_gamut_map(double l, double c, double h, double *r, double *g, double *b) {
    // Edge cases: pure black or white don't need gamut mapping
    if (!isfinite(l) || !isfinite(c) || !isfinite(h) || l <= 0) { *r = 0; *g = 0; *b = 0; return; }
    if (l >= 1) { *r = 1; *g = 1; *b = 1; return; }
    // Constants from CSS Color Module Level 4
    static const double JND = 0.02;  // Just Noticeable Difference threshold (2% in deltaEOK)
    static const double MIN_CONVERGENCE = 0.0001;  // Binary search precision (0.01% chroma)
    static const double EPSILON = 0.00001;  // Small value for doubleing point comparisons

    // If chroma is very small, color is essentially achromatic
    if (c < EPSILON) { *r = linear_to_srgb(l); *g = *r; *b = *r; return; }
    // Try the original color first
    oklch_to_srgb(l, c, h, r, g, b);
#define in_gamut(r,g,b) (0. <= r && r <= 1. && 0. <= g && g <= 1. && 0. <= b && b <= 1.)
    if (in_gamut(*r,*g,*b)) return;
    // Binary search for maximum in-gamut chroma
    double low_chroma = 0, high_chroma = c, r_test, g_test, b_test, r_clipped, g_clipped, b_clipped;

    // Convert original color to OKLab for deltaE calculations
    while ((high_chroma - low_chroma) > MIN_CONVERGENCE) {
        double mid_chroma = (high_chroma + low_chroma) * 0.5;
        // Try this chroma value
        oklch_to_srgb(l, mid_chroma, h, &r_test, &g_test, &b_test);
        // Check if in gamut (before clipping)
        if (in_gamut(r_test, g_test, b_test)) {
            // In gamut - try higher chroma
            low_chroma = mid_chroma;
        } else {
            // Out of gamut - clip and check deltaE
            r_clipped = clamp(r_test); g_clipped = clamp(g_test); b_clipped = clamp(b_test);

            // Convert both to OKLab for comparison
            double l_test, a_test, lb_test, l_clipped, a_clipped, lb_clipped;
            srgb_to_oklab(r_test, g_test, b_test, &l_test, &a_test, &lb_test);
            srgb_to_oklab(r_clipped, g_clipped, b_clipped, &l_clipped, &a_clipped, &lb_clipped);

            // Calculate perceptual difference
            double de = distance(l_test, a_test, lb_test, l_clipped, a_clipped, lb_clipped);

            if (de < JND) {
                // Difference is imperceptible - accept this chroma
                low_chroma = mid_chroma;
            } else {
                // Difference is noticeable - reduce chroma more
                high_chroma = mid_chroma;
            }
        }
    }
    // Use the final chroma value and clip to ensure in-gamut
    oklch_to_srgb(l, low_chroma, h, r, g, b);
    *r = clamp(*r); *g = clamp(*g); *b = clamp(*b);
#undef in_gamut
}

static double
f_inv(double t) {
    static const double delta = 6. / 29.;
    return t > delta ? t*t*t : 3 * delta * delta * (t - 4. / 29.);
}


static void
lab_to_oklch(double l, double a, double b, double *okl, double *c, double *h) {
    const double y = (l + 16.) / 116.;
    const double x = a / 500. + y;
    const double z = y - b / 200.;
    const double x_val = 0.95047 * f_inv(x);
    const double y_val = f_inv(y);
    const double z_val = 1.08883 * f_inv(z);

    // XYZ to Linear sRGB (don't clip here to preserve out-of-gamut info)
    const double r_lin = +3.2404542 * x_val - 1.5371385 * y_val - 0.4985314 * z_val;
    const double g_lin = -0.9692660 * x_val + 1.8760108 * y_val + 0.0415560 * z_val;
    const double b_lin = +0.0556434 * x_val - 0.2040259 * y_val + 1.0572252 * z_val;

    // Convert linear sRGB to sRGB gamma
    const double r_srgb = r_lin >= 0 ? linear_to_srgb(r_lin) : 0;
    const double g_srgb = g_lin >= 0 ? linear_to_srgb(g_lin) : 0;
    const double b_srgb = b_lin >= 0 ? linear_to_srgb(b_lin) : 0;

    // Convert to OKLab
    double a_ok, b_ok;
    srgb_to_oklab(r_srgb, g_srgb, b_srgb, okl, &a_ok, &b_ok);
    // Convert OKLab to OKLCH
    *c = sqrt(a_ok * a_ok + b_ok * b_ok);
    *h = fmod(radians_to_degrees(atan2(b_ok, a_ok)), 360.f);
}

static PyObject*
parse_oklch(const char *spec, size_t len) {
    if (len < 10 || spec[--len] != ')') Py_RETURN_NONE;
    if (spec[0] != 'k' || spec[1] != 'l' || spec[2] != 'c' || spec[3] != 'h' || spec[4] != '(') Py_RETURN_NONE;
    spec += 5; len -= 5;
    char buf[256]; if (len >= sizeof(buf)) Py_RETURN_NONE;
    memcpy(buf, spec, len); buf[len] = 0;
    double l, c, h; char *tok;
#define p(buf, out) if (!(tok = strtok(buf, " ,")) || !parse_double_intensity(tok, &out, 100)) Py_RETURN_NONE;
    p(buf, l); p(NULL, c); p(NULL, h);
#undef p
    // Clamp to reasonable ranges
    l = clamp(l);
    c = MAX(0.f, c);  // Chroma is unbounded but we don't clamp high end
    h = fmod(h, 360);  // Wrap hue to 0-360
    double r, g, b;
    oklch_to_srgb_gamut_map(l, c, h, &r, &g, &b);
    return (PyObject*)alloc_color(as8bit(r), as8bit(g), as8bit(b), 0);
}

static PyObject*
parse_lab(const char *spec, size_t len) {
    if (len < 8 || spec[--len] != ')') Py_RETURN_NONE;
    if (spec[0] != 'a' || spec[1] != 'b' || spec[2] != '(') Py_RETURN_NONE;
    spec += 3; len -= 3;
    char buf[256]; if (len >= sizeof(buf)) Py_RETURN_NONE;
    memcpy(buf, spec, len); buf[len] = 0;
    double l, a, b; char *tok;
#define p(buf, out) if (!(tok = strtok(buf, " ,")) || !parse_double_intensity(tok, &out, 1)) Py_RETURN_NONE;
    p(buf, l); p(NULL, a); p(NULL, b);
#undef p
    // Clamp to reasonable ranges
    double okl, c, h, r, g, bb;
    lab_to_oklch(MAX(0., MIN(l, 100.)), a, b, &okl, &c, &h);
    oklch_to_srgb_gamut_map(okl, c, h, &r, &g, &bb);
    return (PyObject*)alloc_color(as8bit(r), as8bit(g), as8bit(bb), 0);
}

static const char*
trim_view(const char *str, Py_ssize_t *len) {
    if (str == NULL || *len == 0) return str;
    const char *start = str;
    const char *end = str + *len - 1;
    while (start <= end && isspace((unsigned char)*start)) start++;
    while (end > start && isspace((unsigned char)*end)) end--;
    if (start > end) { *len = 0; } else { *len = (size_t)(end - start + 1); }
    return start;
}

static PyObject*
parse_color(PyTypeObject *type UNUSED, PyObject *pspec) {
    if (!PyUnicode_Check(pspec)) { PyErr_SetString(PyExc_TypeError, "spec must be a string"); return NULL; }
    RAII_PyObject(lower_cased, PyObject_CallMethod(pspec, "lower", NULL));
    if (!lower_cased) return NULL;
    Py_ssize_t len;
    const char *spec = PyUnicode_AsUTF8AndSize(lower_cased, &len);
    spec = trim_view(spec, &len);
    if (len < 2) Py_RETURN_NONE;
    // Remove trailing comments
    switch (spec[0]) {
        case '#': {
            const char *s = strchr(spec, ' ');
            if (s) len = s - spec;
        } break;
        default: {
            const char *s = strchr(spec, '#');
            if (s) {
                len = s - spec;
                spec = trim_view(spec, &len);
            }
        } break;
    }
    const struct Keyword *k = in_color_name_set(spec, len);
    if (k) return (PyObject*)alloc_color((k->value >> 16) & 0xff, (k->value >> 8) & 0xff, k->value & 0xff, 0);
    if (len < 4) Py_RETURN_NONE;
    switch (spec[0]) {
        case '#': return parse_sharp(spec + 1, len - 1);
        case 'r':
            if (spec[1] != 'g' || spec[2] != 'b' || len < 6) Py_RETURN_NONE;
            switch(spec[3]) {
                case ':': return parse_rgb(spec + 4, len - 4);
                case 'i':
                    if (spec[4] == 'i' && spec[5] == ':') return parse_rgbi(spec + 5, len - 5);
            }
            Py_RETURN_NONE;
        case 'o': return parse_oklch(spec + 1, len - 1);
        case 'l': return parse_lab(spec + 1, len - 1);
    }
    Py_RETURN_NONE;
}

static PyMethodDef color_methods[] = {
    METHODB(contrast, METH_O),
    METHODB(parse_color, METH_O | METH_CLASS),
    {NULL}  /* Sentinel */
};


static PyObject *
repr(Color *self) {
    if (self->color.alpha) return PyUnicode_FromFormat("Color(red=%u, green=%u, blue=%u, alpha=%u)", self->color.r, self->color.g, self->color.b, self->color.a);
    return PyUnicode_FromFormat("Color(%u, %u, %u)", self->color.r, self->color.g, self->color.b);
}

static Py_hash_t
color_hash(PyObject *x) {
    return ((Color*)x)->color.val;
}

PyTypeObject Color_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "kitty.fast_data_types.Color",
    .tp_basicsize = sizeof(Color),
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_doc = "Color",
    .tp_new = new_color,
    .tp_vectorcall = color_vectorcall,
    .tp_getset = color_getsetters,
    .tp_as_number = &color_number_methods,
    .tp_methods = color_methods,
    .tp_repr = (reprfunc)repr,
    .tp_hash = color_hash,
    .tp_richcompare = color_cmp,
};


static PyObject*
all_color_names(PyObject *self UNUSED, PyObject *args UNUSED) {
    RAII_PyObject(ans, PyTuple_New(TOTAL_KEYWORDS));
    if (!ans) return NULL;
    const struct Keyword *k;
    Py_ssize_t n = 0;
    for (unsigned i = 0; i <= MAX_HASH_VALUE; i++) {
        if ((k = &color_names[i])->name > -1) {
            const char *name = color_names[i].name + stringpool;
            PyObject *t = Py_BuildValue("sN", name, alloc_color((k->value >> 16) & 0xff, (k->value >> 8) & 0xff, k->value & 0xff, 0));
            if (!t) return NULL;
            PyTuple_SET_ITEM(ans, n, t); n++;
        }
    }
    return Py_NewRef(ans);
}

static PyMethodDef module_methods[] = {
    METHODB(default_color_table, METH_NOARGS),
    METHODB(patch_color_profiles, METH_VARARGS),
    METHODB(all_color_names, METH_NOARGS),
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

int init_ColorProfile(PyObject *module) {\
    if (PyType_Ready(&ColorProfile_Type) < 0) return 0;
    if (PyModule_AddObject(module, "ColorProfile", (PyObject *)&ColorProfile_Type) != 0) return 0;
    Py_INCREF(&ColorProfile_Type);

    if (PyType_Ready(&Color_Type) < 0) return 0;
    if (PyModule_AddObject(module, "Color", (PyObject *)&Color_Type) != 0) return 0;
    Py_INCREF(&Color_Type);

    if (PyModule_AddFunctions(module, module_methods) != 0) return false;
    PyModule_AddIntMacro(module, NULL_COLOR_VALUE);
    return 1;
}


// }}}
