import { assert } from "../../assert.js";
import { ceilPowerOfTwo } from "../../binary/operations/ceilPowerOfTwo.js";
import { UINT32_MAX } from "../../binary/UINT32_MAX.js";
import { max2 } from "../../math/max2.js";
import { array_copy } from "../array/array_copy.js";

const DEFAULT_SIZE = 16;

const STATUS_FULL = 0;
const STATUS_EMPTY = 1;
const STATUS_NORMAL = 2;

/**
 * When growing the underlying array, how much to grow by. This is a multiplier.
 * Must be greater than 1
 * @type {number}
 */
const GROWTH_FACTOR = 2;

const EMPTY_ARRAY = new Array(0);

/**
 * Queue data structure, first-in-first-out (FIFO).
 * Double-ended queue backed by an array.
 * The fact that it's double-ended means you can add and remove from both ends.
 *
 * Note that the implementation is optimized for high speed and low memory usage; it avoids allocations entirely.
 * @template T
 */
export class Deque {

    /**
     * Using static array allocator to preserve data locality.
     * Initialization via "[]" would give us a dynamically sized array,
     * but it would have unpredictable locality as array may or may not have its elements stored next to each other in memory
     * @type {T[]}
     * @private
     */
    #data = EMPTY_ARRAY;

    /**
     * Index of the front of the queue inside the internal data array
     * @type {number}
     * @private
     */
    #head = 0;

    /**
     * Index of the back of the queue inside the internal data array
     * @type {number}
     * @private
     */
    #tail = 0;

    /**
     *
     * @type {number}
     * @private
     */
    #status = STATUS_EMPTY;

    /**
     * @template T
     * @param {number} [min_size]
     */
    constructor(min_size = DEFAULT_SIZE) {

        assert.isNonNegativeInteger(min_size, 'minSize');

        const size = ceilPowerOfTwo(max2(1, min_size));

        this.#data = new Array(size);

    }

    /**
     *
     * @param {boolean} adding
     * @private
     */
    #reset_status(adding) {
        const head = this.#head;
        const tail = this.#tail;

        if (head === tail) {
            this.#status = adding ? STATUS_FULL : STATUS_EMPTY;
        } else {
            this.#status = STATUS_NORMAL;
        }
    }

    /**
     *
     * @param {number} current
     * @returns {number}
     * @private
     */
    #circular_next_position(current) {
        const next = current + 1;

        const length = this.#data.length;

        return (next >= length) ? 0 : next;
    }

    /**
     *
     * @param {number} current
     * @returns {number}
     * @private
     */
    #circular_previous_position(current) {
        const prev = current - 1;

        const length = this.#data.length;

        const last_index = length - 1;

        return (prev < 0) ? last_index : prev;
    }

    #check_and_expand() {
        const status = this.#status;

        if (status !== STATUS_FULL) {
            // queue still has space, we're done
            return;
        }

        const length = this.#data.length;

        if (UINT32_MAX === length) {
            // array is already as big as it can get
            throw new Error('Maximum array size exceeded');
        }

        // double existing length
        let new_length = length * GROWTH_FACTOR;

        if (new_length > UINT32_MAX) {
            // clamp to max uint32 value
            new_length = UINT32_MAX;
        }

        /**
         *
         * @type {T[]}
         */
        const new_data = new Array(new_length);

        const head = this.#head;

        // copy the front portion
        array_copy(this.#data, head, new_data, 0, length - head);
        // copy the remainder
        array_copy(this.#data, 0, new_data, length - head, head);

        this.#head = 0;
        this.#tail = length;

        this.#status = STATUS_NORMAL;

        this.#data = new_data;
    }

    /**
     *
     * @return {boolean}
     */
    isEmpty() {
        return this.#status === STATUS_EMPTY;
    }

    /**
     * Clear data from the queue.
     * Makes the queue empty.
     * @returns {void}
     */
    clear() {
        if (this.#status !== STATUS_EMPTY) {
            let cursor = this.#head;
            const tail = this.#tail;

            do {
                this.#data[cursor] = undefined;
                cursor = this.#circular_next_position(cursor);
            } while (cursor !== tail);

            this.#status = STATUS_EMPTY;
        }

        this.#head = 0;
        this.#tail = 0;
    }

    /**
     * Number of elements in the collection
     * @returns {number}
     */
    size() {
        const data = this.#data;

        if (this.#status === STATUS_FULL) {
            return data.length;
        }

        const head = this.#head;
        const tail = this.#tail;

        return (head <= tail) ? (tail - head) : (tail + data.length - head);
    }

    /**
     *
     * @param {number} current
     * @private
     */
    #remove_internal_shift_backward(current) {
        let cursor = current;

        // shift towards head, this has a better data access pattern than shifting the other way as we're going through the data in forward sequence

        const tail = this.#tail;

        while (cursor !== tail) {
            const next = this.#circular_next_position(cursor);
            this.#data[cursor] = this.#data[next];
            cursor = next;
        }

        this.#tail = this.#circular_previous_position(tail);


        // fill in slot of last moved element
        this.#data[cursor] = undefined;

        this.#reset_status(false);
    }

    /**
     * Remove the first occurrence of an element from the queue. This means that if there are 2 instances of an element in the queue, only one will be removed.
     *
     * Note: the queue is intended for sequential access primarily, so if you often find yourself needing this method - consider using an alternative data structure, such as a {@link Map} or a {@link Set} as those excel at random access.
     *
     * @param {T} e element to remove
     * @returns {boolean} true if the element was removed, false if the element was not found.
     */
    remove(e) {
        const i = this.#index_of(e);

        if (i === -1) {
            return false;
        }

        if(i === this.#head){
            // special case
            this.removeFirst();
        } else if(i === this.#circular_previous_position(this.#tail)){
            // special case
            this.removeLast();
        }else {
            // somewhere in the middle, slower option
            this.#remove_internal_shift_backward(i);
        }

        return true;
    }

    /**
     *
     * @param {T} e
     * @returns {number} absolute index of the element in the backing array, or -1 if not found
     * @private
     */
    #index_of(e) {
        const size = this.size();

        const data = this.#data;
        const capacity = data.length;
        const head = this.#head;

        for (let i = 0; i < size; i++) {
            const index = (head + i) % capacity;

            const el = data[index];

            if (el === e) {
                return index;
            }
        }

        return -1;
    }

    /**
     *
     * @param {T} e
     * @returns {boolean} if the queue contains the element
     */
    has(e) {
        return this.#index_of(e) !== -1;
    }

    /**
     * Add the element at the front of the queue
     * @param {T} e
     */
    addFirst(e) {
        this.#check_and_expand();
        this.#head = this.#circular_previous_position(this.#head);
        this.#data[this.#head] = e;
        this.#reset_status(true);
    }

    /**
     * Remove an element from the front of the queue
     * @returns {T|undefined}
     */
    removeFirst() {
        const element = this.#data[this.#head];
        this.#data[this.#head] = undefined;
        this.#head = this.#circular_next_position(this.#head);
        this.#reset_status(false);
        return element;
    }

    /**
     * Peek an element from the front of the queue without removing it
     * @returns {T|undefined}
     */
    getFirst() {
        return this.#data[this.#head];
    }

    /**
     * Add the element at the end of the queue
     * @param {T} e
     * @returns {void}
     */
    addLast(e) {
        this.#check_and_expand();
        this.#data[this.#tail] = e;
        this.#tail = this.#circular_next_position(this.#tail);
        this.#reset_status(true);
    }

    /**
     * Remove an element from the end of the queue
     * @returns {T}
     */
    removeLast() {
        const last = this.#circular_previous_position(this.#tail);
        const element = this.#data[last];
        this.#data[last] = undefined;
        this.#tail = last;
        this.#reset_status(false);
        return element;
    }


    /**
     * Peek an element from the end of the queue without removing it
     * @returns {T|undefined}
     */
    getLast() {
        const last = this.#circular_previous_position(this.#tail);
        return this.#data[last];
    }

    /**
     * Retrieves the element by sequential position from the start of the queue; the element at the start is position 0.
     * @param {number} index
     * @returns {T|undefined} undefined if indexing the element that is past the end, otherwise returns element at the position given
     */
    getElementByIndex(index) {
        assert.isNonNegativeInteger(index, 'index');

        if (index >= this.size()) {
            // overflow
            return undefined;
        }

        const data = this.#data;

        /**
         *
         * @type {number}
         */
        const position = (this.#head + index) % data.length;

        return data[position];
    }

    /**
     * Returns a copy of this queue represented as an array without gaps, with the head of the queue (first element) being at position 0
     * @param {T[]} [result]
     * @param {number} [result_offset]
     * @returns {T[]}
     */
    toArray(result = [], result_offset = 0) {
        const size = this.size();

        for (let i = 0; i < size; i++) {
            result[result_offset + i] = this.getElementByIndex(i);
        }

        return result;
    }

    /**
     *
     * @returns {Generator<T,void>}
     */
    * [Symbol.iterator]() {

        const size = this.size();

        for (let i = 0; i < size; i++) {
            yield this.getElementByIndex(i);
        }
    }
}

/*
Stack methods
 */

/**
 * Stack operation. Alias of {@link Deque.prototype.getFirst}
 */
Deque.prototype.peek = Deque.prototype.getFirst;
/**
 * Stack operation. Alias of {@link Deque.prototype.addFirst}
 */
Deque.prototype.push = Deque.prototype.addFirst;
/**
 * Stack operation. Alias of {@link Deque.prototype.removeFirst}
 */
Deque.prototype.pop = Deque.prototype.removeFirst;

/**
 * Alias of {@link Deque.prototype.addLast}
 */
Deque.prototype.add = Deque.prototype.addLast;
