import ExpandedSymbol from '../entities/ExpandedSymbol';
import ReelSymbol from '../entities/ReelSymbol';
import BaseMachine from '../slotMachines/BaseMachine';
import AnimationProgressHook from '../utils/hooks/AnimationProgressHook';
import Hook from '../utils/hooks/Hook';
import HooksChain from '../utils/hooks/HooksChain';
import BaseSystem from './BaseSystem';

/**
 * A pair of expanded symbols and top reel symbols.
 * */
type ExpandedSymbolPair = {
    expandedSymbol: ExpandedSymbol; // replace with the proper type
    reelSymbol: ReelSymbol; // replace with the proper type
};

/**
 * Configuration settings for symbols class and zIndex.
 */
export interface SpinningExpandedSymbolsSystemSettings
{
    entityClass: typeof ExpandedSymbol,
}

/**
 * @class SpinningExpandedSymbolsSystem
 * @extends BaseSystem<SpinningExpandedSymbolsSystemSettings>
 * @description Handles the spinning behavior of ExpandedSymbol entities in a slot machine game,
 * including the initialization, updating, and stopping of expanded symbols.
 */
export default class SpinningExpandedSymbolsSystem extends BaseSystem<SpinningExpandedSymbolsSystemSettings>
{
    static override requiredMaps: string[] = ['symbols'];
    static override requiredUtils: string[] = ['grid'];
    static override sharedSystemData: string[] = ['reelsStates'];

    static defaultSettings = {
        entityClass: ExpandedSymbol,
    };

    public state: 'active' | 'stopped';

    protected expandedSymbolsPairs: ExpandedSymbolPair[] = [];

    private reelsStates: string[] = [];
    private hooks: Array<Hook | HooksChain> = [];

    /**
     * Initializes machine settings.
     * @class
     * @param {BaseMachine} machine - The slot machine instance.
     */
    constructor(machine: BaseMachine)
    {
        super(machine);
        this.setSettings(SpinningExpandedSymbolsSystem.defaultSettings);
    }

    /**
     * Handles the actions to be performed when entering the system.
     * Initializes the state to active.
     */
    override onEnter(): void
    {
        this.state = 'active';
        this.expandedSymbolsPairs = [];
        const existingExpandedSymbols = this.machine.children.filter(c => c instanceof this.settings.entityClass);
        existingExpandedSymbols.forEach(expandedSymbol =>
        {
            const reelSymbols = this.getReelSymbols(expandedSymbol.reelIndex);
            this.expandedSymbolsPairs.push({
                expandedSymbol,
                reelSymbol: reelSymbols[0],
            });
        });
    }

    /**
     * Updates the state of the spin system based on the current state.
     * @param dt - The delta time since the last update, in milliseconds.
     */
    update(dt: number): void
    {
        // Track the state of each reel and perform actions based on the state
        this.machine.sharedSystemData.reelsStates.forEach((currentState: string, reelIndex: number) =>
        {
            // Handle symbol creation, animation, and landing based on reel state
            if (currentState !== this.reelsStates[reelIndex])
            {
                const expandedSymbolData = this.machine.inputs.expandedSymbolsData?.[reelIndex];
                const reelSymbols = this.getReelSymbols(reelIndex);
                const reelSymbol = reelSymbols[0];
                const expandedSymbol = reelSymbol && this.getExpandedSymbolPair(reelSymbol);
                switch (currentState)
                {
                    case 'Deceleration':
                        if (expandedSymbolData && reelSymbol && !expandedSymbol)
                        {
                            this.createSymbol(expandedSymbolData, reelIndex);
                        }
                        break;
                    case 'Bouncing':
                        if (expandedSymbol)
                        {
                            this.animateSymbol(expandedSymbol);
                        }
                        break;
                    case 'Stopped':
                        if (expandedSymbol)
                        {
                            this.landSymbol(expandedSymbol);
                        }
                        break;
                }
                this.reelsStates[reelIndex] = currentState;
            }
        });

        // Update expanded symbols
        this.expandedSymbolsPairs.forEach(({ expandedSymbol }) =>
        {
            if (this.reelsStates[expandedSymbol.reelIndex] !== 'Stopped')
            {
                this.updateSymbol(expandedSymbol);

            }
        });

        // Update hooks
        this.hooks = this.hooks.filter((hook) =>
        {
            hook.update(dt);
            return !hook.isComplete();
        });

        // Stop when reels are stopped and no hooks are running
        if (this.hooks.length === 0 && this.reelsStates.every((state) => state === 'Stopped'))
        {
            this.state = 'stopped';
        }
    }

    /**
     * Handles the actions to be performed when a reel is decelerating.
     * @param expandedSymbolData - The data for the expanded symbol to create.
     * @param reelIndex - The index of the reel that is decelerating.
     * @returns {ExpandedSymbol}
     */
    protected createSymbol(expandedSymbolData: any, reelIndex: number): ExpandedSymbol
    {
        const {key, value} = expandedSymbolData;
        const config = this.machine.config.expandedSymbols[key];
        const expandedSymbol = new this.settings.entityClass({ ...config, value });
        expandedSymbol.reelIndex = reelIndex;
        expandedSymbol.zIndex = config.zIndex;
        this.machine.addChild(expandedSymbol);
        const reelSymbols = this.getReelSymbols(reelIndex);
        reelSymbols.forEach((symbol) => symbol && (symbol.alpha = 0));
        this.expandedSymbolsPairs.push({
            expandedSymbol,
            reelSymbol: reelSymbols[0],
        });
        return expandedSymbol;
    }

    /**
     * Handles the actions to be performed when a reel is stopped.
     * @param expandedSymbol - The expanded symbol to animate.
     * @returns {void}
     */
    protected animateSymbol(expandedSymbol: ExpandedSymbol): void
    {
        const inHook = new AnimationProgressHook(() => expandedSymbol.in());
        const chain = new HooksChain();
        chain.addHook(inHook);
        this.hooks.push(chain);
    }

    /**
     * Handles the actions to be performed when a reel is stopped.
     * @param expandedSymbol - The expanded symbol to land.
     * @returns {void}
     */
    protected landSymbol(expandedSymbol: ExpandedSymbol): void
    {
        expandedSymbol.isLanded = true;
    }

    /**
     * Handles the actions to be performed while a reel is spinning.
     * @param expandedSymbol - The expanded symbol to update.
     * @returns {void}
     */
    updateSymbol(expandedSymbol: ExpandedSymbol): void
    {
        this.expandedSymbolsPairs = this.expandedSymbolsPairs.filter(pair =>
        {
            if (pair.expandedSymbol !== expandedSymbol) return true;
            const isPairedReelSymbolInMachine = this.machine.getChildByName(pair.reelSymbol.name);
            if (isPairedReelSymbolInMachine)
            {
                const offset = 0.5 * (this.machine.config.height - this.machine.config.height / this.machine.config.slots);
                expandedSymbol.position.set(
                    pair.reelSymbol.position.x,
                    pair.reelSymbol.position.y + offset,
                );
                return true;
            }
            expandedSymbol.destroy();
            return false;
        });
    }

    /**
     * Retrieves the symbols for a specific reel index.
     * @param reelIndex - The index of the reel to get symbols for.
     * @returns An array of symbols for the specified reel.
     */
    protected getReelSymbols(reelIndex: number): ReelSymbol[]
    {
        return this.machine.maps.symbols
            .filter((_: string, idx: number) =>
            {
                const reelCoords = this.machine.utils.grid.fromMapIndexToGridCoords(idx);
                return reelCoords.x === reelIndex;
            })
            .map((id: string) => this.machine.getChildByName(id));
    }

    /**
     * Retrieves the reelSymbol for expandedSymbol.
     * @param expandedSymbol - The expandedSymbol to get reelSymbol for.
     * @returns The reelSymbol for the expandedSymbol.
     */
    protected getReelSymbolPair(expandedSymbol: ExpandedSymbol)
    {
        return this.expandedSymbolsPairs.find(pair => pair.expandedSymbol === expandedSymbol)?.reelSymbol;
    }

    /**
     * Retrieves the expandedSymbol for reelSymbol.
     * @param reelSymbol - The reelSymbol to get expandedSymbol for.
     * @returns The expandedSymbol for the reelSymbol.
     */
    protected getExpandedSymbolPair(reelSymbol: ReelSymbol)
    {
        return this.expandedSymbolsPairs.find(pair => pair.reelSymbol === reelSymbol)?.expandedSymbol;
    }

    /**
     * Handles the actions to be performed when leaving the current state.
     * @returns {void}
     */
    onLeave(): void
    {
    }
}
