/**
 * Инвентарь — жанронезависимый контейнер предметов.
 * Хранит счётчики по id: `Map<itemId, count>`. Стеки неявные — количество.
 * Смена состава оповещает подписчиков (view-агностично): подписка — как у EventBus,
 * но события эмитит сам контейнер, чтобы работать без внешней шины.
 */

export interface InventoryState {
    /** Предмет -> количество (только > 0). */
    items: Record<string, number>;
}

/** Создать инвентарь из сериализованного состояния. */
export function inventoryFromData(data: InventoryData | undefined | null): Inventory {
    const inv = new Inventory();
    if (data) inv.load(data);
    return inv;
}

/** Сериализованное состояние инвентаря (для сейвов). */
export interface InventoryData {
    items: Record<string, number>;
}

export class Inventory {
    private counts = new Map<string, number>();
    private listeners = new Set<() => void>();

    /** Подписка на любое изменение содержимого. Возвращает отписку. */
    onChange(fn: () => void): () => void {
        this.listeners.add(fn);
        return () => this.listeners.delete(fn);
    }

    /** Добавить n штук предмета (n >= 1). */
    add(itemId: string, n = 1): void {
        if (n <= 0) return;
        this.counts.set(itemId, (this.counts.get(itemId) ?? 0) + n);
        this.notify();
    }

    /**
     * Убрать n штук предмета. Если после вычитания стало <= 0 — слот исчезает.
     * Возвращает, сколько реально убрано (нельзя убрать больше, чем есть).
     */
    remove(itemId: string, n = 1): number {
        const have = this.counts.get(itemId) ?? 0;
        if (have <= 0 || n <= 0) return 0;
        const left = Math.max(0, have - n);
        if (left === 0) this.counts.delete(itemId);
        else this.counts.set(itemId, left);
        this.notify();
        return Math.min(n, have);
    }

    /** Сколько штук предмета лежит (0, если нет). */
    count(itemId: string): number {
        return this.counts.get(itemId) ?? 0;
    }

    /** Есть ли предмет (в количестве >= 1). */
    has(itemId: string): boolean {
        return this.count(itemId) > 0;
    }

    /** Все слоты (id -> количество); порядок вставки. */
    get all(): Array<{ id: string; count: number }> {
        return [...this.counts].map(([id, count]) => ({ id, count }));
    }

    get empty(): boolean {
        return this.counts.size === 0;
    }

    /** Полностью очистить (новая игра). */
    clear(): void {
        if (this.counts.size === 0) return;
        this.counts.clear();
        this.notify();
    }

    serialize(): InventoryData {
        return { items: Object.fromEntries(this.counts) };
    }

    load(data: InventoryData): void {
        this.counts.clear();
        for (const [id, count] of Object.entries(data.items ?? {})) {
            if (typeof count === 'number' && count > 0) this.counts.set(id, count);
        }
        this.notify();
    }

    private notify(): void {
        for (const fn of this.listeners) fn();
    }
}