const ComputerParameterMode = require("./ComputerParameterMode");

/**
 * The stack, or memory, for the Intcode Computer
 *
 * @author Apis Necros
 */
module.exports = class Stack {
    constructor(stack) {
        this.pointer = 0;
        this._stack = stack;

        /**
         * The relative base offset for the stack
         *
         * This value is used when finding a parameter in Relative Parameter Mode.
         * It is added to an address to find the needed value at the calculated
         * address.
         */
        this.relativeBaseOffset = 0;
    }

    /**
     * Move the stack's pointer to the right by 1
     * @returns {void}
     */
    Next() {
        this.pointer++;
        return this;
    }

    /**
     * Move the stack's pointer to the left by 1
     * @returns {void}
     */
    Prev() {
        this.pointer--;
    }

    /**
     * Get the value from the stack by the pointer's current position
     *
     * When `isOutput` is set to `true`, then a value retrieved in Immediate Mode,
     * or the result of a Relative Mode offset is returned.
     *
     * @param {number} [parameterMode=0] The Parameter Mode to use to retrieve the value
     * @param {boolean} [isOutput=false] Is the parameter going to be an output position
     * @returns {number} The value at the current pointer position on the stack
     */
    Get(parameterMode = ComputerParameterMode.POSITION_MODE, isOutput = false) {
        let value = this._stack[this.pointer];
        if (!isOutput && parameterMode == ComputerParameterMode.POSITION_MODE) {
            value = this._stack[value];
        }
        else if (parameterMode == ComputerParameterMode.RELATIVE_MODE) {
            const newPointer = value + this.relativeBaseOffset;
            value = isOutput ? newPointer : this._stack[newPointer];
        }

        return value ?? 0;
    }

    /**
     * Get a value at a given index on the stack
     *
     * @param {number} index The index of the value to retrieve
     * @param {number} [parameterMode=0] The Parameter Mode to use to retrieve the value
     * @returns {number} The value at the given index, or 0 if the index hasn't been defined yet
     */
    GetAtIndex(index, parameterMode = ComputerParameterMode.POSITION_MODE) {
        if (index == null || Number.isNaN(index) || !Number.isInteger(index)) {
            throw new TypeError("index must be an integer");
        }

        let value = this._stack[index];
        if (parameterMode == ComputerParameterMode.POSITION_MODE) {
            value = this._stack[value];
        }
        else if (parameterMode == ComputerParameterMode.RELATIVE_MODE) {
            value = this._stack[value + this.relativeBaseOffset];
        }

        return value ?? 0;
    }

    /**
     * Push a new value onto the end of the stack
     *
     * @param {number} value The value to add
     * @returns {void}
     */
    Push(value) {
        this._stack[++this.pointer] = value;
    }

    /**
     * Pop a value off the end of the stack
     *
     * @returns {number}
     * @returns {void}
     */
    Pop() {
        return this._stack.pop();
    }

    /**
     * Overwrite the value at a given index with a new value
     *
     * The index need not be defined
     *
     * @param {number} index The index on the stack to write a value to
     * @param {number} value The value to write to that position
     * @returns {void}
     */
    Put(index, value) {
        this._stack[index] = value;
    }

    /**
     * Sets the pointer to a new address in memory
     *
     * If the address is out of the upper bounds of the current memory stack,
     * the stack will be expanded, filling with 0's as needed.
     *
     * @param {number} newAddress The new pointer address
     * @returns {void}
     */
    SetPointerAddress(newAddress) {
        if (newAddress > this._stack.length) {
            // Expand the stack if needed
            const oldLength = this._stack.length;
            this._stack[newAddress] = 0;
            this._stack.fill(0, oldLength, newAddress);
        }

        this.pointer = newAddress;
    }

    /**
     * Adjust the Relative Base Offset by an amount
     *
     * @param {number} adjustment A positive or negative integer to add to the Relative Base Offset
     * @returns {void}
     */
    AdjustRelativeBaseOffset(adjustment) {
        this.relativeBaseOffset += adjustment;
    }

    /**
     * Return the whole stack
     *
     * @returns {number[]} The current stack
     */
    Dump() {
        return this._stack;
    }
};