//** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */ /** */

/**
 * A pool of integers in a specified range [min, max]. Integers can be popped from the pool and later pushed
 * back so they become available for pop again. When all numbers in range [min, max] have been popped the pool
 * is empty. The pool is optimized to use as little memory as possible with the assumption that
 * {@link push} is called a lot less than {@link pop}.
 */
export class NumberPool {
    private _nextNumberIfReusableNumbersIsEmpty: number;

    private _popCount = 0;

    /**
     * Pool of resuable integers we have called {@link push} for.
     */
    private readonly _reusableNumbers = new Set<number>();

    /**
     * Integer that is returned by {@link peek} and {@link pop} when pool is empty.
     */
    public readonly outOfNumbers: number;

    /** Maximum integer available in the pool.*/
    public readonly maxNumber: number;

    /**
     * Constructor
     * @param minNumber Smallest integer available to the pool
     * @param maxCount Number of integers available in the pool.
     */
    public constructor(
        public readonly minNumber: number,
        public readonly maxCount: number
    ) {
        this.outOfNumbers = minNumber - 1;
        this._nextNumberIfReusableNumbersIsEmpty = minNumber;
        this.maxNumber = minNumber + maxCount - 1;
    }

    /**
     * Peek on next integers that will be returned when {@link pop} is called `popCallCount` number
     * of times (default is 1, must be >= 1).
     * If pool would be empty after `popCallCount` number of calls to {@link pop} then {@link outOfNumbers} is returned.
     * @param popCallCount Default is 1, must be >= 1.
     * @returns Next integer that will be returned when {@link pop} is called `popCallCount` number of
     * of times.
     */
    public peek(popCallCount = 1): number {
        if (popCallCount < 1) throw new Error(`popCallCount: ${popCallCount} < 1`);
        if (this.isEmpty) {
            return this.outOfNumbers;
        }

        // Attempt to find the first number that will be returned
        // if we call pop popCallCount amount of times. First we have to
        // look in the set of reusable numbers.

        if (popCallCount <= this._reusableNumbers.size) {
            // TODO this operation can actually be made a lot faster if we
            // tracked reusableNumbers in a array as well. In that case we could
            // simply use popCallCount as  a index directly into the array to find the
            // next number. If popCallCount falls outside the bounds of the array we would
            // simply calculate next number like normal.
            const keys = this._reusableNumbers.keys();
            let nextNumber = 0;
            for (let i = 0; i < popCallCount; ++i) {
                nextNumber = keys.next().value!;
            }
            return nextNumber;
        }

        popCallCount -= this._reusableNumbers.size;
        // Nothing found in reusable numbers. Attempt to calculate it instead
        const nextNumber = this._nextNumberIfReusableNumbersIsEmpty + popCallCount - 1;
        return nextNumber > this.maxNumber ? this.outOfNumbers : nextNumber;
    }

    /**
     * If `true` then no more integers can be popped from the pool (by calling {@link pop}).
     * @returns `true` then no more integers can be popped from the pool.
     */
    public get isEmpty(): boolean {
        return this._popCount === this.maxCount;
    }

    /**
     * If `true` then pool is full.
     * @returns `true` if pool is full.
     */
    public get isFull(): boolean {
        return this._popCount === 0;
    }

    /**
     * Count of integers that have been popped from the pool by calling {@link pop} and that have not yet been
     * pushed by into the pool by using {@link push}.
     */
    public get popCount(): number {
        return this._popCount;
    }

    /** Count of integers still available in the pool. */
    public get availableCount(): number {
        return this.maxCount - this._popCount;
    }

    /**
     * Pop next available integer. Can be a number that have been pushed by calling {@link push}.
     * @returns Next available integer. Will be {@link outOfNumbers} if pool is empty.
     */
    public pop(): number {
        if (this.isEmpty) {
            return this.outOfNumbers;
        }
        this._popCount++;
        if (this._reusableNumbers.size === 0) {
            // No reusable integer available. Create a new one.
            return this._nextNumberIfReusableNumbersIsEmpty++;
        } else {
            // We have reusable integers so return one of those instead.
            const ret: number = this._reusableNumbers.keys().next().value!;
            this._reusableNumbers.delete(ret);
            return ret;
        }
    }

    /**
     * Reset pool. Makes all integers available again.
     */
    public clear(): void {
        this._popCount = 0;
        this._nextNumberIfReusableNumbersIsEmpty = this.minNumber;
        this._reusableNumbers.clear();
    }

    /**
     * Push a integer to the pool. It must previously have been popped by calling {@link pop}. A later call to {@link pop} may then return it.
     * @param n number to push.
     * @returns `true` if integer was added to the pool. Otherwise `false`. Either the integer already existed in the pool
     * or it has never been popped by calling {@link pop}.
     */
    public push(n: number): boolean {
        if (Math.round(n) !== n) throw new Error(`n: ${n} is not a integer`);
        if (
            n < this.minNumber ||
            n > this.maxNumber ||
            this.isFull ||
            n >= this._nextNumberIfReusableNumbersIsEmpty ||
            this._reusableNumbers.has(n)
        ) {
            return false;
        }
        this._popCount--;
        this._reusableNumbers.add(n);
        return true;
    }
}
