import { assert } from "../../../../../../core/assert.js";
import { combine_hash } from "../../../../../../core/collection/array/combine_hash.js";
import { isArrayEqualStrict } from "../../../../../../core/collection/array/isArrayEqualStrict.js";
import { computeHashFloatArray } from "../../../../../../core/math/hash/computeHashFloatArray.js";
import { inverseLerp } from "../../../../../../core/math/inverseLerp.js";
import { max2 } from "../../../../../../core/math/max2.js";
import { min2 } from "../../../../../../core/math/min2.js";
import { computeHashFloat } from "../../../../../../core/primitives/numbers/computeHashFloat.js";
import { ParameterLookupTableSerializationAdapter } from "../emitter/serde/ParameterLookupTableSerializationAdapter.js";
import { ParameterLookupTableFlags } from "./ParameterLookupTableFlags.js";


export class ParameterLookupTable {

    /**
     *
     * @type {number[]|Float32Array}
     */
    data = [];

    /**
     *
     * @type {number[]|Float32Array}
     */
    positions = [];

    /**
     * Transient value
     * @type {number}
     */
    valueMin = 0;

    /**
     * Transient value
     * @type {number}
     */
    valueMax = 0;

    /**
     * @private
     * @type {number}
     */
    flags = ParameterLookupTableFlags.WriteMode;

    /**
     *
     * @param {number} [itemSize]
     * @constructor
     */
    constructor(itemSize = 1) {
        assert.isNonNegativeInteger(itemSize, 'itemSize');

        /**
         *
         * @type {number}
         */
        this.itemSize = itemSize;

    }

    /**
     *
     * @param {ParameterLookupTableFlags} f
     */
    setFlag(f) {
        this.flags |= f;
    }

    /**
     *
     * @param {ParameterLookupTableFlags|number} f
     */
    clearFlag(f) {
        this.flags &= ~f;
    }

    /**
     *
     * @param {ParameterLookupTableFlags} f
     * @returns {boolean}
     */
    getFlag(f) {
        return (this.flags & f) !== 0;
    }

    disableWriteMode() {
        this.clearFlag(ParameterLookupTableFlags.WriteMode);
    }

    fromJSON(json) {
        this.itemSize = json.itemSize;
        this.write(json.data, json.positions);
    }

    toJSON() {
        return {
            itemSize: this.itemSize,
            data: Array.from(this.data),
            positions: Array.from(this.positions)
        };
    }

    /**
     *
     * @deprecated Use serialization adapter instead
     * @param {BinaryBuffer} buffer
     */
    toBinaryBuffer(buffer) {
        const adapter = new ParameterLookupTableSerializationAdapter();

        adapter.serialize(buffer, this);
    }

    /**
     * @deprecated Use serialization adapter instead
     * @param {BinaryBuffer} buffer
     */
    fromBinaryBuffer(buffer) {
        const adapter = new ParameterLookupTableSerializationAdapter();

        adapter.deserialize(buffer, this);
    }

    /**
     *
     * @param {number} position
     * @param {number[]|Float32Array|Float64Array} result
     */
    sample(position, result) {
        assert.isNumber(position, 'position');
        assert.notNaN(position, 'position');
        // assert.greaterThanOrEqual(position, 0, 'position');
        // assert.ok(position >= 0 && position <= 1, `position must be between 0 and 1, instead was ${position}`);

        const itemSize = this.itemSize;
        const data = this.data;
        const numValues = data.length / itemSize;

        let i;

        const positions = this.positions;

        let lowIndex = numValues - 1;
        let highIndex = lowIndex;
        let fraction = 0;

        if (position <= positions[0]) {

            // special case, seek position is at or before the start of the table
            lowIndex = 0;
            highIndex = 0;

        } else {

            //find position index
            for (i = 1; i < numValues; i++) {
                //NOTE: no complex search is used, since for small data sizes linear scan is typically faster
                const p = positions[i];

                if (p >= position) {
                    // next position if over the seek position
                    highIndex = i;
                    lowIndex = i - 1;

                    fraction = inverseLerp(positions[lowIndex], p, position);

                    break;
                }
            }
        }


        const low_offset = lowIndex * itemSize;
        const high_offset = highIndex * itemSize;

        for (i = 0; i < itemSize; i++) {
            const lowValue = data[low_offset + i];
            const highValue = data[high_offset + i];

            // inlined for speed: lerp(low, high, fraction)
            const value = lowValue * (1 - fraction) + highValue * fraction;

            result[i] = value;
        }
    }

    computeUniformPositions() {
        const numValues = this.data.length / this.itemSize;

        const positions = [];

        this.positions = positions;

        for (let i = 0; i < numValues; i++) {
            positions[i] = i / (numValues - 1);
        }
    }

    /**
     *
     * @param {number[]|Float64Array|Float32Array|Uint8Array} values
     * @param {number[]|Float64Array|Float32Array} [positions]
     */
    write(values, positions) {
        if (!this.getFlag(ParameterLookupTableFlags.WriteMode)) {
            throw new Error(`Cannot write, WriteMode disabled`);
        }

        assert.isArrayLike(values, 'values');

        const numValues = values.length;

        assert.equal(numValues % this.itemSize, 0, `number of elements in the array[=${numValues}] is not multiple of itemSize(=${this.itemSize})`);

        this.data = values;

        if (positions === undefined) {

            //console.warn('positions are undefined, assuming uniform distribution');
            this.computeUniformPositions();

        } else {

            assert.equal(numValues / this.itemSize, positions.length, `number of positions[=${positions.length}] is not equal to number of values[=${numValues / this.itemSize}]`);

            this.positions = positions;
        }

        this.computeStatistics();
    }

    /**
     *
     * @param {number} position
     * @param {number[]} value
     */
    addValue(position, value) {
        if (!this.getFlag(ParameterLookupTableFlags.WriteMode)) {
            throw new Error(`Cannot add value, WriteMode disabled`);
        }

        //insert operation cannot be done on a TypedArray, so we need to make sure we are working with dynamic array
        if (!Array.isArray(this.positions)) {
            this.positions = Array.from(this.positions);
        }

        if (!Array.isArray(this.data)) {
            this.data = Array.from(this.data);
        }

        //find a place to insert the value at
        const positions = this.positions;
        const numValues = positions.length;

        let i;

        for (i = 0; i < numValues; i++) {
            const p = positions[i];
            if (p > position) {
                break;
            }
        }


        //insert value
        positions.splice(i, 0, position);

        const spliceArgs = value.slice();
        spliceArgs.unshift(i * this.itemSize, 0);

        Array.prototype.splice.apply(this.data, spliceArgs);
    }

    /**
     * Populates statistics values
     */
    computeStatistics() {
        //first find min and max values
        let min = Number.POSITIVE_INFINITY, max = Number.NEGATIVE_INFINITY;


        const numElements = this.data.length;

        let i;

        for (i = 0; i < numElements; i++) {
            const inputElement = this.data[i];

            min = min2(min, inputElement);
            max = max2(max, inputElement);
        }

        this.valueMin = min;
        this.valueMax = max;
    }

    hash() {
        const valueMax = this.valueMax;
        const valueMin = this.valueMin;
        if (valueMin === valueMax) {
            //special case, all values are the same
            return computeHashFloat(valueMin);
        } else {
            const dataHash = computeHashFloatArray(this.data);
            const positionHash = computeHashFloatArray(this.positions);

            return combine_hash(
                dataHash,
                positionHash,
                this.itemSize
            );
        }
    }

    /**
     *
     * @param {ParameterLookupTable} other
     * @returns {boolean}
     */
    equals(other) {
        if (this === other) {
            // identity shortcut
            return true;
        }

        if (this.itemSize !== other.itemSize) {
            return false;
        }

        return isArrayEqualStrict(this.data, other.data)
            && isArrayEqualStrict(this.positions, other.positions)
            ;
    }

    validate() {
        //check data size
        const data = this.data;
        const dataLength = data.length;

        const positions = this.positions;
        const positionLength = positions.length;

        if ((dataLength / this.itemSize) !== positionLength) {
            //number of samples between position and data doesn't match
            return false;
        }


        //check position values
        let prevPosition = Number.NEGATIVE_INFINITY;

        for (let i = 0; i < positionLength; i++) {
            const position = positions[i];

            if (position < 0) {
                //position value must be greater or equal to 0
                return false;
            }

            if (position > 1) {
                //position must be less than or equal to 1
                return false;
            }

            if (position <= prevPosition) {
                //position values must always increase
                return false;
            }

            prevPosition = position;
        }

        //all is good
        return true;
    }

    /**
     *
     * @param {number} itemSize
     * @param {number[]} values
     * @param {number[]} [positions]
     */
    static from(itemSize, values, positions) {
        const r = new ParameterLookupTable(itemSize);

        r.write(values, positions);

        return r;
    }

    /**
     *
     * @param {Color} colors
     * @returns {ParameterLookupTable}
     */
    static color_uint8_uniform(...colors) {
        assert.defined(colors, 'colors');

        const r = new ParameterLookupTable(4);

        const length = colors.length;
        const values = new Uint8Array(length * 4);

        for (let i = 0; i < length; i++) {
            const color = colors[i];

            const i4 = i * 4;

            values[i4] = Math.round(color.r * 255);
            values[i4 + 1] = Math.round(color.g * 255);
            values[i4 + 2] = Math.round(color.b * 255);
            values[i4 + 3] = Math.round(color.a * 255);
        }

        r.write(values);

        r.computeUniformPositions();

        return r;
    }
}

/**
 * @readonly
 * @type {boolean}
 */
ParameterLookupTable.prototype.isParameterLookupTable = true;
