import Bouncing from '../../systems/reels/Bouncing';
import ReelsSpinSystem, { ReelsSpinSystemCompatibleEntity } from '../../systems/ReelsSpinSystem';
import { Entity } from '../../types/Entity';
import AnimationProgressHook from '../../utils/hooks/AnimationProgressHook';
import Acceleration from './Acceleration';
import Deceleration from './Deceleration';
import Spinning from './Spinning';
import { REEL_STATES } from './states';
import Stopped from './Stopped';
import { StateMachine } from '@popiplay/state-machine';

type ReelState = Stopped | Acceleration | Deceleration | Spinning | Bouncing;

type ReelItemRecord = [position: number, symbol: ReelsSpinSystemCompatibleEntity];

/**
 * @class Reel
 * @extends StateMachine
 * @description Manages the spinning of a reel in a slot machine, with states for acceleration, spinning, deceleration, and stopping.
 */
export default class Reel extends StateMachine<ReelsSpinSystem, ReelState>
{
    public system: ReelsSpinSystem;
    public index: number;
    public position: number = 0;
    public stopPosition: number = 0;
    public speed: number = 0;
    public symbols: ReelItemRecord[] = [];
    protected _prevPosition: number = 0;

    /**
     * @class
     * @param target
     * @param {number} index - The index of the reel, used for positioning.
     */
    constructor(target: ReelsSpinSystem, index: number)
    {
        super(target);
        this.index = index;

        // Provide states for StateMachine and set initial state
        this.provideStates([
            new Stopped(this),
            new Acceleration(this),
            new Spinning(this),
            new Deceleration(this),
            new Bouncing(this)
        ]);

        this.setState(REEL_STATES.STOPPED);
    }

    /**
     * @returns {number} - Returns the acceleration time setting for the reel.
     */
    get accelerationTime(): number
    {
        return this.target.settings.accelerationTime;
    }

    /**
     * @returns {number} - Returns the maximum speed setting for the reel.
     */
    get maxSpeed(): number
    {
        return this.target.settings.maxSpeed;
    }

    /**
     * @returns {number} - Returns the back coefficient setting for the reel, used in easing calculations.
     */
    get backCoef(): number
    {
        return this.target.settings.backCoef;
    }

    /**
     * Sets an array of symbols for the reel, resetting position and stop position.
     * @param {Array<ReelsSpinSystemCompatibleEntity>} symbols - The symbols to set on the reel.
     */
    setSymbols(symbols: ReelsSpinSystemCompatibleEntity[]): void
    {
        this.position = 0;
        this._prevPosition = 0;
        this.stopPosition = 0;
        this.symbols = symbols.reverse().map((symbol, i) =>
        {
            symbol.isBlurred = false;
            symbol.filters = null;
            symbol.blurEnabled = false;
            symbol.cacheAsBitmap = false;
            symbol.filterArea = null;
            return [i, symbol];
        });
    }

    /**
     * Adds symbols to the reel starting at a specified position.
     * @param {Array<symbol>} symbols - Array of symbols to add.
     * @param {number} position - The position to start adding symbols.
     */
    addSymbols(symbols: ReelsSpinSystemCompatibleEntity[], position: number): void
    {
        symbols.forEach((symbol, i) =>
        {
            this._setSymbolX(symbol);
            this.symbols.push([position + i, symbol]);
            this.target.machine.addChild(symbol);
        });
    }

    /** Starts the reel by setting its position to zero and switching to the acceleration state. */
    start(): void
    {
        this.position = 0;
        this.stopPosition = 0;
        this._prevPosition = 0;
        this.setState(REEL_STATES.ACCELERATION);
    }

    /**
     * Stops the reel at a specified stop position and switches to the deceleration state.
     * @param {number} [stopPosition=0] - The target stop position for the reel.
     */
    stop(stopPosition: number = 0): void
    {
        this.stopPosition = stopPosition;
        this.setState(REEL_STATES.DECELERATION);
    }

    /**
     * Updates the reel's current state, position, and symbols.
     * @param {number} dt - Delta time since the last update, in milliseconds.
     */
    override update(dt: number): void
    {
        super.handleInput(this);
        super.update(dt); // Calls StateMachine's update to handle the current state logic
        this._handleSwitchingPosition();
        this._updateSymbols();
    }

    /** Triggers a bounce effect on each symbol in the reel. */
    bounce(): AnimationProgressHook[]
    {
        return this.symbols.map(([_, symbol]) =>
            new AnimationProgressHook(() => symbol.bounce()));
    }

    /** Is called when entering Stopped state */
    public onStop()
    {
        // do something
    }

    /** Is called when leaving Deceleration state */
    public onDecelerationEnded()
    {
        // do something
    }

    /**
     * Updates symbol positions based on the reel's current position.
     * Each symbol's position is calculated to provide a smooth scrolling effect as the reel spins.
     * Symbols are removed from the view when they exit below the visible area, and are returned to the pool.
     * @private
     */
    protected _updateSymbols(): void
    {
        const slotHeight = this.target.machine.utils.grid.getSlotHeight();
        const baseY = this.getBaseY();

        this.symbols.forEach(([position, symbol]) =>
        {
            const distance = this.position - position;

            symbol.y = (distance * slotHeight) + baseY;
        });
        this.symbols = this.symbols.filter(([position, symbol]) =>
        {
            if (position <= this.position - 1)
            {
                symbol.isBlurred = false;
                symbol.filters = null;
                symbol.blurEnabled = false;
                symbol.cacheAsBitmap = false;
                symbol.filterArea = null;
                this.target.machine.removeChild(symbol);
                if (!this.target.pool.add(symbol.key, symbol)) symbol.destroy();

                return false;
            }

            return true;
        });
    }

    /**
     * Manages the addition of new symbols as the reel position changes, handling cases where multiple positions are skipped.
     * @private
     */
    protected _handleSwitchingPosition(): void
    {
        if (!this._isReelSpinning()) return;

        const slots = this.getSlotsCount();
        const currentPos = Math.ceil(this.position);

        if (this._prevPosition === currentPos) return;

        for (let i = 0; i < slots; i++)
        {
            if (this.symbols.findIndex(([pos]) => pos === currentPos + i) >= 0) continue;
            this._addRandomSymbol(currentPos + i);
        }

        this._prevPosition = currentPos;
    }

    public getLastSymbolPosition()
    {
        if (this.symbols.length === 0) return 0;
        const [position] = this.symbols[this.symbols.length - 1];

        return position;
    }

    /**
     * Checks if the reel is in a spinning state.
     * @private
     */
    protected _isReelSpinning(): boolean
    {
        return !(this.currentState.name === 'Deceleration' || this.currentState.name === 'Stopped');
    }

    /**
     * Adds a random symbol at the specified position on the reel.
     * @param {number} position - The position to add the symbol.
     * @private
     */

    protected _addRandomSymbol(position: number): void
    {
        const randomKey = this.target.getRandomSymbolKey();
        const symbol: ReelsSpinSystemCompatibleEntity = this.target.getSymbol(randomKey);

        this._setSymbolX(symbol);
        this.symbols.push([position, symbol]);
        this.target.machine.addChild(symbol);
    }

    /**
     * Sets the x-coordinate of a symbol based on the reel index.
     * @param {symbol} symbol - The symbol object to position.
     * @protected
     */
    protected _setSymbolX(symbol: Entity): void
    {
        const slotWidth = this.target.machine.utils.grid.getSlotWidth();
        const halfSlotWidth = slotWidth * 0.5;
        const col = this.getColumnIndex();

        symbol.x = halfSlotWidth + (col * slotWidth);
    }

    /**
     * Returns the column index for this reel. Subclasses can override to decouple from this.index.
     */
    protected getSlotsCount(): number
    {
        return this.target.machine.config.slots;
    }

    protected getColumnIndex(): number
    {
        return this.index;
    }

    /**
     * Returns base Y coordinate for symbols when position equals 0. Subclasses can override to anchor per row.
     */
    protected getBaseY(): number
    {
        const slotHeight = this.target.machine.utils.grid.getSlotHeight();
        const halfSlotHeight = slotHeight / 2;
        const reelHeight = this.target.machine.config.height;
        return reelHeight - halfSlotHeight;
    }
}
