/**
 * Generic object pool used by damage effects, bullets, enemies and VFX
 * (requirement 18.5). Pure-TS, platform-agnostic, Jest-testable.
 *
 * Design notes:
 *  - `factory` creates a brand-new instance when the free list is empty.
 *  - `resetter` is invoked on every released object, letting the caller wipe
 *    transient state (position, timers, listeners) before it goes back to
 *    the pool.
 *  - `maxSize` caps the retained instances; objects released beyond the cap
 *    are dropped (letting the GC collect them) to bound memory usage
 *    (requirement 18.4: memory peak ≤ 200MB).
 *  - Double-release is silently ignored but reported through `onDoubleRelease`
 *    so tests / Logger can assert correctness.
 */

export type ObjectFactory<T> = () => T;
export type ObjectResetter<T> = (obj: T) => void;

export interface ObjectPoolOptions<T> {
    /** Required creator invoked when the pool is empty. */
    factory: ObjectFactory<T>;
    /** Optional cleaner invoked on every `release`. */
    resetter?: ObjectResetter<T>;
    /** Max retained objects; excess releases are discarded. Default 128. */
    maxSize?: number;
    /** Optional pre-warm count (creates this many objects upfront). Default 0. */
    preAlloc?: number;
    /** Optional diagnostic hook. */
    onDoubleRelease?: (obj: T) => void;
}

export class ObjectPool<T> {
    private readonly free: T[] = [];
    private readonly borrowed = new Set<T>();
    private readonly factory: ObjectFactory<T>;
    private readonly resetter?: ObjectResetter<T>;
    private readonly maxSize: number;
    private readonly onDoubleRelease?: (obj: T) => void;

    // Diagnostics
    private _acquiredTotal = 0;
    private _recycledTotal = 0;
    private _createdTotal = 0;

    constructor(options: ObjectPoolOptions<T>) {
        this.factory = options.factory;
        this.resetter = options.resetter;
        this.maxSize = options.maxSize ?? 128;
        this.onDoubleRelease = options.onDoubleRelease;

        const preAlloc = options.preAlloc ?? 0;
        for (let i = 0; i < preAlloc; i++) {
            const inst = this.factory();
            this._createdTotal++;
            this.free.push(inst);
        }
    }

    /** Acquire an object from the pool (creates one if empty). */
    public acquire(): T {
        this._acquiredTotal++;
        const inst = this.free.pop();
        if (inst !== undefined) {
            this.borrowed.add(inst);
            return inst;
        }
        const created = this.factory();
        this._createdTotal++;
        this.borrowed.add(created);
        return created;
    }

    /** Release an object back to the pool. Double-releases are ignored. */
    public release(obj: T): void {
        if (!this.borrowed.has(obj)) {
            this.onDoubleRelease?.(obj);
            return;
        }
        this.borrowed.delete(obj);
        this.resetter?.(obj);
        if (this.free.length < this.maxSize) {
            this.free.push(obj);
            this._recycledTotal++;
        }
        // else: drop the object so the GC can reclaim it.
    }

    /** Number of objects currently held in the free list. */
    public get freeCount(): number {
        return this.free.length;
    }

    /** Number of objects that are currently out on loan. */
    public get borrowedCount(): number {
        return this.borrowed.size;
    }

    /** Drop everything. Used on scene unload. */
    public drain(): void {
        this.free.length = 0;
        this.borrowed.clear();
    }

    /** Diagnostic stats (used by Logger / perf BI埋点). */
    public stats() {
        return {
            free: this.freeCount,
            borrowed: this.borrowedCount,
            acquired: this._acquiredTotal,
            recycled: this._recycledTotal,
            created: this._createdTotal,
        };
    }
}
