import BaseMachine from '../slotMachines/BaseMachine';
import { ScreenMapRecord } from '../types/ScreenInputMap';
import DelayHook from '../utils/hooks/DelayHook';
import Hook from '../utils/hooks/Hook';
import HooksChain from '../utils/hooks/HooksChain';
import BaseSystem from './BaseSystem';

/** Configuration settings for the reel spinning behavior in the AnticipationSystem. */
export interface AnticipationSystemSettings
{
    enabled: boolean,
    reelStopDelay: number,
    duration: number,
    test: RegExp,
    triggerThreshold: number,
    zIndex: number,
}

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

/**
 * @class AnticipationSystem
 * @extends BaseSystem<AnticipationSystemSettings>
 * @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 AnticipationSystem extends BaseSystem<AnticipationSystemSettings>
{
    /**
     * Default configuration settings for reel spin behavior.
     * @type {ReelsSpinSystemSettings}
     */
    static defaultSettings: AnticipationSystemSettings = {
        enabled: true,
        reelStopDelay: 0.5,
        duration: 2,
        test: /A/,
        triggerThreshold: 2,
        zIndex: 1000,
    };

    static override requiredFlags: string[] = ['isSpinning'];
    static override requiredMaps: string[] = ['symbols', 'preventReelStop', 'anticipatingReels'];
    static override requiredInputs: string[] = ['screenMap'];
    static override requiredMethods: string[] = ['createSymbol'];
    static override requiredUtils: string[] = ['grid'];
    static override sharedSystemData: string[] = ['spinSystemState', 'currentStoppingReel', 'anticipatingSymbolsTests'];

    private state: string;
    private stoppingReel: number = 0;
    private hooks: (Hook | HooksChain)[] = [];
    private skipped: boolean = false;

    /**
     * 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(AnticipationSystem.defaultSettings);
        this.stoppingReel = 0;
        this.hooks = [];
    }

    /**
     * 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.hooks = [];
        this.stoppingReel = 0;
        this.skipped = false;
        this.resetAnticipationReels();
    }

    /**
     * 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
    {
        if (!this.settings.enabled)
        {
            this.hooks = [];
            this.state = SPIN_STATES.STOPPED;
            this.resetAnticipationReels();

            return;
        }

        if (this.state === SPIN_STATES.STARTING)
        {
            if (this.machine.sharedSystemData.spinSystemState === 'stopping')
            {
                this.state = SPIN_STATES.SPINNING;
            }
        }

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

        if (this.machine.sharedSystemData.spinSystemState === 'stopping')
        {
            this.handleSkip();
            this.evaluateAnticipationTrigger();
        }

        if (this.machine.sharedSystemData.spinSystemState === 'stopped')
        {
            this.state = SPIN_STATES.STOPPED;
            this.resetAnticipationReels();
            this.hooks = [];
        }

        this.stoppingReel = this.machine.sharedSystemData.currentStoppingReel;

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

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

    private resetAnticipationReels()
    {
        this.machine.maps.anticipatingReels = new Array(this.machine.config.reels).fill(false);
        this.machine.sharedSystemData.anticipatingSymbolsTests = this.machine.sharedSystemData.anticipatingSymbolsTests.filter((t: RegExp) =>
        {
            return t !== this.settings.test;
        });
    }

    private handleSkip()
    {
        if (!this.skipped && this.machine.inputs.fastStop)
        {
            this.skipped = true;
            this.machine.maps.preventReelStop.forEach((_: boolean, i: number) =>
            {
                this.machine.maps.preventReelStop[i] = false;
            });
            this.hooks = this.hooks.filter((hook) =>
            {
                hook.update(Infinity);
                return !hook.isComplete();
            });
        }
    }

    private evaluateAnticipationTrigger()
    {
        if (this.skipped) return;
        if (!this.isAnticipationStartConditionIsMet()) return;
        const duration = this.settings.duration;
        const currentStoppingReelIndex = this.machine.sharedSystemData.currentStoppingReel;
        const { reels, slots } = this.machine.config;
        const map = this.updateTriggerArray(this.machine.inputs.screenMap, reels, slots, this.settings.test, this.settings.triggerThreshold);
        if (map[currentStoppingReelIndex] && !this.machine.sharedSystemData.anticipatingSymbolsTests.includes(this.settings.test))
        {
            this.machine.sharedSystemData.anticipatingSymbolsTests.push(this.settings.test);
        }
        if (this.isReelAlreadyAnticipating(currentStoppingReelIndex)) return;
        this.machine.maps.anticipatingReels[currentStoppingReelIndex] = true;
        const chain = new HooksChain()
            .addHook(new DelayHook(duration))
            .setOnComplete(() =>
            {
                this.machine.maps.preventReelStop[currentStoppingReelIndex] = false;
                this.machine.maps.anticipatingReels[currentStoppingReelIndex] = false;
            });

        this.hooks.push(chain);
    }

    private isReelAlreadyAnticipating(reelIndex: number): boolean
    {
        return this.machine.maps.anticipatingReels[reelIndex];
    }

    private isAnticipationStartConditionIsMet()
    {
        const currentStoppingReelIndex = this.machine.sharedSystemData.currentStoppingReel;
        const isReelIndexChanged = this.stoppingReel !== currentStoppingReelIndex;
        const isReelStopPrevented = this.machine.maps.preventReelStop[currentStoppingReelIndex];

        return isReelStopPrevented && isReelIndexChanged;
    }

    handleScreenMapIsProvided()
    {
        if (this.machine.inputs.screenMap)
        {
            const { reels, slots } = this.machine.config;
            const map = this.updateTriggerArray(this.machine.inputs.screenMap, reels, slots, this.settings.test, this.settings.triggerThreshold);

            this.machine.maps.preventReelStop.forEach((v: boolean, i: number) =>
            {
                this.machine.maps.preventReelStop[i] = v || map[i];
            });
            this.state = SPIN_STATES.STOPPING;
        }
    }

    private updateTriggerArray(data: ScreenMapRecord[], columns: number, rows: number, regExp: RegExp, triggerThreshold: number): boolean[]
    {
        const matchingColumns = this.findColumnsWithRegExp(data, columns, rows, regExp);
        const triggerArray: boolean[] = new Array(columns).fill(false);

        if (matchingColumns.length === 0) return triggerArray;
        const thresholdIndex = matchingColumns[triggerThreshold - 1] ?? columns;

        for (let i = thresholdIndex + 1; i < columns; i++)
        {
            triggerArray[i] = true;
        }

        return triggerArray;
    }

    private findColumnsWithRegExp(data: ScreenMapRecord[], columns: number, rows: number, regExp: RegExp): number[]
    {
        const matrix: ScreenMapRecord[][] = [];

        for (let i = 0; i < rows; i++)
        {
            matrix.push(data.slice(i * columns, (i + 1) * columns));
        }

        const result: number[] = [];

        for (let col = 0; col < columns; col++)
        {
            for (let row = 0; row < rows; row++)
            {
                if (regExp.test(matrix[row][col].key))
                {
                    result.push(col);
                    break;
                }
            }
        }

        return result;
    }

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

    }
}
