Source: state_tracker.js

import { InformationSource } from "./reporter/information_source.js";

/** 
 * StateTracker
 * Tracks states and brings undo/redo functionality
 */

class StateTracker extends InformationSource {

    /**
     * @param {Reporter} reporter 
     * @param {Object} settings 
     */

    constructor(reporter, settings) {

        super(reporter, settings);

        this.maxStateCount = Number( settings.maxStateCount || 5 );

        this.addEvent('statechange');

        this.resetState();
    }

    /** 
     * @protected
     * @type {Array<any>} 
     */

    _stateRegister;


    /**
     * @protected
     * @type {number}
     */

    _cursor;

    get cursor() {
        return this._cursor;
    }


    get state() {
        return this._stateRegister[ this._cursor ];
    }


    resetState() {
        // console.log('reset')

        this._stateRegister = [];
        this._cursor = 0;
    }


    cleanUpStateItem( item ) {
        if (typeof item.cleanUp === 'function')  {
            item.cleanUp({ 
                state: item, 
                stateRegister: this._stateRegister, 
                maxStateCount: this.maxStateCount 
            });
        }
    }

    /**
     * Add a new state to the register
     * @param {Object} data
     * @param {any} data.newState
     * @param {(false|true|'silent')} [data.moveCursor = true]
     */

    async addState({ newState, moveCursor = true }) {

        // console.log(this.label, 'addState', newState );

        this._stateRegister = this._stateRegister.slice(
            // Math.max( 0, this._cursor - this.maxStateCount + 1),
            0,
            this._cursor + 1
        );


        if ( this._stateRegister.length > this.maxStateCount) {
            //this.log(this.label, 'too many states', this._stateRegister.length);

            for ( let i = 0 ; i < this._stateRegister.length - this.maxStateCount ; i += 1) {

                let removeItem = this._stateRegister.shift()

                this.report({
                    msg: `Popping old state ${removeItem.label || removeItem}`,
                    level: 'debug'
                });

                this.cleanUpStateItem( removeItem );

                // when a configurator was "only" moved, the configuration object isnt changed
                // so dont destroy it, in that case (compare with _stateRegister is ok, bc removeItem was
                // spliced off)
                // console.log('config still at index', this._stateRegister.findIndex( state => state.configuration.id === newState.configuration.id))
                // console.log( newState.configuration.id)
                // console.log( this._stateRegister.map( state => state.configuration.id ))
                //if (removeItem.configuration && this._stateRegister.find( state => state.configuration === newState.configuration) === undefined )  {
                    // console.log('destroy config')
                    //removeItem.configuration.destroy();
                    //removeItem.configuration = undefined;
                    //removeItem = undefined;
                //}

                removeItem = null;
            }
        }

        this._stateRegister.push(newState);

        // console.log(this._stateRegister.length)

        if (moveCursor === true) {
            await this.setCursor(this._stateRegister.length - 1);
        }
        else if (moveCursor === 'silent') {
            this._cursor = this._stateRegister.length - 1;
        }
        else {
            // do nothing
        }
    }

    /**
     * @param {number} newPosition
     * @returns {Promise<Object>}
     */

    async setCursor(newPosition) {

        if (!this._stateRegister[newPosition]) {
            throw new Error(`${this.label} unset cursor position ${newPosition}`);
        }

        this._cursor = newPosition;

        await this.onStateCursorMoved(this._stateRegister[newPosition]);
    }

    overwritePreviousState() {

        const head = this._stateRegister.length-1;
        const headMinusOne = this._stateRegister.length-2;

        // console.log('head', this._stateRegister[head])
        // console.log('head-1', this._stateRegister[headMinusOne])

        // const newState = Object.assign(
        //     {},
        //     this._stateRegister[headMinusOne],
        //     this._stateRegister[head]
        // )

        // console.log(newState);
        
        // this._stateRegister[headMinusOne] = newState;
        // this._stateRegister.pop();

        // console.log(this._stateRegister);

        // this.setCursor(this._cursor = newPosition;)
        this._stateRegister.splice(headMinusOne,1);
        this._cursor = headMinusOne;
    }
    
    /** @param {any} selectedState */

    onStateCursorMoved(selectedState) {
        throw new Error('Interface not implemented in extending class');
    }

    /** 
     * Go back to the previous configuration by
     * decreasing the cursor position
     * @param {number} [steps =1]
     * @returns {Promise<Object>}
     */

    undo(steps = 1) {
        return this.setCursor(Math.max(0, this._cursor - steps));
    }

    /**
     * Whether an undo is currently possible
     * @returns {Boolean}
     */

    get canUndo() {
        // console.log(`${this.label} can redo`, this._cursor, this._stateRegister.length);
        return this._cursor > 0;
    }


    /** 
     * Go forward in the configuration timeline
     * by increasing the cursor position
     * @param {number} steps
     * @returns {Promise<Object>}
     */

    redo(steps = 1) {
        return this.setCursor(Math.min(this._stateRegister.length - 1, this._cursor + steps));
    }

    /**
     * Whether a redo is currently possible
     * @returns {Boolean}
     */

    get canRedo() {
        // console.log(`${this.label} can redo`, this._cursor, this._stateRegister.length);
        return this._cursor < this._stateRegister.length - 1;
    }


    destroy() {
        this.resetState();
        if ( typeof super.destroy === 'function' ) {
            super.destroy()
        }
    }
}

export { StateTracker };