import BaseMachine from '../slotMachines/BaseMachine';
import { CanBeBlocked, CanPlayBouncing, CanPlayFalling, CanPlayWin, Entity } from '../types/Entity';
import { ScreenMapRecord } from '../types/ScreenInputMap';
import AnimationProgressHook from '../utils/hooks/AnimationProgressHook';
import DelayHook from '../utils/hooks/DelayHook';
import GravityHook from '../utils/hooks/GravityHook';
import HooksChain from '../utils/hooks/HooksChain';
import ImmediateHook from '../utils/hooks/ImmediateHook';
import BaseSystem from './BaseSystem';

export type CascadeSpinSystemCompatibleEntity = Entity & CanBeBlocked & CanPlayBouncing & CanPlayFalling & CanPlayWin;

export interface CascadeSpinSystemSettings
{
    gravity: number,
    reelDelay: number,
    slotDelay: number,
    refillDelay: number,
    spawnOffsetY: number
}

/**
 * @class CascadeSpinSystem
 * @extends BaseSystem
 * @description Manages the cascade spinning effect in the slot machine. Handles dropping,
 * refilling, and bouncing symbols with gravity effects.
 */
export default class CascadeSpinSystem extends BaseSystem<CascadeSpinSystemSettings>
{
    /** Default settings for cascade spin system. */
    static defaultSettings: CascadeSpinSystemSettings = {
        gravity: 4000,
        reelDelay: 0.12,
        slotDelay: 0.03,
        refillDelay: 0.5,
        spawnOffsetY: 0
    };

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

    private droppingHooks: Array<HooksChain | ImmediateHook> = [];
    private refillingHooks: Array<HooksChain> = [];
    private delayHook: DelayHook | undefined;
    private state: 'stopped' | 'starting' | 'pending' | 'refilling' = 'stopped';

    /**
     * Initializes the cascade spin system with specified machine settings.
     * @class
     * @param {BaseMachine} machine - The slot machine instance.
     */
    constructor(machine: BaseMachine)
    {
        super(machine);
        this.setSettings(CascadeSpinSystem.defaultSettings);
    }

    onEnter(): void
    {
        this.state = 'starting';
        this.machine.flags.isSpinning = true;
    }

    onLeave(): void
    {
    }

    /**
     * Updates the cascade spin system based on the current state.
     * Manages drop, refill, and bounce animations.
     * @param {number} dt - Delta time since the last update, in milliseconds.
     */
    override update(dt: number): void
    {
        const stateHandlers: Record<typeof this.state, () => void> = {
            stopped: () => {},
            starting: () => this._handleStarting(),
            pending: () => this._handlePending(dt),
            refilling: () => this._handleRefilling(),
        };

        const handler = stateHandlers[this.state];

        if (handler) handler();

        this.delayHook?.update(dt);
        this.processDropping(dt);
        this.processRefilling(dt);
    }

    /**
     * Handles logic for the "stopped" state.
     * Prepares dropping hooks and transitions to "pending".
     * @private
     */
    private _handleStarting(): void
    {
        this.state = 'pending';
        this._createDroppingHooks();
        this.delayHook = new DelayHook(this.settings.refillDelay);
        this.machine.events.addEvent('outProcessingStarted');
    }

    /**
     * Handles the "pending" state logic. If complete, moves to "refilling".
     * @param {number} dt - Delta time for the update cycle.
     * @private
     */
    private _handlePending(dt: number): void
    {
        if (this.isPending(dt))
        {
            this.state = 'refilling';
            this._createRefillingHooks();
            this.machine.events.addEvent('inProcessingStarted');
        }
    }

    /**
     * Handles logic for the "refilling" state, resetting to "stopped" when complete.
     * @private
     */
    private _handleRefilling(): void
    {
        if (this.refillingHooks.length === 0)
        {
            this.state = 'stopped';
            this.machine.flags.isSpinning = false;
            this.machine.events.addEvent('inProcessingEnded');
        }
    }

    /**
     * Processes all active dropping hooks.
     * @param {number} dt - Delta time for the update cycle.
     */
    processDropping(dt: number): void
    {
        this.droppingHooks = this.droppingHooks.filter((hook) =>
        {
            hook.update(dt);

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

    /**
     * Processes all active refilling hooks.
     * @param {number} dt - Delta time for the update cycle.
     */
    processRefilling(dt: number): void
    {
        this.refillingHooks = this.refillingHooks.filter((hook) =>
        {
            hook.update(dt);

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

    /**
     * Checks if the pending state is complete.
     * @param {number} _dt - Delta time for the update cycle.
     * @returns {boolean} True if pending state is complete, otherwise false.
     */
    isPending(_dt: number): boolean
    {
        return this.delayHook?.isComplete() && !!this.machine.inputs.screenMap;
    }

    /**
     * Creates hooks for dropping symbols with gravity effects.
     * @private
     */
    private _createDroppingHooks(): void
    {
        this.droppingHooks = this.machine.maps.symbols.map((name: string, i: number) =>
        {
            if (!name) return new ImmediateHook();
            const symbol: CascadeSpinSystemCompatibleEntity = this.machine.getChildByName(name);
            const targetY = this.machine.config.height + symbol.y;
            const gravity = this.settings.gravity;

            return new HooksChain()
                .addHook(new DelayHook(this._getAnimationDelay(i)))
                .addHook(new ImmediateHook(() => symbol.falling()))
                .addHook(new GravityHook(symbol, targetY, gravity))
                .setOnComplete(() => symbol.destroy());
        });
    }

    /**
     * Creates hooks for refilling symbols that have fallen out of view.
     * @private
     */
    private _createRefillingHooks(): void
    {
        let landedCount = 0;
        this.refillingHooks = this.machine.inputs.screenMap.map((data: ScreenMapRecord, i: number) =>
        {
            const symbol = this.machine.createSymbol(data.key);

            this.machine.addChild(symbol);
            this.machine.maps.symbols[i] = symbol.name;
            this.machine.utils.grid.fromMapIndexToXY(i).copyTo(symbol.position);
            const reelIndex = this.machine.utils.grid.fromMapIndexToGridCoords(i).x;
            const targetY = symbol.y;

            symbol.y -= this.machine.config.height + this.settings.spawnOffsetY;
            const gravity = this.settings.gravity;

            return new HooksChain()
                .addHook(new DelayHook(this._getAnimationDelay(i)))
                .addHook(new ImmediateHook(() => symbol.falling()))
                .addHook(new GravityHook(symbol, targetY, gravity))
                .addHook(new ImmediateHook(() => {
                    ++landedCount % this.machine.config.slots === 1 ? this.machine.events.addEvent('reelLanded', {reelIndex}) : null
                }))
                .addHook(new AnimationProgressHook(() => symbol.bounce()));
        });
    }

    /**
     * Calculates animation delay based on symbol's reel and slot positions.
     * @private
     * @param {number} i - The index of the symbol.
     * @returns {number} The calculated delay for the animation.
     */
    private _getAnimationDelay(i: number): number
    {
        const { slots } = this.machine.config;
        const position = this.machine.utils.grid.fromMapIndexToGridCoords(i);
        const slotDelay = (slots - position.y) * this.settings.slotDelay;
        const reelDelay = position.x * this.settings.reelDelay;

        return slotDelay + reelDelay;
    }
}
