/***************************** LICENSE START ***********************************

 Copyright 2014 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

#include "MvQPalette.h"

#include <QDebug>
#include <QPainter>

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QtCore5Compat/QRegExp>
#else
#include <QRegExp>
#endif

#include <algorithm>
#include <array>
#include <cmath>
#include <cctype>

#include "ConfigLoader.h"
#include "Tokenizer.h"

std::vector<std::string> MvQPalette::names_;
std::map<std::string, MvQPaletteItem> MvQPalette::items_;
std::vector<std::string> MvQPalette::pseudoColourNames_;

static const float toRadFactor = 3.14159265359 / 180.;
static const float toDegFactor = 180. / 3.14159265359;

//#define DEBUG_MV_MVQPALETTE__

namespace
{
// CIE contants
const float CIE_EPS = 0.008856451679035631;
const float CIE_KAPPA = 903.2962962962963;

// reference values for the XYZ colour model. Observer=2 deg, illuminant=d65
const float D65_X = 95.047;
const float D65_Y = 100.000;
const float D65_Z = 108.883;

// reference values for the XYZ colour model. Observer=2 deg, illuminant=d50
// const float D50_X = 0.96422;
const float D50_Y = 1;
// const float D50_Z = 0.82521;
const float D50_U = 0.20916005282038627;
const float D50_V = 0.48807338454488514;

// conversion matrix for srgb (d65) to xyz (d50)
const std::array<float, 9> matrixLrgbToXyzd50 = {
    0.4360747, 0.3850649, 0.1430804,
    0.2225045, 0.7168786, 0.0606169,
    0.0139322, 0.0971045, 0.7141733};

// conversion matrix for xyz (d50) to srgb (d65)
const std::array<float, 9> matrixXyzd50ToLrgb = {
    3.1338561, -1.6168667, -0.4906146,
    -0.9787684, 1.9161415, 0.0334540,
    0.0719453, -0.2289914, 1.4052427};

void rgbToXyz(float r, float g, float b, float& x, float& y, float& z);
void xyzToRgb(float x, float y, float z, float& r, float& g, float& b);
void xyzToHclLab(float x, float y, float z, float& h, float& c, float& l);
void hclLabToXyz(float h, float c, float l, float& x, float& y, float& z);
void xyzToHclLuv(float x, float y, float z, float& h, float& c, float& l);
void hclLuvToXyz(float h, float c, float l, float& x, float& y, float& z);
float rgbToLinearLightRgb(float v);
float linearLightRgbToRgb(float v);
void rgbToXyzd50(float r, float g, float b, float& x, float& y, float& z);
void xyzd50ToRgb(float x, float y, float z, float& r, float& g, float& b);


// r,g,b : 0-1
// x,y,z: 0-100
void rgbToXyz(float r, float g, float b, float& x, float& y, float& z)
{
    if (r > 0.04045)
        r = pow((r + 0.055) / 1.055, 2.4);
    else
        r = r / 12.92;

    if (g > 0.04045)
        g = pow((g + 0.055) / 1.055, 2.4);
    else
        g = g / 12.92;

    if (b > 0.04045)
        b = pow((b + 0.055) / 1.055, 2.4);
    else
        b = b / 12.92;

    r *= 100;
    g *= 100;
    b *= 100;

    x = r * 0.4124 + g * 0.3576 + b * 0.1805;
    y = r * 0.2126 + g * 0.7152 + b * 0.0722;
    z = r * 0.0193 + g * 0.1192 + b * 0.9505;
}

// x,y,z: 0-100
// r,g,b : 0-1
void xyzToRgb(float x, float y, float z, float& r, float& g, float& b)
{
    x = x / 100.;  // x from 0 to  95.047
    y = y / 100.;  // y from 0 to 100.000
    z = z / 100.;  // z from 0 to 108.883

    r = x * 3.2406 + y * (-1.5372) + z * (-0.4986);
    g = x * (-0.9689) + y * 1.8758 + z * 0.0415;
    b = x * 0.0557 + y * (-0.2040) + z * 1.0570;

    if (r > 0.0031308)
        r = 1.055 * pow(r, 1. / 2.4) - 0.055;
    else
        r = 12.92 * r;

    if (g > 0.0031308)
        g = 1.055 * pow(g, 1. / 2.4) - 0.055;
    else
        g = 12.92 * g;

    if (b > 0.0031308)
        b = 1.055 * pow(b, 1. / 2.4) - 0.055;
    else
        b = 12.92 * b;

    if (r > 1.)
        r = 1.;
    if (g > 1.)
        g = 1.;
    if (b > 1.)
        b = 1.;

    if (r < 0.)
        r = 0.;
    if (g < 0.)
        g = 0.;
    if (b < 0.)
        b = 0.;
}


float rgbToLinearLightRgb(float v)
{
    return (v > 0.04045) ? std::pow((v + 0.055) / 1.055, 2.4) : (v / 12.92);
}

float linearLightRgbToRgb(float v)
{
    return (v > 0.0031308) ? (1.055 * std::pow(v, 1. / 2.4) - 0.055) : (12.92 * v);
}

// Convert normalised sRGB (illuminant=d65) to CIE XYZ  (illuminant=d50)
// r,g,b : 0-1
// x,y,z: 0-100
void rgbToXyzd50(float r, float g, float b, float& x, float& y, float& z)
{
    // convert gamma encoded sRGB to linear-light RGB (inverse companding)
    r = rgbToLinearLightRgb(r);
    g = rgbToLinearLightRgb(g);
    b = rgbToLinearLightRgb(b);

    r *= 100;
    g *= 100;
    b *= 100;

    // Apply the conversion matrix. Since sRGB uses d65 and the target XYZ uses d50
    // a chromatic adaptation must also be performed on the result. This operation
    // is already incorporated into the conversion matrix.
    x = r * matrixLrgbToXyzd50[0] + g * matrixLrgbToXyzd50[1] + b * matrixLrgbToXyzd50[2];
    y = r * matrixLrgbToXyzd50[3] + g * matrixLrgbToXyzd50[4] + b * matrixLrgbToXyzd50[5];
    z = r * matrixLrgbToXyzd50[6] + g * matrixLrgbToXyzd50[7] + b * matrixLrgbToXyzd50[8];
}

// Convert CIE XYZ  (illuminant=d50) to normalised sRGB (illuminant=d65)
// x,y,z: 0-100
// r,g,b : 0-1
void xyzd50ToRgb(float x, float y, float z, float& r, float& g, float& b)
{
    x = x / 100.;  // x from 0 to  95.047
    y = y / 100.;  // y from 0 to 100.000
    z = z / 100.;  // z from 0 to 108.883

    // Apply the conversion matrix. Since the source XYZ uses d50 and sRGB uses
    // d65 the chromatic adaptation must also be performed on the result. This operation
    // is already incorporated into the conversion matrix.
    r = x * matrixXyzd50ToLrgb[0] + y * matrixXyzd50ToLrgb[1] + z * matrixXyzd50ToLrgb[2];
    g = x * matrixXyzd50ToLrgb[3] + y * matrixXyzd50ToLrgb[4] + z * matrixXyzd50ToLrgb[5];
    b = x * matrixXyzd50ToLrgb[6] + y * matrixXyzd50ToLrgb[7] + z * matrixXyzd50ToLrgb[8];

    // convert linear-light RGB to gamma encoded sRGB (companding)
    r = linearLightRgbToRgb(r);
    g = linearLightRgbToRgb(g);
    b = linearLightRgbToRgb(b);

    r = std::max(0.f, std::min(r, 1.f));
    g = std::max(0.f, std::min(g, 1.f));
    b = std::max(0.f, std::min(b, 1.f));
}

// x,y,z: 0-100
// h,c,l : 0-1
void xyzToHclLab(float x, float y, float z, float& h, float& c, float& l)
{
    x /= D65_X;
    y /= D65_Y;
    z /= D65_Z;

    if (x > 0.008856)
        x = pow(x, 1. / 3.);
    else
        x = (7.787 * x) + (16. / 116.);

    if (y > 0.008856)
        y = pow(y, 1. / 3.);
    else
        y = (7.787 * y) + (16. / 116.);

    if (z > 0.008856)
        z = pow(z, 1. / 3.);
    else
        z = (7.787 * z) + (16. / 116.);

    l = (116. * y) - 16.;
    float a = 500. * (x - y);
    float b = 200. * (y - z);
#ifdef DEBUG_MV_MVQPALETTE__
    qDebug() << "   abl" << a << b << l;
#endif
    h = atan2(b, a);
#ifdef DEBUG_MV_MVQPALETTE__
    qDebug() << "   h1" << h;
#endif
    if (h > 0)
        h = h * toDegFactor;
    else
        h = 360. + h * toDegFactor;
#ifdef DEBUG_MV_MVQPALETTE__
    qDebug() << "   h2" << h;
#endif
    h = h / 360.;
    c = sqrt(a * a + b * b);
}

// h,c,l : 0-1
// x,y,z: 0-100
void hclLabToXyz(float h, float c, float l, float& x, float& y, float& z)
{
    float a = cos(360 * h * toRadFactor) * c;
    float b = sin(360 * h * toRadFactor) * c;

    y = (l + 16.) / 116.;
    x = a / 500. + y;
    z = y - b / 200.;

    if (pow(y, 3) > 0.008856)
        y = pow(y, 3);
    else
        y = (y - 16. / 116.) / 7.787;

    if (pow(x, 3) > 0.008856)
        x = pow(x, 3);
    else
        x = (x - 16. / 116.) / 7.787;

    if (z > 0.008856)
        z = pow(z, 3);
    else
        z = (z - 16. / 116.) / 7.787;

    x *= D65_X;
    y *= D65_Y;
    z *= D65_Z;
}

// x,y,z: 0-100
// h : 0-1, c: 1-150 l: 0-100
void xyzToHclLuv(float x, float y, float z, float& h, float& c, float& l)
{
    x /= 100.;
    y /= 100.;
    z /= 100.;

    const float d = x + 15 * y + 3 * z;
    float u = 0.;
    float v = 0.;
    if (d != 0) {
        u = 4.0 * x / d;
        v = 9.0 * y / d;
    }

    y /= D50_Y;
    y = (y > CIE_EPS) ? std::cbrt(y) : ((7.787 * y) + 16. / 116.);

    // D50_U = (4.0 * D50_X) / (D50_X + (15.0 * D50_Y) + (3.0 * D50_Z));
    // D50_V = (9.0 * D50_Y) / (D50_X + (15.0 * D50_Y) + (3.0 * D50_Z));

    l = (116.0 * y) - 16.0;
    u = 13.0 * l * (u - D50_U);
    v = 13.0 * l * (v - D50_V);

    //#ifdef DEBUG_MV_MVQPALETTE__
    // qDebug() << "   uvl" << u << v << l;
    //#endif
    h = atan2(v, u);
    //#ifdef DEBUG_MV_MVQPALETTE__
    // qDebug() << "   h1" << h;
    //#endif
    if (h > 0) {
        h = h * toDegFactor;
    }
    else {
        h = 360. + h * toDegFactor;
    }
    //#ifdef DEBUG_MV_MVQPALETTE__
    // qDebug() << "   h2" << h;
    //#endif
    h = h / 360.;
    c = sqrt(u * u + v * v);

    qDebug() << "xyzToHclLuv: (" << x << y << z << ") ->" << h << c << l;
}

// h,c,l : 0-1
// x,y,z: 0-100
void hclLuvToXyz(float h, float c, float l, float& x, float& y, float& z)
{
    if (l <= 0) {
        x = 0.;
        y = 0.;
        z = 0.;
        return;
    }
    float u = cos(360 * h * toRadFactor) * c;
    float v = sin(360 * h * toRadFactor) * c;

    const float up = u / (13 * l) + D50_U;
    const float vp = v / (13 * l) + D50_V;

    if (l > l / CIE_KAPPA * CIE_EPS) {
        y = std::pow((l + 16) / 116., 3.);
    }
    else {
        y = l / CIE_KAPPA;
    }

    const float d = 4 * vp;
    x = y * 9 * up / d;
    z = y * (12 - 3 * up - 20 * vp) / d;

    //#ifdef DEBUG_MV_MVQPALETTE__
    // qDebug() << "hclLuvToXyz: " <<  x << y << z;
    //#endif
    x *= 100;
    y *= 100;
    z *= 100;
}
}  // namespace

void MvQPalette::load(request* r)
{
    // populate our list of 'special'/'pseudo' colour names
    if (pseudoColourNames_.empty()) {
        pseudoColourNames_ = {"AUTOMATIC", "CONTOUR_LINE_COLOUR", "BACKGROUND", "FOREGROUND"};
    }

    const char* fname = get_value(r, "colour_file", 0);
    if (!fname)
        return;

    FILE* f = fopen(fname, "r");

    if (!f)
        return;

    char line[500];
    char color[80];
    char name[80];

    while (fgets(line, sizeof(line), f)) {
        char* p = line;
        int i = 0;

        sscanf(p, "%79s ; %79s", color, name);
        while (*p && !i) {
            if (*p == '#') {
                color[i++] = *p++;
                while (*p && isxdigit(*p))
                    color[i++] = *p++;
                color[i] = 0;
                if (i != 4 && i != 7 && i != 13)
                    i = 0;
            }
            p++;
        }
        if (i) {
            bool pseudo = MvQPalette::isPseudo(name);
            names_.emplace_back(name);
            items_[name] = MvQPaletteItem(name,
                                          (pseudo) ? QColor() : MvQPalette::hexaCharToColour(color), pseudo);
        }
    }

    fclose(f);
}

bool MvQPalette::isPseudo(const std::string& name)
{
    if (pseudoColourNames_.empty() == false &&
        std::find(pseudoColourNames_.begin(), pseudoColourNames_.end(), name) != pseudoColourNames_.end())
        return true;

    return false;
}

QColor MvQPalette::hexaCharToColour(const char* hexaChar)
{
    if (strlen(hexaChar) == 13) {
        unsigned int r = 0, g = 0, b = 0;
        char cval[5];

        strncpy(cval, hexaChar + 1, 4);
        cval[4] = '\0';
        sscanf(cval, "%x", &r);

        strncpy(cval, hexaChar + 5, 4);
        cval[4] = '\0';
        sscanf(cval, "%x", &g);

        strncpy(cval, hexaChar + 9, 4);
        cval[4] = '\0';
        sscanf(cval, "%x", &b);

        return QColor(r / 256, g / 256, b / 256);
    }

    return {};
}


void MvQPalette::scan(PaletteScanner& p)
{
    for (auto& name : names_) {
        if (!isPseudo(name)) {
            auto it = items_.find(name);
            if (it != items_.end()) {
                p.next(name, it->second.col_, it->second.pseudo_);
            }
        }
    }
}

std::string MvQPalette::toString(QColor col)
{
    for (auto& item : items_) {
        if (item.second.pseudo_ == false && item.second.col_ == col)
            return item.first;
    }

    return toRgbString(col).toStdString();
}

QString MvQPalette::toRgbString(QColor col)
{
    bool hasAlpha = (col.alpha() != 255);

    QString s = "RGB(";
    if (hasAlpha)
        s = "RGBA(";

    s += QString::number(col.redF(), 'g', 4) + "," +
         QString::number(col.greenF(), 'g', 4) + "," +
         QString::number(col.blueF(), 'g', 4);

    if (!hasAlpha) {
        s += ")";
    }
    else {
        s += "," + QString::number(col.alphaF(), 'g', 4) + ")";
    }

    return s;
}

QString MvQPalette::toRgbIntString(QColor col)
{
    bool hasAlpha = (col.alpha() != 255);

    QString s = "rbg(";
    if (hasAlpha)
        s = "RGBA(";

    s += QString::number(col.red()) + "," +
         QString::number(col.green()) + "," +
         QString::number(col.blue());

    if (!hasAlpha) {
        s += ")";
    }
    else {
        s += "," + QString::number(col.alpha()) + ")";
    }

    return s;
}

QString MvQPalette::toHslString(QColor col)
{
    bool hasAlpha = (col.alpha() != 255);

    QString s = "HSL(";
    if (hasAlpha)
        s = "HSLA(";

    s += QString::number(col.hslHueF(), 'g', 4) + "," +
         QString::number(col.hslSaturationF(), 'g', 4) + "," +
         QString::number(col.lightnessF(), 'g', 4);

    if (!hasAlpha) {
        s += ")";
    }
    else {
        s += "," + QString::number(col.alphaF(), 'g', 4) + ")";
    }

    return s;
}

QString MvQPalette::toHslIntString(QColor col)
{
    bool hasAlpha = (col.alpha() != 255);

    QString s = "HSL(";
    if (hasAlpha)
        s = "HSLA(";

    s += QString::number(col.hslHue()) + "," +
         QString::number(col.hslSaturation()) + "," +
         QString::number(col.lightness());

    if (!hasAlpha) {
        s += ")";
    }
    else {
        s += "," + QString::number(col.alphaF()) + ")";
    }

    return s;
}


QString MvQPalette::toStringAsList(QList<QColor> colLst)
{
    QStringList txtLst;
    foreach (QColor col, colLst) {
        txtLst << "\"" + QString::fromStdString(MvQPalette::toString(col)) + "\"";
    }

    return "[" + txtLst.join(",") + "]";
}

QList<QColor> MvQPalette::toColourList(QString txt)
{
    // The list format is something like this:
    // ["RGB(0.8328,0.4237,0.1868)","PURPLE_RED","REDDISH_ORANGE"]

    QList<QColor> cLst;

    // strip off the closing and ending brackets
    txt = txt.simplified();
    if (!txt.startsWith("[") || !txt.endsWith("]"))
        return cLst;

    txt = txt.mid(1, txt.size() - 2);

    QRegExp rx(R"((".*")(?=\s*,|\s*$))");
    rx.setMinimal(true);

    int pos = 0;
    while ((pos = rx.indexIn(txt, pos)) != -1) {
        QString s = rx.cap(1);
        cLst << magics(s.toStdString());
        pos += rx.matchedLength();
    }

    return cLst;
}

QColor MvQPalette::magics(const std::string& nameIn)
{
    QColor col;

    std::string name = nameIn;
    std::transform(name.begin(), name.end(), name.begin(), ::toupper);

    // remove quotes
    if (name.empty())
        return col;

    if (name[0] == '\"' || name[0] == '\'')
        name = name.substr(1);
    {
        size_t len = name.size();
        if (len > 1 && (name[len - 1] == '\"' || name[len - 1] == '\'')) {
            name = name.substr(0, len - 1);
        }
    }

    auto it = items_.find(name);
    if (it != items_.end()) {
        col = it->second.col_;
    }
    else if (name == "NONE") {
        col = QColor(Qt::transparent);
    }
    // #ABCDEF
    else if (name[0] == '#') {
        col = QColor(QString::fromStdString(name));
    }
    else {
        Tokenizer parse("(, )");
        std::vector<std::string> v;

        parse(name, v);

        // Try rgb, hsl
        if (v.size() >= 4) {
            double x1 = atof(v[1].c_str());
            double x2 = atof(v[2].c_str());
            double x3 = atof(v[3].c_str());

            bool hasInt = (x1 > 1 || x2 > 1 || x3 > 1);

            switch (v[0][0]) {
                case 'r':
                case 'R':
                    if (x1 > 255.5)
                        x1 = 255.;  // to handle values like r=256 !!!
                    if (x2 > 255.5)
                        x2 = 255.;
                    if (x3 > 255.5)
                        x3 = 255.;
                    col = (!hasInt) ? QColor::fromRgbF(x1, x2, x3) : QColor::fromRgb(x1, x2, x3);

                    break;

                case 'h':
                case 'H':
                    col = QColor::fromHslF(x1 / 360., x2, x3);
                    break;

                default:
                    break;
            }

            // Alpha
            if (v.size() == 5) {
                double x4 = atof(v[4].c_str());
                col.setAlphaF(x4);
            }

            if (col.isValid()) {
                // colors[name] = col;
            }
        }
        else if (name.empty()) {
            col = QColor(Qt::blue);
        }
        else if (MvQPalette::isPseudo(name)) {
            names_.push_back(name);
            items_[name] = MvQPaletteItem(name, QColor(), true);
        }
    }

    // colors[name] = c;
    // return PaletteRGBColor(palette,&c);

    return col;
}

void MvQPalette::toHclLabF(QColor c, float& hue, float& chroma, float& luminance)
{
    float x = 0., y = 0., z = 0.;
    rgbToXyz(c.redF(), c.greenF(), c.blueF(), x, y, z);
#ifdef DEBUG_MV_MVQPALETTE__
    qDebug() << "xyz" << x << y << z;
#endif
    xyzToHclLab(x, y, z, hue, chroma, luminance);
    if (c.red() == 255 && c.green() == 255 && c.blue() == 255)
        hue = 40. / 360.;
}

void MvQPalette::toHclLab(QColor c, int& hue, int& chroma, int& luminance)
{
    float h = 0., ch = 0., l = 0.;
    toHclLabF(c, h, ch, l);
    // qDebug() << "toHcl=" << h << ch << l;
    hue = static_cast<int>(h * 360);
    chroma = static_cast<int>(ch);
    luminance = static_cast<int>(l);
}

QColor MvQPalette::fromHclLab(int hue, int chroma, int luminance)
{
    float h = static_cast<float>(hue) / 359.;
    auto ch = static_cast<float>(chroma);
    auto l = static_cast<float>(luminance);
    return fromHclLabF(h, ch, l);
}

QColor MvQPalette::fromHclLabF(float hue, float chroma, float luminance)
{
    float x = 0., y = 0., z = 0., r = 0., g = 0., b = 0.;
    hclLabToXyz(hue, chroma, luminance, x, y, z);
    xyzToRgb(x, y, z, r, g, b);
    return QColor::fromRgbF(r, g, b);
}


void MvQPalette::toHclLuvF(QColor c, float& hue, float& chroma, float& luminance)
{
    float x = 0., y = 0., z = 0.;
    rgbToXyzd50(c.redF(), c.greenF(), c.blueF(), x, y, z);
    //#ifdef DEBUG_MV_MVQPALETTE__
    // qDebug() << "xyz" << x << y << z;
    //#endif
    xyzToHclLuv(x, y, z, hue, chroma, luminance);
    if (c.red() == 255 && c.green() == 255 && c.blue() == 255)
        hue = 40. / 360.;
}

void MvQPalette::toHclLuv(QColor c, int& hue, int& chroma, int& luminance)
{
    float h = 0., ch = 0., l = 0.;
    toHclLuvF(c, h, ch, l);
    qDebug() << "toHclLuv:" << c << "->" << h << ch << l;
    hue = static_cast<int>(h * 360);
    chroma = static_cast<int>(ch);
    luminance = static_cast<int>(l);
    qDebug() << "->" << hue << chroma << luminance;
}

QColor MvQPalette::fromHclLuv(int hue, int chroma, int luminance)
{
    float h = static_cast<float>(hue) / 355.;
    auto ch = static_cast<float>(chroma);
    auto l = static_cast<float>(luminance);
    auto col = fromHclLuvF(h, ch, l);
    // qDebug() << "toHclLuv:" << h << ch << l << "->" << col;
    return col;
}

QColor MvQPalette::fromHclLuvF(float hue, float chroma, float luminance)
{
    float x = 0., y = 0., z = 0., r = 0., g = 0., b = 0.;
    hclLuvToXyz(hue, chroma, luminance, x, y, z);
    xyzd50ToRgb(x, y, z, r, g, b);
    return QColor::fromRgbF(r, g, b);
}

// paint the chequered background when alpha is set
void MvQPalette::paintAlphaBg(QPainter* painter, QRect area)
{
    painter->save();
    painter->setClipRect(area);

    QColor c1(170, 170, 170);
    QColor c2(190, 190, 190);
    int rs = 8;
    int nx = area.width() / rs;
    int ny = area.height() / rs;
    QColor cAct = c1;
    for (int i = 0; i <= nx; i++)
        for (int j = 0; j <= ny; j++) {
            QRect r(area.x() + 1 + i * rs, area.y() + 1 + j * rs, rs, rs);
            if (j % 2 == 0) {
                if (i % 2 == 0)
                    cAct = c1;
                else
                    cAct = c2;
            }
            else {
                if (i % 2 == 0)
                    cAct = c2;
                else
                    cAct = c1;
            }
            painter->fillRect(r, cAct);
        }
    painter->restore();
}


static SimpleLoader<MvQPalette> load("resources", 0);
