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

export type CascadeRefillSystemCompatibleEntity = Entity
& CanBeBlocked
& CanPlayFalling
& CanPlayIdle
& CanPlayBouncing;

export interface CascadeRefillSystemSettings
{
    gravity: number;
    refillDelay: number;
    slotDelay: number;
    spawnOffsetY: number;
}

/**
 * @class CascadeRefillSystem
 * @extends BaseSystem
 * @description System that manages the refill of empty slots in a cascading slot machine game.
 */
export default class CascadeRefillSystem extends BaseSystem<CascadeRefillSystemSettings>
{
    private _droppingHooks: (Hook | HooksChain)[];
    private _refillingHooks: (Hook | HooksChain)[];
    private _delayHook?: (dt: number) => boolean;
    private state: 'stopped' | 'pending' | 'refilling';

    static defaultSettings: CascadeRefillSystemSettings = {
        gravity: 4000,
        refillDelay: 0,
        slotDelay: 0,
        spawnOffsetY: 0,
    };

    /** Required flags for the system to operate. */
    static override requiredFlags: string[] = [];

    /** Required maps for the system to operate. */
    static override requiredMaps: string[] = ['symbols'];

    /** Required inputs for the system to operate. */
    static override requiredInputs: string[] = ['screenMap'];

    /** Required methods that `machine` should implement. */
    static override requiredMethods: string[] = ['getChildByName', 'createSymbol'];

    /** Required utilities for the system to operate. */
    static override requiredUtils: string[] = ['grid'];

    constructor(machine: BaseMachine)
    {
        super(machine);
        this.setSettings(CascadeRefillSystem.defaultSettings);
        this._droppingHooks = [];
        this._refillingHooks = [];
        this._delayHook = undefined;
        this.state = 'stopped';
    }

    /**
     * Updates the state of the refill system based on the current game conditions.
     * @param {number} dt - Delta time in milliseconds since the last update.
     */
    update(dt: number): void
    {
        if (this.state === 'stopped')
        {
            if (!this.shouldBeRefilled())
            {
                this.state = 'stopped';

                return;
            }
            this.state = 'pending';
            this._createDroppingHooks();
            this._createDelayHook();
        }

        if (this.state === 'pending')
        {
            if (this.isPending(dt)) return;
            this.state = 'refilling';
            this._createRefillingHooks();
        }

        if (this.state === 'refilling')
        {
            if (this._refillingHooks.length === 0)
            {
                this.machine.events.addEvent('respinEnded', {});
                this.state = 'stopped';
            }
        }

        this.processDroppingHooks(dt);
        this.processRefillingHooks(dt);
    }

    /**
     * Processes dropping hooks to animate symbols dropping into place.
     * @param {number} dt - Delta time in milliseconds since the last update.
     */
    private processDroppingHooks(dt: number): void
    {
        this._droppingHooks = this._processHooks(this._droppingHooks, dt);
    }

    /**
     * Processes refilling hooks to animate symbols refilling empty slots.
     * @param {number} dt - Delta time in milliseconds since the last update.
     */
    private processRefillingHooks(dt: number): void
    {
        this._refillingHooks = this._processHooks(this._refillingHooks, dt);
    }

    /**
     * Processes each hook and filters out completed ones.
     * @param {Hook[]} hooks - List of hooks to process.
     * @param {number} dt - Delta time in milliseconds.
     * @returns {Hook[]} Updated list of active hooks.
     */
    private _processHooks(hooks: (Hook | HooksChain)[], dt: number): (Hook | HooksChain)[]
    {
        return hooks.filter((hook) =>
        {
            hook.update(dt);

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

    /** Creates dropping hooks for symbols that should fall into empty slots. */
    private _createDroppingHooks(): void
    {
        const distancesMap = this.machine.maps.symbols
            .map((_: string, i: number) => this.getNumberOfEmptySlotsUnderEntity(i));
        const symbolsMap: CascadeRefillSystemCompatibleEntity[] = this.machine.maps.symbols
            .map((name: string) => name ? this.machine.getChildByName(name) : null);

        this.machine.nullMap('symbols');

        this._droppingHooks = distancesMap.map((distanceInSlots: number, i: number) =>
        {
            const symbol = symbolsMap[i];

            if (!symbol) return new ImmediateHook();
            if (distanceInSlots === 0)
            {
                this.machine.maps.symbols[i] = symbol.name;

                return new ImmediateHook();
            }

            return this._createDroppingHook(symbol, distanceInSlots, i);
        });
    }

    /**
     * Creates a dropping hook for a specific symbol.
     * @param {Entity} symbol - The symbol to drop.
     * @param {number} distanceInSlots - Distance the symbol should fall in slots.
     * @param {number} i - Index in the grid.
     * @returns {HooksChain} A chain of hooks for the dropping animation.
     */
    private _createDroppingHook(symbol: CascadeRefillSystemCompatibleEntity, distanceInSlots: number, i: number): HooksChain
    {
        this.machine.maps.symbols[i + (distanceInSlots * this.machine.config.reels)] = symbol.name;
        this.machine.utils.grid.fromMapIndexToXY(i).copyTo(symbol.position);
        this.machine.addChild(symbol);
        const slotHeight = this.machine.utils.grid.getSlotHeight();
        const gravity: number = this.settings.gravity;

        return new HooksChain()
            .addHook(new ImmediateHook(() => ((symbol as CanBeBlocked).isBlocked = true)))
            .addHook(new DelayHook(this._getAnimationDelay(i)))
            .addHook(new ImmediateHook(() => (symbol as CanPlayFalling).falling()))
            .addHook(new GravityHook(symbol, symbol.y + (distanceInSlots * slotHeight), gravity))
            .addHook(new AnimationProgressHook(() => symbol.bounce()))
            .addHook(new ImmediateHook(() => symbol.idle()))
            .addHook(new ImmediateHook(() => (symbol.isBlocked = false)));
    }

    /** Creates refilling hooks for empty slots that need new symbols. */
    private _createRefillingHooks(): void
    {
        const { reels } = this.machine.config;
        const emptySlotsInReelsMap = new Array(this.machine.config.reels).fill(0);

        this.machine.maps.symbols.forEach((name: any, i: number) =>
        {
            const reel = i % reels;

            if (!name) emptySlotsInReelsMap[reel]++;
        });
        this.machine.inputs.respinMap.forEach((data: { key: string; }, i: number) =>
        {
            if (this.machine.maps.symbols[i] !== null) return;
            const symbol = this.machine.createSymbol(data.key);

            this.machine.addChild(symbol);
            this.machine.maps.symbols[i] = symbol.name;
            symbol.position = this.machine.utils.grid.fromMapIndexToXY(i);
            const distance = emptySlotsInReelsMap[i % reels] * this.machine.utils.grid.getSlotHeight() + this.settings.spawnOffsetY;
            const targetY = symbol.position.y;

            symbol.y -= distance;

            const chain = new HooksChain()
                .addHook(new ImmediateHook(() => (symbol.isBlocked = true)))
                .addHook(new DelayHook(this._getAnimationDelay(i)))
                .addHook(new GravityHook(symbol, targetY, this.settings.gravity))
                .addHook(new AnimationProgressHook(() => symbol.bounce()))
                .addHook(new ImmediateHook(() => symbol.idle()))
                .addHook(new ImmediateHook(() => (symbol.isBlocked = false)));

            this._refillingHooks.push(chain);
        });
    }

    /**
     * Counts the number of empty slots under a specific entity in a column.
     * @param {number} index - The index of the entity in the grid.
     * @returns {number} The count of empty slots.
     */
    private getNumberOfEmptySlotsUnderEntity(index: number): number
    {
        let counter = 0;
        const { reels, slots } = this.machine.config;

        for (let i = index + reels; Math.floor(i / reels) < slots; i += reels)
        {
            if (this.machine.maps.symbols[i] === null) counter++;
        }

        return counter;
    }

    /** Creates a delay hook for the refill process. */
    private _createDelayHook(): void
    {
        let time = 0;

        this._delayHook = (dt) =>
        {
            time += dt * 0.001;

            return time >= this.settings.refillDelay;
        };
    }

    /**
     * Checks if the system is still pending refill.
     * @param {number} dt - Delta time since last update.
     * @returns {boolean} - Returns `true` if still pending.
     */
    private isPending(dt: number): boolean
    {
        return !this._delayHook!(dt) || !this.machine.inputs.respinMap;
    }

    /**
     * Determines if the refill is needed based on empty slots in the grid.
     * @returns {boolean} - Returns `true` if refill is needed.
     */
    private shouldBeRefilled(): boolean
    {
        return this.machine.maps.symbols.some((symbol: string | null) => symbol === null);
    }

    /**
     * Gets the animation delay for a specific slot index.
     * @param {number} _i - Slot index.
     * @returns {number} - The delay in milliseconds.
     */
    private _getAnimationDelay(_i: number): number
    {
        return 0;
    }

    onEnter(): void
    {
    }

    onLeave(): void
    {
    }
}
