Source: fragments/lifecycle/fragment_lifecycle_manager.js

//@ts-check
/**
 * Manages fragment and view panel life cycle
 */
class FragmentLifeCycleManager{

    constructor(){

        /**
         * @type {number}
         */
        this.currentLifeCycleStage = FragmentLifeCycleManager._LIFECYCLE_STAGES.destroyed;

        /**
         * @type {LifeCycleListenerGroup[]}
         */
        this.registeredLifeCycleListeners = [];

        this.viewReady = false;
    }

    static get _LIFECYCLE_STAGES(){

        return {

            running: 0,
            destroyed: 1,
            cancelled: 2 //Special stage where the fragment or panel not destroyed. Current build cancelled for a new one
            //Above needed cause functionally, will not remove existing listeners. So, only add listeners on construct
        }
    }

    /**
     * 
     * @param {number} stage 
     * @returns 
     */
    static getNameOfLifecycleStage(stage){

        let stageName = "";
        for(let key in FragmentLifeCycleManager._LIFECYCLE_STAGES){

            if(FragmentLifeCycleManager._LIFECYCLE_STAGES[key] = stage){

                stageName = key;
                break;
            }
        }

        return stageName;
    }

    /**
     * 
     * @param {number} newStage
     */
    transitionLifeCycle(newStage){

        if(newStage === this.currentLifeCycleStage){

            const msg = newStage === FragmentLifeCycleManager._LIFECYCLE_STAGES.destroyed ? FragmentLifeCycleManager.getNameOfLifecycleStage(FragmentLifeCycleManager._LIFECYCLE_STAGES.running) : FragmentLifeCycleManager.getNameOfLifecycleStage(FragmentLifeCycleManager._LIFECYCLE_STAGES.destroyed);
            throwLifeCycleTransitionError(this.currentLifeCycleStage, newStage, msg)
        }

        if(this.currentLifeCycleStage === FragmentLifeCycleManager._LIFECYCLE_STAGES.destroyed){

            if(newStage !== FragmentLifeCycleManager._LIFECYCLE_STAGES.running){

                throwLifeCycleTransitionError(this.currentLifeCycleStage, newStage, FragmentLifeCycleManager.getNameOfLifecycleStage(FragmentLifeCycleManager._LIFECYCLE_STAGES.running));
            }
        }

        //Kinda redundant check, FOR NOW, cause same as not repeating state as only running only valid. FOR NOW
        else if(this.currentLifeCycleStage === FragmentLifeCycleManager._LIFECYCLE_STAGES.running){

            if(newStage !== FragmentLifeCycleManager._LIFECYCLE_STAGES.destroyed && newStage !== FragmentLifeCycleManager._LIFECYCLE_STAGES.cancelled){

                throwLifeCycleTransitionError(this.currentLifeCycleStage, newStage, FragmentLifeCycleManager.getNameOfLifecycleStage(FragmentLifeCycleManager._LIFECYCLE_STAGES.destroyed));
            }
        }


        if(newStage === FragmentLifeCycleManager._LIFECYCLE_STAGES.destroyed){

            this.onLifeCycleDestroy()
        } else if(newStage === FragmentLifeCycleManager._LIFECYCLE_STAGES.running){

            this.onLifeCycleRunning();
        } else if(newStage === FragmentLifeCycleManager._LIFECYCLE_STAGES.cancelled){

            this.onLifeCycleCancelled();
        }

        /**
         * 
         * @param {number} currStage 
         * @param {number} newStage 
         * @param {string} validStage
         */
        function throwLifeCycleTransitionError(currStage, newStage, validStage){

            throw new Error(`Cannot transition fragment's life cycle from ${FragmentLifeCycleManager.getNameOfLifecycleStage(currStage)} to ${FragmentLifeCycleManager.getNameOfLifecycleStage(newStage)}. Only next valid stage is ${validStage}`);
        }
    }

    onLifeCycleDestroy(){

        this.currentLifeCycleStage = FragmentLifeCycleManager._LIFECYCLE_STAGES.destroyed;
        this.registeredLifeCycleListeners.forEach((listenerGroup) => {

            listenerGroup.onFragmentDestroyed();

            //deregistering all listeners. And cause of list shrinking,  globally below
            //LOGIC - All destroyed fragments or view panels (extending this) are never reinflated
            //so, don't want memory leaks or null calls
        });

        this.registeredLifeCycleListeners = [];
    }

    onLifeCycleRunning(){

        this.currentLifeCycleStage = FragmentLifeCycleManager._LIFECYCLE_STAGES.running;

        this.registeredLifeCycleListeners.forEach((listenerGroup) => {

            listenerGroup.onFragmentRunning();
        });
    }

    onLifeCycleCancelled(){

        this.currentLifeCycleStage = FragmentLifeCycleManager._LIFECYCLE_STAGES.cancelled;

        this.registeredLifeCycleListeners.forEach((listenerGroup) => {

            listenerGroup.onFragmentCancelled();
        });
    }

    onViewReady(){

        if(this.currentLifeCycleStage === FragmentLifeCycleManager._LIFECYCLE_STAGES.running){

            this.viewReady = true;
            //trigger listeners
            this.registeredLifeCycleListeners.forEach((listenerGroup) => {

                listenerGroup.onViewReady?.();
            })
        } else {

            throw new Error("Early onViewReady call. Your fragment/view manager is not running");
        }
    }

    /**
     * 
     * @param {LifeCycleListenerGroup} listenerGroup
     */
    registerLifeCycleListeners(listenerGroup){

        this.registeredLifeCycleListeners.push(listenerGroup);

        //Fire for running, if already running, then viewReady. In case developer wants to act based on that
        if(this.currentLifeCycleStage === FragmentLifeCycleManager._LIFECYCLE_STAGES.running){

            listenerGroup.onFragmentRunning();
            if(this.viewReady){

                listenerGroup.onViewReady?.();
            }
        }
    }

    /**
     * 
     * @param {LifeCycleListenerGroup} listeners 
     */
    deregisterLifeCycleListeners(listeners){

        const targetIndex = this.registeredLifeCycleListeners.findIndex((listenerGroup) => listenerGroup === listeners);
        if(targetIndex !== -1){

            this.registeredLifeCycleListeners.splice(targetIndex, 1);
        } else {

            console.error("Failed to deregister lifecycle events listener");
            console.log(this.registeredLifeCycleListeners.length);
        }
    }

    /**
     * @returns {boolean}
     */
    isFragmentLifeCycleRunning(){

        return this.currentLifeCycleStage === FragmentLifeCycleManager._LIFECYCLE_STAGES.running;
    }
}

if(false){

    /**
     * @type {import("FragmentLifeCycleManager").FragmentLifeCycleManagerConstructor}
     */
    const check = FragmentLifeCycleManager;
}

export default FragmentLifeCycleManager;