import ExpandedSymbol from '../entities/ExpandedSymbol';
import BaseMachine from '../slotMachines/BaseMachine';
import { SymbolConfig } from '../types/SlotMachineConfig';
import AnimationProgressHook from '../utils/hooks/AnimationProgressHook';
import DelayHook from '../utils/hooks/DelayHook';
import Hook from '../utils/hooks/Hook';
import HooksChain from '../utils/hooks/HooksChain';
import BaseSystem from './BaseSystem';

/**
 * Represents an entity compatible with the ReelsSpinSystem.
 * This entity must be able to be blocked, play idle animations, play bouncing animations, and be blurred.
 */
export type AnimatedExpandedSymbolSystemCompatibleEntity = ExpandedSymbol;

/** Configuration settings for the reel spinning behavior in the ReelsSpinSystem. */
export interface AnimatedExpandedSymbolsSystemSettings
{
    accelerationTime: number,
    reelStartDelay: number,
    reelStopDelay: number,
    spinningDuration: number,
    key: string,
    zIndex: number,
}

/** Enum representing different states of the spin process. */
const SPIN_STATES = {
    STOPPED: 'stopped',
    STARTING: 'starting',
    SPINNING: 'spinning',
    STOPPING: 'stopping'
};

/**
 * @class AnimatedExpandedSymbolsSystem
 * @extends BaseSystem<AnimatedExpandedSymbolsSystemSettings>
 * @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 AnimatedExpandedSymbolsSystem extends BaseSystem<AnimatedExpandedSymbolsSystemSettings>
{
    /**
     * Default configuration settings for reel spin behavior.
     * @type {ReelsSpinSystemSettings}
     */
    static defaultSettings: AnimatedExpandedSymbolsSystemSettings = {
        accelerationTime: 0.1,
        reelStartDelay: 0.2,
        reelStopDelay: 0.5,
        spinningDuration: 2,
        key: 'K',
        zIndex: 1000,
    };

    static override requiredFlags: string[] = ['isSpinning'];
    static override requiredMaps: string[] = ['symbols'];
    static override requiredInputs: string[] = ['screenMap'];
    static override requiredMethods: string[] = ['createSymbol'];
    static override requiredUtils: string[] = ['grid'];

    private state: string;
    private elapsedTime: number = 0;
    private startingReel: number = 0;
    private stoppingReel: number = 0;
    private expandedSymbols: AnimatedExpandedSymbolSystemCompatibleEntity[] = [];
    private hooks: Array<Hook | HooksChain> = [];

    /**
     * Initializes the reel spin system with specified machine settings and prepares reels.
     * @class
     * @param {BaseMachine} machine - The slot machine instance that the spin system controls.
     */
    constructor(machine: BaseMachine)
    {
        super(machine);
        this.setSettings(AnimatedExpandedSymbolsSystem.defaultSettings);
        this.expandedSymbols = new Array(this.machine.config.reels).fill(null);
    }

    /**
     * Handles the actions to be performed when entering the spin state.
     * Initializes the state to STARTING, and resets the starting and stopping reels, as well as the elapsed time.
     */
    override onEnter(): void
    {
        this.state = SPIN_STATES.STARTING;
        this.startingReel = 0;
        this.stoppingReel = 0;
        this.elapsedTime = 0;
    }

    /**
     * Updates the state of the spin system based on the elapsed time and current state.
     * @param dt - The delta time since the last update, in milliseconds.
     */
    update(dt: number): void
    {
        this.elapsedTime += dt * 0.001;

        if (this.state === SPIN_STATES.STARTING)
        {
            this._processStarting();
        }

        if (this.state === SPIN_STATES.SPINNING)
        {
            this._processSpinning();
        }

        if (this.state === SPIN_STATES.STOPPING)
        {
            this._processStopping();
        }

        this.hooks = this.hooks.filter((hook) =>
        {
            hook.update(dt);

            return !hook.isComplete();
        });
    }

    /**
     * Processes the starting state of the spin system.
     * @returns {void}
     */
    _processStarting(): void
    {
        if (this.startingReel < this.machine.config.reels
            && this.elapsedTime >= this.startingReel * this.settings.reelStartDelay)
        {
            this.startNext();
        }

        if (this.machine.sharedSystemData.spinSystemState === 'spinning' && this.startingReel >= this.machine.config.reels)
        {
            this.state = SPIN_STATES.SPINNING;
        }
    }

    /**
     * Processes the spinning state of the slot machine.
     * @returns {void}
     */
    _processSpinning(): void
    {
        if (this.hooks.length === 0 && this.machine.sharedSystemData.spinSystemState === 'stopping')
        {
            this.state = SPIN_STATES.STOPPING;
            this.elapsedTime = 0;
            this.machine.events.addEvent('inProcessingStarted');
        }
    }

    /**
     * Handles the stopping process of the reels and updates the spin state.
     * @returns {void}
     */
    _processStopping(): void
    {
        if (this.stoppingReel < this.machine.config.reels && this.elapsedTime >= this.settings.reelStopDelay)
        {
            this.elapsedTime = 0;
            this.stopNext();
        }

        if (this.hooks.length === 0 && this.machine.sharedSystemData.spinSystemState === 'stopped')
        {
            this.state = SPIN_STATES.STOPPED;
        }
    }

    /**
     * Initiates the next step in processing expanded symbols on the reels.
     * @returns {void}
     */
    startNext(): void
    {
        if (!this.expandedSymbols[this.startingReel])
        {
            this.startingReel++;

            return;
        }
        const expandedSymbol = this.expandedSymbols[this.startingReel];
        const currentReelIndex = this.startingReel;

        const chain = new HooksChain()
            .addHook(new DelayHook(this.settings.accelerationTime))
            .addHook(new AnimationProgressHook(() => expandedSymbol.out()))
            .setOnComplete(() =>
            {
                expandedSymbol.destroy();
                this.expandedSymbols[currentReelIndex] = null;
            });

        this.hooks.push(chain);
        this.startingReel++;
    }

    /**
     * Stops the next reel and handles the expanded symbols animation.
     * @returns {void}
     */
    stopNext(): void
    {
        if (!this.checkReelForExpandeds(this.stoppingReel))
        {
            this.stoppingReel++;

            return;
        }
        const expandedSymbol = new ExpandedSymbol({ spine: 'expanding', skin: 'high' } as SymbolConfig);

        expandedSymbol.reelIndex = this.stoppingReel;
        expandedSymbol.zIndex = this.settings.zIndex;
        expandedSymbol.position.set(
            this.machine.utils.grid.getSlotWidth() * (0.5 + this.stoppingReel),
            this.machine.utils.grid.getSlotHeight() * 2
        );
        this.machine.addChild(expandedSymbol);

        const spinHook = new AnimationProgressHook(() => expandedSymbol.spin());
        const inHook = new AnimationProgressHook(() => expandedSymbol.in());
        const currentReelIndex = this.stoppingReel;

        spinHook.setOnComplete(() =>
        {
            expandedSymbol.isLanded = true;
            const reelSymbols = this.getReelSymbols(currentReelIndex);

            reelSymbols.forEach((symbol) => { symbol.alpha = 0; });
        });

        const chain = new HooksChain();

        chain.addHook(spinHook);
        chain.addHook(inHook);

        this.hooks.push(chain);
        this.expandedSymbols[this.stoppingReel] = expandedSymbol;
        this.stoppingReel++;
    }

    /**
     * Retrieves the symbols for a specific reel index.
     * @param reelIndex - The index of the reel to get symbols for.
     * @returns An array of AnimatedExpandedSymbolSystemCompatibleEntity objects for the specified reel.
     */
    getReelSymbols(reelIndex: number): AnimatedExpandedSymbolSystemCompatibleEntity[]
    {
        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));
    }

    /**
     * Checks if there are expanded symbols on the specified reel.
     * @param index - The index of the reel to check.
     * @returns True if there is a matching expanded symbol, otherwise false.
     */
    private checkReelForExpandeds(index: number)
    {
        const isMatch = this.machine.inputs.screenMap
            .filter((_: any, idx: number) =>
            {
                const reelIndex = this.machine.utils.grid.fromMapIndexToGridCoords(idx);

                return reelIndex.x === index;
            })
            .some((symbol: ExpandedSymbol) =>
            {
                const isMatch = symbol.key === this.settings.key;

                return isMatch;
            });

        return isMatch;
    }

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