Source: collections/Stack.js

const { Collection, CollectionEvent } = require('./Collection')
, { Resolve } = require('../tools/Resolve')
, { EqualityComparer } = require('./EqualityComparer')
, { Observable, fromEvent } = require('rxjs')
, symbolStackPush = Symbol('stackPush')
, symbolStackPop = Symbol('stackPop')
, symbolStackPopBottom = Symbol('stackPopBottom');


/**
 * @template T
 * @author Sebastian Hönel <development@hoenel.net>
 */
class Stack extends Collection {
	/**
	 * Creates a new, empty Stack<T>.
	 * 
	 * @param {EqualityComparer<T>} [eqComparer] Optional. Defaults To EqualityComparer<T>.default.
	 */
	constructor(eqComparer = EqualityComparer.default) {
		super(eqComparer);
		
		/** @type {Observable<T>} */
		this.observablePush = Object.freeze(fromEvent(this, symbolStackPush));
		/** @type {Observable<T>} */
		this.observablePop = Object.freeze(fromEvent(this, symbolStackPop));
		/** @type {Observable<T>} */
		this.observablePopBottom = Object.freeze(fromEvent(this, symbolStackPopBottom));
	};

	/**
	 * Push an item to the top of the Stack.
	 * 
	 * @param {T} item 
	 * @returns {this}
	 */
	push(item) {
		this._items.push(item);
		this.emit(symbolStackPush, new CollectionEvent(item));
		return this;
	};

	/**
	 * @throws {Error} If the Stack is empty.
	 * @returns {T} The item at the top of the Stack (the item
	 * inserted last).
	 */
	pop() {
		if (this.isEmpty) {
			throw new Error('The Stack is empty.');
		}
		
		const item = this._items.pop();
		this.emit(symbolStackPop, new CollectionEvent(item));
		return item;
	};

	/**
	 * @throws {Error} If the Stack is empty.
	 * @returns {T} The item at the bottom of the Stack (the item
	 * inserted first).
	 */
	popBottom() {
		if (this.isEmpty) {
			throw new Error('The Stack is empty.');
		}

		const item = this._items.shift();
		this.emit(symbolStackPopBottom, new CollectionEvent(item));
		return item;
	};

	/**
	 * @throws {Error} If the Stack is empty.
	 * @returns {T} The item on top of the Stack without popping it.
	 */
	peek() {
		if (this.isEmpty) {
			throw new Error('The Stack is empty.');
		}

		return this._items[this.size - 1];
	};

	/**
	 * @throws {Error} If the Stack is empty.
	 * @returns {T} The item at the bottom of the Stack without popping it.
	 */
	peekBottom() {
		if (this.isEmpty) {
			throw new Error('The Stack is empty.');
		}

		return this._items[0];
	};
};


class ConstrainedStack extends Stack {
	/**
	 * Creates a new, empty Stack<T>.
	 * 
	 * @param {Number} [maxSize] Optional. Defaults to Number.MAX_SAFE_INTEGER. Use
	 * this parameter to limit the maximum amount of elements this Stack can hold.
	 * When the limit is reached and items are being further pushed, the
	 * ConstrainedStack will pop items from the BOTTOM. I.e., when pushing a new item
	 * to the top of the Stack when it is full, will discard one item at its bottom.
	 * This parameter must be a positive integer larger than zero.
	 * @param {EqualityComparer<T>} [eqComparer] Optional. Defaults To
	 * EqualityComparer<T>.default.
	 */
	constructor(maxSize = Number.MAX_SAFE_INTEGER, eqComparer = EqualityComparer.default) {
		super(eqComparer);
		
		this._maxSize = 1;
		this.maxSize = maxSize;
	};
	

	/**
	 * @type {Number}
	 */
	get maxSize() {
		return this._maxSize;
	};

	/**
	 * Sets the maximum size of this ConstrainedStack. If currently there are more
	 * items, the stack will pop items from the bottom until the number of items
	 * does not longer exceed the new maximum size. The items will be popped from
	 * the BOTTOM.
	 * 
	 * @param {Number} value The new value for maxSize. Must be an integer equal
	 * to or larger than 1.
	 * @throws {Error} If parameter value is not a number or less than one (1).
	 * @type {void}
	 */
	set maxSize(value) {
		if (!Resolve.isTypeOf(value, Number) || !Number.isInteger(value)) {
			throw new Error(`The value given for maxSize is not a number.`);
		}
		
		if (value < 1) {
			throw new Error(`The value given is less than 1: ${value}`);
		}

		this._maxSize = value;
		this._truncate();
	};

	/**
	 * @returns {this}
	 */
	_truncate() {
		let excess = this.size - this.maxSize;
		while (excess > 0) {
			// Triggers/emits symbol for poppong items from bottom.
			this.popBottom();
			excess--;
		}
		
		return this;
	};

	/**
	 * Push an item to the top of the Stack. If, after pushing this item,
	 * the Stack is larger than its specified maximum size, it will discard
	 * an item from the BOTTOM.
	 * 
	 * @override
	 * @inheritdoc
	 * @param {T} item 
	 * @returns {this}
	 */
	push(item) {
		super.push(item);
		return this._truncate();
	};
};


module.exports = Object.freeze({
	Stack,
	ConstrainedStack,
	symbolStackPop,
	symbolStackPush,
	symbolStackPopBottom
});