import BaseMachine from '../slotMachines/BaseMachine';
import ReelsSpinSystem, { ReelsSpinSystemCompatibleEntity, ReelsSpinSystemSettings } from './ReelsSpinSystem';
import SingleSlotReel from './reels/SingleSlotReel';
import Reel from './reels/Reel';
import { ScreenMapRecord } from '../types/ScreenInputMap';

/** Settings for {@link SingleSlotReelsSpinSystem}. */
export interface SingleSlotReelsSpinSystemSettings extends ReelsSpinSystemSettings {
    /** Delay in seconds between stopping individual slot reels within the same column. */
    slotStopDelay: number;
}

interface ColumnStopTask {
    reelIndex: number;
    mapIndex: number;
    symbol: ReelsSpinSystemCompatibleEntity | null;
}

interface ColumnStopQueue {
    column: number;
    unifiedStop: number;
    tasks: ColumnStopTask[];
    nextTask: number;
    lastStopTime: number;
}

/**
 * SingleSlotReelsSpinSystem: specialized ReelsSpinSystem that creates N per-cell SingleSlotReel instances
 * and maps symbols per slot.
 */
export default class SingleSlotReelsSpinSystem extends ReelsSpinSystem {
    public static override defaultSettings: SingleSlotReelsSpinSystemSettings = {
        ...ReelsSpinSystem.defaultSettings,
        slotStopDelay: 0,
    };

    private _cellPreventReelsSpin: boolean[] = [];
    private _columnStopQueues: ColumnStopQueue[] = [];
    private _nextColumnToSchedule: number = 0;
    private _lastColumnFirstSlotTime: number = Number.NEGATIVE_INFINITY;

    constructor(machine: BaseMachine) {
        super(machine);
        this.settings = {
            ...SingleSlotReelsSpinSystem.defaultSettings,
            ...this.settings,
        } as SingleSlotReelsSpinSystemSettings;
    }

    /** Returns the settings as {@link SingleSlotReelsSpinSystemSettings}. */
    private get _typedSettings(): SingleSlotReelsSpinSystemSettings {
        return this.settings as SingleSlotReelsSpinSystemSettings;
    }

    protected override getReelCount(): number {
        return this.machine.config.reels * this.machine.config.slots;
    }

    protected override createReel(index: number): Reel {
        return new SingleSlotReel(this, index);
    }

    /**
     * Override onEnter to reconcile per-cell prevent map with base column-level logic.
     * - Cache per-cell map locally for SingleSlot start/stop decisions.
     * - Expose a column-level prevent map to the base class so its stopping logic works correctly.
     */
    public override onEnter(): void {
        super.onEnter();
        const reels = this.machine.config.reels;
        const slots = this.machine.config.slots;
        // Accept shorter per-cell arrays and pad with false up to total cells
        const total = reels * slots;
        const input = (this.machine.maps.preventReelsSpin || []) as boolean[];
        const padded: boolean[] = new Array(total).fill(false);
        for (let i = 0; i < Math.min(input.length, total); i++) {
            padded[i] = !!input[i];
        }
        this._cellPreventReelsSpin = padded;
        this._columnStopQueues = [];
        this._nextColumnToSchedule = 0;
        this._lastColumnFirstSlotTime = Number.NEGATIVE_INFINITY;
        // Keep per-cell map in machine.maps to let base checks (reels.every) treat prevented cell-reels as already spun
        this.machine.maps.preventReelsSpin = this._cellPreventReelsSpin;
    }

    protected override _getSymbolsFromScreen(index: number): ReelsSpinSystemCompatibleEntity[] {
        const id = this.machine.maps.symbols[index];
        if (!id) return [];
        const symbol = this.machine.getChildByName(id) as unknown as ReelsSpinSystemCompatibleEntity;
        return symbol ? [symbol] : [];
    }

    protected override _getReelMapForStop(): ReelsSpinSystemCompatibleEntity[] {
        const stoppingIndex = this.machine.sharedSystemData.currentStoppingReel ?? 0;
        const record: ScreenMapRecord = this.machine.inputs.screenMap[stoppingIndex];
        if (!record) return [];
        const symbol = this.machine.createSymbol(record.key);
        if (record.value !== undefined) {
            (symbol as any).value = record.value as any;
        }
        return [symbol];
    }

    /**
     * Callback hook: in SingleSlotReelsSpinSystem the screen map is set per-cell during stopNext().
     * Do not rebuild or mutate the map here.
     */
    override _updateScreenMap(): void {
        // no-op: screen map was already updated during stopNext()
    }

    /**
     * Override update to use per-cell prevention without column aggregation.
     * Skips stopping a column only if all its cell-reels are prevented.
     */
    public override update(dt: number): void {
        const settings = this._typedSettings;
        // advance time in seconds
        this.elapsedTime += dt * 0.001;

        // STARTING phase
        if (this.state === 'starting') {
            const canStart = this.startingReel < this.machine.config.reels
                && this.elapsedTime >= this.startingReel * settings.reelStartDelay;
            if (canStart) {
                this.startNext();
                if (settings.reelStartDelay === 0) {
                    // start remaining columns immediately
                    this.update(0);
                    return;
                }
            }
            // transition to SPINNING when all reels are spinning or prevented
            const prevent = this.machine.maps.preventReelsSpin || [];
            if (this.reels.every((reel, i) => reel.currentState.name === 'Spinning' || !!prevent[i])) {
                this.state = 'spinning';
            }
        }

        // SPINNING phase
        if (this.state === 'spinning') {
            if (this.elapsedTime >= settings.spinningDuration && this.machine.inputs.screenMap) {
                this.state = 'stopping';
                this.stoppingStartTime = this.elapsedTime;
                this.machine.events.addEvent('inProcessingStarted');
            }
        }

        // STOPPING phase
        if (this.state === 'stopping') {
            this._scheduleColumnStops(settings.reelStopDelay);
            this._processColumnStopQueues();
            // Finish when every per-cell reel is Stopped or Prevented
            const prevent = this.machine.maps.preventReelsSpin || [];
            if (this.reels.every((reel, i) => reel.currentState.name === 'Stopped' || !!prevent[i])) {
                this.machine.events.addEvent('inProcessingEnded');
                this._updateScreenMap();
                this.state = 'stopped';
                this.machine.flags.isSpinning = false;
            }
        }

        // Update all reels
        this.reels.forEach((reel) => reel.update(dt));

        // Shared data update
        this.machine.sharedSystemData.spinSystemState = this.state as any;
        this.machine.sharedSystemData.currentStoppingReel = this._nextColumnToSchedule;
        this.machine.sharedSystemData.reelsStates = this.reels.map((reel) => reel.currentState.name);
    }

    private _isColumnFullyPrevented(col: number): boolean {
        const reels = this.machine.config.reels;
        const slots = this.machine.config.slots;
        for (let row = 0; row < slots; row++) {
            const idx = col + row * reels;
            if (!this._cellPreventReelsSpin[idx]) return false;
        }
        return true;
    }

    /** Starts all row reels for the current column in a single step. */
    public override startNext(): void {
        const col = this.startingReel;
        const reels = this.machine.config.reels;
        const slots = this.machine.config.slots;

        // Guard: nothing to start if column index exceeds total columns
        if (col >= reels) return;

        const preventMap: boolean[] = this._cellPreventReelsSpin || [];
        for (let row = 0; row < slots; row++) {
            const idx = col + row * reels; // per-cell reel index
            if (!preventMap[idx]) {
                this.reels[idx].start();
            }
        }
        // Advance to next column regardless, so fully prevented columns are skipped
        this.startingReel++;
    }

    /** Stops all row reels for the current column in a single step. */
    public override stopNext(): void {
        this._scheduleNextColumnStop();
    }

    // Ensure landed marking works per column for per-cell reels
    protected override onReelDecelerationEnded(reel: Reel): void {
        const reels = this.machine.config.reels;
        const reelCol = reel.index % reels;
        this.machine.maps.symbols.forEach((id: string, i: number) => {
            if (reelCol !== this.machine.utils.grid.fromMapIndexToGridCoords(i).x) return;
            const entity = this.machine.getChildByName(id) as unknown as ReelsSpinSystemCompatibleEntity;
            if (!entity) return;
            entity.isLanded = true;
        });
    }

    /**
     * Builds a queue describing how reels in a column should be stopped.
     */
    private _createColumnStopQueue(column: number, unifiedStop: number, rows: number[]): ColumnStopQueue {
        const reels = this.machine.config.reels;
        const queue: ColumnStopTask[] = rows.map((row) => {
            const reelIndex = column + row * reels;
            const mapIndex = this.machine.utils.grid.fromCRtoMapIndex(column, row);
            const record: ScreenMapRecord = this.machine.inputs.screenMap[mapIndex];
            let symbol: ReelsSpinSystemCompatibleEntity | null = null;
            if (record) {
                symbol = this.machine.createSymbol(record.key);
                if (record.value !== undefined) {
                    (symbol as any).value = record.value as any;
                }
            }

            return { reelIndex, mapIndex, symbol };
        });

        return {
            column,
            unifiedStop,
            tasks: queue,
            nextTask: 0,
            lastStopTime: Number.NEGATIVE_INFINITY,
        };
    }

    /**
     * Progresses the currently active column stop queue while respecting the configured delay.
     */
    private _processColumnStopQueues(): void {
        const slotDelay = this._typedSettings.slotStopDelay ?? 0;

        for (let i = 0; i < this._columnStopQueues.length;) {
            const queue = this._columnStopQueues[i];

            while (queue.nextTask < queue.tasks.length) {
                const isFirst = queue.nextTask === 0;
                if (!isFirst && (this.elapsedTime - queue.lastStopTime) < slotDelay) {
                    break;
                }

                this._stopNextSlotFromQueue(queue);

                if (queue.nextTask >= queue.tasks.length) {
                    break;
                }
            }

            if (queue.nextTask >= queue.tasks.length) {
                this._finalizeQueueAtIndex(i);
            } else {
                i++;
            }
        }
    }

    /**
     * Stops the next slot reel according to the active queue definition.
     */
    private _stopNextSlotFromQueue(queue: ColumnStopQueue): void {
        const task = queue.tasks[queue.nextTask];
        const reel = this.reels[task.reelIndex] as SingleSlotReel;
        const symbols = task.symbol ? [task.symbol] : [];

        this._setSymbolMapEntry(task.mapIndex, task.symbol);

        const stopPosition = queue.unifiedStop;

        reel.trimSymbolsFrom(stopPosition);
        reel.addSymbols(symbols, stopPosition);
        reel.retainOnlySymbolAt(stopPosition);
        reel.stop(stopPosition);

        if (queue.nextTask === 0) {
            this._lastColumnFirstSlotTime = this.elapsedTime;
        }

        queue.nextTask++;
        queue.lastStopTime = this.elapsedTime;
    }

    /** Removes a completed queue and advances the stopped column counter. */
    private _finalizeQueueAtIndex(index: number): void {
        this._columnStopQueues.splice(index, 1);
        this.stoppingReel++;
    }

    /** Updates the machine symbol map for the provided index. */
    private _setSymbolMapEntry(mapIndex: number, symbol: ReelsSpinSystemCompatibleEntity | null): void {
        if (!Array.isArray(this.machine.maps.symbols)) {
            this.machine.maps.symbols = [];
        }
        if (this.machine.maps.symbols.length <= mapIndex) {
            this.machine.maps.symbols.length = mapIndex + 1;
        }
        this.machine.maps.symbols[mapIndex] = symbol ? symbol.name : null;
    }

    /**
     * Calculates the appropriate stop position for the reel based on the number of symbols it holds.
     * Ensures that enough symbols are present on the reel to reach the target stop position.
     * @private
     * @param {Reel} reel - The reel for which to calculate the stop position.
     * @returns {number} - The calculated stop position for the reel.
     */
    protected override _calculateStopPosition(reel: SingleSlotReel): number {
        const lastSymbolPosition = reel.getLastSymbolPosition();
        return lastSymbolPosition + 1;
    }

    /** Attempts to schedule the next column stop if timing conditions are met. */
    private _scheduleColumnStops(reelStopDelay: number): void {
        const totalColumns = this.machine.config.reels;
        const delay = Math.max(0, reelStopDelay);

        while (this._nextColumnToSchedule < totalColumns) {
            const columnIndex = this._nextColumnToSchedule;

            if (this._isColumnFullyPrevented(columnIndex)) {
                this._markColumnAsCompleted();
                continue;
            }

            if (!this._canStartNextColumn(delay)) {
                break;
            }

            this.stopNext();
        }
    }

    /** Returns true when enough time has elapsed to begin the next column stop. */
    private _canStartNextColumn(delay: number): boolean {
        if (this._nextColumnToSchedule === 0) {
            return true;
        }
        return (this.elapsedTime - this._lastColumnFirstSlotTime) >= delay;
    }

    /** Schedules the next available column stop queue. */
    private _scheduleNextColumnStop(): void {
        const totalColumns = this.machine.config.reels;
        const reels = this.machine.config.reels;
        const slots = this.machine.config.slots;

        while (this._nextColumnToSchedule < totalColumns) {
            const column = this._nextColumnToSchedule;
            if (this._isColumnFullyPrevented(column)) {
                this._markColumnAsCompleted();
                continue;
            }

            const rowsToStop: number[] = [];
            for (let row = 0; row < slots; row++) {
                const idx = column + row * reels;
                if (!this._cellPreventReelsSpin[idx]) rowsToStop.push(row);
            }

            if (rowsToStop.length === 0) {
                this._markColumnAsCompleted();
                continue;
            }

            let unifiedStop = 0;
            for (const row of rowsToStop) {
                const idx = column + row * reels;
                const reel = this.reels[idx] as SingleSlotReel;
                const pos = this._calculateStopPosition(reel);
                if (pos > unifiedStop) unifiedStop = pos;
            }

            const queue = this._createColumnStopQueue(column, unifiedStop, rowsToStop);
            this._columnStopQueues.push(queue);
            this._nextColumnToSchedule++;
            this._stopNextSlotFromQueue(queue);
            return;
        }
    }

    /** Marks a fully prevented column as finished. */
    private _markColumnAsCompleted(): void {
        this._nextColumnToSchedule++;
        this.stoppingReel++;
    }
}
