import { Assets } from 'pixi.js';
import LobbyLinkNotFoundError from '../errors/LobbyLinkNotFoundError';
import { Network, ReplayNetwork } from '../services';
import Locator from '../services/Locator';

export type ThresholdHandler<TArgs extends unknown[] = unknown[]> = (...args: TArgs) => void;

interface Onceable
{
    once(event: PropertyKey, listener: (...args: unknown[]) => void): void;
}

interface StorageService
{
    get<T = unknown>(key: string): T | undefined;
    set<T = unknown>(key: string, value: T): void;
}

/** Returns the active storage service instance. */
function resolveStorage(): StorageService
{
    return Locator.storage as StorageService;
}

/**
 * Creates a handler that triggers a callback every N invocations.
 * @param threshold
 * @param callback
 * @param eachCallback
 */
export function createThresholdCallback<TArgs extends unknown[]>(
    threshold: number,
    callback?: ThresholdHandler<TArgs>,
    eachCallback?: ThresholdHandler<TArgs>,
): ThresholdHandler<TArgs>
{
    let counter = 0;

    return (...args: TArgs) =>
    {
        counter += 1;
        eachCallback?.(...args);

        if (counter === threshold)
        {
            counter = 0;
            callback?.(...args);
        }
    };
}

/**
 * Resolves when the requested event fires once on the target.
 * @param target
 * @param event
 */
export function waitForEventOnce(target: Onceable, event: PropertyKey): Promise<void>
{
    return new Promise((resolve) =>
    {
        target.once(event, () => resolve());
    });
}

/**
 * Resolves when the first event from a list fires.
 * @param events
 */
export function waitForFirstEmittedEvent(events: Array<{ target: Onceable; event: PropertyKey }>): Promise<void>
{
    const promises = events.map(({ target, event }) => waitForEventOnce(target, event));

    return Promise.race(promises);
}

/**
 * Capitalizes the first letter of the provided string.
 * @param value
 */
export function capitalizeFirstLetter(value: string): string
{
    return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
}

/**
 * Transposes a two dimensional matrix.
 * @param matrix
 */
export function transpose<T>(matrix: T[][]): T[][]
{
    if (matrix.length === 0)
    {
        return [];
    }

    return matrix[0].map((_, columnIndex) => matrix.map((row) => row[columnIndex]));
}

/**
 * Resolves once the callback condition evaluates to true.
 * @param conditionCallback
 */
export function awaitCallback(conditionCallback: () => boolean): Promise<void>
{
    const check = (resolve: () => void): void =>
    {
        if (conditionCallback())
        {
            resolve();

            return;
        }

        requestAnimationFrame(() => check(resolve));
    };

    return new Promise(check);
}

/**
 * Builds a storage key for the latest spin result.
 * @param cacheId
 */
export function getLatestSpinKey(cacheId: string | number | undefined = __OPTIONS__?.cache_id): string
{
    return `latestSpinResultV2${cacheId ?? ''}`;
}

/**
 * Builds a storage key for the latest spin timestamp.
 * @param cacheId
 */
export function getLatestSpinTimeKey(cacheId: string | number | undefined = __OPTIONS__?.cache_id): string
{
    return `latestSpinResultTimeV2${cacheId ?? ''}`;
}

/**
 * Normalizes bet configuration for the client.
 * @param bets
 * @param defaultIndex
 * @param currentIndex
 */
export function createBetData(bets: number | number[], defaultIndex = 0, currentIndex = 0): {
    bets: number[];
    defaultBet: number;
    current: number;
}
{
    return {
        bets: Array.isArray(bets) ? bets : [bets],
        defaultBet: defaultIndex,
        current: currentIndex,
    };
}

const STORAGE_KEY = 'isSpaceBarHandlingActive';
const TURBO_MODE_KEY = 'turboMode';
const LAST_SET_USER_BET_KEY = 'lastSetUserBet';

/**
 * Normalizes arbitrary persisted values to a boolean representation.
 * @param value Raw value retrieved from storage.
 * @param defaultValue Fallback when the value is `null` or `undefined`.
 */
function normalizeBoolean(value: unknown, defaultValue = false): boolean
{
    if (value == null)
    {
        return defaultValue;
    }

    if (typeof value === 'boolean')
    {
        return value;
    }

    if (typeof value === 'number')
    {
        return value !== 0;
    }

    if (typeof value === 'string')
    {
        const normalized = value.trim().toLowerCase();

        if (normalized === 'true')
        {
            return true;
        }

        if (normalized === 'false')
        {
            return false;
        }

        const numericValue = Number(normalized);

        if (!Number.isNaN(numericValue))
        {
            return numericValue !== 0;
        }
    }

    return Boolean(value);
}

/** Reads the space-bar handling state from storage; defaults to enabled when missing. */
export function loadSpaceBarHandlingState(): boolean
{
    const saved = resolveStorage().get(STORAGE_KEY);

    return normalizeBoolean(saved, true);
}

/**
 * Persists the space-bar handling state.
 * @param state
 */
export function saveSpaceBarHandlingState(state: boolean): void
{
    resolveStorage().set(STORAGE_KEY, Boolean(state));
}

/** Reads the turbo mode flag from storage. */
export function loadTurboModeState(): boolean
{
    const saved = resolveStorage().get<boolean>(TURBO_MODE_KEY);

    return saved === undefined ? false : Boolean(saved);
}

/**
 * Persists the turbo mode flag.
 * @param state
 */
export function saveTurboModeState(state: boolean): void
{
    resolveStorage().set(TURBO_MODE_KEY, Boolean(state));
}

/** Returns the gamble limit from configuration. */
export function loadGambleLimit(): number
{
    const value = __OPTIONS__?.math?.gamble_limit;

    return Number.isFinite(value) ? Number(value) : 0;
}

/** Returns the max multiplier from configuration. */
export function loadMaxMultiplier(): number
{
    const value = __OPTIONS__?.math?.max_multiplier;

    return Number.isFinite(value) ? Number(value) : 0;
}

/**
 * Redirects the player to the lobby if a link is configured.
 * @param onError
 */
export function navigateToLobby(onError?: (error: LobbyLinkNotFoundError) => void): void
{
    const returnAction = __OPTIONS__?.actions?.return as { link?: string } | undefined;
    const link = returnAction?.link;

    if (typeof link === 'string' && link.trim())
    {
        window.location.href = link;

        return;
    }

    onError?.(new LobbyLinkNotFoundError());
}

export interface LastUserBet
{
    bet: number;
    isChanged: boolean;
}

/** Reads the last set user bet from storage. */
export function loadLastSetUserBet(): LastUserBet
{
    const saved = resolveStorage().get<LastUserBet | number>(LAST_SET_USER_BET_KEY);

    if (saved === undefined)
    {
        return { bet: 0, isChanged: false };
    }

    if (typeof saved === 'number')
    {
        return { bet: Number(saved), isChanged: true };
    }

    if (typeof saved === 'object' && saved !== null)
    {
        return {
            bet: Number(saved.bet ?? 0),
            isChanged: Boolean((saved as LastUserBet).isChanged),
        };
    }

    return { bet: 0, isChanged: false };
}

/**
 * Persists the last set user bet to storage.
 * @param bet
 * @param isChanged
 */
export function saveLastSetUserBet(bet: number, isChanged = false): void
{
    resolveStorage().set(LAST_SET_USER_BET_KEY, {
        bet: Number(bet),
        isChanged: Boolean(isChanged),
    });
}

export function detectNetworkService()
{
    return isReplay()
        ? new ReplayNetwork()
        : new Network();
}

export function isReplay()
{
    return __OPTIONS__.server_id === 'replay';
}

export function getFallbackFontFamilyName(lang: string): string
{
    if (lang === 'ja')
    {
        return 'NotoSansJP';
    }
    else if (lang === 'ko')
    {
        return 'NotoSansKR';
    }
    else if (lang === 'zh-CN')
    {
        return 'NotoSansSC';
    }
    else if (lang === 'th')
    {
        return 'NotoSansThai';
    }

    return '';
}

export function getLoadedFallbackFontFamily(lang: string): string
{
    const fallbackFontFamilyName = getFallbackFontFamilyName(lang);

    if (Assets.cache.has(fallbackFontFamilyName))
    {
        return fallbackFontFamilyName;
    }

    return '';
}

export function tryToApplyFallbackFontFamily(textObject: any): void
{
    const fallbackFontFamily: string = getLoadedFallbackFontFamily(__OPTIONS__.locale);

    if (fallbackFontFamily)
    {
        // fallback font family is loaded
        const fontFamily = textObject.style.fontFamily;

        if (!fontFamily.includes(fallbackFontFamily))
        {
        // fallback font family is different from the original font family
            textObject.style.fontFamily = `${fontFamily}, ${fallbackFontFamily}`;
        }
    }
}

abstract class TextScaler
{
    textObject: any;
    constructor(textObject: any)
    {
        this.textObject = textObject;
    }

    abstract scaleToFit(size: any): void;
}

export class WordWrapScaler extends TextScaler
{
    override scaleToFit(size: any)
    {
        this.textObject.scale.set(1);
        if (!this.textObject.style.defaultWordWrapWidth)
        {
            this.textObject.style.defaultWordWrapWidth = this.textObject.style.wordWrapWidth;
        }
        this.textObject.style.wordWrapWidth = this.textObject.style.defaultWordWrapWidth;
        const scaleStep = 0.7;

        while (size.width / this.textObject.width < 1 || size.height / this.textObject.height < 1)
        {
            this.textObject.scale.set(this.textObject.scale.x * scaleStep, this.textObject.scale.y * scaleStep);
            this.textObject.style.wordWrapWidth = this.textObject.style.wordWrapWidth / scaleStep;
        }
    }
}
export class NonWordWrapScaler extends TextScaler
{
    override scaleToFit(size: any)
    {
        this.textObject.scale.set(1);
        const factor = Math.min(1, size.width / this.textObject.width, size.height / this.textObject.height);

        this.textObject.scale.set(factor);
    }
}
