import { assert } from "../../assert.js";
import Signal from "../../events/signal/Signal.js";
import { computeHashFloat } from "../../primitives/numbers/computeHashFloat.js";
import { inverseLerp } from "../inverseLerp.js";

/**
 * Defines a numeric interval, by min and max.
 * Provides a suite of interval logic operations.
 * Interval can be of 0 width.
 */
export class NumericInterval {
    /**
     *
     * @param {number} [min=-Infinity]
     * @param {number} [max=Infinity]
     * @constructor
     */
    constructor(
        min = Number.NEGATIVE_INFINITY,
        max = Number.POSITIVE_INFINITY
    ) {
        assert.isNumber(min, 'min');
        assert.isNumber(max, 'max');

        assert.greaterThanOrEqual(max, min, `max [${max}] must be >= than min[${min}]`);

        /**
         *
         * @type {number}
         */
        this.min = min;
        /**
         *
         * @type {number}
         */
        this.max = max;

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


    /**
     *
     * @param {number} min
     * @param {number} max
     * @returns {this}
     */
    set(min, max) {
        assert.isNumber(min, 'min');
        assert.isNumber(max, 'max');

        assert.notNaN(min, 'min');
        assert.notNaN(max, 'max');

        assert.greaterThanOrEqual(max, min, `max [${max}] must be >= than min[${min}]`);

        const oldMin = this.min;
        const oldMax = this.max;

        if (min === oldMin && max === oldMax) {
            // no change
            return this;
        }

        this.min = min;
        this.max = max;

        if (!this.onChanged.hasHandlers()) {
            return this;
        }

        this.onChanged.send4(min, max, oldMin, oldMax);

        return this;
    }


    /**
     *
     * @param {number} value
     * @returns {this}
     */
    multiplyScalar(value) {
        const v0 = this.min * value;
        const v1 = this.max * value;

        if (v0 > v1) {
            //probably negative scale
            this.set(v1, v0);
        } else {
            this.set(v0, v1);
        }

        return this;
    }

    /**
     * Compute normalized position of input within this interval, where result is 0 if input is equal to `min`, and 1 when input is equal to `max`
     * @param {number} v value to be normalized
     * @returns {number} value between 0..1 if input is within [min,max] range, otherwise result will be extrapolated proportionately outside the 0,1 region
     */
    normalizeValue(v) {
        return inverseLerp(this.min, this.max, v);
    }

    /**
     * Both min and max are exactly 0
     * @returns {boolean}
     */
    isZero() {
        return this.min === 0 && this.max === 0;
    }

    /**
     * Whether min and max are the same
     * In other words if span is 0
     * @returns {boolean}
     */
    isExact() {
        return this.min === this.max;
    }


    /**
     *
     * @param {function} random Random number generator function, must return values between 0 and 1
     * @returns {number}
     */
    sampleRandom(random) {
        assert.isFunction(random, "random");

        const span = this.max - this.min;
        return this.min + random() * span;
    }

    fromJSON(json) {
        this.set(json.min, json.max);
    }

    toJSON() {
        return {
            min: this.min,
            max: this.max
        };
    }

    /**
     *
     * @param {number[]|Float32Array|Float64Array} array
     * @param {number} [offset=0]
     */
    fromArray(array, offset = 0) {
        return this.set(array[offset], array[offset + 1]);
    }

    /**
     *
     * @param {number[]|Float32Array|Float64Array} [destination=[]]
     * @param {number} [offset=0]
     * @return {number[]}
     */
    toArray(destination = [], offset = 0) {
        destination[offset] = this.min;
        destination[offset + 1] = this.max;

        return destination;
    }

    toString() {
        return `NumericInterval{ min=${this.min}, max=${this.max} }`;
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    toBinaryBuffer(buffer) {
        buffer.writeFloat64(this.min);
        buffer.writeFloat64(this.max);
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    fromBinaryBuffer(buffer) {
        this.min = buffer.readFloat64();
        this.max = buffer.readFloat64();
    }

    /**
     *
     * @param {NumericInterval} other
     * @returns {this}
     */
    copy(other) {
        return this.set(other.min, other.max);
    }

    /**
     * Performs union operation with another interval, mutates current interval.
     * @param {NumericInterval} other
     * @returns {this}
     */
    union(other) {
        assert.equal(other.isNumericInterval, true, "other must be a NumericInterval");

        return this.set(
            Math.min(this.min, other.min),
            Math.max(this.max, other.max)
        );
    }

    /**
     *
     * @param {NumericInterval} other
     * @return {boolean}
     */
    overlaps(other) {
        assert.equal(other.isNumericInterval, true, "other must be a NumericInterval");

        return this.min < other.max && this.max > other.min;
    }

    /**
     *
     * @param {NumericInterval} other
     * @return {boolean}
     */
    contains(other) {
        assert.equal(other.isNumericInterval, true, "other must be a NumericInterval");

        return this.min <= other.min && this.max >= other.max;
    }

    /**
     *
     * @param {NumericInterval} other
     * @returns {boolean}
     */
    equals(other) {
        assert.equal(other.isNumericInterval, true, "other must be a NumericInterval");

        return this.min === other.min && this.max === other.max;
    }

    /**
     *
     * @returns {number}
     */
    hash() {
        let hash = computeHashFloat(this.min);

        hash = ((hash << 5) - hash) + computeHashFloat(this.max);

        return hash;
    }

    /**
     * Distance between min and max (`= max - min`)
     * @returns {number}
     */
    get span() {
        return this.max - this.min;
    }

    get middle() {
        return (this.max + this.min) * 0.5;
    }

    /**
     * @deprecated use {@link middle} instead
     * @returns {number}
     */
    computeAverage() {
        return this.middle;
    }
}

/**
 * @readonly
 * @type {boolean}
 */
NumericInterval.prototype.isNumericInterval = true;

/**
 * @readonly
 * @type {NumericInterval}
 */
NumericInterval.zero_zero = Object.freeze(new NumericInterval(0, 0));

/**
 * @readonly
 * @type {NumericInterval}
 */
NumericInterval.zero_one = Object.freeze(new NumericInterval(0, 1));

/**
 * @readonly
 * @type {NumericInterval}
 */
NumericInterval.one_one = Object.freeze(new NumericInterval(1, 1));

