import { assert } from "../assert.js";
import { min2 } from "../math/min2.js";

/**
 * Fixed-sized container structure, where ends are connected into a circle/ring
 * This is a FIFO structure (first in, first out)
 * @see https://en.wikipedia.org/wiki/Circular_buffer
 * @template V
 */
export class RingBuffer {
    /**
     * @param {number} size
     */
    constructor(size) {
        assert.isNumber(size, 'size');
        assert.greaterThan(size, 0, `size`);
        assert.isNonNegativeInteger(size, 'size');

        /**
         * Capacity of the buffer, number of slots
         * @type {number}
         */
        this.size = size;

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

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

        /**
         * Number of used slots. Starts at 0, goes up to {@link #size}
         * @type {number}
         */
        this.count = 0;

        /**
         *
         * @type {V[]}
         */
        this.data = new Array(size);
    }

    /**
     *
     * @param {number} new_size
     */
    resize(new_size) {
        assert.isNonNegativeInteger(new_size, 'new_size');
        assert.greaterThan(new_size, 0, `new_size`);

        if (new_size === this.size) {
            // already the right size
            return;
        }

        /**
         *
         * @type {V[]}
         */
        const array = new Array(new_size);

        this.data = array;
        this.size = new_size;
        this.count = min2(new_size, this.count);

        this.clear();

        // TODO implement a way to keep the old data, need to figure out the correct adjustments to head and tail
    }

    /**
     *
     * @return {V}
     */
    getHead() {
        return this.data[this.head - 1];
    }

    /**
     *
     * @param {number} offset
     * @returns {V}
     */
    getFromHead(offset) {
        assert.isNonNegativeInteger(offset, 'offset');

        let i = this.head - (offset + 1);

        while (i < 0) {
            i += this.size;
        }

        return this.data[i];
    }

    clear() {
        this.head = 0
        this.tail = 0;
        this.count = 0;
    }

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

        this.data[head] = element;

        const size = this.size;

        const newHead = (head + 1) % size;

        this.head = newHead;

        if (this.count === size) {
            //update tail
            this.tail = (this.tail + 1) % size;
        } else {
            this.count++;
        }

        if (newHead < head) {
            //wrap around just happened
        }
    }

    /**
     * Remove element from the tail
     * @returns {V|undefined}
     */
    shift() {
        if (this.count === 0) {
            //nothing to remove
            return undefined;
        }

        const element = this.data[this.tail];

        this.count--;
        this.tail = (this.tail + 1) % this.size;

        return element;
    }

    /**
     *
     * @param {number} index
     */
    removeElementByIndex(index) {
        if (index >= this.count) {
            return;
        }

        //shift elements back
        const size = this.size;
        const tail = this.tail;
        const count = this.count;

        const data = this.data;

        for (let i = index; i < count; i++) {
            const p = tail + i;

            const j = (p) % size;
            const k = (p + 1) % size;

            data[j] = data[k];
        }

        // Clear the last element (important for GC if holding objects)
        data[(tail + count - 1) % size] = undefined;

        // update head and handle underflow
        this.head = (this.head - 1 + size) % size;

        this.count--;
    }

    /**
     * Removed first element that fulfills the criteria
     * @param {function(V):boolean} condition
     * @param {*} [thisArg]
     * @returns {V|undefined}
     */
    removeIf(condition, thisArg) {
        assert.isFunction(condition, 'condition');

        const count = this.count;
        const size = this.size;

        const tail = this.tail;
        const data = this.data;

        for (let i = 0; i < count; i++) {
            const index = (tail + i) % size;
            const el = data[index];

            if (condition.call(thisArg, el)) {

                this.removeElementByIndex(i);

                return el;
            }
        }
    }

    /**
     *
     * @param {function(V)} visitor
     * @param {*} [thisArg]
     */
    forEach(visitor, thisArg) {
        assert.isFunction(visitor, 'visitor');

        const count = this.count;
        const size = this.size;

        const tail = this.tail;
        const data = this.data;

        for (let i = 0; i < count; i++) {

            const index = (tail + i) % size;
            const el = data[index];

            visitor.call(thisArg, el);
        }
    }

    /**
     *
     * @param {V} value
     * @returns {boolean}
     */
    contains(value) {
        // TODO do a bounds check
        return this.data.indexOf(value) !== -1;
    }
}
