// https://github.com/colorjs/color-name/blob/ab89e11d535d7ef46ed84e549cf69e1e3a156c83/index.js
export const NamedColors = {
	aliceblue: [240, 248, 255],
	antiquewhite: [250, 235, 215],
	aqua: [0, 255, 255],
	aquamarine: [127, 255, 212],
	azure: [240, 255, 255],
	beige: [245, 245, 220],
	bisque: [255, 228, 196],
	black: [0, 0, 0],
	blanchedalmond: [255, 235, 205],
	blue: [0, 0, 255],
	blueviolet: [138, 43, 226],
	brown: [165, 42, 42],
	burlywood: [222, 184, 135],
	cadetblue: [95, 158, 160],
	chartreuse: [127, 255, 0],
	chocolate: [210, 105, 30],
	coral: [255, 127, 80],
	cornflowerblue: [100, 149, 237],
	cornsilk: [255, 248, 220],
	crimson: [220, 20, 60],
	cyan: [0, 255, 255],
	darkblue: [0, 0, 139],
	darkcyan: [0, 139, 139],
	darkgoldenrod: [184, 134, 11],
	darkgray: [169, 169, 169],
	darkgreen: [0, 100, 0],
	darkgrey: [169, 169, 169],
	darkkhaki: [189, 183, 107],
	darkmagenta: [139, 0, 139],
	darkolivegreen: [85, 107, 47],
	darkorange: [255, 140, 0],
	darkorchid: [153, 50, 204],
	darkred: [139, 0, 0],
	darksalmon: [233, 150, 122],
	darkseagreen: [143, 188, 143],
	darkslateblue: [72, 61, 139],
	darkslategray: [47, 79, 79],
	darkslategrey: [47, 79, 79],
	darkturquoise: [0, 206, 209],
	darkviolet: [148, 0, 211],
	deeppink: [255, 20, 147],
	deepskyblue: [0, 191, 255],
	dimgray: [105, 105, 105],
	dimgrey: [105, 105, 105],
	dodgerblue: [30, 144, 255],
	firebrick: [178, 34, 34],
	floralwhite: [255, 250, 240],
	forestgreen: [34, 139, 34],
	fuchsia: [255, 0, 255],
	gainsboro: [220, 220, 220],
	ghostwhite: [248, 248, 255],
	gold: [255, 215, 0],
	goldenrod: [218, 165, 32],
	gray: [128, 128, 128],
	green: [0, 128, 0],
	greenyellow: [173, 255, 47],
	grey: [128, 128, 128],
	honeydew: [240, 255, 240],
	hotpink: [255, 105, 180],
	indianred: [205, 92, 92],
	indigo: [75, 0, 130],
	ivory: [255, 255, 240],
	khaki: [240, 230, 140],
	lavender: [230, 230, 250],
	lavenderblush: [255, 240, 245],
	lawngreen: [124, 252, 0],
	lemonchiffon: [255, 250, 205],
	lightblue: [173, 216, 230],
	lightcoral: [240, 128, 128],
	lightcyan: [224, 255, 255],
	lightgoldenrodyellow: [250, 250, 210],
	lightgray: [211, 211, 211],
	lightgreen: [144, 238, 144],
	lightgrey: [211, 211, 211],
	lightpink: [255, 182, 193],
	lightsalmon: [255, 160, 122],
	lightseagreen: [32, 178, 170],
	lightskyblue: [135, 206, 250],
	lightslategray: [119, 136, 153],
	lightslategrey: [119, 136, 153],
	lightsteelblue: [176, 196, 222],
	lightyellow: [255, 255, 224],
	lime: [0, 255, 0],
	limegreen: [50, 205, 50],
	linen: [250, 240, 230],
	magenta: [255, 0, 255],
	maroon: [128, 0, 0],
	mediumaquamarine: [102, 205, 170],
	mediumblue: [0, 0, 205],
	mediumorchid: [186, 85, 211],
	mediumpurple: [147, 112, 219],
	mediumseagreen: [60, 179, 113],
	mediumslateblue: [123, 104, 238],
	mediumspringgreen: [0, 250, 154],
	mediumturquoise: [72, 209, 204],
	mediumvioletred: [199, 21, 133],
	midnightblue: [25, 25, 112],
	mintcream: [245, 255, 250],
	mistyrose: [255, 228, 225],
	moccasin: [255, 228, 181],
	navajowhite: [255, 222, 173],
	navy: [0, 0, 128],
	oldlace: [253, 245, 230],
	olive: [128, 128, 0],
	olivedrab: [107, 142, 35],
	orange: [255, 165, 0],
	orangered: [255, 69, 0],
	orchid: [218, 112, 214],
	palegoldenrod: [238, 232, 170],
	palegreen: [152, 251, 152],
	paleturquoise: [175, 238, 238],
	palevioletred: [219, 112, 147],
	papayawhip: [255, 239, 213],
	peachpuff: [255, 218, 185],
	peru: [205, 133, 63],
	pink: [255, 192, 203],
	plum: [221, 160, 221],
	powderblue: [176, 224, 230],
	purple: [128, 0, 128],
	rebeccapurple: [102, 51, 153],
	red: [255, 0, 0],
	rosybrown: [188, 143, 143],
	royalblue: [65, 105, 225],
	saddlebrown: [139, 69, 19],
	salmon: [250, 128, 114],
	sandybrown: [244, 164, 96],
	seagreen: [46, 139, 87],
	seashell: [255, 245, 238],
	sienna: [160, 82, 45],
	silver: [192, 192, 192],
	skyblue: [135, 206, 235],
	slateblue: [106, 90, 205],
	slategray: [112, 128, 144],
	slategrey: [112, 128, 144],
	snow: [255, 250, 250],
	springgreen: [0, 255, 127],
	steelblue: [70, 130, 180],
	tan: [210, 180, 140],
	teal: [0, 128, 128],
	thistle: [216, 191, 216],
	tomato: [255, 99, 71],
	turquoise: [64, 224, 208],
	violet: [238, 130, 238],
	wheat: [245, 222, 179],
	white: [255, 255, 255],
	whitesmoke: [245, 245, 245],
	yellow: [255, 255, 0],
	yellowgreen: [154, 205, 50],
};

const D50 = [0.3457 / 0.3585, 1.0, (1.0 - 0.3457 - 0.3585) / 0.3585];

// https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
// https://drafts.csswg.org/css-syntax-3/#number-token-diagram
const Num = `[+-]?(?:\\d*\\.\\d+|\\d+)(?:[eE][+-]?\\d+)?`;

// Units
const Angle = `(?:(${Num})(deg|grad|rad|turn)?|none)`;
const Percent = `(?:(${Num})(%)?|none)`;
const Alpha = `(?:\\s*(?:\\/|,|\\s)\\s*${Percent})?`;
const Comma = `(?:\\s+|\\s*,\\s*)`;

// Color regular expressions
const RgbRegex = `^rgba?\\(\\s*${Percent}${Comma}${Percent}${Comma}${Percent}${Alpha}\\s*\\)$`;
const HslRegex = `^hsla?\\(\\s*${Angle}${Comma}${Percent}${Comma}${Percent}${Alpha}\\s*\\)$`;
const HwbRegex = `^hwb\\(\\s*${Angle}${Comma}${Percent}${Comma}${Percent}${Alpha}\\s*\\)$`;
const LinearRgbRegex = `^color\\(\\s*srgb-linear${Comma}${Percent}${Comma}${Percent}${Comma}${Percent}${Alpha}\\s*\\)$`;
const XyzRegex = `^color\\(\\s*xyz${Comma}${Percent}${Comma}${Percent}${Comma}${Percent}${Alpha}\\s*\\)$`;
const LabRegex = `^lab\\(\\s*${Percent}${Comma}${Percent}${Comma}${Percent}${Alpha}\\s*\\)$`;
const LchRegex = `^lch\\(\\s*${Percent}${Comma}${Percent}${Comma}${Angle}${Alpha}\\s*\\)$`;
const OklabRegex = `^oklab\\(\\s*${Percent}${Comma}${Percent}${Comma}${Percent}${Alpha}\\s*\\)$`;
const OklchRegex = `^oklch\\(\\s*${Percent}${Comma}${Percent}${Comma}${Angle}${Alpha}\\s*\\)$`;

function getAngle(angle, unit) {
	if (angle === undefined) return 0;
	const n = Number(angle);
	switch (unit) {
		case 'deg':
			return n;
		case 'grad':
			return (n / 400) * 360;
		case 'rad':
			return (n / Math.PI) * 180;
		case 'turn':
			return n * 360;
		default:
			return n;
	}
}

function getPercent(number, percent, max) {
	if (number === undefined) return 0;
	const n = Number(number);
	return percent ? (n / 100) * max : n;
}

function getAlpha(number, percent) {
	if (number === undefined) return 1;
	const n = Number(number);
	return percent ? n / 100 : n;
}

/* Color conversions

                ┌───────┐
             ┌──┤  HSL  │
             │  └───────┘
  ┌───────┐  │  ┌───────┐
  │  RGB  ├──┼──┤  HWB  │                ┌───────┐  ┌───────┐
  └───────┘  │  └───────┘             ┌──┤  LAB  ├──┤  LCH  │
             │  ┌───────┐  ┌───────┐  │  └───────┘  └───────┘
             └──┤ l RGB ├──┤  XYZ  ├──┤
                └───────┘  └───────┘  │  ┌───────┐  ┌───────┐
                                      └──┤ OKLAB ├──┤ OKLCH │
                                         └───────┘  └───────┘

https://www.w3.org/TR/css-color-4/#color-conversion-code
 */
export const ColorSpace = {
	Rgb: 'rgb',
	Hex: 'hex',
	Hsl: 'hsl',
	Hwb: 'hwb',
	LinearRgb: 'linear-rgb',
	Xyz: 'xyz',
	Lab: 'lab',
	Lch: 'lch',
	Oklab: 'oklab',
	Oklch: 'oklch',
};

export const ColorSpaces = [
	ColorSpace.Rgb,
	ColorSpace.Hex,
	ColorSpace.Hsl,
	ColorSpace.Hwb,
	ColorSpace.LinearRgb,
	ColorSpace.Xyz,
	ColorSpace.Lab,
	ColorSpace.Lch,
	ColorSpace.Oklab,
	ColorSpace.Oklch,
];

export class Color {
	space;
	c1;
	c2;
	c3;
	alpha;

	constructor(space, c1, c2, c3, alpha) {
		this.space = space;
		this.c1 = c1;
		this.c2 = c2;
		this.c3 = c3;
		this.alpha = alpha;
		switch (this.space) {
			// r: [0, 255], g: [0, 255], b: [0, 255]
			case ColorSpace.Rgb:
				this.c1 = Math.clamp(c1, 0, 255);
				this.c2 = Math.clamp(c2, 0, 255);
				this.c3 = Math.clamp(c3, 0, 255);
				break;

			// r: [0, 255], g: [0, 255], b: [0, 255]
			case ColorSpace.Hex:
				this.c1 = Math.clamp(c1, 0, 255);
				this.c2 = Math.clamp(c2, 0, 255);
				this.c3 = Math.clamp(c3, 0, 255);
				break;

			// h: [0, 360), s: [0, 100], l: [0, 100]
			case ColorSpace.Hsl:
				this.c1 = (c1 % 360) + (c1 < 0 ? 360 : 0);
				this.c2 = Math.clamp(c2, 0, 100);
				this.c3 = Math.clamp(c3, 0, 100);
				break;

			// h: [0, 360), w: [0, 100], b: [0, 100]
			case ColorSpace.Hwb:
				this.c1 = (c1 % 360) + (c1 < 0 ? 360 : 0);
				this.c2 = Math.clamp(c2, 0, 100);
				this.c3 = Math.clamp(c3, 0, 100);
				break;

			// r: [0, 1], g: [0, 1], b: [0, 1]
			case ColorSpace.LinearRgb:
				this.c1 = Math.clamp(c1, 0, 1);
				this.c2 = Math.clamp(c2, 0, 1);
				this.c3 = Math.clamp(c3, 0, 1);
				break;

			// x: [0, ∞), y: [0, ∞), z: [0, ∞)
			case ColorSpace.Xyz:
				this.c1 = Math.max(0, c1);
				this.c2 = Math.max(0, c2);
				this.c3 = Math.max(0, c3);
				break;

			// l: [0, 100], a: [-125, 125], b: [-125, 125]
			case ColorSpace.Lab:
				this.c1 = Math.clamp(c1, 0, 100);
				this.c2 = Math.clamp(c2, -125, 125);
				this.c3 = Math.clamp(c3, -125, 125);
				break;

			// l: [0, 100], c: [0, 150], h, [0, 360)
			case ColorSpace.Lch:
				this.c1 = Math.clamp(c1, 0, 100);
				this.c2 = Math.clamp(c2, 0, 150);
				this.c3 = (c3 % 360) + (c3 < 0 ? 360 : 0);
				break;

			// l: [0, 1], a: [-0.4, 0.4], b: [-0.4, 0.4]
			case ColorSpace.Oklab:
				this.c1 = Math.clamp(c1, 0, 1);
				this.c2 = Math.clamp(c2, -0.4, 0.4);
				this.c3 = Math.clamp(c3, -0.4, 0.4);
				break;

			// l: [0, 1], c: [0, 0.4], h: [0, 360)
			case ColorSpace.Oklch:
				this.c1 = Math.clamp(c1, 0, 1);
				this.c2 = Math.clamp(c2, 0, 0.4);
				this.c3 = (c3 % 360) + (c3 < 0 ? 360 : 0);
				break;
		}

		// alpha: [0, 1]
		this.alpha = Math.clamp(alpha, 0, 1);
	}

	static fromRgba(r, g, b, a) {
		return new Color(ColorSpace.Rgb, r, g, b, a);
	}

	static parse(str) {
		return (
			Color.parseNamed(str) ??
			Color.parseHex(str) ??
			Color.parseRgb(str) ??
			Color.parseHsl(str) ??
			Color.parseHwb(str) ??
			Color.parseLinearRgb(str) ??
			Color.parseXyz(str) ??
			Color.parseLab(str) ??
			Color.parseLch(str) ??
			Color.parseOkLab(str) ??
			Color.parseOkLch(str)
		);
	}

	static parseNamed(str) {
		str = str.toLocaleLowerCase();
		if (str in NamedColors) {
			const [r, g, b] = NamedColors[str];
			return new Color(ColorSpace.Rgb, r, g, b, 1);
		}
		return null;
	}

	static parseHex(str) {
		const regex = /^#([0-9a-fA-F]+)$/;
		const match = str.match(regex);
		if (!match) return null;
		const hex = match[1];
		if (hex.length === 3 || hex.length === 4) {
			const rgba = hex.split('').map((c) => Number(`0x${c}${c}`));
			return new Color(ColorSpace.Hex, rgba[0], rgba[1], rgba[2], rgba[3] ? rgba[3] / 255 : 1);
		} else if (hex.length === 6 || hex.length === 8) {
			const rgba = hex.match(/.{2}/g).map((c) => Number(`0x${c}`));
			return new Color(ColorSpace.Hex, rgba[0], rgba[1], rgba[2], rgba[3] ? rgba[3] / 255 : 1);
		} else {
			return null;
		}
	}

	static parseRgb(str) {
		const regex = new RegExp(RgbRegex);
		const match = str.match(regex);
		if (!match) return null;
		const r = getPercent(match[1], match[2], 255);
		const g = getPercent(match[3], match[4], 255);
		const b = getPercent(match[5], match[6], 255);
		const a = getAlpha(match[7], match[8]);
		return new Color(ColorSpace.Rgb, r, g, b, a);
	}

	static parseHsl(str) {
		const regex = new RegExp(HslRegex);
		const match = str.match(regex);
		if (!match) return null;
		const h = getAngle(match[1], match[2]);
		const s = getPercent(match[3], match[4], 100);
		const l = getPercent(match[5], match[6], 100);
		const a = getAlpha(match[7], match[8]);
		return new Color(ColorSpace.Hsl, h, s, l, a);
	}

	static parseHwb(str) {
		const regex = new RegExp(HwbRegex);
		const match = str.match(regex);
		if (!match) return null;
		const h = getAngle(match[1], match[2]);
		const w = getPercent(match[3], match[4], 100);
		const b = getPercent(match[5], match[6], 100);
		const a = getAlpha(match[7], match[8]);
		return new Color(ColorSpace.Hwb, h, w, b, a);
	}

	static parseLinearRgb(str) {
		const regex = new RegExp(LinearRgbRegex);
		const match = str.match(regex);
		if (!match) return null;
		const r = getPercent(match[1], match[2], 1);
		const g = getPercent(match[3], match[4], 1);
		const b = getPercent(match[5], match[6], 1);
		const a = getAlpha(match[7], match[8]);
		return new Color(ColorSpace.LinearRgb, r, g, b, a);
	}

	static parseXyz(str) {
		const regex = new RegExp(XyzRegex);
		const match = str.match(regex);
		if (!match) return null;
		const x = getPercent(match[1], match[2], 1);
		const y = getPercent(match[3], match[4], 1);
		const z = getPercent(match[5], match[6], 1);
		const a = getAlpha(match[7], match[8]);
		return new Color(ColorSpace.Xyz, x, y, z, a);
	}

	static parseLab(str) {
		const regex = new RegExp(LabRegex);
		const match = str.match(regex);
		if (!match) return null;
		const l = getPercent(match[1], match[2], 100);
		const a = getPercent(match[3], match[4], 125);
		const b = getPercent(match[5], match[6], 125);
		const alpha = getAlpha(match[7], match[8]);
		return new Color(ColorSpace.Lab, l, a, b, alpha);
	}

	static parseLch(str) {
		const regex = new RegExp(LchRegex);
		const match = str.match(regex);
		if (!match) return null;
		const l = getPercent(match[1], match[2], 100);
		const c = getPercent(match[3], match[4], 150);
		const h = getAngle(match[5], match[6]);
		const a = getAlpha(match[7], match[8]);
		return new Color(ColorSpace.Lch, l, c, h, a);
	}

	static parseOkLab(str) {
		const regex = new RegExp(OklabRegex);
		const match = str.match(regex);
		if (!match) return null;
		const l = getPercent(match[1], match[2], 1);
		const a = getPercent(match[3], match[4], 0.4);
		const b = getPercent(match[5], match[6], 0.4);
		const alpha = getAlpha(match[7], match[8]);
		return new Color(ColorSpace.Oklab, l, a, b, alpha);
	}

	static parseOkLch(str) {
		const regex = new RegExp(OklchRegex);
		const match = str.match(regex);
		if (!match) return null;
		const l = getPercent(match[1], match[2], 1);
		const c = getPercent(match[3], match[4], 0.4);
		const h = getAngle(match[5], match[6]);
		const a = getAlpha(match[7], match[8]);
		return new Color(ColorSpace.Oklch, l, c, h, a);
	}

	toString() {
		function hex(c) {
			return Math.round(c).toString(16).padStart(2, '0');
		}

		const twoDigits = [ColorSpace.LinearRgb, ColorSpace.Lab, ColorSpace.Lch, ColorSpace.Oklab, ColorSpace.Oklch];
		const digits = this.space === ColorSpace.Xyz ? 4 : twoDigits.includes(this.space) ? 2 : 0;
		const c1 = Number.parseFloat(this.c1.toFixed(digits));
		const c2 = Number.parseFloat(this.c2.toFixed(digits));
		const c3 = Number.parseFloat(this.c3.toFixed(digits));
		const alpha = this.alpha === 1 ? '' : ` / ${Number.parseFloat(this.alpha.toFixed(2))}`;
		switch (this.space) {
			case ColorSpace.Rgb:
				return `rgb(${c1} ${c2} ${c3}${alpha})`;
			case ColorSpace.Hex:
				return `#${hex(c1)}${hex(c2)}${hex(c3)}${this.alpha === 1 ? '' : hex(this.alpha * 255)}`;
			case ColorSpace.Hsl:
				return `hsl(${c1} ${c2}% ${c3}%${alpha})`;
			case ColorSpace.Hwb:
				return `hwb(${c1} ${c2}% ${c3}%${alpha})`;
			case ColorSpace.Lab:
				return `lab(${c1} ${c2} ${c3}${alpha})`;
			case ColorSpace.Lch:
				return `lch(${c1} ${c2} ${c3}${alpha})`;
			case ColorSpace.Oklab:
				return `oklab(${c1} ${c2} ${c3}${alpha})`;
			case ColorSpace.Oklch:
				return `oklch(${c1} ${c2} ${c3}${alpha})`;
			case ColorSpace.LinearRgb:
				return `color(srgb-linear ${c1} ${c2} ${c3}${alpha})`;
			case ColorSpace.Xyz:
				return `color(xyz ${c1} ${c2} ${c3}${alpha})`;
		}
	}

	toColor(space) {
		switch (space) {
			case ColorSpace.Rgb:
				return this.rgb();
			case ColorSpace.Hex:
				return this.hex();
			case ColorSpace.Hsl:
				return this.hsl();
			case ColorSpace.Hwb:
				return this.hwb();
			case ColorSpace.LinearRgb:
				return this.linear();
			case ColorSpace.Xyz:
				return this.xyz();
			case ColorSpace.Lab:
				return this.lab();
			case ColorSpace.Lch:
				return this.lch();
			case ColorSpace.Oklab:
				return this.oklab();
			case ColorSpace.Oklch:
				return this.oklch();
		}
	}

	luminance() {
		const color = this.linear();
		return color.c1 * 0.2126 + color.c2 * 0.7152 + color.c3 * 0.0722;
	}

	rgb() {
		switch (this.space) {
			case ColorSpace.Rgb:
				return this;
			case ColorSpace.Hex:
				return this.hexToRgb();
			case ColorSpace.Hsl:
				return this.hslToRgb();
			case ColorSpace.Hwb:
				return this.hwbToRgb();
			default:
				return this.linear().linearToRgb();
		}
	}

	hex() {
		return this.space === ColorSpace.Hex ? this : this.rgb().rgbToHex();
	}

	hsl() {
		return this.space === ColorSpace.Hsl ? this : this.rgb().rgbToHsl();
	}

	hwb() {
		return this.space === ColorSpace.Hwb ? this : this.rgb().rgbToHwb();
	}

	linear() {
		switch (this.space) {
			case ColorSpace.LinearRgb:
				return this;
			case ColorSpace.Rgb:
			case ColorSpace.Hex:
			case ColorSpace.Hsl:
			case ColorSpace.Hwb:
				return this.rgb().rgbToLinear();
			default:
				return this.xyz().xyzToLinear();
		}
	}

	xyz() {
		switch (this.space) {
			case ColorSpace.Xyz:
				return this;
			case ColorSpace.Rgb:
			case ColorSpace.Hex:
			case ColorSpace.Hsl:
			case ColorSpace.Hwb:
			case ColorSpace.LinearRgb:
				return this.linear().linearToXyz();
			case ColorSpace.Lab:
			case ColorSpace.Lch:
				return this.lab().labToXyz();
			case ColorSpace.Oklab:
			case ColorSpace.Oklch:
				return this.oklab().oklabToXyz();
		}
	}

	lab() {
		switch (this.space) {
			case ColorSpace.Lab:
				return this;
			case ColorSpace.Lch:
				return this.lchToLab();
			default:
				return this.xyz().xyzToLab();
		}
	}

	lch() {
		return this.space === ColorSpace.Lch ? this : this.lab().labToLch();
	}

	oklab() {
		switch (this.space) {
			case ColorSpace.Oklab:
				return this;
			case ColorSpace.Oklch:
				return this.oklchToOklab();
			default:
				return this.xyz().xyzToOklab();
		}
	}

	oklch() {
		return this.space === ColorSpace.Oklch ? this : this.oklab().oklabToOklch();
	}

	/* Conversions */
	rgbToHex() {
		return new Color(ColorSpace.Hex, this.c1, this.c2, this.c3, this.alpha);
	}

	hexToRgb() {
		return new Color(ColorSpace.Rgb, this.c1, this.c2, this.c3, this.alpha);
	}

	rgbToHsl() {
		const [r, g, b] = [this.c1 / 255, this.c2 / 255, this.c3 / 255];
		const max = Math.max(r, g, b);
		const min = Math.min(r, g, b);
		const h = this.calculateHue();
		const l = (max + min) / 2;
		const s = l === 0 || l === 1 ? 0 : (max - l) / Math.min(l, 1 - l);
		return new Color(ColorSpace.Hsl, h, s * 100, l * 100, this.alpha);
	}

	hslToRgb() {
		const [h, s, l] = [this.c1, this.c2 / 100, this.c3 / 100];
		const C = (1 - Math.abs(2 * l - 1)) * s;
		const H = (h / 60) % 6;
		const X = C * (1 - Math.abs((H % 2) - 1));
		const [r, g, b] = (() => {
			if (0 <= H && H < 1) return [C, X, 0];
			if (1 <= H && H < 2) return [X, C, 0];
			if (2 <= H && H < 3) return [0, C, X];
			if (3 <= H && H < 4) return [0, X, C];
			if (4 <= H && H < 5) return [X, 0, C];
			return [C, 0, X];
		})();
		const m = l - C / 2;
		return new Color(ColorSpace.Rgb, (r + m) * 255, (g + m) * 255, (b + m) * 255, this.alpha);
	}

	rgbToHwb() {
		const epsilon = 0.00001;
		const [R, G, B] = [this.c1 / 255, this.c2 / 255, this.c3 / 255];
		let h = this.calculateHue();
		const w = Math.min(R, G, B);
		const b = 1 - Math.max(R, G, B);
		if (w + b >= 1 - epsilon) h = 0;
		return new Color(ColorSpace.Hwb, h, w * 100, b * 100, this.alpha);
	}

	hwbToRgb() {
		const [h, white, black] = [this.c1, this.c2 / 100, this.c3 / 100];
		if (white + black >= 1) {
			const l = white / (white + black);
			return new Color(ColorSpace.Rgb, l, l, l, this.alpha);
		}
		const color = new Color(ColorSpace.Hsl, h, 100, 50, this.alpha).rgb();
		const x = 1 - white - black;
		const w = white * 255;
		const r = color.c1 * x + w;
		const g = color.c2 * x + w;
		const b = color.c3 * x + w;
		return new Color(ColorSpace.Rgb, r, g, b, this.alpha);
	}

	rgbToLinear() {
		function channel(c) {
			return c > 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92;
		}

		const r = channel(this.c1 / 255);
		const g = channel(this.c2 / 255);
		const b = channel(this.c3 / 255);
		return new Color(ColorSpace.LinearRgb, r, g, b, this.alpha);
	}

	linearToRgb() {
		function channel(c) {
			if (c < 0.0031308) return 12.92 * c;
			return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
		}

		const r = channel(this.c1) * 255;
		const g = channel(this.c2) * 255;
		const b = channel(this.c3) * 255;
		return new Color(ColorSpace.Rgb, r, g, b, this.alpha);
	}

	linearToXyz() {
		const [r, g, b] = [this.c1, this.c2, this.c3];
		const x = (506752 / 1228815) * r + (87881 / 245763) * g + (12673 / 70218) * b;
		const y = (87098 / 409605) * r + (175762 / 245763) * g + (12673 / 175545) * b;
		const z = (7918 / 409605) * r + (87881 / 737289) * g + (1001167 / 1053270) * b;
		return new Color(ColorSpace.Xyz, x, y, z, this.alpha);
	}

	xyzToLinear() {
		const [x, y, z] = [this.c1, this.c2, this.c3];
		const r = (12831 / 3959) * x + (-329 / 214) * y + (-1974 / 3959) * z;
		const g = (-851781 / 878810) * x + (1648619 / 878810) * y + (36819 / 878810) * z;
		const b = (705 / 12673) * x + (-2585 / 12673) * y + (705 / 667) * z;
		return new Color(ColorSpace.LinearRgb, r, g, b, this.alpha);
	}

	xyzToLab() {
		const epsilon = 216 / 24389;
		const kappa = 24389 / 27;

		function channel(c) {
			return c > epsilon ? Math.cbrt(c) : (kappa * c + 16) / 116;
		}

		const x = channel(this.c1 / D50[0]);
		const y = channel(this.c2 / D50[1]);
		const z = channel(this.c3 / D50[2]);
		const l = 116 * y - 16;
		const a = 500 * (x - y);
		const b = 200 * (y - z);
		return new Color(ColorSpace.Lab, l, a, b, this.alpha);
	}

	labToXyz() {
		const epsilon = 216 / 24389;
		const kappa = 24389 / 27;
		const [l, a, b] = [this.c1, this.c2, this.c3];
		const fy = (l + 16) / 116;
		const fy3 = ((l + 16) / 116) ** 3;
		const fx = fy + a / 500;
		const fx3 = fx ** 3;
		const fz = fy - b / 200;
		const fz3 = fz ** 3;
		const x = fx3 > epsilon ? fx3 : (116 * fx - 16) / kappa;
		const y = l > kappa * epsilon ? fy3 : l / kappa;
		const z = fz3 > epsilon ? fz3 : (116 * fz - 16) / kappa;
		return new Color(ColorSpace.Xyz, x * D50[0], y * D50[1], z * D50[2], this.alpha);
	}

	labToLch() {
		const epsilon = 0.0015;
		const [l, a, b] = [this.c1, this.c2, this.c3];
		const c = Math.sqrt(a * a + b * b);
		let h = (Math.atan2(b, a) * 180) / Math.PI;
		if (h < 0) h += 360;
		if (c <= epsilon) h = 0;
		return new Color(ColorSpace.Lch, l, c, h, this.alpha);
	}

	lchToLab() {
		const [l, c, h] = [this.c1, this.c2, this.c3];
		const a = c * Math.cos((h * Math.PI) / 180);
		const b = c * Math.sin((h * Math.PI) / 180);
		return new Color(ColorSpace.Lab, l, a, b, this.alpha);
	}

	xyzToOklab() {
		const [x, y, z] = [this.c1, this.c2, this.c3];
		const l = Math.cbrt(0.819022437996703 * x + 0.3619062600528904 * y - 0.1288737815209879 * z);
		const m = Math.cbrt(0.0329836539323885 * x + 0.9292868615863434 * y + 0.0361446663506424 * z);
		const s = Math.cbrt(0.0481771893596242 * x + 0.2642395317527308 * y + 0.6335478284694309 * z);
		const L = 0.210454268309314 * l + 0.7936177747023054 * m - 0.0040720430116193 * s;
		const a = 1.9779985324311684 * l - 2.4285922420485799 * m + 0.450593709617411 * s;
		const b = 0.0259040424655478 * l + 0.7827717124575296 * m - 0.8086757549230774 * s;
		return new Color(ColorSpace.Oklab, L, a, b, this.alpha);
	}

	oklabToXyz() {
		const [L, a, b] = [this.c1, this.c2, this.c3];
		const l = (L + 0.3963377773761749 * a + 0.2158037573099136 * b) ** 3;
		const m = (L - 0.1055613458156586 * a - 0.0638541728258133 * b) ** 3;
		const s = (L - 0.0894841775298119 * a - 1.2914855480194092 * b) ** 3;
		const x = 1.2268798758459243 * l + -0.5578149944602171 * m + 0.2813910456659647 * s;
		const y = -0.0405757452148008 * l + 1.112286803280317 * m - 0.0717110580655164 * s;
		const z = -0.0763729366746601 * l - 0.4214933324022432 * m + 1.5869240198367816 * s;
		return new Color(ColorSpace.Xyz, x, y, z, this.alpha);
	}

	oklabToOklch() {
		const epsilon = 0.000004;
		const [l, a, b] = [this.c1, this.c2, this.c3];
		const c = Math.sqrt(a * a + b * b);
		let h = (Math.atan2(b, a) * 180) / Math.PI;
		if (h < 0) h += 360;
		if (c <= epsilon) h = 0;
		return new Color(ColorSpace.Oklch, l, c, h, this.alpha);
	}

	oklchToOklab() {
		const [l, c, h] = [this.c1, this.c2, this.c3];
		const a = c * Math.cos((h * Math.PI) / 180);
		const b = c * Math.sin((h * Math.PI) / 180);
		return new Color(ColorSpace.Oklab, l, a, b, this.alpha);
	}

	/* Utilities */
	calculateHue() {
		const [r, g, b] = [this.c1 / 255, this.c2 / 255, this.c3 / 255];
		const max = Math.max(r, g, b);
		const min = Math.min(r, g, b);
		const c = max - min;
		const h = (() => {
			if (c === 0) return 0;
			if (max === r) return (((g - b) / c) % 6) + (b > g ? 6 : 0);
			if (max === g) return (b - r) / c + 2;
			return (r - g) / c + 4;
		})();
		return h * 60;
	}
}
