import type { RNG } from './RNG.js';

// RNG 유틸리티 모음
export class RandUtil {
    constructor(protected rng: RNG) {}

    public nextFloat1(): number {
        return this.rng.nextFloat1();
    }

    public nextRange(min: number, max: number): number {
        const range = max - min;
        return this.nextFloat1() * range + min;
    }

    public nextRangeInt(minInclusive: number, maxInclusive: number): number {
        const range = maxInclusive - minInclusive;
        return this.rng.nextInt(range) + minInclusive;
    }

    public nextInt(minInclusive: number, maxExclusive: number): number {
        const span = maxExclusive - minInclusive;
        if (span <= 1) {
            return minInclusive;
        }
        return minInclusive + this.rng.nextInt(span - 1);
    }

    /**
     * Draw from the wrapped RNG's inclusive integer domain directly.
     *
     * Unlike nextInt(min, max), this deliberately consumes an RNG call when
     * maxInclusive is zero. Legacy RandUtil::choice() has that observable
     * behavior for a one-element collection.
     */
    public nextIntInclusive(maxInclusive: number): number {
        return this.rng.nextInt(maxInclusive);
    }

    public nextBit(): boolean {
        const bits = this.rng.nextBits(1);
        return bits[0]! != 0;
    }

    public nextBool(prob = 0.5): boolean {
        if (prob >= 1) {
            return true;
        }
        if (prob === 0.5) {
            return this.nextBit();
        }
        if (prob <= 0) {
            return false;
        }
        return this.nextFloat1() < prob;
    }

    public shuffle<T>(srcArray: T[]): T[] {
        const cnt = srcArray.length;
        if (cnt === 0) {
            return [];
        }
        if (cnt > this.rng.getMaxInt()) {
            throw 'Invalid random int range';
        }

        const result: T[] = Array.from(srcArray);
        for (let srcIdx = 0; srcIdx < cnt; srcIdx += 1) {
            const destIdx = this.rng.nextInt(cnt - srcIdx - 1) + srcIdx;
            if (srcIdx === destIdx) {
                continue;
            }
            const srcValue = result[srcIdx]!;
            const destValue = result[destIdx]!;
            result[srcIdx] = destValue;
            result[destIdx] = srcValue;
        }

        return result;
    }

    //Object는 integer key에 예외가 있어 shuffleAssoc은 없음

    public choice<T>(items: T[] | Record<string | number, T> | Set<T>): T {
        if (items instanceof Array) {
            if (items.length === 0) {
                throw new Error('Empty items');
            }
            const idx = this.rng.nextInt(items.length - 1);
            return items[idx]!;
        }

        if (items instanceof Set) {
            return this.choice(Array.from(items.values()));
        }

        const key = this.choice(Array.from(Object.keys(items))) as keyof typeof items;
        return items[key]!;
    }

    public choiceUsingWeight(items: Record<string | number, number>): string | number {
        if (Object.keys(items).length === 0) {
            throw new Error('Empty items');
        }
        let sum = 0;
        for (const value of Object.values(items)) {
            if (value <= 0) {
                continue;
            }
            sum += value;
        }

        let rd = this.nextFloat1() * sum;

        for (const [item, value] of Object.entries(items)) {
            if (value <= 0) {
                if (rd <= 0) {
                    return item;
                }
                continue;
            }

            if (rd <= value) {
                return item;
            }
            rd -= value;
        }

        throw new Error('Unreacheable');
    }

    public choiceUsingWeightPair<T>(items: [T, number][]): T {
        if (items.length === 0) {
            throw new Error('Empty items');
        }
        let sum = 0;
        for (const [, value] of items) {
            if (value <= 0) {
                continue;
            }
            sum += value;
        }

        let rd = this.nextFloat1() * sum;

        for (const [item, value] of items) {
            if (value <= 0) {
                if (rd <= 0) {
                    return item;
                }
                continue;
            }

            if (rd <= value) {
                return item;
            }
            rd -= value;
        }

        throw new Error('Unreacheable');
    }
}
