/* This was part of the KDE project - see KGuiAddons
 * Copyright (C) 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
 * Copyright (C) 2007 Olaf Schmidt <ojschmidt@kde.org>
 * Copyright (C) 2007 Thomas Zander <zander@kde.org>
 * Copyright (C) 2007 Zack Rusin <zack@kde.org>
 * Copyright (C) 2015 Petr Mrazek <peterix@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "../include/rainbow.h"

#include <QColor>
#include <QImage>
#include <QtNumeric> // qIsNaN

#include <math.h>

//BEGIN internal helper functions

static inline qreal wrap(qreal a, qreal d = 1.0)
{
    qreal r = fmod(a, d);
    return (r < 0.0 ? d + r : (r > 0.0 ? r : 0.0));
}

// normalize: like qBound(a, 0.0, 1.0) but without needing the args and with
// "safer" behavior on NaN (isnan(a) -> return 0.0)
static inline qreal normalize(qreal a)
{
    return (a < 1.0 ? (a > 0.0 ? a : 0.0) : 1.0);
}


///////////////////////////////////////////////////////////////////////////////
// HCY color space

#define HCY_REC 709 // use 709 for now
#if HCY_REC == 601
static const qreal yc[3] = {0.299, 0.587, 0.114};
#elif HCY_REC == 709
static const qreal yc[3] = {0.2126, 0.7152, 0.0722};
#else // use Qt values
static const qreal yc[3] = {0.34375, 0.5, 0.15625};
#endif

class KHCY
{
public:
    explicit KHCY(const QColor &color)
    {
        qreal r = gamma(color.redF());
        qreal g = gamma(color.greenF());
        qreal b = gamma(color.blueF());
        a = color.alphaF();

        // luma component
        y = lumag(r, g, b);

        // hue component
        qreal p = qMax(qMax(r, g), b);
        qreal n = qMin(qMin(r, g), b);
        qreal d = 6.0 * (p - n);
        if (n == p)
        {
            h = 0.0;
        }
        else if (r == p)
        {
            h = ((g - b) / d);
        }
        else if (g == p)
        {
            h = ((b - r) / d) + (1.0 / 3.0);
        }
        else
        {
            h = ((r - g) / d) + (2.0 / 3.0);
        }

        // chroma component
        if (r == g && g == b)
        {
            c = 0.0;
        }
        else
        {
            c = qMax((y - n) / y, (p - y) / (1 - y));
        }
    }
    explicit KHCY(qreal h_, qreal c_, qreal y_, qreal a_ = 1.0)
    {
        h = h_;
        c = c_;
        y = y_;
        a = a_;
    }

    QColor qColor() const
    {
        // start with sane component values
        qreal _h = wrap(h);
        qreal _c = normalize(c);
        qreal _y = normalize(y);

        // calculate some needed variables
        qreal _hs = _h * 6.0, th, tm;
        if (_hs < 1.0)
        {
            th = _hs;
            tm = yc[0] + yc[1] * th;
        }
        else if (_hs < 2.0)
        {
            th = 2.0 - _hs;
            tm = yc[1] + yc[0] * th;
        }
        else if (_hs < 3.0)
        {
            th = _hs - 2.0;
            tm = yc[1] + yc[2] * th;
        }
        else if (_hs < 4.0)
        {
            th = 4.0 - _hs;
            tm = yc[2] + yc[1] * th;
        }
        else if (_hs < 5.0)
        {
            th = _hs - 4.0;
            tm = yc[2] + yc[0] * th;
        }
        else
        {
            th = 6.0 - _hs;
            tm = yc[0] + yc[2] * th;
        }

        // calculate RGB channels in sorted order
        qreal tn, to, tp;
        if (tm >= _y)
        {
            tp = _y + _y * _c * (1.0 - tm) / tm;
            to = _y + _y * _c * (th - tm) / tm;
            tn = _y - (_y * _c);
        }
        else
        {
            tp = _y + (1.0 - _y) * _c;
            to = _y + (1.0 - _y) * _c * (th - tm) / (1.0 - tm);
            tn = _y - (1.0 - _y) * _c * tm / (1.0 - tm);
        }

        // return RGB channels in appropriate order
        if (_hs < 1.0)
        {
            return QColor::fromRgbF(igamma(tp), igamma(to), igamma(tn), a);
        }
        else if (_hs < 2.0)
        {
            return QColor::fromRgbF(igamma(to), igamma(tp), igamma(tn), a);
        }
        else if (_hs < 3.0)
        {
            return QColor::fromRgbF(igamma(tn), igamma(tp), igamma(to), a);
        }
        else if (_hs < 4.0)
        {
            return QColor::fromRgbF(igamma(tn), igamma(to), igamma(tp), a);
        }
        else if (_hs < 5.0)
        {
            return QColor::fromRgbF(igamma(to), igamma(tn), igamma(tp), a);
        }
        else
        {
            return QColor::fromRgbF(igamma(tp), igamma(tn), igamma(to), a);
        }
    }

    qreal h, c, y, a;
    static qreal luma(const QColor &color)
    {
        return lumag(gamma(color.redF()), gamma(color.greenF()), gamma(color.blueF()));
    }

private:
    static qreal gamma(qreal n)
    {
        return pow(normalize(n), 2.2);
    }
    static qreal igamma(qreal n)
    {
        return pow(normalize(n), 1.0 / 2.2);
    }
    static qreal lumag(qreal r, qreal g, qreal b)
    {
        return r * yc[0] + g * yc[1] + b * yc[2];
    }
};

static inline qreal mixQreal(qreal a, qreal b, qreal bias)
{
    return a + (b - a) * bias;
}
//END internal helper functions

qreal Rainbow::luma(const QColor &color)
{
    return KHCY::luma(color);
}

void Rainbow::getHcy(const QColor &color, qreal *h, qreal *c, qreal *y, qreal *a)
{
    if (!c || !h || !y)
    {
        return;
    }
    KHCY khcy(color);
    *c = khcy.c;
    *h = khcy.h;
    *y = khcy.y;
    if (a)
    {
        *a = khcy.a;
    }
}

static qreal contrastRatioForLuma(qreal y1, qreal y2)
{
    if (y1 > y2)
    {
        return (y1 + 0.05) / (y2 + 0.05);
    }
    else
    {
        return (y2 + 0.05) / (y1 + 0.05);
    }
}

qreal Rainbow::contrastRatio(const QColor &c1, const QColor &c2)
{
    return contrastRatioForLuma(luma(c1), luma(c2));
}

QColor Rainbow::lighten(const QColor &color, qreal ky, qreal kc)
{
    KHCY c(color);
    c.y = 1.0 - normalize((1.0 - c.y) * (1.0 - ky));
    c.c = 1.0 - normalize((1.0 - c.c) * kc);
    return c.qColor();
}

QColor Rainbow::darken(const QColor &color, qreal ky, qreal kc)
{
    KHCY c(color);
    c.y = normalize(c.y * (1.0 - ky));
    c.c = normalize(c.c * kc);
    return c.qColor();
}

QColor Rainbow::shade(const QColor &color, qreal ky, qreal kc)
{
    KHCY c(color);
    c.y = normalize(c.y + ky);
    c.c = normalize(c.c + kc);
    return c.qColor();
}

static QColor tintHelper(const QColor &base, qreal baseLuma, const QColor &color, qreal amount)
{
    KHCY result(Rainbow::mix(base, color, pow(amount, 0.3)));
    result.y = mixQreal(baseLuma, result.y, amount);

    return result.qColor();
}

QColor Rainbow::tint(const QColor &base, const QColor &color, qreal amount)
{
    if (amount <= 0.0)
    {
        return base;
    }
    if (amount >= 1.0)
    {
        return color;
    }
    if (qIsNaN(amount))
    {
        return base;
    }

    qreal baseLuma = luma(base); // cache value because luma call is expensive
    double ri = contrastRatioForLuma(baseLuma, luma(color));
    double rg = 1.0 + ((ri + 1.0) * amount * amount * amount);
    double u = 1.0, l = 0.0;
    QColor result;
    for (int i = 12; i; --i)
    {
        double a = 0.5 * (l + u);
        result = tintHelper(base, baseLuma, color, a);
        double ra = contrastRatioForLuma(baseLuma, luma(result));
        if (ra > rg)
        {
            u = a;
        }
        else
        {
            l = a;
        }
    }
    return result;
}

QColor Rainbow::mix(const QColor &c1, const QColor &c2, qreal bias)
{
    if (bias <= 0.0)
    {
        return c1;
    }
    if (bias >= 1.0)
    {
        return c2;
    }
    if (qIsNaN(bias))
    {
        return c1;
    }

    qreal r = mixQreal(c1.redF(), c2.redF(), bias);
    qreal g = mixQreal(c1.greenF(), c2.greenF(), bias);
    qreal b = mixQreal(c1.blueF(), c2.blueF(), bias);
    qreal a = mixQreal(c1.alphaF(), c2.alphaF(), bias);

    return QColor::fromRgbF(r, g, b, a);
}

QColor Rainbow::overlayColors(const QColor &base, const QColor &paint,
                              QPainter::CompositionMode comp)
{
    // This isn't the fastest way, but should be "fast enough".
    // It's also the only safe way to use QPainter::CompositionMode
    QImage img(1, 1, QImage::Format_ARGB32_Premultiplied);
    QPainter p(&img);
    QColor start = base;
    start.setAlpha(255); // opaque
    p.fillRect(0, 0, 1, 1, start);
    p.setCompositionMode(comp);
    p.fillRect(0, 0, 1, 1, paint);
    p.end();
    return img.pixel(0, 0);
}