#include "colour.h"

#include <ctype.h>
#include <math.h>
#include <stddef.h>
#include <string.h>

static int hex_digit(char value) {
    if (value >= '0' && value <= '9') {
        return value - '0';
    }
    value = (char)tolower((unsigned char)value);
    if (value >= 'a' && value <= 'f') {
        return value - 'a' + 10;
    }
    return -1;
}

bool colour_parse_hex(const char *text, RGB *out) {
    int digits[6];
    size_t index;

    if (text == NULL || out == NULL || strlen(text) != 7 || text[0] != '#') {
        return false;
    }
    for (index = 0; index < 6; ++index) {
        digits[index] = hex_digit(text[index + 1]);
        if (digits[index] < 0) {
            return false;
        }
    }
    out->r = (uint8_t)((digits[0] << 4) | digits[1]);
    out->g = (uint8_t)((digits[2] << 4) | digits[3]);
    out->b = (uint8_t)((digits[4] << 4) | digits[5]);
    return true;
}

double colour_srgb_to_linear(double value) {
    if (value <= 0.04045) {
        return value / 12.92;
    }
    return pow((value + 0.055) / 1.055, 2.4);
}

double colour_linear_to_srgb(double value) {
    if (value <= 0.0031308) {
        return value * 12.92;
    }
    return 1.055 * pow(value, 1.0 / 2.4) - 0.055;
}

static uint8_t linear_channel_to_u8(double value) {
    double encoded;
    if (value < 0.0) {
        value = 0.0;
    } else if (value > 1.0) {
        value = 1.0;
    }
    encoded = colour_linear_to_srgb(value);
    return (uint8_t)lround(encoded * 255.0);
}

RGB colour_from_linear(double red, double green, double blue) {
    RGB result;
    result.r = linear_channel_to_u8(red);
    result.g = linear_channel_to_u8(green);
    result.b = linear_channel_to_u8(blue);
    return result;
}

Oklab colour_to_oklab(RGB colour) {
    const double red = colour_srgb_to_linear((double)colour.r / 255.0);
    const double green = colour_srgb_to_linear((double)colour.g / 255.0);
    const double blue = colour_srgb_to_linear((double)colour.b / 255.0);
    const double l = cbrt(0.4122214708 * red + 0.5363325363 * green +
                          0.0514459929 * blue);
    const double m = cbrt(0.2119034982 * red + 0.6806995451 * green +
                          0.1073969566 * blue);
    const double s = cbrt(0.0883024619 * red + 0.2817188376 * green +
                          0.6299787005 * blue);
    Oklab result;
    result.l = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
    result.a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
    result.b = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
    return result;
}

double colour_oklab_distance(Oklab left, Oklab right) {
    const double dl = left.l - right.l;
    const double da = left.a - right.a;
    const double db = left.b - right.b;
    return sqrt(dl * dl + da * da + db * db);
}

static bool oklab_linear_rgb(Oklab lab, double *red, double *green,
                             double *blue) {
    double l = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
    double m = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
    double s = lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b;
    l = l * l * l;
    m = m * m * m;
    s = s * s * s;
    *red = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
    *green = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
    *blue = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
    return *red >= 0.0 && *red <= 1.0 && *green >= 0.0 &&
           *green <= 1.0 && *blue >= 0.0 && *blue <= 1.0;
}

RGB colour_tweak_accent(RGB colour, RGB background) {
    Oklab lab = colour_to_oklab(colour);
    const Oklab background_lab = colour_to_oklab(background);
    const double chroma = sqrt(lab.a * lab.a + lab.b * lab.b);
    double target_lightness = background_lab.l +
                              (lab.l >= background_lab.l ? 0.30 : -0.30);
    double target_chroma = 0.15;
    double adjusted_chroma;
    double red = 0.0;
    double green = 0.0;
    double blue = 0.0;
    uint32_t attempt;

    if (target_lightness < 0.0) target_lightness = 0.0;
    if (target_lightness > 1.0) target_lightness = 1.0;
    lab.l += (target_lightness - lab.l) * 0.25;
    adjusted_chroma = chroma + (target_chroma - chroma) * 0.25;
    if (chroma > 0.000001) {
        lab.a *= adjusted_chroma / chroma;
        lab.b *= adjusted_chroma / chroma;
    }

    /* Keep hue and lightness while reducing chroma into the sRGB gamut. */
    for (attempt = 0; attempt < 32; ++attempt) {
        if (oklab_linear_rgb(lab, &red, &green, &blue)) {
            return colour_from_linear(red, green, blue);
        }
        lab.a *= 0.90;
        lab.b *= 0.90;
    }
    (void)oklab_linear_rgb(lab, &red, &green, &blue);
    return colour_from_linear(red, green, blue);
}

static void accumulator_add(ColourAccumulator *acc, double red, double green,
                            double blue, double alpha) {
    acc->red += colour_srgb_to_linear(red) * alpha;
    acc->green += colour_srgb_to_linear(green) * alpha;
    acc->blue += colour_srgb_to_linear(blue) * alpha;
    acc->alpha += alpha;
}

void colour_accumulator_add_u8(ColourAccumulator *acc, uint8_t red,
                               uint8_t green, uint8_t blue, uint8_t alpha) {
    const double opacity = (double)alpha / 255.0;
    accumulator_add(acc, (double)red / 255.0, (double)green / 255.0,
                    (double)blue / 255.0, opacity);
}

void colour_accumulator_add_u16(ColourAccumulator *acc, uint16_t red,
                                uint16_t green, uint16_t blue, uint16_t alpha) {
    const double opacity = (double)alpha / 65535.0;
    accumulator_add(acc, (double)red / 65535.0, (double)green / 65535.0,
                    (double)blue / 65535.0, opacity);
}

bool colour_accumulator_average(const ColourAccumulator *acc, RGB *out) {
    if (acc == NULL || out == NULL || acc->alpha <= 0.0) {
        return false;
    }
    *out = colour_from_linear(acc->red / acc->alpha,
                              acc->green / acc->alpha,
                              acc->blue / acc->alpha);
    return true;
}