export const colorChannels = ['red', 'green', 'blue', 'alpha'] as const
export type ColorChannel = typeof colorChannels[number]

export type Rgba = {
    [C in ColorChannel]: number
}
export type Rgb = Omit<Rgba, 'alpha'>

export const hslChannels = ['hue', 'saturation', 'lightness'] as const
export type HslChannel = typeof hslChannels[number]
export type CapitalizedHslChannel = Capitalize<HslChannel>

export type Hsla = {
    [C in HslChannel]: number
}
export type Hsl = Omit<Hsla, 'alpha'>

type ChannelTuple = [number, number, number, number]

export const colorsNammed = {
    transparent: '#00000000',
    black: '#000000',
    silver: '#c0c0c0',
    gray: '#808080',
    white: '#ffffff',
    maroon: '#800000',
    red: '#ff0000',
    purple: '#800080',
    fuchsia: '#ff00ff',
    green: '#008000',
    lime: '#00ff00',
    olive: '#808000',
    yellow: '#ffff00',
    navy: '#000080',
    blue: '#0000ff',
    teal: '#008080',
    aqua: '#00ffff',
    orange: '#ffa500',
    aliceblue: '#f0f8ff',
    antiquewhite: '#faebd7',
    aquamarine: '#7fffd4',
    azure: '#f0ffff',
    beige: '#f5f5dc',
    bisque: '#ffe4c4',
    blanchedalmond: '#ffebcd',
    blueviolet: '#8a2be2',
    brown: '#a52a2a',
    burlywood: '#deb887',
    cadetblue: '#5f9ea0',
    chartreuse: '#7fff00',
    chocolate: '#d2691e',
    coral: '#ff7f50',
    cornflowerblue: '#6495ed',
    cornsilk: '#fff8dc',
    crimson: '#dc143c',
    cyan: '#00ffff',
    darkblue: '#00008b',
    darkcyan: '#008b8b',
    darkgoldenrod: '#b8860b',
    darkgray: '#a9a9a9',
    darkgreen: '#006400',
    darkgrey: '#a9a9a9',
    darkkhaki: '#bdb76b',
    darkmagenta: '#8b008b',
    darkolivegreen: '#556b2f',
    darkorange: '#ff8c00',
    darkorchid: '#9932cc',
    darkred: '#8b0000',
    darksalmon: '#e9967a',
    darkseagreen: '#8fbc8f',
    darkslateblue: '#483d8b',
    darkslategray: '#2f4f4f',
    darkslategrey: '#2f4f4f',
    darkturquoise: '#00ced1',
    darkviolet: '#9400d3',
    deeppink: '#ff1493',
    deepskyblue: '#00bfff',
    dimgray: '#696969',
    dimgrey: '#696969',
    dodgerblue: '#1e90ff',
    firebrick: '#b22222',
    floralwhite: '#fffaf0',
    forestgreen: '#228b22',
    gainsboro: '#dcdcdc',
    ghostwhite: '#f8f8ff',
    gold: '#ffd700',
    goldenrod: '#daa520',
    greenyellow: '#adff2f',
    grey: '#808080',
    honeydew: '#f0fff0',
    hotpink: '#ff69b4',
    indianred: '#cd5c5c',
    indigo: '#4b0082',
    ivory: '#fffff0',
    khaki: '#f0e68c',
    lavender: '#e6e6fa',
    lavenderblush: '#fff0f5',
    lawngreen: '#7cfc00',
    lemonchiffon: '#fffacd',
    lightblue: '#add8e6',
    lightcoral: '#f08080',
    lightcyan: '#e0ffff',
    lightgoldenrodyellow: '#fafad2',
    lightgray: '#d3d3d3',
    lightgreen: '#90ee90',
    lightgrey: '#d3d3d3',
    lightpink: '#ffb6c1',
    lightsalmon: '#ffa07a',
    lightseagreen: '#20b2aa',
    lightskyblue: '#87cefa',
    lightslategray: '#778899',
    lightslategrey: '#778899',
    lightsteelblue: '#b0c4de',
    lightyellow: '#ffffe0',
    limegreen: '#32cd32',
    linen: '#faf0e6',
    magenta: '#ff00ff',
    mediumaquamarine: '#66cdaa',
    mediumblue: '#0000cd',
    mediumorchid: '#ba55d3',
    mediumpurple: '#9370db',
    mediumseagreen: '#3cb371',
    mediumslateblue: '#7b68ee',
    mediumspringgreen: '#00fa9a',
    mediumturquoise: '#48d1cc',
    mediumvioletred: '#c71585',
    midnightblue: '#191970',
    mintcream: '#f5fffa',
    mistyrose: '#ffe4e1',
    moccasin: '#ffe4b5',
    navajowhite: '#ffdead',
    oldlace: '#fdf5e6',
    olivedrab: '#6b8e23',
    orangered: '#ff4500',
    orchid: '#da70d6',
    palegoldenrod: '#eee8aa',
    palegreen: '#98fb98',
    paleturquoise: '#afeeee',
    palevioletred: '#db7093',
    papayawhip: '#ffefd5',
    peachpuff: '#ffdab9',
    peru: '#cd853f',
    pink: '#ffc0cb',
    plum: '#dda0dd',
    powderblue: '#b0e0e6',
    rosybrown: '#bc8f8f',
    royalblue: '#4169e1',
    saddlebrown: '#8b4513',
    salmon: '#fa8072',
    sandybrown: '#f4a460',
    seagreen: '#2e8b57',
    seashell: '#fff5ee',
    sienna: '#a0522d',
    skyblue: '#87ceeb',
    slateblue: '#6a5acd',
    slategray: '#708090',
    slategrey: '#708090',
    snow: '#fffafa',
    springgreen: '#00ff7f',
    steelblue: '#4682b4',
    tan: '#d2b48c',
    thistle: '#d8bfd8',
    tomato: '#ff6347',
    turquoise: '#40e0d0',
    violet: '#ee82ee',
    wheat: '#f5deb3',
    whitesmoke: '#f5f5f5',
    yellowgreen: '#9acd32',
    rebeccapurple: '#663399'
} as const

export type ColorName = keyof typeof colorsNammed

export class ColorTypeError extends Error {
    constructor() {
        super('ColorTypeError: type must be hex(a), rgb(a) or hsl(a)')
    }
}

class Color {
    // @ts-expect-error - TypeScript failed to inter the initialization of this property
    $channels: Uint8Array

    // Since getters and setters for the color channels, e.g., "alpha", are
    // dynamically defined with `Object.defineProperty` in the constructor, we
    // cannot write property declarations inside the class body. Instead, we
    // augment the `Color` class with an ambient module declared in `color.ts`.

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    constructor(...args: any[]) {
        if (args.length > 0) {
            return Color.parse(...args)
        }

        this.$channels = new Uint8Array(colorChannels.length)
    }

    get red(): number {
        return this.$channels[0]
    }

    set red(byte: number) {
        if (!Number.isNaN(byte / 1)) {
            this.$channels[0] = Math.min(255, Math.max(0, byte))
        }
    }

    get green(): number {
        return this.$channels[1]
    }

    set green(byte: number) {
        if (!Number.isNaN(byte / 1)) {
            this.$channels[1] = Math.min(255, Math.max(0, byte))
        }
    }

    get blue(): number {
        return this.$channels[2]
    }

    set blue(byte: number) {
        if (!Number.isNaN(byte / 1)) {
            this.$channels[2] = Math.min(255, Math.max(0, byte))
        }
    }

    get alpha(): number {
        return this.$channels[3]
    }

    set alpha(byte: number) {
        if (!Number.isNaN(byte / 1)) {
            this.$channels[3] = Math.min(255, Math.max(0, byte))
        }
    }

    get hue(): number {
        return this.getHue()
    }

    set hue(value: number) {
        if (!Number.isNaN(value / 1)) {
            this.setHue(value)
        }
    }

    get saturation(): number {
        return this.getSaturation()
    }

    set saturation(value: number) {
        if (!Number.isNaN(value / 1)) {
            this.setSaturation(value)
        }
    }

    get lightness(): number {
        return this.getLightness()
    }

    set lightness(value: number) {
        if (!Number.isNaN(value / 1)) {
            this.setLightness(value)
        }
    }

    getHue() {
        const [red, green, blue] = Array.from(this.$channels).map((c) => c / 255)
        const [min, max] = [Math.min(red, green, blue), Math.max(red, green, blue)]
        const delta = max - min
        let hue = 0

        if (delta === 0) {
            return hue
        }

        if (red === max) {
            hue = ((green - blue) / delta) % 6
        } else if (green === max) {
            hue = (blue - red) / delta + 2
        } else {
            hue = (red - green) / delta + 4
        }

        hue *= 60
        while (hue !== -Infinity && hue < 0) hue += 360

        return Math.round(hue % 360)
    }

    setHue(value: number) {
        const color = Color.fromHSL(value, this.saturation, this.lightness, this.alpha / 255)
        for (let i = 0; i < this.$channels.length; i++) {
            this.$channels[i] = Number(color.$channels[i])
        }
    }

    getSaturation() {
        const [red, green, blue] = Array.from(this.$channels).map((c) => c / 255)
        const [min, max] = [Math.min(red, green, blue), Math.max(red, green, blue)]
        const delta = max - min

        return delta !== 0
            ? Math.round(delta / (1 - Math.abs(2 * this.lightness - 1)) * 100) / 100
            : 0
    }

    setSaturation(value: number) {
        const color = Color.fromHSL(this.hue, value, this.lightness, this.alpha / 255)
        colorChannels.forEach((_, i) => (this.$channels[i] = color.$channels[i]))
    }

    getLightness() {
        const [red, green, blue] = Array.from(this.$channels).map((c) => c / 255)
        const [min, max] = [Math.min(red, green, blue), Math.max(red, green, blue)]

        return Math.round((max + min) / 2 * 100) / 100
    }

    setLightness(value: number) {
        const color = Color.fromHSL(this.hue, this.lightness, value, this.alpha / 255)
        colorChannels.forEach((_, i) => (this.$channels[i] = color.$channels[i]))
    }

    clone() {
        const color = new Color()
        colorChannels.forEach((_, i) => (color.$channels[i] = this.$channels[i]))

        return color
    }

    toString(type = 'hex') {
        switch (String(type).toLowerCase()) {
            case 'hex':
                return '#' + colorChannels.slice(0, 3)
                    .map((channel) => this[channel].toString(16).padStart(2, '0'))
                    .join('')
            case 'hexa':
                return '#' + colorChannels
                    .map((channel) => this[channel].toString(16).padStart(2, '0'))
                    .join('')
            case 'rgb':
                return `rgb(${this.red}, ${this.green}, ${this.blue})`
            case 'rgba':
                return `rgba(${this.red}, ${this.green}, ${this.blue}, ${Math.round(this.alpha / 2.55) / 100})`
            case 'hsl':
                return `hsl(${Math.round(this.hue)}deg, ${Math.round(this.saturation * 100)}%, ${Math.round(this.lightness * 100)}%)`
            case 'hsla':
                return `hsla(${Math.round(this.hue)}deg, ${Math.round(this.saturation * 100)}%, ${Math.round(this.lightness * 100)}%, ${Math.round(this.alpha / 2.55) / 100})`
            default:
                throw new ColorTypeError()
        }
    }

    get [Symbol.toStringTag]() {
        return this.toString('hex')
    }

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    static parse(...args: any[]): Color {
        if (typeof args[0] === 'object') {
            return Color.parseObject(args[0])
        } else if (args.every((arg) => !Number.isNaN(arg / 1))) {
            const color = new Color()
            if (args.length > 3) {
                color.red = args[0]
                color.green = args[1]
                color.blue = args[2]

                if (args[3]) {
                    color.alpha = args[3]
                }
            } else if (args.length === 1) {
                const index = Number(args[0])

                return Color.parseIndex(index, index > 2 ** 24 ? 3 : 4)
            }
        } else if (typeof args[0] === 'string') {
            let match = null

            if (typeof colorsNammed[args[0].toLowerCase() as ColorName] === 'string') {
                return Color.parseHex(colorsNammed[args[0].toLowerCase() as ColorName])
            } else if ((match = args[0].match(/^(#|&h|0x)?(([a-f0-9]{3,4}){1,2})$/i)) !== null) {
                return Color.parseHex(match[2])
            } else if ((match = args[0].match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(\s*,\s*(\d*\.?\d+))?\s*\)$/i)) !== null) {
                const channels = [
                    match[1],
                    match[2],
                    match[3],
                    typeof match[5] !== 'undefined'
                        ? match[5]
                        : 1
                ]

                return Color.fromRGB(...(channels.map((value) => Number(value)) as ChannelTuple))
            } else if ((args[0].match(/^(h(sl|wb)a?|lab|color|cmyk)\(/i))) {
                throw new Error('Color expression not implemented yet')
            }
        }

        throw new Error('Invalid color expression')
    }

    static parseObject(object: unknown) {
        const color = new Color()

        if (object === null || typeof object !== 'object') {
            return color
        }
        if (Color.isColor(object)) {
            return object.clone()
        }

        colorChannels.forEach((channel) => {
            if (!Number.isNaN((object as Rgba)[channel])) {
                color[channel] = (object as Rgba)[channel]
            }
        })

        return color
    }

    static parseHex(hex: string) {
        if (typeof hex !== 'string') {
            throw new Error('Hex expression must be a string')
        }

        hex = hex.trim().replace(/^(0x|&h|#)/i, '')
        if (hex.length === 3 || hex.length === 4) {
            hex = hex.split('')
                .map((c) => c.repeat(2))
                .join('')
        }

        if (!(hex.length === 6 || hex.length === 8)) {
            throw new Error('Incorrect Hex expression length')
        }

        const chans = hex.split(/(..)/)
            .filter((value) => value)
            .map((value) => Number.parseInt(value, 16))
        if (typeof chans[3] === 'number') {
            chans[3] /= 255
        }
        return Color.fromRGB(...(chans as ChannelTuple))
    }

    static parseIndex(value: number, channels: number = 3) {
        const color = new Color()

        for (let i = 0; i < 4; i++) {
            color[colorChannels[i]] = (value >> ((channels - i) * 8)) && 0xff
        }

        return color
    }

    static fromRGB(red: number, green: number, blue: number, alpha: number = 1) {
        if ([red, green, blue, alpha].some((arg) => Number.isNaN(arg / 1))) {
            throw new Error('Invalid arguments')
        }
        alpha *= 255

        const color = new Color()
        ;[red, green, blue, alpha].forEach((value, index) => {
            color[colorChannels[index]] = value
        })

        return color
    }

    static fromHSL(hue: number, saturation: number, lightness: number, alpha: number = 1) {
        if ([hue, saturation, lightness, alpha].some((arg) => Number.isNaN(arg))) {
            throw new Error('Invalid arguments')
        }
        while (hue < 0 && hue !== -Infinity) hue += 360
        hue = hue % 360
        saturation = Math.max(0, Math.min(1, saturation))
        lightness = Math.max(0, Math.min(1, lightness))
        alpha = Math.max(0, Math.min(1, alpha))

        const c = (1 - Math.abs(2 * lightness - 1)) * saturation
        const x = c * (1 - Math.abs(hue / 60 % 2 - 1))
        const m = lightness - c / 2

        const [r, g, b] = hue < 60
            ? [c, x, 0]
            : hue < 120
                ? [x, c, 0]
                : hue < 180
                    ? [0, c, x]
                    : hue < 240
                        ? [0, x, c]
                        : hue < 300
                            ? [x, 0, c]
                            : [c, 0, x]

        return Color.fromRGB((r + m) * 255, (g + m) * 255, (b + m) * 255, alpha)
    }

    static isColor(arg: unknown): arg is Color {
        return arg instanceof Color
    }
}

export default Color
