Source: FishStack.js

"use strict";

/**************************
 * Import important stuff *
 **************************/

// Nothing

/***********************
 * The FishStack class *
 ***********************/

/**
 * A stack for a ><> program. Also holds the program's register
 */
class FishStack {
	/**
	 * Creates a new FishStack
	 *
	 * @param {FishProgram} program	The program this stack is part of
	 * @param {Number[]} [initialStack]	Initial stack of the program. Defaults to an empty stack
	 */
	constructor(program, initialStack = []) {
		/**
		 * The program the stack is working for
		 *
		 * @type FishProgram
		 *
		 * @private
		 */
		this._program = program;

		/**
		 * The actual stack
		 *
		 * @type Number
		 *
		 * @private
		 */
		this._stack = [];

		// Fill the stack
		initialStack.forEach((element) => this.push(element));

		/**
		 * The stack of stacks below the current stack
		 *
		 * @type Array[]
		 *
		 * @private
		 */
		this._stackstack = [];

		/**
		 * The program's register
		 *
		 * @type Number
		 *
		 * @private
		 */
		this._register = null;
	}

	/**
	 * Number of elements in the stack
	 *
	 * @type Integer
	 *
	 * @readonly
	 */
	get length() {
		return this._stack.length;
	}

	/**
	 * The program's register
	 *
	 * @type Number
	 *
	 * @readonly
	 */
	get register() {
		return this._register;
	}

	/**
	 * A snapshot of the stack
	 *
	 * @type Number[]
	 *
	 * @readonly
	 */
	get snapshot() {
		// Return a clone of the actual stack
		return [...this._stack];
	}

	/**
	 * Pushes a number onto the stack
	 *
	 * @param {Number} number	The number to push
	 *
	 * @throws {Error}	If anything but a number was given
	 */
	push(number) {
		// Check that a number was given
		if (Number(number) !== number) {
			throw new Error("The stack works only with numbers");
		}

		// Push it onto the stack
		this._stack.push(number);
	}

	/**
	 * Pops the top value off the stack
	 *
	 * @retuns {Number}	The top number of the stack
	 *
	 * @throws {Error}	If the stack is empty
	 */
	pop() {
		// Check if the stack is empty
		if (this._stack.length === 0) {
			throw new Error("The stack is empty");
		}

		// Pop and return the top element
		return this._stack.pop();
	}

	/**
	 * Duplicates the top element of the stack, making the two top elements equal
	 *
	 * @throws {Error}	If the stack is empty
	 */
	":"() {
		// Just pop the top and push it twice
		const e = this.pop();
		this.push(e);
		this.push(e);
	}

	/**
	 * Removes the top element from the stack
	 *
	 * @throws {Error}      If the stack is empty
	 */
	"~"() {
		// Simply pop the stack and do nothing about it
		this.pop();
	}

	/**
	 * Swaps the two top elements of the stack
	 *
	 * @throws {Error}      If the stack does not contain at least two elements
	 */
	"$"() {
		// Pop the stack twice and push in reverse order
		const a = this.pop();
		const b = this.pop();
		this.push(a);
		this.push(b);
	}

	/**
	 * Swaps the three top elements, shifting them rightwards (e.g. if your stack is 1,2,3,4, calling @ results in 1,4,2,3) 
	 *
	 * @throws {Error}      If the stack does not contain at least three elements
	 */
	"@"() {
		// Pop the top three and push them back in correct order
		const a = this.pop();
		const b = this.pop();
		const c = this.pop();
		this.push(a);
		this.push(c);
		this.push(b);
	}

	/**
	 * Shifts the entire stack right. 1,2,3,4 -> 4,1,2,3
	 *
	 * @throws {Error}	If the stack is empty
	 */
	"}"() {
		// Pop the top and unshift it back in
		const a = this.pop();
		this._stack.unshift(a);
	}

	/**
	 * Shifts the entire stack left. 1,2,3,4 -> 2,3,4,1
	 *
	 * @throws {Error}	If the stack is empty
	 */
	"{"() {
		// Shift out the bottom and push it to the top
		const a = this._stack.shift();
		this.push(a);
	}

	/**
	 * Reverses the entire stack
	 */
	"r"() {
		this._stack.reverse();
	}

	/**
	 * Pushes the length of the stack onto the stack
	 */
	"l"() {
		this.push(this._stack.length);
	}

	/**
	 * Pop x off the stack and create a new stack, moving x values from the old stack onto the new one
	 *
	 * @throws {Error}	If the stack is empty or does not contain x+1 values
	 */
	"["() {
		// Pop the top number to find out how many other elements to pop
		const x = this.pop();

		// Create a new stack
		const newStack = [];

		// Fill it with the top `x` values from the previous stack
		for (let i = 0; i < x; i++) {
			newStack.push(this.pop());
		}

		// The new stack is now in the wrong order. Reverse it
		newStack.reverse();

		// Archive the old stack
		this._stackstack.push(this._stack);

		// Make the new one the active stack
		this._stack = newStack;

		// Reset the register
		this._register = null;
	}

	/**
	 * Removes the current stack, putting its values onto the one below it. If there is no stack below, the stack and register is emptied
	 */
	"]"() {
		// Check if there is a stack below
		if (this._stackstack.length > 0) {
			// Yup. Move the values of the current stack to the top of the one below
			// Reverse the current stack for easier moving
			this["r"]();

			// Get the stack below
			const oldStack = this._stackstack.pop();

			// Transfer the values to it
			while (this._stack.length > 0) {
				oldStack.push(this.pop());
			}

			// Make the old stack the current one
			this._stack = oldStack
		} else {
			// Just empty the stack
			this._stack = [];
		}

		// Empty the register
		this._register = null;
	}

	/**
	 * Manipulates the register. If it is empty, pops the top value and puts it in the registry. Otherwise, take the registry value and push it to the stack, leaving a `null` in the registry
	 */
	"&"() {
		if (this._register === null) {
			// The register is empty. Put the top value there
			this._register = this.pop();
		} else {
			// Push the register's value to the top of the stack
			this.push(this._register);

			// ..and remove the value from the register
			this._register = null;
		}
	}
}

/*************
 * Export it *
 *************/

module.exports = FishStack;