import { assert } from "../../../../core/assert.js";
import { array_shrink_to_size } from "../../../../core/collection/array/array_shrink_to_size.js";
import { binarySearchHighIndex } from "../../../../core/collection/array/binarySearchHighIndex.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { IllegalStateException } from "../../../../core/fsm/exceptions/IllegalStateException.js";

/**
 * @readonly
 * @enum {number}
 */
const IncrementalDeltaSetState = {
    Clear: 0,
    Building: 1,
    Ready: 2
};

/**
 * accelerated Set data structure optimized for incremental re-building
 * Always sorted
 * @template V
 */
export class IncrementalDeltaSet {
    /**
     *
     * @param {function(V,V):number} compare stable comparator
     */
    constructor(compare) {
        assert.isFunction(compare, 'compare');

        /**
         *
         * @type {V[]}
         * @private
         */
        this.__elements = [];

        /**
         *
         * @type {number}
         * @private
         */
        this.__elements_cursor = 0;

        /**
         *
         * @type {V[]}
         * @private
         */
        this.__swap_elements = [];
        /**
         *
         * @type {number}
         * @private
         */
        this.__swap_elements_count = 0;

        /**
         *
         * @type {function(V, V): number}
         * @private
         */
        this.__compare = compare;

        /**
         * Current version of the state, each time the set is updated - version changes
         * @type {number}
         */
        this.version = 0;

        /**
         * Number of currently held elements
         * @type {number}
         */
        this.size = 0;

        /**
         *
         * @type {Signal}
         */
        this.onUpdateFinished = new Signal();

        /**
         *
         * @type {Signal}
         */
        this.onAdded = new Signal();

        /**
         *
         * @type {Signal}
         */
        this.onRemoved = new Signal();

        /**
         *
         * @type {IncrementalDeltaSetState|number}
         */
        this.state = IncrementalDeltaSetState.Clear;
    }

    /**
     *
     * @return {V[]}
     */
    get elements() {
        return this.__elements;
    }

    /**
     *
     * @param {(V)=>any} visitor
     * @param {*} [visitorContext]
     */
    forEach(visitor, visitorContext) {
        const n = this.size;
        const elements = this.__elements;

        for (let i = 0; i < n; i++) {
            const el = elements[i];

            visitor.call(visitorContext, el);
        }
    }

    /**
     *
     * @param {V} element
     * @returns {boolean}
     */
    contains(element) {
        return this.__indexOf(element) !== -1;
    }

    /**
     *
     * @param {V} element
     * @return {number}
     * @private
     */
    __indexOf(element) {
        const i = this.__indexExpected(element);

        if (this.__elements[i] === element) {
            return i;
        } else {
            return -1;
        }
    }

    /**
     *
     * @param {V} element
     * @return {number}
     * @private
     */
    __indexExpected(element) {
        return binarySearchHighIndex(this.__elements, element, this.__compare, 0, this.size - 1);
    }

    /**
     *
     * @param {V} element
     * @return {boolean}
     */
    forceAdd(element) {
        const i = this.__indexExpected(element);

        if (this.elements[i] === element) {
            return false;
        }

        this.__elements.splice(i, 0, element);

        this.size++;
        this.version++;

        return true;
    }

    /**
     *
     * @param {V} element
     * @returns {boolean}
     */
    forceRemove(element) {
        if (this.state === IncrementalDeltaSetState.Building) {
            throw new IllegalStateException("Cannot remove during build");
        }

        const i = this.__indexOf(element);

        if (i !== -1) {
            this.__elements.splice(i, 1);
            this.size--;

            this.version++;

            return true;
        } else {
            return false;
        }
    }

    clear() {
        if (this.size === 0) {
            // already empty
            return;
        }

        this.size = 0;

        this.state = IncrementalDeltaSetState.Clear;

        this.version++;
    }

    includeAll() {
        throw new Error('deprecated #includeAll');
    }

    initializeUpdate() {
        // perform swap
        const swp_els = this.__elements;
        this.__elements = this.__swap_elements
        this.__swap_elements = swp_els;

        const swp_count = this.size;
        this.size = this.__swap_elements_count;
        this.__swap_elements_count = swp_count;

        this.__elements_cursor = 0;

        this.state = IncrementalDeltaSetState.Building;

        this.version++;
    }

    finalizeUpdate() {
        assert.equal(this.state, IncrementalDeltaSetState.Building, `Expected BUILDING state, instead got '${this.state}'`);

        const array_main_count = this.__elements_cursor;
        const array_previous_count = this.__swap_elements_count;

        this.size = array_main_count;

        // sort newly populated elements array
        const array_main = this.__elements;
        const array_previous = this.__swap_elements;

        const compare = this.__compare;

        const onAdded = this.onAdded;
        const onRemoved = this.onRemoved;

        array_shrink_to_size(array_main, array_main_count);
        array_main.sort(compare);


        let i0 = 0, i1 = 0;

        while (i0 < array_main_count && i1 < array_previous_count) {
            const el_0 = array_main[i0];
            const el_1 = array_previous[i1];

            const diff = compare(el_0, el_1);

            if (diff === 0) {
                i0++;
                i1++;
                // found match
            } else if (diff < 0) {
                i0++;
                // addition
                onAdded.send1(el_0);
            } else {
                i1++;
                // removal
                onRemoved.send1(el_1);
            }
        }

        // process remainders
        for (; i0 < array_main_count; i0++) {
            onAdded.send1(array_main[i0]);
        }
        for (; i1 < array_previous_count; i1++) {
            onRemoved.send1(array_previous[i1]);
        }

        this.state = IncrementalDeltaSetState.Ready;

        //dispatch notification
        this.onUpdateFinished.send0();
    }

    /**
     *
     * @param {V} element
     */
    push(element) {
        const elements = this.__elements;

        elements[this.__elements_cursor++] = element;
    }

    /**
     *
     * @param {IncrementalDeltaSet<V>} other
     */
    pushOther(other) {
        const elements = other.__elements;
        const n = other.size;

        for (let i = 0; i < n; i++) {
            const el = elements[i];

            this.push(el);
        }
    }
}


/**
 * @readonly
 * @type {boolean}
 */
IncrementalDeltaSet.prototype.isIncrementalDeltaSet = true;
