import { assert } from "../assert.js";
import { float_to_uint8 } from "../binary/float_to_uint8.js";
import { uint8_to_float } from "../binary/uint8_to_float.js";
import Signal from "../events/signal/Signal.js";
import { lerp } from "../math/lerp.js";
import { max2 } from "../math/max2.js";
import { min2 } from "../math/min2.js";
import { min3 } from "../math/min3.js";
import { rgb2hex } from "./hex/rgb2hex.js";

import { hsv2rgb_float } from "./hsv/hsv2rgb_float.js";
import { rgb2hsv } from "./hsv/rgb2hsv.js";
import { parse_color } from "./parse_color.js";
import { rgb2uint24 } from "./rgb2uint24.js";
import { rgb2uint32 } from "./rgb2uint32.js";
import { rgb_to_luminance } from "./rgb_to_luminance.js";
import { linear_to_sRGB } from "./sRGB/linear_to_sRGB.js";
import { sRGB_to_linear } from "./sRGB/sRGB_to_linear.js";

/**
 * Representation color and transparency (RGBA).
 *
 * @example
 *  const red = new Color(1,0,0);
 *
 * @extends {Array.<number>}
 * @class
 */
export class Color {
    /**
     *
     * @param {number} [r=0] Red from 0 to 1 for SDR
     * @param {number} [g=0] Green from 0 to 1 for SDR
     * @param {number} [b=0] Blue from 0 to 1 for SDR
     * @param {number} [a=1] Alpha from 0 to 1 (transparency)
     */
    constructor(r = 0, g = 0, b = 0, a = 1) {
        assert.isNumber(r, 'r');
        assert.isNumber(g, 'g');
        assert.isNumber(b, 'b');
        assert.isNumber(a, 'a');

        /**
         * Red channel
         * Value from 0 to 1 for SDR
         * @type {number}
         */
        this.r = r;
        /**
         * Green channel
         * Value from 0 to 1 for SDR
         * @type {number}
         */
        this.g = g;
        /**
         * Blue channel
         * Value from 0 to 1 for SDR
         * @type {number}
         */
        this.b = b;

        /**
         * Alpha channel (transparency)
         * Value from 0 to 1
         * @type {number}
         */
        this.a = a;

        /**
         * @readonly
         * @type {Signal<number,number,number,number, number,number,number,number>}
         */
        this.onChanged = new Signal();
    }

    /**
     *
     * @returns {number}
     */
    get 0() {
        return this.r;
    }

    /**
     *
     * @param {number} v
     */
    set 0(v) {
        this.r = v;
    }

    /**
     *
     * @returns {number}
     */
    get 1() {
        return this.g;
    }

    /**
     *
     * @param {number} v
     */
    set 1(v) {
        this.g = v;
    }

    /**
     *
     * @returns {number}
     */
    get 2() {
        return this.b;
    }

    /**
     *
     * @param {number} v
     */
    set 2(v) {
        this.b = v;
    }

    /**
     *
     * @returns {number}
     */
    get 3() {
        return this.a;
    }

    /**
     *
     * @param {number} v
     */
    set 3(v) {
        this.a = v;
    }

    // XYZW accessors

    /**
     *
     * @returns {number}
     */
    get x() {
        return this.r;
    }

    /**
     *
     * @param {number} v
     */
    set x(v) {
        this.r = v;
    }

    /**
     *
     * @returns {number}
     */
    get y() {
        return this.g;
    }

    /**
     *
     * @param {number} v
     */
    set y(v) {
        this.g = v;
    }

    /**
     *
     * @returns {number}
     */
    get z() {
        return this.b;
    }

    /**
     *
     * @param {number} v
     */
    set z(v) {
        this.b = v;
    }

    /**
     *
     * @returns {number}
     */
    get w() {
        return this.a;
    }

    /**
     *
     * @param {number} v
     */
    set w(v) {
        this.a = v;
    }

    /**
     * Pretending to be an array
     * @return {number}
     */
    get length() {
        return 4;
    }

    /**
     *
     * @param {number} r
     * @param {number} g
     * @param {number} b
     */
    setRGB(r, g, b) {

        this.set(r, g, b, this.a);
    }

    /**
     *
     * @param {number} r in range of 0..255
     * @param {number} g in range of 0..255
     * @param {number} b in range of 0..255
     */
    setRGBUint8(r, g, b) {
        this.setRGB(
            uint8_to_float(r),
            uint8_to_float(g),
            uint8_to_float(b),
        )
    }

    /**
     * set alpha
     * @param {number} a
     */
    setA(a) {
        this.set(this.r, this.g, this.b, a);
    }

    /**
     *
     * @param {number} r
     * @param {number} g
     * @param {number} b
     * @param {number} a
     * @returns {this}
     */
    set(r, g, b, a = 1.0) {
        assert.isNumber(r, 'r');
        assert.isNumber(g, 'g');
        assert.isNumber(b, 'b');
        assert.isNumber(a, 'a');

        assert.notNaN(r, 'r');
        assert.notNaN(g, 'g');
        assert.notNaN(b, 'b');
        assert.notNaN(a, 'a');

        assert.isFinite(r, 'r');
        assert.isFinite(g, 'g');
        assert.isFinite(b, 'b');
        assert.isFinite(a, 'a');

        // remember old values
        const _r = this.r;
        const _g = this.g;
        const _b = this.b;
        const _a = this.a;

        if (
            _r === r
            && _g === g
            && _b === b
            && _a === a
        ) {
            // no change
            return this;
        }

        // set new values
        this.r = r;
        this.g = g;
        this.b = b;
        this.a = a;

        this.onChanged.send8(r, g, b, a, _r, _g, _b, _a);

        return this;
    }

    /**
     * Hue Saturation Lightness
     * @param {number} h 0..1 (hue rotation compressed to normal 0 to 1 range where 1 represents 360deg)
     * @param {number} s 0..1
     * @param {number} l 0..1
     */
    setHSL(h, s, l) {
        // pull hue into 0..1 range
        let _h = h % 1;

        if (_h < 0) {
            // move into positive range
            _h = _h + Math.ceil(Math.abs(_h));
        }

        const k_r = (_h * 12) % 12;
        const k_g = (8 + _h * 12) % 12;
        const k_b = (4 + _h * 12) % 12;

        const a = s * min2(l, 1 - l);

        const r = l - a * max2(-1, min3(k_r - 3, 9 - k_r, 1));
        const g = l - a * max2(-1, min3(k_g - 3, 9 - k_g, 1));
        const b = l - a * max2(-1, min3(k_b - 3, 9 - k_b, 1));

        this.setRGB(r, g, b);
    }

    /**
     * Hue Chroma Luma
     * @param {number} h Hue
     * @param {number} c Chroma
     * @param {number} l Luma
     */
    setHCL(h, c, l) {
        // pull hue into 0..1 range
        let _h = h % 1;

        if (_h < 0) {
            // move into positive range
            _h = _h + Math.ceil(Math.abs(_h));
        }

        const h_face = _h * 6;

        const x = c * (1 - Math.abs(h_face % 2 - 1));

        let r, g, b;

        // map to RGB
        switch (Math.floor(h_face)) {
            case 0:
                r = c;
                g = x;
                b = 0;
                break;
            case 1:
                r = x;
                g = c;
                b = 0;
                break;
            case 2:
                r = 0;
                g = c;
                b = x;
                break;
            case 3:
                r = 0;
                g = x;
                b = c;
                break;
            case 4:
                r = x;
                g = 0;
                b = c;
                break;
            case 5:
                r = c;
                g = 0;
                b = x;
        }

        const m = l - (0.3 * r + 0.59 * g + 0.11 * b);

        this.setRGB(r + m, g + m, b + m);
    }

    /**
     * NOTE: I'm not sure if the implementation is correct, based on wikipedia article
     * Hue Saturation Intensity
     * @param {number} h 0..1 (hue rotation compressed to normal 0 to 1 range where 1 represents 360deg)
     * @param {number} s 0..1
     * @param {number} i 0..1
     * @see https://en.wikipedia.org/wiki/HSL_and_HSV
     */
    setHSI(h, s, i) {
        // pull hue into 0..1 range
        let _h = h % 1;

        if (_h < 0) {
            // move into positive range
            _h = _h + Math.ceil(Math.abs(_h));
        }

        const h_face = _h * 6;

        const z = 1 - Math.abs(h_face % 2 - 1);

        const c = (3 * i * s) / (1 + z);

        const x = c * z;

        let r, g, b;
        // map to RGB
        switch (Math.floor(h_face)) {
            case 0:
                r = c;
                g = x;
                b = 0;
                break;
            case 1:
                r = x;
                g = c;
                b = 0;
                break;
            case 2:
                r = 0;
                g = c;
                b = x;
                break;
            case 3:
                r = 0;
                g = x;
                b = c;
                break;
            case 4:
                r = x;
                g = 0;
                b = c;
                break;
            case 5:
                r = c;
                g = 0;
                b = x;
        }

        const m = i * (1 - s);

        this.setRGB(r + m, g + m, b + m);
    }

    /**
     * Hue Saturation Value
     * @param {number} h 0..1
     * @param {number} s 0..1
     * @param {number} v 0..1
     */
    setHSV(h, s, v) {
        const { r, g, b } = hsv2rgb_float(h, s, v);

        this.setRGB(r, g, b);
    }

    /**
     *
     * @return {number}
     */
    computeLuminance() {
        return rgb_to_luminance(this.r, this.g, this.b);
    }

    getHSV() {
        return rgb2hsv(
            this.r,
            this.g,
            this.b
        );
    }

    /**
     * Encodes RGB to uint24
     * @returns {number}
     */
    toUint() {
        return rgb2uint24(this.r, this.g, this.b);
    }

    /**
     * Encodes RGBA to uint32
     * @returns {number}
     */
    toUint32() {
        return rgb2uint32(this.r, this.g, this.b, this.a);
    }

    /**
     *
     * @param {number} value
     */
    fromUint(value) {
        assert.isNonNegativeInteger(value, 'value');

        const r = value >> 16;
        const g = (value >> 8) & 0xFF;
        const b = (value) & 0xFF;

        this.setRGBUint8(r, g, b);
    }

    /**
     * Returns color in CSS hex color format
     * @example #FF0000
     * @returns {string}
     */
    toHex() {
        return '#' + rgb2hex(
            float_to_uint8(this.r),
            float_to_uint8(this.g),
            float_to_uint8(this.b)
        );
    }

    /**
     *
     * @returns {string}
     */
    toCssRGBAString() {
        return `rgba(${float_to_uint8(this.r)},${float_to_uint8(this.g)},${float_to_uint8(this.b)},${this.a})`;
    }

    /**
     *
     * @param {Color} other
     */
    equals(other) {
        return this.r === other.r
            && this.g === other.g
            && this.b === other.b
            && this.a === other.a;
    }

    /**
     *
     * @param {Color} other
     */
    copy(other) {
        this.set(other.r, other.g, other.b, other.a);
    }

    /**
     *
     * @returns {Color}
     */
    clone() {
        const result = new Color();

        result.copy(this);

        return result;
    }

    /**
     *
     * @returns {number}
     */
    hash() {
        return this.toUint();
    }

    fromJSON({ r, g, b, a = 1 }) {
        this.set(r, g, b, a);
    }

    toJSON() {
        return {
            r: this.r,
            g: this.g,
            b: this.b,
            a: this.a
        };
    }

    toString() {
        return `Color{r:${this.r},g:${this.g},b:${this.b},a:${this.a}}`
    }

    * [Symbol.iterator]() {

        yield this.r;
        yield this.g;
        yield this.b;
        yield this.a;

    }

    /**
     *
     * @param {number[]} [destination] where to write the output, if not provided a new array will be created
     * @param {number} [destination_offset]
     * @returns {number[]} [g,g,b,a], same object as input destination (if provided)
     */
    toArray(destination = [], destination_offset = 0) {
        destination[destination_offset] = this.r;
        destination[destination_offset + 1] = this.g;
        destination[destination_offset + 2] = this.b;
        destination[destination_offset + 3] = this.a;

        return destination;
    }

    /**
     * Can be either RGB or RGBA, in case of 3 element array, A(alpha) channel will be se to 1
     * @param {number[]|Float32Array} source
     * @param {number} [offset]
     * @returns {Color}
     */
    fromArray(source, offset = 0) {
        const r = source[offset + 0];
        const g = source[offset + 1];
        const b = source[offset + 2];
        const a = source[offset + 3] ?? 1;

        return this.set(r, g, b, a);
    }

    /**
     *
     * @param {number[]|Float32Array} source
     * @param {number} [offset]
     * @returns {Color}
     */
    static fromArray(source, offset) {
        const r = new Color();

        r.fromArray(source, offset);

        return r;
    }


    /**
     * @deprecated please use your own {@link BinaryClassSerializationAdapter}
     * @param {BinaryBuffer} buffer
     */
    toBinaryBuffer(buffer) {
        buffer.writeFloat32(this.r);
        buffer.writeFloat32(this.g);
        buffer.writeFloat32(this.b);
        buffer.writeFloat32(this.a);
    }

    /**
     * @deprecated please use your own {@link BinaryClassSerializationAdapter}
     * @param {BinaryBuffer} buffer
     */
    fromBinaryBuffer(buffer) {
        const r = buffer.readFloat32();
        const g = buffer.readFloat32();
        const b = buffer.readFloat32();
        const a = buffer.readFloat32();

        this.set(r, g, b, a);
    }

    /**
     * @param {string|number} str CSS-style string
     * @returns {void}
     */
    parse(str) {
        const c = parse_color(str);

        if (typeof c[3] === "number") {
            this.a = c[3];
        } else {
            this.a = 1;
        }

        this.setRGB(c[0] / 255, c[1] / 255, c[2] / 255);
    }

    /**
     *
     * @param {Color} a
     * @param {Color} b
     * @param {number} f
     * @returns {this}
     */
    lerpColors(a, b, f) {
        assert.defined(a, 'a');
        assert.defined(b, 'b');

        const _r = lerp(a.r, b.r, f);
        const _g = lerp(a.g, b.g, f);
        const _b = lerp(a.b, b.b, f);
        const _a = lerp(a.a, b.a, f);

        return this.set(_r, _g, _b, _a);
    }

    /**
     * Multiply every component by a scalar value
     * @param {number} x
     * @returns {this}
     */
    multiplyScalar(x) {
        assert.isNumber(x, 'x');
        assert.notNaN(x, 'x');

        const r = this.r * x;
        const g = this.g * x;
        const b = this.b * x;
        const a = this.a * x;

        return this.set(r, g, b, a);
    }

    /**
     * Component-wise vector multiplication
     * @param {Color} other
     * @returns {this}
     */
    multiply(other) {
        assert.defined(other, 'other');
        assert.isObject(other, 'other');
        assert.equal(other.isColor, true, 'other.isColor !== true');

        const r = this.r * other.r;
        const g = this.g * other.g;
        const b = this.b * other.b;
        const a = this.a * other.a;

        this.set(r, g, b, a);

        return this;
    }

    /**
     *
     * @param {number} r
     * @param {number} g
     * @param {number} b
     * @returns {Color}
     */
    static fromRGB(r, g, b) {
        return new Color(r, g, b);
    }

    /**
     *
     * @param {number} h
     * @param {number} s
     * @param {number} v
     * @return {Color}
     */
    static fromHSV(h, s, v) {
        const color = new Color();

        color.setHSV(h, s, v);

        return color;
    }


    /**
     * Create a color for numeric or string representation. Supports most CSS color representations.
     * @example
     *  Color.parse("#FF00FF");
     *  Color.parse(0xFF00FF);
     *  Color.parse("rgb(255, 0, 255)");
     *  Color.parse("rgba(255, 0, 255, 0.5)");
     *  Color.parse("hsv(0.5, 1, 0.3)");
     *
     * @param {string|number} value
     * @return {Color}
     * @throws Error if format could not be parsed
     */
    static parse(value) {
        const r = new Color();

        r.parse(value);

        return r;
    }

    /**
     *
     * @param {Color} input
     * @param {Color} [output]
     * @returns {Color}
     */
    static from_linear_to_sRGB(input, output = new Color()) {

        linear_to_sRGB(output, 0, input, 0);

        return output;
    }

    /**
     *
     * @param {Color} input
     * @param {Color} [output]
     * @returns {Color}
     */
    static from_sRGB_to_linear(input, output = new Color()) {
        sRGB_to_linear(output, 0, input, 0);

        return output;
    }
}

/**
 * @readonly
 * @type {boolean}
 */
Color.prototype.isColor = true;

/**
 * @readonly
 * @type {Readonly<Color>}
 */
Color.red = Object.freeze(new Color(1, 0, 0));

/**
 * @readonly
 * @type {Readonly<Color>}
 */
Color.green = Object.freeze(new Color(0, 1, 0));

/**
 * @readonly
 * @type {Readonly<Color>}
 */
Color.blue = Object.freeze(new Color(0, 0, 1));

/**
 * @readonly
 * @type {Readonly<Color>}
 */
Color.yellow = Object.freeze(new Color(1, 1, 0));

/**
 * @readonly
 * @type {Readonly<Color>}
 */
Color.cyan = Object.freeze(new Color(0, 1, 1));

/**
 * @readonly
 * @type {Readonly<Color>}
 */
Color.magenta = Object.freeze(new Color(1, 0, 1));

/**
 * @readonly
 * @type {Readonly<Color>}
 */
Color.white = Object.freeze(new Color(1, 1, 1));

/**
 * @readonly
 * @type {Readonly<Color>}
 */
Color.black = Object.freeze(new Color(0, 0, 0));

/**
 * @readonly
 * @type {Readonly<Color>}
 */
Color.transparent = Object.freeze(new Color(0, 0, 0, 0));
