/**
 * @packageDocumentation
 * This module defines the main {@link Color} class as well as the types it uses.
 */
import { WHITES } from "./adapt.js";
import defaults from "./defaults.js";
import hooks from "./hooks.js";
import * as util from "./util.js";
import ColorSpace, { Ref } from "./ColorSpace.js";
import SpaceAccessors from "./space-coord-accessors.js";
import { ToGamutOptions, ParseOptions, TryColorOptions } from "./types.js";

import {
	to as toFn,
	parse,
	serialize,
	inGamut as inGamutFn,
	toGamut as toGamutFn,
	distance as distanceFn,
	equals as equalsFn,
	get,
	getAll as getAllFn,
	display,
} from "./index-fn.js";

import { uv, xy } from "./chromaticity.js";
import contrast from "./contrast.js";
import {
	contrastWCAG21,
	contrastAPCA,
	contrastMichelson,
	contrastWeber,
	contrastLstar,
	contrastDeltaPhi,
} from "./contrast/index.js";
import deltaE from "./deltaE.js";
import deltaEMethods, {
	deltaE76,
	deltaECMC,
	deltaE2000,
	deltaEJz,
	deltaEITP,
	deltaEOK,
} from "./deltaE/index.js";
import { range, Range, MixOptions, StepsOptions } from "./interpolation.js";
import { getLuminance } from "./luminance.js";
import { lighten, darken } from "./variations.js";

export type { SpaceAccessor } from "./space-coord-accessors.js";

export type Coords = [number | null, number | null, number | null];

export interface ColorObject {
	spaceId?: string | ColorSpace | undefined;
	space?: string | ColorSpace | undefined;
	coords: Coords;
	alpha?: number | undefined | null;
}

export interface PlainColorObject {
	space: ColorSpace;
	coords: Coords;
	alpha: number | null;
}

export interface ColorConstructor {
	spaceId: string;
	coords: Coords;
	alpha: number | undefined | null;
}

export type ColorTypes = ColorObject | ColorConstructor | string | PlainColorObject;

export type DefineFunctionCode = (...args: any[]) => any;

export interface DefineFunctionOptions {
	instance?: boolean | undefined;
	returns?: "color" | "function<color>" | "array<color>" | undefined;
}

export type DefineFunctionHybrid = DefineFunctionCode & DefineFunctionOptions;

/** Remove the first element of an array type */
type RemoveFirstElement<T extends any[]> = T extends [any, ...infer R] ? R : T[number][];

/** Convert a function to a prototype for Color */
export type ToColorPrototype<T extends (...args: any[]) => any> = T extends (
	color: Color,
	...args: infer A
) => infer R
	? T extends { returns: "color" }
		? (...args: A) => Color
		: (...args: A) => R
	: never;

/** Convert a function to a Color namespace property (returning a Color) */
export type ToColorNamespace<T extends (...args: any[]) => any> = T extends (
	...args: infer A
) => infer R
	? T extends { returns: "color" }
		? (...args: A) => Color
		: (...args: A) => R
	: never;

declare namespace Color {
	// Functions defined using Color.defineFunctions
	export const getAll: ToColorNamespace<typeof getAllFn>;
	export const to: ToColorNamespace<typeof toFn>;
	export const equals: ToColorNamespace<typeof equalsFn>;
	export const inGamut: ToColorNamespace<typeof inGamutFn>;
	export const distance: ToColorNamespace<typeof distanceFn>;
	// `get` is defined below as a static method on the Class,
	// and `toString` is intentionally not overridden for the namespace

	export { util, hooks, WHITES, ColorSpace as Space, parse, defaults };
	export const spaces: (typeof ColorSpace)["registry"];

	// Must be manually defined due to overloads
	// These should always match the signature of the original function
	// prettier-ignore
	export function set (
		color: ColorTypes,
		prop: Ref,
		value: number | ((coord: number) => number),
	): Color;
	// prettier-ignore
	export function set (
		color: ColorTypes,
		props: Record<string, number | ((coord: number) => number)>,
	): Color;
	// prettier-ignore
	export function setAll (color: ColorTypes, coords: Coords, alpha?: number): Color;
	// prettier-ignore
	export function setAll (
		color: ColorTypes,
		space: string | ColorSpace,
		coords: Coords,
		alpha?: number,
	): Color;
	// prettier-ignore
	export function toGamut (color: ColorTypes, options?: ToGamutOptions): Color;
	// prettier-ignore
	export function toGamut (color: ColorTypes, space?: string): Color;
}

/**
 * Class that represents a single color.
 * All of Color.js’s tree-shakeable methods are also available as instance methods on this class,
 * as well as static methods that take the color as the first argument.
 */
declare class Color extends SpaceAccessors implements PlainColorObject {
	constructor (color: ColorTypes);
	constructor (space: string | ColorSpace, coords: Coords, alpha?: number | null);

	// These signatures should always be the same as the constructor
	static get (color: ColorTypes, options?: ParseOptions): Color;
	static get (space: string | ColorSpace, coords: Coords, alpha: number | null): Color;

	static try (color: ColorTypes, options?: TryColorOptions): Color | null;

	static defineFunction (name: string, code: DefineFunctionHybrid): void;
	static defineFunction (
		name: string,
		code: DefineFunctionCode,
		options: DefineFunctionOptions,
	): void;

	static defineFunctions (objects: Record<string, DefineFunctionHybrid>): void;

	static extend (
		exports: { register: (color: typeof Color) => void } | Record<string, DefineFunctionHybrid>,
	): void;

	get space (): ColorSpace;
	get spaceId (): string;

	alpha: number;
	coords: Coords;

	clone (): this;

	// Copy parameter types from display function, except for the first one
	display (...args: RemoveFirstElement<Parameters<typeof display>>): string & { color: Color };

	toJSON (): ColorConstructor;

	// Functions defined using Color.defineFunctions
	get: ToColorPrototype<typeof get>;
	getAll: ToColorPrototype<typeof getAllFn>;
	to: ToColorPrototype<typeof toFn>;
	equals: ToColorPrototype<typeof equalsFn>;
	inGamut: ToColorPrototype<typeof inGamutFn>;
	distance: ToColorPrototype<typeof distanceFn>;
	toString: ToColorPrototype<typeof serialize>;

	// Must be manually defined due to overloads
	// These should always match the signature of the original function
	set (prop: Ref, value: number | ((coord: number) => number)): Color;
	set (props: Record<string, number | ((coord: number) => number)>): Color;
	setAll (coords: Coords, alpha?: number): Color;
	setAll (space: string | ColorSpace, coords: Coords, alpha?: number): Color;
	toGamut (options?: ToGamutOptions): Color;
	toGamut (space?: string): Color;

	/*
	 * ==========================================
	 * Types for properties defined in `index.js`
	 * ==========================================
	 */
	// chromaticity
	uv: ToColorPrototype<typeof uv>;
	xy: ToColorPrototype<typeof xy>;

	// contrast
	contrast: ToColorPrototype<typeof contrast>;
	static contrast: typeof contrast;

	// contrastMethods
	contrastWCAG21: ToColorPrototype<typeof contrastWCAG21>;
	contrastAPCA: ToColorPrototype<typeof contrastAPCA>;
	contrastMichelson: ToColorPrototype<typeof contrastMichelson>;
	contrastWeber: ToColorPrototype<typeof contrastWeber>;
	contrastLstar: ToColorPrototype<typeof contrastLstar>;
	contrastDeltaPhi: ToColorPrototype<typeof contrastDeltaPhi>;

	static contrastWCAG21: typeof contrastWCAG21;
	static contrastAPCA: typeof contrastAPCA;
	static contrastMichelson: typeof contrastMichelson;
	static contrastWeber: typeof contrastWeber;
	static contrastLstar: typeof contrastLstar;
	static contrastDeltaPhi: typeof contrastDeltaPhi;

	// deltaE
	deltaE: ToColorPrototype<typeof deltaE>;
	deltaE76: ToColorPrototype<typeof deltaE76>;
	deltaECMC: ToColorPrototype<typeof deltaECMC>;
	deltaE2000: ToColorPrototype<typeof deltaE2000>;
	deltaEJz: ToColorPrototype<typeof deltaEJz>;
	deltaEITP: ToColorPrototype<typeof deltaEITP>;
	deltaEOK: ToColorPrototype<typeof deltaEOK>;

	static deltaE: typeof deltaE;
	static deltaE76: typeof deltaE76;
	static deltaECMC: typeof deltaECMC;
	static deltaE2000: typeof deltaE2000;
	static deltaEJz: typeof deltaEJz;
	static deltaEITP: typeof deltaEITP;
	static deltaEOK: typeof deltaEOK;
	static deltaEMethods: typeof deltaEMethods;

	// interpolation
	// These signatures should always match those in interpolation.d.ts,
	// including the static versions
	/** Create color mixtures in any desired proportion between two colors */
	mix (color2: ColorTypes, options?: MixOptions): Color;
	mix (color2: ColorTypes, p: number, options?: MixOptions): Color;
	/**
	 * Creates a function that accepts a number and returns a color.
	 * For numbers in the range 0 to 1, the function interpolates;
	 * for numbers outside that range, the function extrapolates
	 * (and thus may not return the results you expect)
	 */
	range: ToColorPrototype<typeof range>;
	/** Get an array of discrete steps */
	steps (color2: ColorTypes, options?: StepsOptions): Color[];

	/** Create color mixtures in any desired proportion between two colors */
	static mix (color1: ColorTypes, color2: ColorTypes, options?: MixOptions): Color;
	static mix (color1: ColorTypes, color2: ColorTypes, p: number, options?: MixOptions): Color;
	/**
	 * Creates a function that accepts a number and returns a color.
	 * For numbers in the range 0 to 1, the function interpolates;
	 * for numbers outside that range, the function extrapolates
	 * (and thus may not return the results you expect)
	 */
	static range: typeof range;
	/** Get an array of discrete steps */
	static steps (color1: ColorTypes, color2: ColorTypes, options?: StepsOptions): Color[];
	static steps (range: Range, options?: StepsOptions): Color[];

	// luminance
	get luminance (): ReturnType<typeof getLuminance>;
	// the definition for this set in the orignial code like it doesn't actually use the parameter?
	set luminance (_: number);

	// variations
	lighten: ToColorPrototype<typeof lighten>;
	darken: ToColorPrototype<typeof darken>;
	static lighten: ToColorNamespace<typeof lighten>;
	static darken: ToColorNamespace<typeof darken>;
}

export default Color;
