/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

/**
 *  accessibleColor
 *
 *  `accessibleColor(baseColor [, minContrast])` returns a second
 *  color that meets the given WCAG contrast-ratio against
 *  `baseColor`.
 *
 *    • `baseColor`    String – 3- or 6-digit HEX with or without “#”.
 *    • `minContrast`  Number – target ratio (default **4.5** for WCAG-AA).
 *    • Return value   String – always a 6-digit HEX “#rrggbb”.
 *
 *  HOW IT WORKS
 *  ------------
 *  1. Fast path: tries pure **white** and **black**.
 *  2. If both fail, keeps the original hue/saturation and shifts
 *     lightness in 5 % steps until the ratio is reached.
 *  3. Last resort: picks the better of white / black if neither
 *     reaches the exact ratio after 100 %.
 *
 *  TYPICAL USE-CASES
 *  -----------------
 *  • **Dynamic theming**: users pick a brand color → generate safe
 *    text/background automatically.
 *  • **Badges / chips**: text on colored pills that must stay legible.
 *  • **Charts**: ensure labels on bars/slices have enough contrast.
 *  • **Map markers**: pick a halo color that keeps icons readable.
 *  • **Data tables**: alternating row colors that adapt to theme.
 *
 *  BASIC EXAMPLES
 *  --------------
 *  // Default AA (4.5 : 1)
 *  const badgeFg = '#007bff';                 // brand blue
 *  const badgeBg = accessibleColor(badgeFg);  // → '#ffffff'
 *
 *  // Pick a background for an arbitrary text color
 *  const text   = '#e52929';
 *  const bg     = accessibleColor(text);      // may return '#ffffff'
 *
 *  // Request AAA (7 : 1) for small text
 *  const hiVis  = accessibleColor('#888888', 7); // → '#000000'
 *
 *  EXTENDED EXAMPLES
 *  -----------------
 *  // 1) Generate theme object
 *  const base = '#1abc9c';
 *  const theme = {
 *    primary:   base,
 *    onPrimary: accessibleColor(base),        // safe text color
 *    border:    accessibleColor(base, 3),     // borders need only 3 : 1
 *  };
 *
 *  // 2) Build badge component
 *  function Badge({ color, children }) {
 *    const background = color;
 *    const foreground = accessibleColor(color);  // text or icon
 *    return `<span style="
 *              background:${background};
 *              color:${foreground};
 *              padding:0.25em 0.5em;
 *              border-radius:0.25rem;">${children}</span>`;
 *  }
 *
 *  // 3) Ensure chart label contrast
 *  const segmentColor = '#ffcc00';
 *  const labelColor   = accessibleColor(segmentColor, 3); // 3 : 1 OK for large labels
 *
 *  NOTES & TIPS
 *  ------------
 *  • `minContrast = 3` is allowed for:
 *      – Large / bold text (≥ 24 px normal or ≥ 19 px bold).
 *      – Non-text UI parts (icons, borders).
 *  • The algorithm preserves hue & saturation whenever possible,
 *    so resulting colors match the original palette.
 *  • Performance: pure JS, no DOM required – safe in workers.
 *  • Edge-case: middle-gray “#777777” – function will return black
 *    because white misses 4.5 : 1 by a hair (~4.3 : 1).
 *
 * @param color
 * @param minContrast
 * @returns {string|string}
 * @summary This function generates a color that meets the WCAG contrast ratio requirements against a given base color.
 */
export function accessibleColor(color, minContrast = 4.5) {
	const baseColor = toHex(color);
	const baseHex = normalizeHex(baseColor);

	const white = "#ffffff";
	const black = "#000000";

	const baseLum = luminance(baseHex);

	// 1) Quick check: try pure white or pure black first
	if (contrastRatio(baseLum, luminance(white)) >= minContrast) return white;
	if (contrastRatio(baseLum, luminance(black)) >= minContrast) return black;

	// 2) Fallback: keep hue/saturation but shift lightness until contrast is OK
	let { h, s, l } = hexToHsl(baseHex);
	const direction = l < 0.5 ? +1 : -1; // brighten or darken?

	for (let i = 1; i <= 20; i++) {
		// max 20 × 5 % = 100 %
		l = clamp(l + direction * 0.05, 0, 1);
		const candidate = hslToHex({ h, s, l });
		if (contrastRatio(luminance(candidate), baseLum) >= minContrast) {
			return candidate;
		}
	}

	// 3) Last resort: pick the better of (almost-good) white/black
	return contrastRatio(baseLum, luminance(white)) >
		contrastRatio(baseLum, luminance(black))
		? white
		: black;
}

/**
 * Normalizes a given hexadecimal color code to ensure it is in the standard 6-character and lowercase format, prefixed with a '#'.
 *
 * @param {string} hex - The hexadecimal color code, which can be in 3-character or 6-character format, with or without the leading '#'.
 * @return {string} - The normalized 6-character hexadecimal color code in lowercase, prefixed with a '#'.
 */
function normalizeHex(hex) {
	hex = hex.toString().replace(/^#/, "");
	if (hex.length === 3)
		hex = hex
			.split("")
			.map((c) => c + c)
			.join("");
	return "#" + hex.toLowerCase();
}

/**
 * Converts a hexadecimal color code to an RGB object.
 *
 * @param {string} hex - A hexadecimal color string (e.g., "#ff0000" or "ff0000").
 * @return {Object} An object representing the RGB color with properties:
 *                  - r: The red component (0-255).
 *                  - g: The green component (0-255).
 *                  - b: The blue component (0-255).
 */
function hexToRgb(hex) {
	const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
	return {
		r: parseInt(m[1], 16),
		g: parseInt(m[2], 16),
		b: parseInt(m[3], 16),
	};
}

/**
 * Calculates the relative luminance of a color based on its hexadecimal representation.
 * The luminance is calculated using the standard formula for relative luminance
 * in the sRGB color space.
 *
 * @param {string} hex - The hexadecimal color code (e.g., "#FFFFFF" or "#000000").
 * @return {number} The relative luminance value, ranging from 0 (darkest) to 1 (lightest).
 */
function luminance(hex) {
	const { r, g, b } = hexToRgb(hex);
	const srgb = [r, g, b]
		.map((v) => v / 255)
		.map((v) =>
			v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4),
		);
	return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
}

/**
 * Calculates the contrast ratio between two luminance values.
 *
 * The contrast ratio is determined using a formula defined by the Web Content Accessibility Guidelines (WCAG).
 * This ratio helps evaluate the readability of text or visual elements against a background.
 *
 * @param {number} l1 - The relative luminance of the lighter element.
 * @param {number} l2 - The relative luminance of the darker element.
 * @returns {number} The contrast ratio, a value ranging from 1 to 21.
 */
const contrastRatio = (l1, l2) =>
	(Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);

/**
 * Converts RGB color values to HSL color space.
 *
 * @param {Object} rgb - The RGB color object.
 * @param {number} rgb.r - The red component (0-255).
 * @param {number} rgb.g - The green component (0-255).
 * @param {number} rgb.b - The blue component (0-255).
 * @return {Object} An object with the HSL values.
 * @return {number} return.h - The hue component (0-1).
 * @return {number} return.s - The saturation component (0-1).
 * @return {number} return.l - The lightness component (0-1).
 */
function rgbToHsl({ r, g, b }) {
	r /= 255;
	g /= 255;
	b /= 255;
	const max = Math.max(r, g, b),
		min = Math.min(r, g, b);
	let h,
		s,
		l = (max + min) / 2;

	if (max === min) {
		h = s = 0;
	} else {
		const d = max - min;
		s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
		switch (max) {
			case r:
				h = (g - b) / d + (g < b ? 6 : 0);
				break;
			case g:
				h = (b - r) / d + 2;
				break;
			case b:
				h = (r - g) / d + 4;
				break;
		}
		h /= 6;
	}
	return { h, s, l };
}

/**
 * Converts an HSL color value to its hexadecimal representation.
 *
 * @param {Object} hsl - An object representing the HSL color.
 * @param {number} hsl.h - Hue, a number between 0 and 1.
 * @param {number} hsl.s - Saturation, a number between 0 and 1.
 * @param {number} hsl.l - Lightness, a number between 0 and 1.
 * @return {string} The hexadecimal color string in the format "#RRGGBB".
 */
function hslToHex({ h, s, l }) {
	const hue2rgb = (p, q, t) => {
		if (t < 0) t += 1;
		if (t > 1) t -= 1;
		if (t < 1 / 6) return p + (q - p) * 6 * t;
		if (t < 1 / 2) return q;
		if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
		return p;
	};
	let r, g, b;
	if (s === 0) {
		r = g = b = l;
	} // achromatic
	else {
		const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
		const p = 2 * l - q;
		r = hue2rgb(p, q, h + 1 / 3);
		g = hue2rgb(p, q, h);
		b = hue2rgb(p, q, h - 1 / 3);
	}
	const toHex = (x) => ("0" + Math.round(x * 255).toString(16)).slice(-2);
	return "#" + toHex(r) + toHex(g) + toHex(b);
}

// Keep number between 0 and 1
const clamp = (val, min, max) => Math.min(max, Math.max(min, val));

/**
 * Converts any CSS color to full 6-digit HEX "#rrggbb".
 * Falls back to throwing an Error if the input is not a valid color.
 *
 * Works in all browsers (DOM required). For Node.js use a library like
 * `color` or `tinycolor2`, or adapt the parser accordingly.
 */
function toHex(color) {
	// Use the browser to parse the color for us
	const temp = new Option().style; // <option> exists in all browsers
	temp.color = color;

	// Invalid → empty string
	if (!temp.color) {
		throw new Error(`"${color}" is not a valid CSS color`);
	}

	// temp.color is now normalized, e.g. "rgb(255, 0, 0)" or "rgba(0, 0, 0, 0.5)"
	const m = temp.color.match(/\d+/g).map(Number); // [r, g, b, (a)]
	const [r, g, b] = m;

	// If alpha channel < 1 the color is translucent; composite onto white
	const a = m[3] !== undefined ? m[3] / 255 || 0 : 1;
	const toWhite = (c) => Math.round(a * c + (1 - a) * 255);

	const rr = (a < 1 ? toWhite(r) : r).toString(16).padStart(2, "0");
	const gg = (a < 1 ? toWhite(g) : g).toString(16).padStart(2, "0");
	const bb = (a < 1 ? toWhite(b) : b).toString(16).padStart(2, "0");

	return `#${rr}${gg}${bb}`;
}
