/* SPDX-License-Identifier: LGPL-2.0-or-later
 *
 * 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>
 * Copyright (C) 2026 Project Tick
 *
 * 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);
}
