// tslint:disable:no-unnecessary-qualifier

const { abs, atan2, cos, floor, log, min, max, round, sign, sin, sqrt, cbrt, PI, hypot } = Math

function lerp(a: number, b: number, f: number) {
	return a + (b - a) * f
}
function lerpInv(a: number, b: number, f: number) {
	return (f - a) / (b - a)
}
function clamp(x: number, min = 0, max = 1) {
	return x < min ? min : x > max ? max : x
}

function newtonIterate1d(f: (x: number) => number, xStart: number, max_steps: number, eps: number = 1e-8): number {
	let x = xStart,
		fx
	while (max_steps-- && abs((fx = f(x))) > eps) {
		const dfdx = (f(x + eps) - fx) / eps
		console.log("fx / dfdx", fx / dfdx, "fx", fx, "x", x)
		x = x - fx / dfdx
	}
	return x
}
function bisect(f: (x: number) => number, a: number, b: number, steps: number) {
	let fA = f(a)
	while (steps--) {
		const c = (a + b) / 2
		const fC = f(c)
		if (sign(fA) == sign(fC)) {
			a = c
			fA = fC
		} else {
			b = c
		}
	}
	return (a + b) / 2
}

const TWOPI = 2 * PI

const DEG2RAD = PI / 180

const RAD2DEG = 180 / PI
/**
 * @param alpha1 default=1
 * @example chroma.color(99, 99, 44, 0.7)
 */
export function color(red255: number, green255: number, blue255: number, alpha1?: number): Color
/**
 * @example chroma.color('mediumorchid') // a css string
 * @example chroma.color([56, 203, 30]) // a RGB triple
 * @example chroma.color(0x4b0082) // a hex num
 * @example chroma.color([30, 0.8, 0.3], 'hsl') // explicit format
 */
export function color(x: Chromable, format?: ColorFormat): Color
/**
 * This overload allows VS Code to suggest color names when you type `color('`.
 */
export function color(colorname: keyof typeof w3cx11, format?: "name"): Color
/** @example chroma.color(30, 0.8, 0.3, 'hsl') */
export function color(channel0: number, channel1: number, channel2: number, format: ColorFormat): Color
/** @example chroma.color(0.3, 0.8, 0.3, 1, 'gl') */
export function color(
	channel0: number,
	channel1: number,
	channel2: number,
	channel3: number,
	format: ColorFormat,
): Color
export function color(...args: any[]) {
	if (args[0] instanceof Color) {
		return args[0]
	}
	if (args.length > 1 && "string" == typeof args[args.length - 1]) {
		return guess(args.slice(0, args.length - 1), args[args.length - 1])
	} else if (Array.isArray(args[0])) {
		return guess(args[0])
	} else {
		return guess(args)
	}
}
/**
 * A Chromable is any value which can be converted to a color. For ease of use, most functions accept these instead of
 * only Color values.
 */
export type Chromable = number | string | Color | number[]

export class Color {
	/** @internal */
	constructor(
		private readonly r: number,
		private readonly g: number,
		private readonly b: number,
		private readonly a = 1,
	) {}

	/**
	 * @see [[mix]]
	 */
	public mix(col2: Chromable, f: number, m: InterpolationMode = "rgb") {
		return mix(this, col2, f, m)
	}
	public rgb(doRound = true, clamp_ = true): RGB {
		const f = (t: number) => {
			if (doRound) t = round(t)
			if (clamp_) t = clamp(t, 0, 255)
			return t
		}
		const { r, g, b } = this
		return [f(r), f(g), f(b)]
	}
	public rgba(doRound = true, clamp_ = true): RGBA {
		const f = (t: number) => {
			if (doRound) t = round(t)
			if (clamp_) t = clamp(t, 0, 255)
			return t
		}
		const { r, g, b, a } = this
		return [f(r), f(g), f(b), a]
	}

	/**
	 * Return a hex-string representation of this color.
	 *
	 * @param mode
	 * @see #num for a hex-number representation.
	 * @example chroma.color('yellow').alpha(0.7).hex()
	 * @example chroma.color('yellow').alpha(0.7).hex('rgba')
	 * @example chroma.color('yellow').alpha(0.7).hex('argb')
	 */
	public hex(mode: "rgb" | "rgba" | "argb" = "rgb") {
		const { r, g, b, a } = this
		return rgb2hex(r, g, b, a, mode)
	}

	/**
	 * Returns the [HSL] representation of this color. hue will always be in [0;360). Values are never NaN.
	 *
	 * @example chroma.color('purple').hsl()
	 */
	public hsl(): HSL {
		const { r, g, b } = this
		return rgb2hsl(r, g, b)
	}

	/**
	 * Returns the [HSL] representation of this color. hue will always be in [0;360). Values are never NaN.
	 *
	 * @example chroma.color('purple').hsv()
	 */
	public hsv() {
		const { r, g, b } = this
		return rgb2hsv(r, g, b)
	}

	/**
	 * Returns the [HSL] representation of this color. hue will always be in [0;360). Values are never NaN.
	 *
	 * @example chroma.color('purple').hcg()
	 */
	public hcg() {
		const { r, g, b } = this
		return rgb2hcg(r, g, b)
	}

	/**
	 * Returns a CSS `rgb(...)` or `hsl(...)` string representation that can be used as CSS-color definition. The alpha
	 * value is not output if it 1.
	 * @example chroma.color('teal').css() // == "rgb(0,128,128)"
	 * @example chroma.color('teal').alpha(0.5).css() // == "rgba(0,128,128,0.5)"
	 * @example chroma.color('teal').css('hsl') // == "hsl(180,100%,25.1%)"
	 */
	public css(mode: "rgb" | "hsl" = "rgb") {
		if ("rgb" == mode) {
			const { r, g, b, a } = this
			return rgb2css(r, g, b, a)
		} else if ("hsl" == mode) {
			return hsl2css(this.hsl(), this.alpha())
		} else {
			throw new Error()
		}
	}

	public name(closest: true): string
	/**
	 * Get the name of a color. By default, this method will try to match the color exactly (comparing rounded RGB
	 * values). Pass `true` to return the name of the color which is closest to `this` in CIELAB color space. CIELAB is
	 * used as it is perceptually uniform.
	 * @param closest Whether this should find the closest color name. default=false
	 * @return If `closest == false`, the name of this color or `undefined` if there is no match. Otherwise, will always
	 * return a color name.
	 * @example chroma.color('#ff0000').name() // == "red"
	 * @example chroma.color('#ff0001').name() // == undefined
	 * @example chroma.color('#ff0001').name(true) // == "red"
	 */
	public name(closest?: boolean): string | undefined
	public name(closest: boolean = false): string | undefined {
		const thisNum = this.num()
		const name = Object.keys(w3cx11).find((name) => (w3cx11 as any)[name] == thisNum)
		if (!name && closest) {
			const [thisLStar, thisAStar, thisBStar] = this.lab()
			return withMax(Object.keys(w3cx11) as (keyof typeof w3cx11)[], (name) => {
				const [lStar, aStar, bStar] = num(w3cx11[name]).lab()
				return -hypot(thisLStar - lStar, thisAStar - aStar, thisBStar - bStar)
			})
		}
		return name
	}

	/**
	 * Get the [CMYK](#CMYK) representation of this color.
	 *
	 * @example chroma.color('red').cmyk()
	 */
	public cmyk() {
		const { r, g, b } = this
		return rgb2cmyk(r, g, b)
	}

	/**
	 * Returns the [GL] representation of this color.
	 * @example chroma.color('33cc00').gl()
	 */
	public gl(): GL {
		const { r, g, b, a } = this
		return [r / 255, g / 255, b / 255, a]
	}

	/**
	 * Get luminance of the color. This is equal to the Y channel of the XYZ color space.
	 * @example chroma.color('black').luminance() // == 0
	 * @example chroma.color('white').luminance() // == 1
	 * @example chroma.color('red').luminance() // == ~0.21
	 * @see https://en.wikipedia.org/wiki/Relative_luminance
	 */
	public luminance(): number
	/**
	 * Return a new [Color] with `lum1` by linearly interpolating `this` with white (when increasing the luminance) or
	 * black (otherwise) in the [XYZ] color space.
	 * @see https://en.wikipedia.org/wiki/Relative_luminance
	 * @example // Approximately doubling the luminance of red
	 * @example chroma.color('red').luminance(0.4) // == #ff8686 // "Vivid Tangerine"
	 * @param lum1 The desired luminance.
	 */
	public luminance(lum1: number): this
	public luminance(lum1?: number) {
		const { r, g, b, a } = this
		const [, Y] = rgb2xyz(r, g, b)
		if (undefined === lum1) {
			return Y
		}
		const inverseLerp = (a: number, b: number, val: number) => (val - a) / (b - a)
		if (lum1 > Y) {
			// lerp to white
			return mix(this, white, inverseLerp(Y, 1, lum1), "xyz").alpha(a)
		} else {
			// lerp to black
			return mix(black, this, inverseLerp(0, Y, lum1), "xyz").alpha(a)
		}
	}

	/**
	 * Get color temperature of this color in Kelvin. This only makes sense for colors close to those output by
	 * kelvin
	 *
	 * @example [c = chroma.color('#ff3300'), c.temperature()]
	 * @example [c = chroma.color('#ffe3cd'), c.temperature()]
	 * @example [c = chroma.color('#b3ccff'), c.temperature()]
	 */
	public temperature() {
		const { r, g, b } = this
		return rgb2kelvin(r, g, b)
	}

	/**
	 * Returns a new [Color] with a channel changed.
	 * @example chroma.color('skyblue').set('hsl.h', 0) // change hue to 0 deg (=red)
	 * @example chroma.color('hotpink').set('lch.c', 30) // set chromaticity to 30
	 * @example chroma.color('orangered').set('lab.l', x => x / 2) // half Lab lightness
	 * @example chroma.color('darkseagreen').set('lch.c', x => x * 2) // double Lch saturation
	 */
	public set(modeAndChannel: string, value: number | ((channel: number) => number)) {
		const [mode, channel] = modeAndChannel.split(".") as [ColorMode, string]
		const src = this[mode]()
		const i = mode.indexOf(channel)
		if (-1 == i) throw new Error("invalid channel")
		src[i] = "number" == typeof value ? value : value(src[i])
		return color(src, mode).alpha(this.a)
	}

	/**
	 * Returns whether this color is outside the RGB color cube and will be clipped/clamped when calling .rgb()
	 *
	 * @example [c = chroma.lch( 20, 40, 50), c.clipped()]
	 * @example [c = chroma.lch( 40, 40, 50), c.clipped()]
	 * @example [c = chroma.lch( 60, 40, 50), c.clipped()]
	 * @example [c = chroma.lch( 80, 40, 50), c.clipped()]
	 * @example [c = chroma.lch(100, 40, 50), c.clipped()]
	 */
	public clipped() {
		const { r, g, b } = this
		return !(0 <= r && r <= 255 && 0 <= g && g <= 255 && 0 <= b && b <= 255)
	}

	/**
	 * Returns black or white, whichever has the highest contrast to `this`.
	 * In the readme you should see the result of this.
	 *
	 * @example chroma.color('red')
	 * @example chroma.color('yellow')
	 */
	public textColor() {
		return this.luminance() > 0.5 ? black : white
	}

	/**
	 * Get alpha value of color.
	 * @example chroma.rgb(0, 0, 255, 0.5).alpha() // == 0.5
	 */
	public alpha(): number
	/**
	 * Return new [Color] with given alpha value.
	 * @example chroma.color('green').alpha(0.3)
	 * @example chroma.color('green').alpha(0.3).hex('rgba') // == "#00ff004d"
	 * @param alpha1 The desired alpha value.
	 */
	public alpha(alpha1: number): Color
	public alpha(alpha1?: number): number | Color {
		if (undefined === alpha1) {
			return this.a
		}
		const { r, g, b } = this
		return rgb(r, g, b, alpha1)
	}

	public darker(amount = 1) {
		const [l, a, b] = this.lab()
		return lab(l - LAB_Kn * amount, a, b, this.alpha())
	}

	/**
	 *
	 * @param amount
	 * @example chroma.color('hotpink')
	 * @example chroma.color('hotpink').brighter()
	 * @example chroma.color('hotpink').brighter(2)
	 * @example chroma.color('hotpink').brighter(3)
	 */
	public brighter(amount = 1) {
		return this.darker(-amount)
	}

	/**
	 * Returns a new [Color] with increased saturation.
	 * @param amount How much.
	 * @example chroma.color('slategray')
	 * @example chroma.color('slategray').saturate()
	 * @example chroma.color('slategray').saturate(2)
	 * @example chroma.color('slategray').saturate(3)
	 */
	public saturate(amount = 1) {
		const [l, c, h] = this.lch()
		return lch(l, max(0, c + amount * LAB_Kn), h, this.alpha())
	}

	/**
	 * Equivalent to `saturate(-amount)`.
	 * @see #saturate
	 */
	public desaturate(amount = 1) {
		return this.saturate(-amount)
	}

	public premultiplied() {
		const { r, g, b, a } = this
		return rgb(r * a, g * a, b * a, a)
	}

	/**
	 * Returns the [HSI] representation of this color. hue will always be in [0; 360). Values are never NaN.
	 *
	 * @example chroma.color('purple').hsi()
	 */
	public hsi() {
		const { r, g, b } = this
		return rgb2hsi(r, g, b)
	}

	/**
	 * Returns the [LAB] representation of this color.
	 *
	 * @example chroma.color('purple').lab()
	 */
	public lab() {
		const { r, g, b } = this
		return rgb2lab(r, g, b)
	}

	/**
	 * Return a hex-num of this color.
	 *
	 * @param mode
	 * @see #num for a hex-number representation.
	 * @example chroma.color('yellow').alpha(0.7).hex()
	 * @example chroma.color('yellow').alpha(0.7).hex('rgba')
	 * @example chroma.color('yellow').alpha(0.7).hex('argb')
	 */
	public num(mode: "rgb" | "rgba" | "argb" = "rgb") {
		const { r, g, b, a } = this
		return rgb2num(r, g, b, a, mode)
	}

	/**
	 * Returns the [LCH] representation of this color. hue will always be in [0; 360). Values are never NaN.
	 *
	 * @example chroma.color('purple').lch()
	 */
	public lch() {
		const { r, g, b } = this
		return rgb2lch(r, g, b)
	}

	/**
	 * Returns the [XYZ] representation of this color. hue will always be in [0; 360). Values are never NaN.
	 *
	 * @example chroma.color('purple').xyz()
	 */
	public xyz() {
		const { r, g, b } = this
		return rgb2xyz(r, g, b)
	}

	/**
	 * Whether this [Color](#Color) is identical (strict equality of r, g, b, a) to `color`.
	 */
	public equals(color: Color) {
		const { r, g, b, a } = this
		const { r: r2, g: g2, b: b2, a: a2 } = color
		return r == r2 && g == g2 && b == b2 && a == a2
	}

	public hashCode() {
		return this.num("rgba")
	}

	/**
	 * @example chroma.color('red').toSource() // == "rgb(255, 0, 0)"
	 * @example chroma.rgb(-2, 100.02, 200, 0.5).toSource() // == "rgb(-2, 100.02, 200, 0.5)"
	 */
	public toSource() {
		const { r, g, b, a } = this
		return "chroma.rgb(" + r + ", " + g + ", " + b + (a === 1 ? ")" : ", " + a + ")")
	}
}
export interface Color {
	toString(): string
	//darker(amount: number): Color
	//brighter(amount: number): Color
	kelvin(): number
}
Color.prototype.toString = Color.prototype.css
Color.prototype.kelvin = Color.prototype.temperature
/**
 * @example chroma.black
 */
export const black = new Color(0, 0, 0, 1)

/**
 * @example chroma.black
 */
export const white = new Color(255, 255, 255, 1)

export const brewer = {
	OrRd: [0xfff7ec, 0xfee8c8, 0xfdd49e, 0xfdbb84, 0xfc8d59, 0xef6548, 0xd7301f, 0xb30000, 0x7f0000],
	PuBu: [0xfff7fb, 0xece7f2, 0xd0d1e6, 0xa6bddb, 0x74a9cf, 0x3690c0, 0x0570b0, 0x045a8d, 0x023858],
	BuPu: [0xf7fcfd, 0xe0ecf4, 0xbfd3e6, 0x9ebcda, 0x8c96c6, 0x8c6bb1, 0x88419d, 0x810f7c, 0x4d004b],
	Oranges: [0xfff5eb, 0xfee6ce, 0xfdd0a2, 0xfdae6b, 0xfd8d3c, 0xf16913, 0xd94801, 0xa63603, 0x7f2704],
	BuGn: [0xf7fcfd, 0xe5f5f9, 0xccece6, 0x99d8c9, 0x66c2a4, 0x41ae76, 0x238b45, 0x006d2c, 0x00441b],
	YlOrBr: [0xffffe5, 0xfff7bc, 0xfee391, 0xfec44f, 0xfe9929, 0xec7014, 0xcc4c02, 0x993404, 0x662506],
	YlGn: [0xffffe5, 0xf7fcb9, 0xd9f0a3, 0xaddd8e, 0x78c679, 0x41ab5d, 0x238443, 0x006837, 0x004529],
	Reds: [0xfff5f0, 0xfee0d2, 0xfcbba1, 0xfc9272, 0xfb6a4a, 0xef3b2c, 0xcb181d, 0xa50f15, 0x67000d],
	RdPu: [0xfff7f3, 0xfde0dd, 0xfcc5c0, 0xfa9fb5, 0xf768a1, 0xdd3497, 0xae017e, 0x7a0177, 0x49006a],
	Greens: [0xf7fcf5, 0xe5f5e0, 0xc7e9c0, 0xa1d99b, 0x74c476, 0x41ab5d, 0x238b45, 0x006d2c, 0x00441b],
	YlGnBu: [0xffffd9, 0xedf8b1, 0xc7e9b4, 0x7fcdbb, 0x41b6c4, 0x1d91c0, 0x225ea8, 0x253494, 0x081d58],
	Purples: [0xfcfbfd, 0xefedf5, 0xdadaeb, 0xbcbddc, 0x9e9ac8, 0x807dba, 0x6a51a3, 0x54278f, 0x3f007d],
	GnBu: [0xf7fcf0, 0xe0f3db, 0xccebc5, 0xa8ddb5, 0x7bccc4, 0x4eb3d3, 0x2b8cbe, 0x0868ac, 0x084081],
	Greys: [0xffffff, 0xf0f0f0, 0xd9d9d9, 0xbdbdbd, 0x969696, 0x737373, 0x525252, 0x252525, 0x000000],
	YlOrRd: [0xffffcc, 0xffeda0, 0xfed976, 0xfeb24c, 0xfd8d3c, 0xfc4e2a, 0xe31a1c, 0xbd0026, 0x800026],
	PuRd: [0xf7f4f9, 0xe7e1ef, 0xd4b9da, 0xc994c7, 0xdf65b0, 0xe7298a, 0xce1256, 0x980043, 0x67001f],
	Blues: [0xf7fbff, 0xdeebf7, 0xc6dbef, 0x9ecae1, 0x6baed6, 0x4292c6, 0x2171b5, 0x08519c, 0x08306b],
	PuBuGn: [0xfff7fb, 0xece2f0, 0xd0d1e6, 0xa6bddb, 0x67a9cf, 0x3690c0, 0x02818a, 0x016c59, 0x014636],
	Viridis: [0x440154, 0x482777, 0x3f4a8a, 0x31678e, 0x26838f, 0x1f9d8a, 0x6cce5a, 0xb6de2b, 0xfee825],
	Spectral: [
		0x9e0142,
		0xd53e4f,
		0xf46d43,
		0xfdae61,
		0xfee08b,
		0xffffbf,
		0xe6f598,
		0xabdda4,
		0x66c2a5,
		0x3288bd,
		0x5e4fa2,
	],
	RdYlGn: [
		0xa50026,
		0xd73027,
		0xf46d43,
		0xfdae61,
		0xfee08b,
		0xffffbf,
		0xd9ef8b,
		0xa6d96a,
		0x66bd63,
		0x1a9850,
		0x006837,
	],
	RdBu: [
		0x67001f,
		0xb2182b,
		0xd6604d,
		0xf4a582,
		0xfddbc7,
		0xf7f7f7,
		0xd1e5f0,
		0x92c5de,
		0x4393c3,
		0x2166ac,
		0x053061,
	],
	PiYG: [
		0x8e0152,
		0xc51b7d,
		0xde77ae,
		0xf1b6da,
		0xfde0ef,
		0xf7f7f7,
		0xe6f5d0,
		0xb8e186,
		0x7fbc41,
		0x4d9221,
		0x276419,
	],
	PRGn: [
		0x40004b,
		0x762a83,
		0x9970ab,
		0xc2a5cf,
		0xe7d4e8,
		0xf7f7f7,
		0xd9f0d3,
		0xa6dba0,
		0x5aae61,
		0x1b7837,
		0x00441b,
	],
	RdYlBu: [
		0xa50026,
		0xd73027,
		0xf46d43,
		0xfdae61,
		0xfee090,
		0xffffbf,
		0xe0f3f8,
		0xabd9e9,
		0x74add1,
		0x4575b4,
		0x313695,
	],
	BrBG: [
		0x543005,
		0x8c510a,
		0xbf812d,
		0xdfc27d,
		0xf6e8c3,
		0xf5f5f5,
		0xc7eae5,
		0x80cdc1,
		0x35978f,
		0x01665e,
		0x003c30,
	],
	RdGy: [
		0x67001f,
		0xb2182b,
		0xd6604d,
		0xf4a582,
		0xfddbc7,
		0xffffff,
		0xe0e0e0,
		0xbababa,
		0x878787,
		0x4d4d4d,
		0x1a1a1a,
	],
	PuOr: [
		0x7f3b08,
		0xb35806,
		0xe08214,
		0xfdb863,
		0xfee0b6,
		0xf7f7f7,
		0xd8daeb,
		0xb2abd2,
		0x8073ac,
		0x542788,
		0x2d004b,
	],
	Set2: [0x66c2a5, 0xfc8d62, 0x8da0cb, 0xe78ac3, 0xa6d854, 0xffd92f, 0xe5c494, 0xb3b3b3],
	Accent: [0x7fc97f, 0xbeaed4, 0xfdc086, 0xffff99, 0x386cb0, 0xf0027f, 0xbf5b17, 0x666666],
	Set1: [0xe41a1c, 0x377eb8, 0x4daf4a, 0x984ea3, 0xff7f00, 0xffff33, 0xa65628, 0xf781bf, 0x999999],
	Set3: [
		0x8dd3c7,
		0xffffb3,
		0xbebada,
		0xfb8072,
		0x80b1d3,
		0xfdb462,
		0xb3de69,
		0xfccde5,
		0xd9d9d9,
		0xbc80bd,
		0xccebc5,
		0xffed6f,
	],
	Dark2: [0x1b9e77, 0xd95f02, 0x7570b3, 0xe7298a, 0x66a61e, 0xe6ab02, 0xa6761d, 0x666666],
	Paired: [
		0xa6cee3,
		0x1f78b4,
		0xb2df8a,
		0x33a02c,
		0xfb9a99,
		0xe31a1c,
		0xfdbf6f,
		0xff7f00,
		0xcab2d6,
		0x6a3d9a,
		0xffff99,
		0xb15928,
	],
	Pastel2: [0xb3e2cd, 0xfdcdac, 0xcbd5e8, 0xf4cae4, 0xe6f5c9, 0xfff2ae, 0xf1e2cc, 0xcccccc],
	Pastel1: [0xfbb4ae, 0xb3cde3, 0xccebc5, 0xdecbe4, 0xfed9a6, 0xffffcc, 0xe5d8bd, 0xfddaec, 0xf2f2f2],
}

/**
 * X11 color names
 * http://www.w3.org/TR/css3-color/#svg-color
 *
 * @example chroma.Object.keys(w3cx11).slice(0, 4)
 */
export const w3cx11 = {
	aliceblue: 0xf0f8ff,
	antiquewhite: 0xfaebd7,
	aqua: 0x00ffff,
	aquamarine: 0x7fffd4,
	azure: 0xf0ffff,
	beige: 0xf5f5dc,
	bisque: 0xffe4c4,
	black: 0x000000,
	blanchedalmond: 0xffebcd,
	blue: 0x0000ff,
	blueviolet: 0x8a2be2,
	brown: 0xa52a2a,
	burlywood: 0xdeb887,
	cadetblue: 0x5f9ea0,
	chartreuse: 0x7fff00,
	chocolate: 0xd2691e,
	coral: 0xff7f50,
	cornflower: 0x6495ed,
	cornflowerblue: 0x6495ed,
	cornsilk: 0xfff8dc,
	crimson: 0xdc143c,
	cyan: 0x00ffff,
	darkblue: 0x00008b,
	darkcyan: 0x008b8b,
	darkgoldenrod: 0xb8860b,
	darkgray: 0xa9a9a9,
	darkgreen: 0x006400,
	darkgrey: 0xa9a9a9,
	darkkhaki: 0xbdb76b,
	darkmagenta: 0x8b008b,
	darkolivegreen: 0x556b2f,
	darkorange: 0xff8c00,
	darkorchid: 0x9932cc,
	darkred: 0x8b0000,
	darksalmon: 0xe9967a,
	darkseagreen: 0x8fbc8f,
	darkslateblue: 0x483d8b,
	darkslategray: 0x2f4f4f,
	darkslategrey: 0x2f4f4f,
	darkturquoise: 0x00ced1,
	darkviolet: 0x9400d3,
	deeppink: 0xff1493,
	deepskyblue: 0x00bfff,
	dimgray: 0x696969,
	dimgrey: 0x696969,
	dodgerblue: 0x1e90ff,
	firebrick: 0xb22222,
	floralwhite: 0xfffaf0,
	forestgreen: 0x228b22,
	fuchsia: 0xff00ff,
	gainsboro: 0xdcdcdc,
	ghostwhite: 0xf8f8ff,
	gold: 0xffd700,
	goldenrod: 0xdaa520,
	gray: 0x808080,
	green: 0x008000,
	greenyellow: 0xadff2f,
	grey: 0x808080,
	honeydew: 0xf0fff0,
	hotpink: 0xff69b4,
	indianred: 0xcd5c5c,
	indigo: 0x4b0082,
	ivory: 0xfffff0,
	khaki: 0xf0e68c,
	laserlemon: 0xffff54,
	lavender: 0xe6e6fa,
	lavenderblush: 0xfff0f5,
	lawngreen: 0x7cfc00,
	lemonchiffon: 0xfffacd,
	lightblue: 0xadd8e6,
	lightcoral: 0xf08080,
	lightcyan: 0xe0ffff,
	lightgoldenrod: 0xfafad2,
	lightgoldenrodyellow: 0xfafad2,
	lightgray: 0xd3d3d3,
	lightgreen: 0x90ee90,
	lightgrey: 0xd3d3d3,
	lightpink: 0xffb6c1,
	lightsalmon: 0xffa07a,
	lightseagreen: 0x20b2aa,
	lightskyblue: 0x87cefa,
	lightslategray: 0x778899,
	lightslategrey: 0x778899,
	lightsteelblue: 0xb0c4de,
	lightyellow: 0xffffe0,
	lime: 0x00ff00,
	limegreen: 0x32cd32,
	linen: 0xfaf0e6,
	magenta: 0xff00ff,
	maroon: 0x800000,
	maroon2: 0x7f0000,
	maroon3: 0xb03060,
	mediumaquamarine: 0x66cdaa,
	mediumblue: 0x0000cd,
	mediumorchid: 0xba55d3,
	mediumpurple: 0x9370db,
	mediumseagreen: 0x3cb371,
	mediumslateblue: 0x7b68ee,
	mediumspringgreen: 0x00fa9a,
	mediumturquoise: 0x48d1cc,
	mediumvioletred: 0xc71585,
	midnightblue: 0x191970,
	mintcream: 0xf5fffa,
	mistyrose: 0xffe4e1,
	moccasin: 0xffe4b5,
	navajowhite: 0xffdead,
	navy: 0x000080,
	oldlace: 0xfdf5e6,
	olive: 0x808000,
	olivedrab: 0x6b8e23,
	orange: 0xffa500,
	orangered: 0xff4500,
	orchid: 0xda70d6,
	palegoldenrod: 0xeee8aa,
	palegreen: 0x98fb98,
	paleturquoise: 0xafeeee,
	palevioletred: 0xdb7093,
	papayawhip: 0xffefd5,
	peachpuff: 0xffdab9,
	peru: 0xcd853f,
	pink: 0xffc0cb,
	plum: 0xdda0dd,
	powderblue: 0xb0e0e6,
	purple: 0x800080,
	purple2: 0x7f007f,
	purple3: 0xa020f0,
	rebeccapurple: 0x663399,
	red: 0xff0000,
	rosybrown: 0xbc8f8f,
	royalblue: 0x4169e1,
	saddlebrown: 0x8b4513,
	salmon: 0xfa8072,
	sandybrown: 0xf4a460,
	seagreen: 0x2e8b57,
	seashell: 0xfff5ee,
	sienna: 0xa0522d,
	silver: 0xc0c0c0,
	skyblue: 0x87ceeb,
	slateblue: 0x6a5acd,
	slategray: 0x708090,
	slategrey: 0x708090,
	snow: 0xfffafa,
	springgreen: 0x00ff7f,
	steelblue: 0x4682b4,
	tan: 0xd2b48c,
	teal: 0x008080,
	thistle: 0xd8bfd8,
	tomato: 0xff6347,
	turquoise: 0x40e0d0,
	violet: 0xee82ee,
	wheat: 0xf5deb3,
	white: 0xffffff,
	whitesmoke: 0xf5f5f5,
	yellow: 0xffff00,
	yellowgreen: 0x9acd32,
}

/**
 * Return a new [[CubeHelix]].
 *
 * @example chroma.cubehelix() // use the default helix
 * @example chroma.cubehelix().start(200).rotations(-0.5).gamma(0.8).lightness([0.3, 0.8])
 */
export function cubehelix(
	start = 300,
	rotations = -1.5,
	hue: number | [number, number] = 1,
	gamma = 1,
	lightness: number | [number, number] = [0, 1],
) {
	const f: CubeHelix = ((t) => f.at(t)) as CubeHelix
	;(Object.getOwnPropertyNames(CubeHelix.prototype) as (keyof typeof CubeHelix)[]).forEach(
		(key) => (f[key] = CubeHelix.prototype[key]),
	)
	f.start(start)
	f.rotations(rotations)
	f.hue(hue)
	f.gamma(gamma)
	f.lightness(lightness)
	return f
}

/**
 * [Dave Green's cubehelix color scheme](http://www.mrao.cam.ac.uk/~dag/CUBEHELIX/)!
 *
 * A CubeHelix is a function defined on [0, 1] which returns colors.
 */
export interface CubeHelix {
	(f: number): Color
}
export class CubeHelix {
	private _start!: number
	private _rotations!: number
	private _gamma!: number
	private _hue!: [number, number]
	private _lightness!: [number, number]

	public start(): number
	public start(s: number): this
	public start(s?: number) {
		if (undefined === s) {
			return this._start
		}
		this._start = s
		return this
	}

	public rotations(): number
	public rotations(r: number): this
	public rotations(r?: number) {
		if (undefined === r) {
			return this._rotations
		}
		this._rotations = r
		return this
	}

	public gamma(): number
	public gamma(g: number): this
	public gamma(g?: number) {
		if (undefined === g) {
			return this._gamma
		}
		this._gamma = g
		return this
	}

	public hue(): [number, number]
	public hue(h: number | [number, number]): this
	public hue(h?: number | [number, number]) {
		if (undefined === h) {
			return this._hue
		}
		this._hue = Array.isArray(h) ? h : [h, h]
		return this
	}

	public lightness(): [number, number]
	public lightness(h: number | [number, number]): this
	public lightness(h?: number | [number, number]) {
		if (undefined === h) {
			return this._lightness
		}
		this._lightness = Array.isArray(h) ? h : [h, h]
		return this
	}

	/**
	 * Convert to a [[Scale]].
	 *
	 * @example chroma.cubehelix().scale().correctLightness().domain(2, 22)
	 */
	public scale() {
		return scale(this)
	}

	public at(fract: number) {
		const a = TWOPI * ((this._start + 120) / 360 + this._rotations * fract)
		const l = lerp(this._lightness[0], this._lightness[1], fract) ** this._gamma
		const h = lerp(this._hue[0], this._hue[1], fract)
		const amp = (h * l * (1 - l)) / 2
		const cos_a = cos(a)
		const sin_a = sin(a)
		const r = l + amp * (-0.14861 * cos_a + 1.78277 * sin_a)
		const g = l + amp * (-0.29227 * cos_a - 0.90649 * sin_a)
		const b = l + amp * (+1.97294 * cos_a)
		return rgb([r * 255, g * 255, b * 255, 1])
	}
}

/**
 * Create a new random [Color] from a random point in the RGB color space.
 * @param randomSource A function which returns random `number`s in the interval [0; 1). Useful if you want to
 *     create a deterministic sequence of "random" colors. Defaults to `Math.random`.
 */
export function random(randomSource = Math.random) {
	return num((randomSource() * 0x1_00_00_00) | 0)
}

/**
 * Mixes two colors. The mix ratio is a value between 0 and 1.
 * The color mixing produces different results based the color space used for interpolation.
 *
 * @param col2
 * @param f
 * @param m
 * @example chroma.mix('red', 'blue')
 * @example chroma.mix('red', 'blue', 0.25)
 * @example chroma.mix('red', 'blue', 0.75)
 *
 * @example chroma.mix('red', 'blue', 0.5, 'rgb')
 * @example chroma.mix('red', 'blue', 0.5, 'hsl')
 * @example chroma.mix('red', 'blue', 0.5, 'lab')
 * @example chroma.mix('red', 'blue', 0.5, 'lch')
 * @example chroma.mix('red', 'blue', 0.5, 'lrgb')
 */
export function mix(col1: Chromable, col2: Chromable, f = 0.5, m: InterpolationMode = "rgb") {
	const c1 = color(col1)
	const c2 = color(col2)
	const res = interpolators[m] && ((interpolators as any)[m](c1, c2, f, m) as Color)
	if (!res) {
		throw new Error("color mode " + m + " is not supported")
	}
	return res.alpha(lerp(c1.alpha(), c2.alpha(), f))
}

/**
 * Parse a CSS color. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/color) for all the possible
 * variants.
 *
 * @example chroma.css('hsl(2rad 90% 50% / 0.9)')
 * @example chroma.css('laserlemon')
 */
export function css(cssString: string) {
	const [r, g, b, a] = css2rgb(cssString)
	return new Color(r, g, b, a)
}

/**
 * @example chroma.cmyk(0.2, 0.8, 0, 0)
 * @example chroma.color(0.2, 0.8, 0, 0, 'cmyk')
 */
export function cmyk(cmyk: CMYK): Color
export function cmyk(cyan1: number, magenta1: number, yellow1: number, key1: number): Color
export function cmyk(...args: any[]) {
	return guess(args, "cmyk")
}

/**
 * @example chroma.gl(1, 1, 0, 1)
 */
export function gl(gl: RGBA | RGB): Color
/**
 * @example chroma.gl([1, 0, 1, 0.5])
 */
export function gl(red1: number, green1: number, blue1: number, alpha1: number): Color
export function gl(...args: any[]) {
	return guess(args, "gl")
}

export function hcg(hcg: HCG): Color
/**
 * @param alpha1 default=1
 */
export function hcg(h: number, c: number, g: number, alpha1?: number): Color
export function hcg(...args: any[]) {
	return guess(args, "hcg")
}

export function lch(lch: LCH): Color
/**
 * @param alpha1 default=1
 */
export function lch(h: number, c: number, l: number, alpha1?: number): Color
export function lch(...args: any[]) {
	return guess(args, "lch")
}

export function hsi(hsi: HSI): Color
/**
 * @param alpha1 default=1
 */
export function hsi(h: number, s: number, i: number, alpha1?: number): Color
export function hsi(...args: any[]) {
	return guess(args, "hsi")
}

export function hsl(hsl: HSL): Color
/**
 * @param alpha1 default=1
 * @example chroma.hsl(30, 1, 0.5)
 * @example chroma.hsl(30, 0.6, 0.5)
 */
export function hsl(hueDegrees: number, saturation1: number, lightness1: number, alpha1?: number): Color
export function hsl(...args: any[]) {
	return guess(args, "hsl")
}

export function hsv(hsv: LAB): Color
export function hsv(h: number, s: number, v: number): Color
export function hsv(...args: any[]) {
	return guess(args, "hsv")
}

/**
 *
 * @param temperature
 * @example chroma.kelvin(2000) // candle light
 * @example chroma.kelvin(3500) // sunset
 * @example chroma.kelvin(6500) // daylight
 * @example x0_1 => chroma.kelvin(x0_1 * 30000) // effective range: [0; 30000]
 */
export function kelvin(temperature: number) {
	const [r, g, b] = kelvin2rgb(temperature)
	return new Color(r, g, b)
}

export function lab(lab: LAB): Color
/**
 * @param alpha1 default=1
 */
export function lab(lightness1: number, a1: number, b: number, alpha1?: number): Color
export function lab(...args: any[]) {
	return guess(args, "lab")
}

/**
 * @example chroma.num(0x663399) // rebeccapurple
 */
export function num(num: number) {
	const [r, g, b] = num2rgb(num)
	return new Color(r, g, b)
}

export function rgb(rgb: RGBA | RGB): Color
/**
 * @param alpha1 default=1
 * @example chroma.rgb(0, 100, 200)
 */
export function rgb(red255: number, green255: number, blue255: number, alpha1?: number): Color
export function rgb(...args: any[]) {
	return guess(args, "rgb")
}

export function xyz(xyz: XYZ): Color
/** @param alpha1 default=1 */
export function xyz(x1: number, y1: number, z1: number, alpha1?: number): Color
export function xyz(...args: any[]) {
	return guess(args, "xyz")
}

/**
 * Similar to mix, but accepts more than two colors.
 *
 * @example colors = ['#ddd', 'yellow', 'red', 'teal']
 * @example chroma.average(colors) // default = 'rgb'
 * @example chroma.average(colors, 'lab')
 * @example chroma.average(colors, 'lch')
 * @example chroma.average(colors, 'lrgb')
 * @example chroma.average(['red', 'rgba(0,0,0,0.5)']).css()
 */
export function average(chromables: Chromable[], mode: InterpolationMode = "rgb") {
	const colors = chromables.map((c) => color(c))
	if (mode == "lrgb") {
		return _average_lrgb(colors)
	}
	if (mode == "num") {
		let numSum = 0,
			alphaSum = 0
		for (const col of colors) {
			numSum += col.num()
			alphaSum += col.alpha()
		}
		return num(numSum / colors.length).alpha(alphaSum / colors.length)
	}
	const xyz = [0, 0, 0]
	let dx = 0
	let dy = 0
	let alphaSum = 0
	for (const c of colors) {
		const xyz2 = c[mode]()
		alphaSum += c.alpha()

		for (let i = 0; i < xyz.length; i++) {
			if (mode.charAt(i) == "h") {
				const A = xyz2[i] * DEG2RAD
				dx += cos(A)
				dy += sin(A)
			} else {
				xyz[i] += xyz2[i]
			}
		}
	}
	for (let i = 0; i < xyz.length; i++) {
		if (mode.charAt(i) == "h") {
			const A = atan2(dy / colors.length, dx / colors.length) * RAD2DEG
			xyz[i] = (A + 360) % 360
		} else {
			xyz[i] = xyz[i] / colors.length
		}
	}
	return guess(xyz, mode).alpha(alphaSum / colors.length)
}

/**
 *
 * @param chromables
 * @example chroma.scale('black', 'red', 'gold') // linear interpolation
 * @example chroma.bezier('black', 'red', 'gold') // bezier interpolation
 */
export function bezier(chromables: Chromable[]): { (t: number): Color; scale(): Scale }
export function bezier(...chromables: Chromable[]): { (t: number): Color; scale(): Scale }
export function bezier(...args: any[]) {
	const chromables: Chromable[] = Array.isArray(args[0]) ? args[0] : args
	const f = _bezier(chromables) as { (t: number): Color; scale(): Scale }
	f.scale = CubeHelix.prototype.scale
	return f
}

/**
 * Blends two colors using RGB channel-wise blend functions.
 * @param bottom
 * @param top
 * @param mode
 * @example chroma.blend('4CBBFC', 'EEEE22', 'multiply')
 * @example chroma.blend('4CBBFC', 'EEEE22', 'darken')
 * @example chroma.blend('4CBBFC', 'EEEE22', 'lighten')
 */
export function blend(bottom: Chromable, top: Chromable, mode: BlendMode) {
	if (!blend_fs[mode]) {
		throw new Error("unknown blend mode " + mode)
	}
	return blend_fs[mode](bottom, top)
}
export type BlendMode = keyof typeof blend_fs
const blend_fs = {
	normal: blend_f(each((a, _) => a)),
	multiply: blend_f(each((a, b) => (a * b) / 255)),
	screen: blend_f(each(_screen)),
	overlay: blend_f(each(_overlay)),
	darken: blend_f(each(min)),
	lighten: blend_f(each(max)),
	dodge: blend_f(each(_dodge)),
	burn: blend_f(each(_burn)),
}

/**
 * @param colors
 * @example scale = chroma.scale(['yellow', '008ae5'])
 * @example scale(0.25)
 * @example scale(0.5)
 * @example scale(0.75)
 * @example chroma.scale('Viridis')
 */
export function scale(colors: Chromable[] | keyof typeof brewer | ((f: number) => Color)): Scale
export function scale(...colors: Chromable[]): Scale
export function scale(...args: any[]) {
	const f: Scale = ((t) => (f as any)._at(t)) as Scale
	;(Object.getOwnPropertyNames(Scale.prototype) as (keyof typeof Scale)[]).forEach(
		(key) => (f[key] = Scale.prototype[key]),
	)
	if (Array.isArray(args[0])) args = args[0]
	if (args.length == 1 && "string" == typeof args[0]) args = brewer[args[0] as keyof typeof brewer]
	;(f as any)._init("function" == typeof args[0] ? args[0] : args.map((a) => color(a)))
	//f.setColors(args.length > 1 ? args : args[0])
	return f
}
/**
 * A color scale, created with scale, is a function that maps numeric values to a color palette.
 *
 * The type parameter describes the output type and can be changed with out(). Defaults to Color objects.
 *
 * @param T The output format. default=Color
 * @example chroma.scale('Purples')
 * @example chroma.scale('Purples')(0.4)
 */
export interface Scale<T = Color> {
	(val: number): T
}
export class Scale<T = Color> {
	private _colors!: Color[] | ((t: number) => Color)
	private _classes!: number[] | undefined
	/**
	 * Color cache. undefined if the cache should not be used.
	 */
	private _cache!: Map<number, Color> | undefined
	private _correctLightness!: boolean
	private _gamma!: number
	private _mode!: InterpolationMode
	private _out!: ColorFormat | undefined
	private _min!: number
	private _max!: number
	private _paddingLeft!: number
	private _paddingRight!: number
	// positions of the colors on the interval [0, 1]. guaranteed to have the same length as _colors
	// undefined when _colors is a function
	private _pos: number[] | undefined

	/**
	 * Get the current scale classes.
	 */
	public classes(): number[]
	/**
	 * Make the scale return a number of distint color instead of a continuous gradient.
	 * If you pass a number the scale will broken into equi-distant classes:
	 *
	 * @example chroma.scale('OrRd') // continous
	 * @example chroma.scale('OrRd').classes(5) // equidistant classes
	 * @example chroma.scale('OrRd').classes(8)
	 *
	 * @example chroma.scale('OrRd').classes([0, 6, 11, 17, 20]) // also sets domain
	 */
	public classes(classes: number | number[]): this
	public classes(classes?: number | number[]) {
		if (undefined === classes) {
			return this._classes
		}
		if (Array.isArray(classes)) {
			this._classes = classes
			this.domain(classes[0], classes[classes.length - 1])
		} else {
			if (classes % 1 != 0 || classes < 1) throw new Error("invalid classes param")
			// const d = analyze(this.domain())
			this._classes = limits(this.domain(), "e", classes)
		}
		return this
	}

	/**
	 * Get the domain.
	 * @return If _colors is a function, [this._min, this._max]. If _colors is an array of colors, an array with the
	 * same length as the number of colors.
	 * @example chroma.scale("red", "white", "blue").domain(0, 20).domain() // == [0, 10, 20]
	 */
	public domain(): number[]
	/**
	 * Set the domain interval on which the scale is defined. Colors are distributed equidistantly along the
	 * interval.
	 * @param start
	 * @param end
	 * @return `this`
	 * @example chroma.scale("red", "white", "blue").domain(0, 100)(50) // == white
	 * @example chroma.scale("red", "white", "blue").domain(0, 0.25, 1).domain(0, 100).domain() // == [0, 25, 100]
	 */
	public domain(start: number, end: number): this
	/**
	 * Set the domain interval and the individual positions of the colors. The number of passed values must match
	 * the number of colors which define the scale. Not valid if the scale is defined by a function.
	 * @param domain The positions of all scale colors. Values must be in ascending order and should not have
	 * duplicates.
	 * @return `this`
	 * @example scale = chroma.scale("red", "white", "blue").domain(0, 25, 100)
	 * @example scale(25) // == white
	 * @example scale(100) // == blue
	 * @example scale(50) // == #aaaaff
	 */
	public domain(...domain: number[]): this
	public domain(...domain: number[]): number[] | this {
		if (undefined === domain[0]) {
			return "function" !== typeof this._colors
				? this._pos!.map((p) => lerp(this._min, this._max, p))
				: [this._min, this._max]
		}
		this._min = domain[0]
		this._max = domain[domain.length - 1]
		if (2 == domain.length) {
			if ("function" !== typeof this._colors) {
				// equidistant positions
				this._pos = this._colors.map((_, c) => c / (this._colors.length - 1))
			}
		} else if ("function" !== typeof this._colors && domain.length == this._colors.length) {
			this._pos = domain.map((d) => lerpInv(this._min, this._max, d))
		} else {
			throw new Error("invalid domain " + domain)
		}
		return this
	}

	/**
	 * Get the interpolation mode used when calculating colors.
	 */
	public mode(): InterpolationMode
	/**
	 * Set the interpolation mode used when calculating colors. The defaut mode is "rgb".
	 * See also {@link chroma#mix}
	 * @param mode the mode to set.
	 * @return `this`
	 * @example chroma.scale("red", "green").mode("lab")
	 * @example chroma.scale("red", "green").mode("lrgb")
	 */
	public mode(mode: InterpolationMode): this
	public mode(mode?: InterpolationMode) {
		if (undefined === mode) {
			return this._mode
		}
		this._mode = mode
		this._resetCache()
		return this
	}

	/**
	 * Set the output format return by `this(x)` and `this.colors(n)`.
	 * @param outputFormat The color format to use. Pass `undefined` to return [Color] objects.
	 * @return `this`
	 * @example chroma.scale("red", "white").out("hex")(0) // == "#ff0000"
	 * @example chroma.scale("red", "white").out("num").colors(2) // == [0xff0000, 0xffffff]
	 */
	public out<M extends ColorFormat | undefined>(
		outputFormat: M,
	): Scale<M extends ColorFormat ? ReturnType<Color[M]> : Color> {
		this._out = outputFormat
		return this as any
	}

	/**
	 * This makes sure the lightness range is spread evenly across a color scale. Especially useful when working
	 * with [multi-hue color scales](https://www.vis4.net/blog/2013/09/mastering-multi-hued-color-scales/), where
	 * simple gamma correction can't help you very much.
	 *
	 * @example chroma.scale('black','red','yellow','white')
	 * @example chroma.scale('black','red','yellow','white').correctLightness()
	 */
	public correctLightness(enableCorrectLightness = true) {
		if (this._correctLightness != enableCorrectLightness) {
			this._resetCache()
			const colors = this._colors
			if (enableCorrectLightness && "function" !== typeof colors) {
				// make sure that the colors have ascending or descending lightnesses
				let sign = 0
				for (let i = 1; i < colors.length; i++) {
					const sign2 = colors[i].lab()[0] - colors[i - 1].lab()[0]
					if (0 == sign) {
						sign = sign2
					} else if (sign * sign2 < 0) {
						throw new Error("scale color lightnesses must be monotonic")
					}
				}
			}
		}
		this._correctLightness = enableCorrectLightness
		return this
	}

	/**
	 * Get the padding.
	 * @returns [paddingLeft, paddingRight]
	 */
	public padding(): [number, number]
	/**
	 * Set the padding. Positive values will "cut off" the ends of gradient, while negative values will add a
	 * section of constant color at the ends.
	 * @example chroma.scale("red", "white").padding(0.2)
	 * @example chroma.scale("red", "white").padding(0.1)(0) // == color('#ff1a1a'), instead of red
	 * @example chroma.scale("red", "white").padding(-0.1)(0) // == color('red')
	 * @param paddingLeft padding on left side.(lower-valued end of the interval).
	 * @param paddingRight padding on right (higher-valued end of the interval).
	 * default=paddingLeft
	 */
	public padding(paddingLeft: number, paddingRight?: number): this
	public padding(paddingLeft?: number, paddingRight: number | undefined = paddingLeft) {
		if (!paddingLeft) {
			return [this._paddingLeft, this._paddingRight]
		}
		this._paddingLeft = paddingLeft
		this._paddingRight = paddingRight!
		return this
	}

	/**
	 * Get a number of equidistant colors.
	 * @param numColors The number of colors to return.
	 * @param format Output format. Defaults to `"hex"`. Pass `"color"` to get {@link Color} objects.
	 * @returns If `numColors` is `undefined`, the colors which define this [Scale]. If `numColors` is 1,
	 * `[this((min + max) / 2)]`. Otherwise, an array where the first element is `this(min)`, the last one is
	 * `this(max)` and the rest are equidistant samples between min and max.
	 * @example chroma.scale('OrRd').colors(5)
	 * @example chroma.scale(['white', 'black']).colors(12)
	 */
	public colors<M extends ColorFormat | "color" | undefined = "hex">(
		numColors?: number,
		format: M = "hex" as any,
	): Array<M extends ColorFormat ? ReturnType<Color[M]> : M extends "color" ? Color : string> {
		let result: Color[]
		if (undefined === numColors) {
			result = (this._colors as Color[]).slice()
		} else if (numColors == 1) {
			result = [this._color((this._min + this._max) / 2)]
		} else if (numColors > 1) {
			result = Array.from({ length: numColors }, (_, i) =>
				this._color(lerp(this._min, this._max, i / (numColors - 1))),
			)
		} else {
			// returns all colors based on the defined classes
			let samples
			if (this._classes && this._classes.length > 2) {
				samples = Array.from(
					{ length: this._classes.length - 1 },
					(_, i) => (this._classes![i] + this._classes![i + 1]) * 0.5,
				)
			} else {
				samples = this.domain() // TODO?!
			}
			result = samples.map((s) => this._color(s))
		}

		return (format != "color" ? result.map((c) => c[format as "hex"]()) : result) as any
	}

	/**
	 * Get whether the cache is enabled. Defaults to true.
	 */
	public cache(): boolean
	/**
	 * Enable or disable the cache.
	 * @param enableCache Whether the cache should be enabled.
	 */
	public cache(enableCache: boolean): this
	public cache(enableCache?: boolean) {
		if (undefined === enableCache) {
			return !!this._cache
		}
		this._cache = enableCache ? this._cache || new Map() : undefined
		return this
	}

	/**
	 * Get the current gamma value. Defaults to 1.
	 */
	public gamma(): number
	/**
	 * Set the gamma value. Gamma-correction can be used to "shift" a scale's center more the the beginning (gamma <
	 * 1) or end (gamma > 1), typically used to "even" the lightness gradient. Default is 1.
	 * @example chroma.scale('YlGn').gamma(0.5)
	 * @example chroma.scale('YlGn').gamma(1)
	 * @example chroma.scale('YlGn').gamma(2)
	 */
	public gamma(gamma: number): this
	public gamma(gamma?: number) {
		if (undefined === gamma) {
			return this._gamma
		}
		this._gamma = gamma
		return this
	}

	/**
	 * @ignore
	 */
	public _at(t: number) {
		const c = this._color(t)
		return this._out ? c[this._out]() : c
	}

	/**
	 * @ignore
	 */
	public _init(colorsOrFunction: Color[] | ((t: number) => Color)) {
		this._colors = colorsOrFunction
		if ("function" != typeof colorsOrFunction) {
			this._pos = colorsOrFunction.map((_, i) => i / (colorsOrFunction.length - 1))
		}
		this._mode = "rgb"
		this.domain(0, 1)
		this._paddingLeft = 0
		this._paddingRight = 0
		this._correctLightness = false
		this._cache = new Map()
		this._gamma = 1
	}

	private _getClass(value: number) {
		return this._classes!.findIndex((cls) => value <= cls) - 1
	}

	private _color(val: number, bypassMap = false) {
		let t
		if (!bypassMap) {
			const min = this._min,
				max = this._max
			if (this._classes && this._classes.length > 2) {
				const c = this._getClass(val)
				t = c / (this._classes.length - 2)
			} else if (max !== min) {
				t = (val - min) / (max - min)
			} else {
				t = 1
			}
			if (this._correctLightness) {
				t = this._tCorrectedLightness(t)
			}
		} else {
			t = val
		}
		t = t ** this._gamma
		t = (this._paddingLeft + t) / (1 + this._paddingLeft + this._paddingRight)
		//	t = this._paddingLeft + t * (1 - this._paddingLeft - this._paddingRight)
		t = clamp(t, 0, 1)
		const tHash = t
		const cacheResult = this._cache && this._cache.get(tHash)
		if (cacheResult) {
			return cacheResult
		} else {
			let col: Color
			if (Array.isArray(this._colors)) {
				for (let i = 0; i < this._pos!.length; i++) {
					const p = this._pos![i]
					if (t <= p) {
						col = this._colors[i]
						break
					}
					if (t >= p && i == this._pos!.length - 1) {
						col = this._colors[i]
						break
					}
					if (t > p && t < this._pos![i + 1]) {
						t = (t - p) / (this._pos![i + 1] - p)
						col = mix(this._colors[i], this._colors[i + 1], t, this._mode)
						break
					}
				}
			} else {
				col = this._colors(t)
			}
			if (this._cache) {
				// tslint:disable-next-line
				this._cache.set(tHash, col!)
			}
			// tslint:disable-next-line
			return col!
		}
	}
	private _tCorrectedLightness(t0_1: number) {
		const L0 = this._color(0, true).lab()[0]
		const L1 = this._color(1, true).lab()[0]
		const L_ideal = lerp(L0, L1, t0_1)
		return bisect((t) => this._color(t, true).lab()[0] - L_ideal, 0, 1, 8)
	}
	private _resetCache() {
		if (this._cache) this._cache.clear()
	}
}

export namespace scales {
	/**
	 * @example chroma.scales.cool()
	 */
	export function cool() {
		return scale([hsl(180, 1, 0.9), hsl(250, 0.7, 0.4)])
	}

	/**
	 * @example chroma.scales.hot()
	 */
	export function hot() {
		return scale(["#000", "#f00", "#ff0", "#fff"]).mode("rgb")
	}
}

/**
 * Computes the WCAG contrast ratio between two colors. A minimum contrast of 4.5:1
 * [is recommended](http://www.w3.org/TR/WCAG20-TECHS/G18.html) to ensure that text is still readable against a
 * background color.
 *
 * @param a
 * @param b
 */
export function contrast(a: Chromable, b: Chromable) {
	const l1 = color(a).luminance()
	const l2 = color(b).luminance()
	if (l1 > l2) {
		return (l1 + 0.05) / (l2 + 0.05)
	} else {
		return (l2 + 0.05) / (l1 + 0.05)
	}
}

/**
 * Compute the [euclidean distance](https://en.wikipedia.org/wiki/Euclidean_distance#Three_dimensions) between two
 * colors in a given color space.
 * @param a First color.
 * @param b Second color.
 * @param mode The color space in which to compute the distance. Defaults to "lab".
 * @example chroma.distance('#fff', '#ff0', 'rgb')
 * @example chroma.distance('#fff', '#f0f', 'rgb')
 * @example chroma.distance('#fff', '#ff0')
 * @example chroma.distance('#fff', '#f0f')
 */
export function distance(a: Chromable, b: Chromable, mode: ColorMode = "lab") {
	const l1 = color(a)[mode]()
	const l2 = color(b)[mode]()
	const channelDifferences = l1.map((channelValue: number, channelIndex: number) => channelValue - l2[channelIndex])
	return hypot(...channelDifferences)
}

/**
 * Computes color difference as developed by the Colour Measurement Committee of the Society of Dyers and Colourists
 * (CMC) in 1984. The implementation is adapted from Bruce Lindbloom. The parameters L and C are weighting factors
 * for lightness and chromaticity.
 * @param reference
 * @param sample
 * @param L
 * @param C
 * @example [r = '#ededee', s = '#edeeed', chroma.deltaE(r, s)]
 * @example [r = '#ececee', s = '#eceeec', chroma.deltaE(r, s)]
 * @example [r = '#e9e9ee', s = '#e9eee9', chroma.deltaE(r, s)]
 * @example [r = '#e4e4ee', s = '#e4eee4', chroma.deltaE(r, s)]
 * @example [r = '#e0e0ee', s = '#e0eee0', chroma.deltaE(r, s)]
 */
export function deltaE(reference: Chromable, sample: Chromable, L = 1, C = 1) {
	const [L1, a1, b1] = color(reference).lab()
	const [L2, a2, b2] = color(sample).lab()
	const c1 = sqrt(a1 * a1 + b1 * b1)
	const c2 = sqrt(a2 * a2 + b2 * b2)
	const sl = L1 < 16.0 ? 0.511 : (0.040975 * L1) / (1.0 + 0.01765 * L1)
	const sc = (0.0638 * c1) / (1.0 + 0.0131 * c1) + 0.638
	const h1 = norm360(c1 < 0.000001 ? 0.0 : atan2(b1, a1) * RAD2DEG)
	const t =
		h1 >= 164.0 && h1 <= 345.0
			? 0.56 + abs(0.2 * cos((h1 + 168.0) * DEG2RAD))
			: 0.36 + abs(0.4 * cos((h1 + 35.0) * DEG2RAD))
	const c4 = c1 * c1 * c1 * c1
	const f = sqrt(c4 / (c4 + 1900.0))
	const sh = sc * (f * t + 1.0 - f)
	const delL = L1 - L2
	const delC = c1 - c2
	const delA = a1 - a2
	const delB = b1 - b2
	const dH2 = delA * delA + delB * delB - delC * delC
	const v1 = delL / (L * sl)
	const v2 = delC / (C * sc)
	const v3 = sh
	return sqrt(v1 * v1 + v2 * v2 + dH2 / (v3 * v3))
}
type DataInfo = {
	min: number
	max: number
	sum: number
	values: number[]
	count: number
	domain: [number, number]
	limits(mode: LimitsMode, num: number): number[]
}
export function analyze(data: number[]): DataInfo {
	const r: DataInfo = {
		min: Infinity,
		max: -Infinity,
		sum: 0,
		values: [] as number[],
		count: 0,
	} as DataInfo
	function add(val: number) {
		if (val != undefined && !isNaN(val)) {
			r.values.push(val)
			r.sum += val
			if (val < r.min) r.min = val
			if (val > r.max) r.max = val
			r.count += 1
		}
	}
	data.forEach((val) => add(val))
	r.domain = [r.min, r.max]
	r.limits = function (mode, num) {
		return limits(this, mode, num)
	}
	return r
}

type LimitsMode = "c" | "e" | "q" | "l" | "k"
export function limits(data: number[] | DataInfo, mode: LimitsMode = "e", num = 7): number[] {
	const info = Array.isArray(data) ? analyze(data) : data
	const { min, max, values } = info
	values.sort((a, b) => a - b)
	if (num == 1) {
		return [min, max]
	}
	if (mode.startsWith("c")) {
		return [min, max]
	} else if (mode.startsWith("e")) {
		return Array.from({ length: num + 1 }, (_, i) => lerp(min, max, i / num))
	} else if (mode.startsWith("l")) {
		if (min <= 0) {
			throw new Error("Logarithmic scales are only possible for values > 0")
		}
		const min_log = Math.LOG10E * log(min)
		const max_log = Math.LOG10E * log(max)
		return Array.from({ length: num + 1 }, (_, i) => 10 ** lerp(min_log, max_log, i / num))
	} else if (mode.startsWith("q")) {
		return Array.from({ length: num + 1 }, (_, i) => {
			const p = ((values.length - 1) * i) / num
			const pb = floor(p)
			return pb == p ? values[pb] : lerp(values[pb], values[pb + 1], p - pb)
		})
	} else if (mode.startsWith("k")) {
		// implementation based on
		// http://code.google.com/p/figue/source/browse/trunk/figue.js#336
		// simplified for 1-d input values
		const n = values.length
		const assignments = new Array(n)
		const clusterSizes = new Array(num)
		let repeat = true
		let nb_iters = 0
		let centroids = Array.from({ length: num + 1 }, (_, i) => lerp(min, max, i / num))
		do {
			// assignment step
			clusterSizes.fill(0)
			for (let i = 0; i < values.length; i++) {
				const value = values[i]
				const minDistIndex = indexOfMax(centroids, (c) => -abs(c - value))
				clusterSizes[minDistIndex]++
				assignments[i] = minDistIndex
			}

			// update centroids step
			const newCentroids = new Array(num).fill(0)
			for (let i = 0; i < assignments.length; i++) {
				const cluster = assignments[i]
				newCentroids[cluster] += values[i]
			}
			for (let j = 0; j < newCentroids.length; j++) {
				newCentroids[j] /= clusterSizes[j]
			}

			// check convergence
			repeat = newCentroids.some((nc, j) => nc != centroids[j])
			centroids = newCentroids
		} while (nb_iters++ < 200 && repeat)

		// finished k-means clustering
		// the next part is borrowed from gabrielflor.it
		const kClusters: number[][] = Array.from({ length: num }, () => [])
		for (let i = 0; i < assignments.length; i++) {
			const cluster = assignments[i]
			kClusters[cluster].push(values[i])
		}
		const tmpKMeansBreaks = []
		for (const kCluster of kClusters) {
			tmpKMeansBreaks.push(kCluster[0], kCluster[kCluster.length - 1])
		}
		tmpKMeansBreaks.sort((a, b) => a - b)

		const limits = []
		limits.push(tmpKMeansBreaks[0])
		for (let i = 1; i < tmpKMeansBreaks.length; i += 2) {
			const v = tmpKMeansBreaks[i]
			if (!isNaN(v) && limits.indexOf(v) == -1) {
				limits.push(v)
			}
		}
		return limits
	} else {
		throw new Error("unknown mode")
	}
}

const interpolators: { [K in InterpolationMode]?: (color1: Color, color2: Color, f: number, mode: K) => Color } = {}

// const _guess_formats: { p: number; test: (args: any[]) => ColorFormat | undefined }[] = []
const _input: {
	[mode: string]: (...args: any[]) => RGB | RGBA
} = {}

function linear_interpolator(col1: Color, col2: Color, f: number, m: ColorMode) {
	const xyz1 = col1[m]()
	const xyz2 = col2[m]()
	return guess(
		[
			lerp(xyz1[0], xyz2[0], f),
			lerp(xyz1[1], xyz2[1], f),
			lerp(xyz1[2], xyz2[2], f),
			lerp(col1.alpha(), col2.alpha(), f),
		],
		m,
	)
}
interpolators.xyz = interpolators.rgb = interpolators.lab = linear_interpolator

interpolators.num = function (col1, col2, f) {
	const n1 = col1.num()
	const n2 = col2.num()
	return num(lerp(n1, n2, f))
}

interpolators.lrgb = function (col1, col2, f) {
	const [r1, g1, b1, a1] = col1.rgba(false, false)
	const [r2, g2, b2, a2] = col2.rgba(false, false)
	return new Color(
		sqrt(r1 ** 2 * (1 - f) + r2 ** 2 * f),
		sqrt(g1 ** 2 * (1 - f) + g2 ** 2 * f),
		sqrt(b1 ** 2 * (1 - f) + b2 ** 2 * f),
		lerp(a1, a2, f),
	)
}

function _bezier(chromables: Chromable[]): (t: number) => Color {
	const colors = chromables.map((c) => color(c))
	const [lab0, lab1, lab2, lab3] = colors.map((c) => c.lab())
	if (2 == chromables.length) {
		// linear interpolation
		return (t) => {
			return lab([0, 1, 2].map((i) => lerp(lab0[i], lab1[i], t)) as LAB)
		}
	} else if (3 == chromables.length) {
		// quadratic bezier interpolation
		const bezier2 = (p0: number, p1: number, p2: number, t: number) =>
			(1 - t) ** 2 * p0 + 2 * (1 - t) * t * p1 + t ** 2 * p2
		return (t) => lab([0, 1, 2].map((i) => bezier2(lab0[i], lab1[i], lab2[i], t)) as LAB)
	} else if (4 == chromables.length) {
		// cubic bezier interpolation
		const bezier3 = (p0: number, p1: number, p2: number, p3: number, t: number) =>
			(1 - t) ** 3 * p0 + 3 * (1 - t) ** 2 * t * p1 + 3 * (1 - t) * t ** 2 * p2 + t ** 3 * p3
		return (t) => lab([0, 1, 2].map((i) => bezier3(lab0[i], lab1[i], lab2[i], lab3[i], t)) as LAB)
	} else if (5 == chromables.length) {
		const I0 = _bezier(colors.slice(0, 3))
		const I1 = _bezier(colors.slice(2, 5))
		return (t) => (t < 0.5 ? I0(t * 2) : I1((t - 0.5) * 2))
	} else throw new Error()
}

function guess(args: any[], mode?: ColorFormat): Color {
	if (Array.isArray(args[0])) args = args[0]
	if (!mode) {
		if (args.length == 1 && args[0] in w3cx11) {
			mode = "name"
		} else if (args.length == 1 && "string" == typeof args[0]) {
			mode = "css"
		} else if (args.length == 3) {
			mode = "rgb"
		} else if (args.length == 4 && "number" == typeof args[3] && args[3] >= 0 && args[3] <= 1) {
			mode = "rgb"
		} else if (args.length == 1 && "number" == typeof args[0] && args[0] >= 0 && args[0] <= 0xffffff) {
			mode = "num"
		} else throw new Error("could not guess mode. args " + JSON.stringify(args))
	}
	const channels = _input[mode](...args)
	return new Color(channels[0], channels[1], channels[2], undefined !== channels[3] ? channels[3] : 1)
}

function _average_lrgb(colors: Color[]) {
	let rSquareSum = 0,
		gSquareSum = 0,
		bSquareSum = 0,
		alphaSum = 0
	for (const col of colors) {
		const [r, g, b, alpha] = col.rgba(false, false)
		rSquareSum += r ** 2
		gSquareSum += g ** 2
		bSquareSum += b ** 2
		alphaSum += alpha
	}
	return new Color(
		sqrt(rSquareSum) / colors.length,
		sqrt(gSquareSum) / colors.length,
		sqrt(bSquareSum) / colors.length,
		alphaSum / colors.length,
	)
}

function hex2rgb(hex: string): RGBA {
	let m
	if ((m = hex.match(/^#?([A-F\d]{2})([A-F\d]{2})([A-F\d]{2})([A-F\d]{2})?$/i))) {
		return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16), m[4] ? parseInt(m[4], 16) / 255 : 1]
	} else if ((m = hex.match(/^#?([A-F\d])([A-F\d])([A-F\d])([A-F\d])?$/i))) {
		return [
			parseInt(m[1], 16) * 0x11,
			parseInt(m[2], 16) * 0x11,
			parseInt(m[3], 16) * 0x11,
			m[4] ? (parseInt(m[4], 16) * 0x11) / 255 : 1,
		]
	}
	throw new Error("invalid hex color: " + hex)
}
// color mode, i.e. representation as array of number
export type ColorMode = "rgb" | "cmyk" | "lab" | "hsv" | "hsi" | "hcg" | "hsl" | "gl" | "lch" | "xyz"
export type InterpolationMode = "rgb" | "lab" | "hsv" | "hsi" | "hcg" | "hsl" | "lch" | "xyz" | "lrgb" | "num"
export type ColorFormat = ColorMode | "hex" | "num" | "name" | "kelvin" | "css"
/**
 * CMYK color space
 * @see https://en.wikipedia.org/wiki/cmyk_color_model
 * [cyan, magenta, yellow, alpha1]
 */
type CMYK = [number, number, number, number]
/**
 * WebGL colors. Like RGB, but floats in [0; 1] instead of 0-255.
 * [red255, green255, blue255, alpha1]
 */
type GL = [number, number, number, number]
/**
 * Red, green, blue ranging from 0-255.
 * Can be floats and outside the above range internally.
 */
type RGB = [number, number, number]
/**
 * RGB plus alpha ranging from 0-1
 * [red255, green255, blue255, alpha1]
 */
type RGBA = [number, number, number, number]
/**
 * CIELAB color space
 * [lightness, A*, B*]
 * @see https://en.wikipedia.org/wiki/CIELAB_color_space
 */
type LAB = [number, number, number]
/**
 * Cylindrical representation of CIELAB
 * @see https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
 *
 */
type LCH = [number, number, number]
/**
 * @see https://en.wikipedia.org/wiki/HSL_and_HSV
 * [hueDegrees, saturation1, lightness1]
 */
type HSL = [number, number, number]
/**
 * @see https://en.wikipedia.org/wiki/HSL_and_HSV
 * [hueDegrees, saturation1, value1]
 */
type HSV = [number, number, number]
/**
 * @see https://en.wikipedia.org/w/index.php?title=HSI_color_space&redirect=no
 * [hueDegrees, saturation1, intensity1]
 */
type HSI = [number, number, number]
type XYZ = [number, number, number]
/**
 * HCG Color Model
 * See https://github.com/acterhd/hcg-color
 * A color value in the HCG format is an array of three numbers [h, c, g], where
 * `h` is the hue as an angle in degrees [0; 360)
 * `c` is the chroma value in [0; 1]
 * `g` is the gray value in [0; 1]
 */
type HCG = [number, number, number]

function rgb2hex(r255: number, g255: number, b255: number, a1: number, mode: "rgb" | "rgba" | "argb" = "rgb") {
	r255 = clamp(round(r255), 0, 255)
	g255 = clamp(round(g255), 0, 255)
	b255 = clamp(round(b255), 0, 255)
	const rgb = (r255 << 16) | (g255 << 8) | b255
	const rgbString = rgb.toString(16).padStart(6, "0")
	const alphaString = round(clamp(a1) * 255)
		.toString(16)
		.padStart(2, "0")
	return "#" + ("argb" == mode ? alphaString + rgbString : "rgba" == mode ? rgbString + alphaString : rgbString)
}

_input.lrgb = _input.rgb

_input.hex = hex2rgb
_input.hsl = hsl2rgb
function norm360(degrees: number) {
	return ((degrees % 360) + 360) % 360
}

_input.hsv = hsv2rgb

function num2rgb(num: number): RGBA {
	if (!("number" == typeof num && num >= 0 && num <= 0xffffff)) {
		throw new Error("unknown num color: " + num)
	}
	const r = num >> 16
	const g = (num >> 8) & 0xff
	const b = num & 0xff
	return [r, g, b, 1]
}

function rgb2num(r255: number, g255: number, b255: number, a1: number = 1, mode: "rgb" | "rgba" | "argb" = "rgb") {
	const rgbNum = (round(r255) << 16) | (round(g255) << 8) | round(b255)
	if ("rgb" === mode) {
		return rgbNum
	} else if ("rgba" === mode) {
		return (rgbNum << 8) | (round(a1 * 255) << 24)
	} else {
		return (round(a1 * 255) << 24) | rgbNum
	}
}

_input.num = num2rgb

const WS = "\\s*"
const FLOAT = "([+-]?(?:\\d*\\.?)?\\d+(?:[eE][+-]?\\d+)?)"
const CSS_RGB_REGEX = new RegExp(
	["^rgba?\\(", FLOAT, ",", FLOAT, ",", FLOAT, "(?:,", FLOAT + "(%)?", ")?\\)$"].join(WS),
	"i",
)
const CSS_RGB_WS_REGEX = new RegExp(["^rgba?\\(", FLOAT, FLOAT, FLOAT, "(?:/", FLOAT + "(%)?", ")?\\)$"].join(WS), "i")
const CSS_RGB_PERCENT_REGEX = new RegExp(
	["^rgba?\\(", FLOAT + "%", ",", FLOAT + "%", ",", FLOAT + "%", "(?:,", FLOAT + "(%)?", ")?\\)$"].join(WS),
	"i",
)
const CSS_RGB_WS_PERCENT_REGEX = new RegExp(
	["^rgba?\\(", FLOAT + "%", FLOAT + "%", FLOAT + "%", "(?:/", FLOAT + "(%)?", ")?\\)$"].join(WS),
	"i",
)
const CSS_HSL_REGEX = new RegExp(
	["^hsla?\\(", FLOAT + "(deg|rad|turn)?", ",", FLOAT + "%", ",", FLOAT + "%", "(?:,", FLOAT + "(%)?", ")?\\)$"].join(
		WS,
	),
	"i",
)
const CSS_HSL_WS_REGEX = new RegExp(
	["^hsla?\\(", FLOAT + "(deg|rad|turn)?\\s+" + FLOAT + "%", FLOAT + "%", "(?:/", FLOAT + "(%)?", ")?\\)$"].join(WS),
	"i",
)
function css2rgb(css: string): RGBA {
	if (w3cx11 && w3cx11.hasOwnProperty(css)) {
		return num2rgb((w3cx11 as any)[css.toLowerCase()])
	}
	let m
	if ((m = css.match(CSS_RGB_REGEX) || css.match(CSS_RGB_WS_REGEX))) {
		return [
			clamp(+m[1], 0, 255),
			clamp(+m[2], 0, 255),
			clamp(+m[3], 0, 255),
			m[4] ? clamp(m[5] ? +m[4] / 100 : +m[4]) : 1,
		]
	} else if ((m = css.match(CSS_RGB_PERCENT_REGEX) || css.match(CSS_RGB_WS_PERCENT_REGEX))) {
		return [
			clamp(+m[1] / 100) * 255,
			clamp(+m[2] / 100) * 255,
			clamp(+m[3] / 100) * 255,
			m[4] ? clamp(m[5] ? +m[4] / 100 : +m[4]) : 1,
		]
	} else if ((m = css.match(CSS_HSL_REGEX) || css.match(CSS_HSL_WS_REGEX))) {
		const CONVERSION = { deg: 1, rad: RAD2DEG, turn: 360 }
		const angleUnit = (m[2] ? m[2].toLowerCase() : "deg") as "deg" | "rad" | "turn"
		return hsl2rgb(
			(((+m[1] * CONVERSION[angleUnit]) % 360) + 360) % 360,
			clamp(+m[3] / 100),
			clamp(+m[4] / 100),
			m[5] ? clamp(m[6] ? +m[5] / 100 : +m[5]) : 1,
		)
	} else {
		return hex2rgb(css)
	}
}

function rgb2css(r: number, g: number, b: number, a = 1) {
	if (a >= 1) {
		return "rgb(" + [r, g, b].map(round).join(",") + ")"
	} else {
		return "rgba(" + [r, g, b].map(round).join(",") + "," + a + ")"
	}
}

function rnd(a: any) {
	return round(a * 100) / 100
}

function hsl2css([h, s, l]: HSL, alpha: number) {
	const mode = alpha < 1 ? "hsla" : "hsl"
	return (
		mode +
		"(" +
		rnd(h) +
		"," +
		rnd(s * 100) +
		"%" +
		"," +
		rnd(l * 100) +
		"%" +
		("hsla" == mode ? "," + rnd(alpha) : "") +
		")"
	)
}

_input.css = css2rgb

_input.name = function (name) {
	return num2rgb((w3cx11 as any)[name])
}

function lch2lab(l: number, c: number, hueDegrees: number): LAB {
	/*
    Convert from a qualitative parameter h and a quantitative parameter l to a 24-bit pixel.
    These formulas were invented by David Dalrymple to obtain maximum contrast without going
    out of gamut if the parameters are in the range 0-1.

    A saturation multiplier was added by Gregor Aisch
     */
	return [l, cos(hueDegrees * DEG2RAD) * c, sin(hueDegrees * DEG2RAD) * c]
}

function lch2rgb(l: number, c: number, hDegrees: number, alpha1 = 1): RGBA {
	const [, a, b] = lch2lab(l, c, hDegrees)
	return cielab2rgb(l, a, b, alpha1)
}

function lab2lch(l: number, a: number, b: number): LCH {
	const c = hypot(a, b)
	const h = (atan2(b, a) * RAD2DEG + 360) % 360
	return [l, c, h]
}

function rgb2lch(r255: number, g255: number, b255: number) {
	const [l, a, b2] = rgb2lab(r255, g255, b255)
	return lab2lch(l, a, b2)
}

_input.lch = lch2rgb

function rgb2cmyk(r255: number, g255: number, b255: number): CMYK {
	r255 /= 255
	g255 /= 255
	b255 /= 255
	const k = 1 - max(r255, g255, b255)
	if (1 == k) return [0, 0, 0, 1]
	const c = (1 - r255 - k) / (1 - k)
	const m = (1 - g255 - k) / (1 - k)
	const y = (1 - b255 - k) / (1 - k)
	return [c, m, y, k]
}

function cmyk2rgb(c1: number, m1: number, y1: number, k1: number, alpha1 = 1): RGBA {
	if (k1 == 1) {
		return [0, 0, 0, alpha1]
	}
	const r255 = 255 * (1 - c1) * (1 - k1)
	const g255 = 255 * (1 - m1) * (1 - k1)
	const b255 = 255 * (1 - y1) * (1 - k1)
	return [r255, g255, b255, alpha1]
}

_input.cmyk = cmyk2rgb

_input.gl = function (r: number, g: number, b: number, a: number = 1): RGBA {
	return [r * 255, g * 255, b * 255, a]
}

//function rgb2luminance(r: number, g: number, b: number) {
//	// https://en.wikipedia.org/wiki/Relative_luminance
//	const [, Y] = rgb2xyz(r, g, b)
//	return Y
//}

function rgbChannel2RgbLinear(x255: number) {
	const x1 = x255 / 255
	// http://entropymine.com/imageworsener/srgbformula/
	if (x1 <= 0.04045) {
		return x1 / 12.92
	} else {
		return ((x1 + 0.055) / 1.055) ** 2.4
	}
}
function rgbLinearChannel2Rgb(xLinear1: number) {
	if (xLinear1 <= 0.0031308) {
		return 255 * (12.92 * xLinear1)
	} else {
		return 255 * ((1 + 0.055) * xLinear1 ** (1 / 2.4) - 0.055)
	}
}

function kelvin2rgb(kelvin: number): RGB {
	const t = kelvin / 100
	let r, g, b
	if (t < 66) {
		r = 255
		g = -155.25485562709179 - 0.44596950469579133 * (t - 2) + 104.49216199393888 * log(t - 2)
		b = t < 20 ? 0 : -254.76935184120902 + 0.8274096064007395 * (t - 10) + 115.67994401066147 * log(t - 10)
	} else {
		r = 351.97690566805693 + 0.114206453784165 * (t - 55) - 40.25366309332127 * log(t - 55)
		g = 325.4494125711974 + 0.07943456536662342 * (t - 50) - 28.0852963507957 * log(t - 50)
		b = 255
	}
	return [r, g, b]
}

_input.rgb = (...args: number[]) => args as RGB

function rgb2kelvin(r255: number, g255: number, b255: number) {
	console.log(b255 - r255)
	if (g255 + b255 < 158.61) {
		console.log("0 < t < 20")
		// calc from green
		return round(
			newtonIterate1d(
				(t) => g255 - (-155.25485562709179 - 0.44596950469579133 * (t - 2) + 104.49216199393888 * log(t - 2)),
				15,
				4,
			) * 100,
		)
		return (Math.E ** ((g255 + 155.25485562709179 + 0.44596950469579133 * (15 - 2)) / 104.49216199393888) + 2) * 100
	} else if (b255 - r255 < 0) {
		console.log("20 < t < 66")
		return round(
			newtonIterate1d(
				(t) => b255 - (-254.76935184120902 + 0.8274096064007395 * (t - 10) + 115.67994401066147 * log(t - 10)),
				43,
				4,
			) * 100,
		)
		return (
			(Math.E ** ((b255 + 254.76935184120902 - 0.8274096064007395 * (43 - 10)) / 115.67994401066147) + 10) * 100
		)
	} else {
		console.log("0 < t < 400, start= " + (-1.4 * (r255 + g255) + 755))
		return round(
			newtonIterate1d(
				(t) => r255 - (351.97690566805693 + 0.114206453784165 * (t - 55) - 40.25366309332127 * log(t - 55)),
				-1.4 * (r255 + g255) + 755,
				8,
			) * 100,
		)
		return ((r255 / 329.698727446) ** (1 / -0.1332047592) + 60) * 100
	}
	return newtonIterate1d(
		(k) => {
			const eps = 1e-9
			const [kr, kg, kb] = kelvin2rgb(k)
			const [kr2, kg2, kb2] = kelvin2rgb(k + eps)
			const dkr = (kr2 - kr) / eps,
				dkg = (kg2 - kg) / eps,
				dkb = (kb2 - kb) / eps
			return dkr * (kr - r255) + dkg * (kg - g255) + dkb * (kb - b255)

			return kb / kr - b255 / r255
		},
		//1000,
		//40000,
		Math.E ** ((b255 / r255 + 2.5) / 0.4),
		20,
		//1e-6,
	)
	let maxTemp = 40000
	let minTemp = 1000
	const eps = 0.4
	let temp: number = 0
	let rgb
	while (maxTemp - minTemp > eps) {
		temp = (maxTemp + minTemp) * 0.5
		rgb = kelvin2rgb(temp)
		if (rgb[2] / rgb[0] >= b255 / r255) {
			maxTemp = temp
		} else {
			minTemp = temp
		}
	}
	return round(temp)
}

_input.temperature = _input.kelvin = _input.K = kelvin2rgb

function blend_f(f: (c1: RGB, c2: RGB) => RGB) {
	return function (bottom: Chromable, top: Chromable) {
		const [r, g, b] = f(color(top).rgb(), color(bottom).rgb())
		return rgb(r, g, b)
	}
}

function each(f: (a: number, b: number) => number) {
	return function (c1: RGB, c2: RGB): RGB {
		return c1.map((e, i) => f(e, c2[i])) as RGB
	}
}

function _screen(a: number, b: number) {
	return 255 * (1 - (1 - a / 255) * (1 - b / 255))
}

function _overlay(a: number, b: number) {
	if (b < 128) {
		return (2 * a * b) / 255
	} else {
		return 255 * (1 - 2 * (1 - a / 255) * (1 - b / 255))
	}
}

function _burn(a: number, b: number) {
	return 255 * (1 - (1 - b / 255) / (a / 255))
}

function _dodge(a: number, b: number) {
	if (a == 255) {
		return 255
	}
	return 255 * min(1, b / 255 / (1 - a / 255))
}

/**
 * r, g, b can be in any interval (0-1 or 0-255)
 * @param r
 * @param g
 * @param b
 */
function rgb2hexhue(r: number, g: number, b: number) {
	const m = min(r, g, b)
	const M = max(r, g, b)
	const delta = M - m
	let hueTurnX6 // angle as value between 0 and 6
	if (0 == delta) {
		hueTurnX6 = 0
	} else if (r == M) {
		// second term to make sure the value is > 0
		hueTurnX6 = (g - b) / delta + (g < b ? 6 : 0)
	} else if (g == M) {
		hueTurnX6 = 2 + (b - r) / delta
	} else {
		hueTurnX6 = 4 + (r - g) / delta
	}
	return [hueTurnX6 * 60, m, M]
}
function hcxm2rgb(hueDegrees: number, c1: number, x1: number, m1: number, alpha1: number): RGBA {
	const m255 = m1 * 255
	const cm255 = c1 * 255 + m255
	const xm255 = x1 * 255 + m255
	if (hueDegrees < 60) {
		return [cm255, xm255, m255, alpha1]
	} else if (hueDegrees < 120) {
		return [xm255, cm255, m255, alpha1]
	} else if (hueDegrees < 180) {
		return [m255, cm255, xm255, alpha1]
	} else if (hueDegrees < 240) {
		return [m255, xm255, cm255, alpha1]
	} else if (hueDegrees < 300) {
		return [xm255, m255, cm255, alpha1]
	} else {
		return [cm255, m255, xm255, alpha1]
	}
}

/**
 * https://en.wikipedia.org/w/index.php?title=HSL_and_HSV&oldid=856714654#From_HSL
 */
function hsl2rgb(hueDegrees: number, s1: number, l1: number, alpha1 = 1): RGBA {
	hueDegrees = norm360(hueDegrees)
	const c1 = (1 - abs(2 * l1 - 1)) * s1
	return hcxm2rgb(hueDegrees, c1, c1 * (1 - abs(((hueDegrees / 60) % 2) - 1)), l1 - c1 / 2, alpha1)
}
function rgb2hsl(r255: number, g255: number, b255: number): HSL {
	const [hue, min1, max1] = rgb2hexhue(r255 / 255, g255 / 255, b255 / 255)
	const l1 = (max1 + min1) / 2
	let s1: number
	if (max1 == min1) {
		s1 = 0
	} else {
		s1 = l1 < 0.5 ? (max1 - min1) / (max1 + min1) : (max1 - min1) / (2 - max1 - min1)
	}
	return [hue, s1, l1]
}

function hsv2rgb(hueDegrees: number, s1: number, v1: number, alpha1: number = 1): RGBA {
	hueDegrees = norm360(hueDegrees)
	const c1 = v1 * s1
	return hcxm2rgb(hueDegrees, c1, c1 * (1 - abs(((hueDegrees / 60) % 2) - 1)), v1 - c1, alpha1)
}

function rgb2hsv(r255: number, g255: number, b255: number): HSV {
	const [hue, min255, max255] = rgb2hexhue(r255, g255, b255)
	const delta255 = max255 - min255
	const v1 = max255 / 255.0
	const s1 = max255 == 0 ? 0 : delta255 / max255
	return [hue, s1, v1]
}

function hcg2rgb(hueDegrees: number, c1: number, g1: number, alpha1 = 1): RGBA {
	hueDegrees = norm360(hueDegrees)
	const p = g1 * (1 - c1)
	return hcxm2rgb(hueDegrees, c1, c1 * (1 - abs(((hueDegrees / 60) % 2) - 1)), p, alpha1)
}

function rgb2hcg(r255: number, g255: number, b255: number): HCG {
	const [hue, min255, max255] = rgb2hexhue(r255, g255, b255)
	const c1 = (max255 - min255) / 255
	const _g1 = c1 < 1 ? min255 / 255 / (1 - c1) : 0
	return [hue, c1, _g1]
}

_input.hcg = hcg2rgb

function cielab2rgb(LStar100: number, aStar: number, bStar: number, alpha = 1): RGBA {
	const [x, y, z] = cielab2xyz(LStar100, aStar, bStar)
	return xyz2rgb(x, y, z, alpha)
}

function cielab2xyz(LStar100: number, aStar: number, bStar: number) {
	function fInv(t: number) {
		if (t > LAB_delta) {
			return t ** 3
		} else {
			return LAB_3DeltaPow2 * (t - 4 / 29)
		}
	}
	return [
		LAB_Xn * fInv((LStar100 + 16) / 116 + aStar / 500),
		LAB_Yn * fInv((LStar100 + 16) / 116),
		LAB_Zn * fInv((LStar100 + 16) / 116 - bStar / 200),
	]
}
function xyz2cielab(x: number, y: number, z: number): LAB {
	// https://en.wikipedia.org/w/index.php?title=CIELAB_color_space&oldid=849576085#Forward_transformation
	function f(t: number) {
		if (t > LAB_deltaPow3) {
			return cbrt(t)
		} else {
			return t / LAB_3DeltaPow2 + 4 / 29
		}
	}
	return [116 * f(y / LAB_Yn) - 16, 500 * (f(x / LAB_Xn) - f(y / LAB_Yn)), 200 * (f(y / LAB_Yn) - f(z / LAB_Zn))]
}

const LAB_Kn = 18
const LAB_Xn = 0.95047
const LAB_Yn = 1
const LAB_Zn = 1.08883
const LAB_delta = 0.206896552 // delta = 6 / 29
const LAB_3DeltaPow2 = 0.12841855 // 3 * delta ** 2
const LAB_deltaPow3 = 0.008856452 // delta ** 3

function rgb2lab(r255: number, g255: number, b255: number): RGB {
	const [x, y, z] = rgb2xyz(r255, g255, b255)
	return xyz2cielab(x, y, z)
}

function rgb2xyz(r255: number, g255: number, b255: number): XYZ {
	// https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
	const r1Linear = rgbChannel2RgbLinear(r255)
	const g1Linear = rgbChannel2RgbLinear(g255)
	const b1Linear = rgbChannel2RgbLinear(b255)
	const X = 0.4124564 * r1Linear + 0.3575761 * g1Linear + 0.1804375 * b1Linear
	const Y = 0.2126729 * r1Linear + 0.7151522 * g1Linear + 0.072175 * b1Linear
	const Z = 0.0193339 * r1Linear + 0.119192 * g1Linear + 0.9503041 * b1Linear
	return [X, Y, Z]
}
function xyz2rgb(X1: number, Y1: number, Z1: number, alpha1 = 1): RGBA {
	// https://en.wikipedia.org/wiki/SRGB#The_forward_transformation_(CIE_XYZ_to_sRGB)
	const r1Linear = 3.2404542 * X1 - 1.5371385 * Y1 - 0.4985314 * Z1
	const g1Linear = -0.969266 * X1 + 1.8760108 * Y1 + 0.041556 * Z1
	const b1Linear = 0.0556434 * X1 - 0.2040259 * Y1 + 1.0572252 * Z1
	return [rgbLinearChannel2Rgb(r1Linear), rgbLinearChannel2Rgb(g1Linear), rgbLinearChannel2Rgb(b1Linear), alpha1]
}

_input.xyz = xyz2rgb
_input.lab = cielab2rgb

/**
 * For HSI, we use the direct angle calculation. I.e. atan2(beta, alpha). See wikipedia link. This is why we don't use
 * hcxm2rgb.
 */
function hsi2rgb(hueDegrees: number, s1: number, i1: number, alpha1 = 1): RGBA {
	/*
    borrowed from here:
    http://hummer.stanford.edu/museinfo/doc/examples/humdrum/keyscape2/hsi2rgb.cpp
     */
	let r, g, b
	let hRad = hueDegrees * DEG2RAD
	if (hRad < (2 * PI) / 3) {
		b = (1 - s1) / 3
		r = (1 + (s1 * cos(hRad)) / cos(PI / 3 - hRad)) / 3
		g = 1 - (b + r)
	} else if (hRad < (4 * PI) / 3) {
		hRad -= (2 * PI) / 3
		r = (1 - s1) / 3
		g = (1 + (s1 * cos(hRad)) / cos(PI / 3 - hRad)) / 3
		b = 1 - (r + g)
	} else {
		hRad -= (4 * PI) / 3
		g = (1 - s1) / 3
		b = (1 + (s1 * cos(hRad)) / cos(PI / 3 - hRad)) / 3
		r = 1 - (g + b)
	}
	return [3 * i1 * r * 255, 3 * i1 * g * 255, 3 * i1 * b * 255, alpha1]
}

/**
 * For HSI, we use the direct angle calculation. I.e. atan2(beta, alpha). See wikipedia link. This is why we don't use
 * rgb2hexhue.
 */
function rgb2hsi(r255: number, g255: number, b255: number): HSI {
	// See https://en.wikipedia.org/wiki/HSL_and_HSV#Hue_and_chroma
	// See https://en.wikipedia.org/wiki/HSL_and_HSV#Lightness
	const r1 = r255 / 255
	const g1 = g255 / 255
	const b1 = b255 / 255
	const i1 = (r1 + g1 + b1) / 3
	if (r1 == g1 && g1 == b1) {
		return [0, 0, i1]
	} else {
		const alpha = (1 / 2) * (2 * r1 - g1 - b1)
		const beta = (sqrt(3) / 2) * (g1 - b1)
		const hRad = atan2(beta, alpha)
		const min1 = min(r1, g1, b1)
		const s1 = 1 - min1 / i1
		return [(hRad < 0 ? 2 * PI + hRad : hRad) * RAD2DEG, s1, i1]
	}
}

_input.hsi = hsi2rgb

interpolators.hsv = interpolators.hsl = interpolators.hsi = interpolators.lch = interpolators.hcg = function interpolate_hsx(
	color1: Color,
	color2: Color,
	f: number,
	m: "hsv" | "hsl" | "hsi" | "lch" | "hcg",
) {
	const [a1, b1, c1] = color1[m]()
	const [a2, b2, c2] = color2[m]()
	function lerpHue(hue1: number, hue2: number, f: number) {
		const dh = norm360(hue2 - hue1 + 180) - 180
		return hue1 + f * dh
	}
	return color(
		("h" == m.charAt(0) ? lerpHue : lerp)(a1, a2, f),
		lerp(b1, b2, f),
		("h" == m.charAt(2) ? lerpHue : lerp)(c1, c2, f),
		m,
	)
}

function indexOfMax<T>(arr: ArrayLike<T>, f: (el: T) => number) {
	let maxValue = -Infinity,
		maxValueIndex = -1
	for (let i = 0; i < arr.length; i++) {
		const value = f(arr[i])
		if (value > maxValue) {
			maxValue = value
			maxValueIndex = i
		}
	}
	return maxValueIndex
}
function withMax<T>(arr: ArrayLike<T>, f: (el: T) => number): T {
	return arr[indexOfMax(arr, f)]
}
