import BaseMachine from '../slotMachines/BaseMachine';
import BaseSystem from '../systems/BaseSystem';
import { CanBeBlocked, CanPlayIdle, CanPlayIn, CanPlayOut, Entity } from '../types/Entity';
import { ScreenMapRecord } from '../types/ScreenInputMap';
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 ImmediateHook from '../utils/hooks/ImmediateHook';

/**
 * 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 StaticReelsSystemCompatibleEntity = Entity
& CanBeBlocked
& CanPlayIdle
& CanPlayIn
& CanPlayOut;

/** Configuration settings for the reel spinning behavior in the StaticReelsSpinSystem. */
export interface StaticReelsSystemSettings
{
    reelDelay: number
    slotDelay: number,
    refillDelay: number,
}

type systemState = 'stopped' | 'processingOut' | 'pending' | 'processingIn' | 'complete';

const STATES: Record<string, systemState> = {
    STOPPED: 'stopped',
    PROCESSING_OUT: 'processingOut',
    PENDING: 'pending',
    PROCESSING_IN: 'processingIn',
    COMPLETE: 'complete'
};

/** Manages the spin system for static reels, handling animations and states. */
export default class StaticReelsSpinSystem extends BaseSystem<StaticReelsSystemSettings>
{
    /**
     * Configuration settings for reel and slot delays.
     * @type {object}
     * @property {number} reelDelay - Delay between reels in seconds.
     * @property {number} slotDelay - Delay between slots in seconds.
     * @property {number} refillDelay - Delay for refilling symbols.
     */
    static defaultSettings: StaticReelsSystemSettings = {
        reelDelay: 0.1,
        slotDelay: 0.03,
        refillDelay: 0.15,
    };

    /** Static list of flags that the machine should provide. Subclasses can override to specify their own requirements. */
    static override requiredFlags: string[] = ['isSpinning'];

    /** Static list of maps that the machine should provide. Subclasses can override to specify their own requirements. */
    static override requiredMaps: string[] = ['symbols'];

    /** Static list of inputs that the machine should provide. Subclasses can override to specify their own requirements. */
    static override requiredInputs: string[] = [];

    /** Static list of methods that the machine should provide. Subclasses can override to specify their own requirements. */
    static override requiredMethods: string[] = [];

    /** Static list of utils that the machine should provide. Subclasses can override to specify their own requirements. */
    static override requiredUtils: string[] = ['grid'];

    public state: systemState;
    private _outHooks: (Hook | HooksChain)[] = [];
    private _inHooks: (Hook | HooksChain)[] = [];
    private _delayHook: DelayHook;

    /**
     * Initializes a new instance of StaticReelsSpinSystem.
     * @param {object} machine - The machine context in which the system operates.
     */
    constructor(machine: BaseMachine)
    {
        super(machine);

        this.setSettings(StaticReelsSpinSystem.defaultSettings);

        this.state = STATES.STOPPED;
    }

    /**
     * Updates the spin system state based on the current delta time.
     * @param {number} dt - Delta time since the last update.
     */
    update(dt: number)
    {
        if (this.state === 'stopped')
        {
            this._createOutHooks();
            this._createDelayHook();
            this.state = 'processingOut';
            this.machine.events.addEvent('outProcessingStarted');
        }

        if (this.state === 'processingOut')
        {
            this._processOutAnimations(dt);
            if (this._outHooks.length === 0)
            {
                this.machine.events.addEvent('pendingStarted');
                this.state = 'pending';
            }
        }

        if (this.state === 'pending')
        {
            if (!this.isPending(dt)) return;
            this.machine.events.addEvent('pendingEnded');
            this._createInHooks();
            this.state = 'processingIn';
            this.machine.events.addEvent('inProcessingStarted');
        }

        if (this.state === 'processingIn')
        {
            this._processInAnimations(dt);
            if (this._inHooks.length === 0) this.state = 'complete';
        }

        if (this.state === 'complete')
        {
            this.machine.events.addEvent('inProcessingEnded');
            this.state = 'stopped';
        }
    }

    /**
     * Checks if the system is in a pending state based on delta time.
     * @param {number} dt - Delta time since the last update.
     * @returns {boolean} True if the system is pending, otherwise false.
     */
    isPending(dt: number): boolean
    {
        this._delayHook.update(dt);

        return !(!this._delayHook.isComplete() || !this.machine.inputs.screenMap);
    }

    /**
     * Initializes a delay hook for refilling symbols.
     * @private
     */
    _createDelayHook()
    {
        this._delayHook = new DelayHook(this.settings.refillDelay);
    }

    /**
     * Processes the outgoing animations for symbols.
     * @param dt
     * @private
     */
    _processOutAnimations(dt: number): void
    {
        this._outHooks = this._outHooks.filter((outHook) =>
        {
            outHook.update(dt);

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

    /**
     * Creates hooks for outgoing animations of symbols.
     * @private
     */
    _createOutHooks()
    {
        this._outHooks = this.machine.maps.symbols
            .map((name: string, i: number) =>
            {
                if (!name) return () => new ImmediateHook();
                const symbol: StaticReelsSystemCompatibleEntity = this.machine.getChildByName(name);

                return new HooksChain()
                    .addHook(new DelayHook(this._getAnimationDelay(i)))
                    .addHook(new AnimationProgressHook(() => symbol.out()));
            });
    }

    /**
     * Processes the incoming animations for symbols.
     * @param dt
     * @private
     */
    _processInAnimations(dt: number)
    {
        this._inHooks = this._inHooks.filter((inHook) =>
        {
            inHook.update(dt);

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

    /**
     * Creates hooks for incoming animations of symbols.
     * @private
     */
    _createInHooks()
    {
        this._inHooks = this.machine.inputs.screenMap
            .map((record: ScreenMapRecord, i: number) =>
                new HooksChain()
                    .addHook(new DelayHook(this._getAnimationDelay(i)))
                    .addHook(new AnimationProgressHook(() =>
                    {
                        if (this.machine.maps.symbols[i])
                        {
                            const id = this.machine.maps.symbols[i];

                            this.machine.getChildByName(id).destroy();
                        }
                        const symbol: StaticReelsSystemCompatibleEntity = this.machine.createSymbol(record.key);

                        this.machine.utils.grid.fromMapIndexToXY(i).copyTo(symbol.position);
                        this.machine.addChild(symbol);
                        this.machine.maps.symbols[i] = symbol.name;

                        return symbol.in();
                    })));
    }

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

        return slotDelay + reelDelay;
    }

    onEnter(): void
    {
    }

    onLeave(): void
    {
    }
}
