import { createDeferredPromise } from '../utils/createDeferredPromise';

import type BaseGameData from './BaseGameData';

/** Minimal mode API required by {@link EndRoundModel}. */
interface EndRoundMode
{
    isBonusMode(): boolean;
    isExitConditionMet(): boolean;
    gameData: BaseGameData;
}

/** Model contract consumed by the end-round workflow. */
export interface EndRoundModelContext
{
    emit(eventName: string, ...args: unknown[]): boolean | void;
    isRespinsGameRestored(): boolean;
    isFreespinsGameRestored(): boolean;
    isBigWinAchieved(): boolean;
    isWinLimitReached(): boolean;
    isRespinsGameTriggered(): boolean;
    isFreespinsGameTriggered(): boolean;
    isCasinoRoundsTriggered(): boolean;
    isCasinoRoundsRestored(): boolean;
    isCasinoRoundsFinished(): boolean;
    currentMode: EndRoundMode | undefined;
    modeStack: { length: number };
    waitingForRestore: boolean;
    waitingForWinProcessing: boolean;
    waitingForModeSwitch: boolean;
    waitingForCasinoRoundsNotification: boolean;
}

/** Events emitted when individual end-round cases are processed. */
type EndRoundEvent =
    | 'respinsGameRestore'
    | 'freespinsGameRestore'
    | 'showBigWin'
    | 'showMaxWin'
    | 'showTotalWin'
    | 'respinsGameTriggered'
    | 'freespinsGameTriggered'
    | 'exitBonusGame'
    | 'casinoRoundsTriggered'
    | 'casinoRoundsRestore'
    | 'exitCasinoRounds'
    | 'switchToCasinoRoundsMode';

/** Flow step executed during end-round processing. */
interface EndRoundCase
{
    /** Checks if the case should run. */
    isMet(model: EndRoundModelContext): boolean;

    /** Executes the side effect associated with the case. */
    execute(model: EndRoundModelContext): void;

    /** Indicates whether the case finished its work. */
    isComplete(model: EndRoundModelContext): boolean;
}

/** Coordinates post-round transitions by running matching end-round cases in sequence. */
export default class EndRoundModel
{
    private readonly model: EndRoundModelContext;

    private isProcessing = false;

    private cases: EndRoundCase[] = [];

    private currentCase: EndRoundCase | null = null;

    public constructor(model: EndRoundModelContext)
    {
        this.model = model;
    }

    /** Indicates whether end-round processing is currently active. */
    public get isRoundEndProcessing(): boolean
    {
        return this.isProcessing;
    }

    /**
     * Runs the processing loop until each end-round case completes.
     * @returns Resolves when all cases are finished.
     */
    public async preformEndRound(): Promise<void>
    {
        if (this.isProcessing)
        {
            return;
        }

        this.startProcessing();

        const { promise, resolve } = createDeferredPromise();

        const loop = (): void =>
        {
            if (this.isAllCasesProcessed())
            {
                this.finishProcessing();
                resolve();

                return;
            }

            const current = this.currentCase;

            if (!current)
            {
                this.processNextCase();
            }
            else if (current.isComplete(this.model))
            {
                this.currentCase = null;
                this.processNextCase();
            }

            requestAnimationFrame(loop);
        };

        loop();

        await promise;
    }

    /** Initializes the queue of end-round cases. */
    private startProcessing(): void
    {
        this.isProcessing = true;
        this.cases = this.createCases();
        this.currentCase = null;
    }

    /** Selects the next matching case and executes it if its condition is met. */
    private processNextCase(): void
    {
        if (this.cases.length === 0)
        {
            return;
        }

        const nextCase = this.cases.shift();

        if (!nextCase)
        {
            return;
        }

        this.currentCase = nextCase;

        if (nextCase.isMet(this.model))
        {
            nextCase.execute(this.model);

            return;
        }

        if (!nextCase.isComplete(this.model))
        {
            return;
        }

        this.processNextCase();
    }

    /** Checks whether all cases have been handled. */
    private isAllCasesProcessed(): boolean
    {
        return this.currentCase === null && this.cases.length === 0;
    }

    /** Resets the internal state after processing is finished. */
    private finishProcessing(): void
    {
        this.isProcessing = false;
        this.currentCase = null;
    }

    /** Creates the ordered list of cases to evaluate after each round. */
    protected createCases(): EndRoundCase[]
    {
        return [
            new BigWinAchievedCase(),
            new MaxWinAchievedCase(),
            new RespinsGameRestoredCase(),
            new FreespinsGameRestoredCase(),
            new TotalWinAchievedCase(),
            new FreespinsGameTriggeredCase(),
            new RespinsGameTriggeredCase(),
            new ExitBonusGameConditionMetCase(),
            new CasinoRoundsTriggeredCase(),
            new CasinoRoundsRestoredCase(),
            new CasinoRoundsFinishedCase(),
            new CasinoRoundsAreAvailableAfterBonusGame(),
        ];
    }
}

/** Base implementation for end-round cases. */
class BaseEndRoundCase implements EndRoundCase
{
    protected constructor(private readonly eventName: EndRoundEvent)
    {}

    public isMet(_model: EndRoundModelContext): boolean
    {
        return false;
    }

    public execute(model: EndRoundModelContext): void
    {
        model.emit(this.eventName);
    }

    public isComplete(_model: EndRoundModelContext): boolean
    {
        return true;
    }
}

/** Notifies listeners when a respins game should be restored. */
class RespinsGameRestoredCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('respinsGameRestore');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        return model.isRespinsGameRestored();
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForRestore = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForRestore;
    }
}

/** Notifies listeners when a free spins game should be restored. */
class FreespinsGameRestoredCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('freespinsGameRestore');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        return model.isFreespinsGameRestored();
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForRestore = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForRestore;
    }
}

/** Signals that a big win animation must be displayed. */
class BigWinAchievedCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('showBigWin');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        return model.isBigWinAchieved() && !model.isWinLimitReached();
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForWinProcessing = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForWinProcessing;
    }
}

/** Signals that a max win animation must be displayed. */
class MaxWinAchievedCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('showMaxWin');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        return model.isWinLimitReached();
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForWinProcessing = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForWinProcessing;
    }
}

/** Emits total win handling once a bonus exit condition is reached. */
class TotalWinAchievedCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('showTotalWin');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        const mode = model.currentMode;

        return Boolean(mode?.isBonusMode() && mode.isExitConditionMet() && !model.isWinLimitReached());
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForWinProcessing = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForWinProcessing;
    }
}

/** Switches the game into respins mode when triggered. */
class RespinsGameTriggeredCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('respinsGameTriggered');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        return model.isRespinsGameTriggered();
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForModeSwitch = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForModeSwitch;
    }
}

/** Switches the game into free spins mode when triggered. */
class FreespinsGameTriggeredCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('freespinsGameTriggered');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        return model.isFreespinsGameTriggered();
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForModeSwitch = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForModeSwitch;
    }
}

/** Leaves the bonus mode when its exit condition is satisfied. */
class ExitBonusGameConditionMetCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('exitBonusGame');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        const mode = model.currentMode;

        return Boolean(mode?.isBonusMode() && mode.isExitConditionMet());
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForModeSwitch = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForModeSwitch;
    }
}

/** Handles casino rounds trigger notification. */
class CasinoRoundsTriggeredCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('casinoRoundsTriggered');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        return model.isCasinoRoundsTriggered();
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForCasinoRoundsNotification = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForCasinoRoundsNotification;
    }
}

/** Handles casino rounds restoration notification. */
class CasinoRoundsRestoredCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('casinoRoundsRestore');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        return model.isCasinoRoundsRestored();
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForCasinoRoundsNotification = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForCasinoRoundsNotification;
    }
}

/** Handles casino rounds completion and mode switch back to the main game. */
class CasinoRoundsFinishedCase extends BaseEndRoundCase
{
    public constructor()
    {
        super('exitCasinoRounds');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        return model.isCasinoRoundsFinished();
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForModeSwitch = true;
        model.waitingForCasinoRoundsNotification = true;
        super.execute(model);
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForModeSwitch && !model.waitingForCasinoRoundsNotification;
    }
}

/** Offers casino rounds when they become available after leaving a bonus. */
class CasinoRoundsAreAvailableAfterBonusGame extends BaseEndRoundCase
{
    public constructor()
    {
        super('switchToCasinoRoundsMode');
    }

    public override isMet(model: EndRoundModelContext): boolean
    {
        const mode = model.currentMode;

        if (!mode)
        {
            return false;
        }

        const isAvailable = mode.gameData.isCasinoRoundsAvailable;
        const isCurrentModeMain = model.modeStack.length === 1;

        return isAvailable && isCurrentModeMain;
    }

    public override execute(model: EndRoundModelContext): void
    {
        model.waitingForModeSwitch = true;
        model.waitingForCasinoRoundsNotification = true;
        model.emit('switchToCasinoRoundsMode');
        model.emit('casinoRoundsTriggered');
    }

    public override isComplete(model: EndRoundModelContext): boolean
    {
        return !model.waitingForModeSwitch && !model.waitingForCasinoRoundsNotification;
    }
}
