export class ColorConverter {
    constructor(ranges) {
        this.ranges = {
            hue: [0, 360],
            sat: [0, 100],
            bri: [0, 100],
            ct: [154, 500],
        }

        if (ranges)
            for (const attr of Object.keys(this.ranges))
                if (ranges[attr])
                    this.ranges[attr] = ranges[attr]
    }

    normalize(x, xRange, yRange) {
        return yRange[0] + (((x-xRange[0]) * (yRange[1]-yRange[0])) / (xRange[1]-xRange[0]))
    }

    hslToRgb(h, s, l) {
        [h, s, l] = [
            this.normalize(h, this.ranges.hue, [0, 360]),
            this.normalize(s, this.ranges.sat, [0, 100]),
            this.normalize(l, this.ranges.bri, [0, 100]),
        ]

        l /= 100
        const a = s * Math.min(l, 1 - l) / 100
        const f = n => {
            const k = (n + h / 30) % 12
            const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
            return Math.round(255 * color)
        }

        return [f(0), f(8), f(4)]
    }

    rgbToHsl(r, g, b){
        r /= 255
        g /= 255
        b /= 255;
        const max = Math.max(r, g, b), min = Math.min(r, g, b);
        let h, s, l = (max + min) / 2;

        if(max === min){
            h = s = 0; // achromatic
        } else {
            const d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

            switch(max){
                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                case b: h = (r - g) / d + 4; break;
            }
            h /= 6;
        }

        return [
            parseInt(this.normalize(h, [0, 1], this.ranges.hue)),
            parseInt(this.normalize(s, [0, 1], this.ranges.sat)),
            parseInt(this.normalize(l, [0, 1], this.ranges.bri)),
        ]
    }

    xyToRgb(x, y, brightness) {
        // Set to maximum brightness if no custom value was given (Not the slick ECMAScript 6 way for compatibility reasons)
        if (brightness == null)
            brightness = this.ranges.bri[1];

        const z = 1.0 - x - y;
        const Y = (brightness / (this.ranges.bri[1]-1)).toFixed(2);
        const X = (Y / y) * x;
        const Z = (Y / y) * z;

        //Convert to RGB using Wide RGB D65 conversion
        let red 	=  X * 1.656492 - Y * 0.354851 - Z * 0.255038;
        let green   = -X * 0.707196 + Y * 1.655397 + Z * 0.036152;
        let blue 	=  X * 0.051713 - Y * 0.121364 + Z * 1.011530;

        //If red, green or blue is larger than 1.0 set it back to the maximum of 1.0
        if (red > blue && red > green && red > 1.0) {
            green = green / red;
            blue = blue / red;
            red = 1.0;
        } else if (green > blue && green > red && green > 1.0) {
            red = red / green;
            blue = blue / green;
            green = 1.0;
        } else if (blue > red && blue > green && blue > 1.0) {
            red = red / blue;
            green = green / blue;
            blue = 1.0;
        }

        //Reverse gamma correction
        red 	= red <= 0.0031308 ? 12.92 * red : (1.0 + 0.055) * Math.pow(red, (1.0 / 2.4)) - 0.055;
        green 	= green <= 0.0031308 ? 12.92 * green : (1.0 + 0.055) * Math.pow(green, (1.0 / 2.4)) - 0.055;
        blue 	= blue <= 0.0031308 ? 12.92 * blue : (1.0 + 0.055) * Math.pow(blue, (1.0 / 2.4)) - 0.055;

        //Convert normalized decimal to decimal
        red 	= Math.round(red * 255);
        green 	= Math.round(green * 255);
        blue 	= Math.round(blue * 255);

        if (isNaN(red))
            red = 0;
        if (isNaN(green))
            green = 0;
        if (isNaN(blue))
            blue = 0;

        return [red, green, blue].map(
            (c) => Math.min(Math.max(0, c), 255))  // lgtm [js/automatic-semicolon-insertion]
    }

    rgbToXY(red, green, blue) {
        if (red > 1) { red /= 255; }
        if (green > 1) { green /= 255; }
        if (blue > 1) { blue /= 255; }

        //Apply a gamma correction to the RGB values, which makes the color more vivid and more the like the color displayed on the screen of your device
        red 	= (red > 0.04045) ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92);
        green 	= (green > 0.04045) ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92);
        blue 	= (blue > 0.04045) ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : (blue / 12.92);

        //RGB values to XYZ using the Wide RGB D65 conversion formula
        const X 		= red * 0.664511 + green * 0.154324 + blue * 0.162028;
        const Y 		= red * 0.283881 + green * 0.668433 + blue * 0.047685;
        const Z 		= red * 0.000088 + green * 0.072310 + blue * 0.986039;

        //Calculate the xy values from the XYZ values
        let x 		= parseFloat((X / (X + Y + Z)).toFixed(4));
        let y 		= parseFloat((Y / (X + Y + Z)).toFixed(4));

        if (isNaN(x))
            x = 0;
        if (isNaN(y))
            y = 0;

        return [x, y];
    }

    rgbToBri(red, green, blue) {
        return Math.min(2 * this.rgbToHsl(red, green, blue)[2], this.ranges.bri[1])
    }

    getRGB(color) {
        if (color.red != null && color.green != null && color.blue != null)
            return [color.red, color.green, color.blue]
        if (color.r != null && color.g != null && color.b != null)
            return [color.r, color.g, color.b]
        if (color.rgb)
            return color.rgb
    }

    getXY(color) {
        if (color.x != null && color.y != null)
            return [color.x, color.y]
        if (color.xy)
            return color.xy
    }

    toRGB(color) {
        const rgb = this.getRGB(color)
        if (rgb)
            return rgb

        const xy = this.getXY(color)
        if (xy && color.bri)
            return this.xyToRgb(...xy, color.bri)
        if (color.hue && color.sat && color.bri)
            return this.hslToRgb(color.hue, color.sat, color.bri)

        console.debug('Could not determine color space')
        console.debug(color)
    }

    toXY(color) {
        const xy = this.getXY(color)
        if (xy && color.bri)
            return [xy[0], xy[1], color.bri]

        const rgb = this.getRGB(color)
        if (rgb)
            return this.rgbToXY(...rgb)

        if (color.hue && color.sat && color.bri) {
            const rgb = this.hslToRgb(color.hue, color.sat, color.bri)
            return this.rgbToXY(...rgb)
        }

        console.debug('Could not determine color space')
        console.debug(color)
    }

    toHSL(color) {
        if (color.hue && color.sat && color.bri)
            return [color.hue, color.sat, color.bri]

        const rgb = this.getRGB(color)
        if (rgb)
            return this.rgbToHsl(...rgb)

        const xy = this.getXY(color)
        if (xy && color.bri) {
            const rgb = this.xyToRgb(...xy, color.bri)
            return this.rgbToHsl(...rgb)
        }

        console.debug('Could not determine color space')
        console.debug(color)
    }

    hexToRgb(hex) {
        return [
          hex.slice(1, 3),
          hex.slice(3, 5),
          hex.slice(5, 7),
        ].map(_ => parseInt(_, 16))
    }

    rgbToHex(rgb) {
      return '#' + rgb.map((x) => {
          let hex = x.toString(16)
          if (hex.length < 2)
            hex = '0' + hex
          return hex
        }).join('')
    }
}
