// Seeded PRNG. Every random value in the engine comes from here — never Math.random,
// which would break frame-for-frame reproducibility between preview and export.
// Mulberry32, carried over from party-stage but with the state held per instance
// instead of on a global, so layers can't perturb each other's sequences.

export class Rng {
    constructor(seed = 1) {
        this.seed = seed >>> 0;
        this.state = this.seed;
    }

    reset(seed = this.seed) {
        this.seed = seed >>> 0;
        this.state = this.seed;
        return this;
    }

    /** Uniform [0, 1) */
    next() {
        let t = (this.state += 0x6d2b79f5) >>> 0;
        t = Math.imul(t ^ (t >>> 15), t | 1);
        t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    }

    /** Uniform [min, max) */
    range(min, max) {
        return min + this.next() * (max - min);
    }

    /** Integer in [min, max] inclusive */
    int(min, max) {
        return Math.floor(this.range(min, max + 1));
    }

    bool(p = 0.5) {
        return this.next() < p;
    }

    pick(array) {
        return array[this.int(0, array.length - 1)];
    }

    /** Weighted pick. `weights` parallels `array`; non-positive weights are skipped. */
    pickWeighted(array, weights) {
        let total = 0;
        for (let i = 0; i < array.length; i++) total += Math.max(0, weights[i] || 0);
        if (total <= 0) return this.pick(array);
        let r = this.next() * total;
        for (let i = 0; i < array.length; i++) {
            r -= Math.max(0, weights[i] || 0);
            if (r <= 0) return array[i];
        }
        return array[array.length - 1];
    }

    /** Fisher-Yates, returns a new array. */
    shuffle(array) {
        const out = array.slice();
        for (let i = out.length - 1; i > 0; i--) {
            const j = this.int(0, i);
            [out[i], out[j]] = [out[j], out[i]];
        }
        return out;
    }

    /** Derive an independent stream. Same parent + same label always yields the same child. */
    fork(label) {
        return new Rng(hashString(String(label), this.seed));
    }
}

/** FNV-1a over a string, optionally salted. Used for deriving seeds from names. */
export function hashString(str, salt = 0x811c9dc5) {
    let h = salt >>> 0;
    for (let i = 0; i < str.length; i++) {
        h ^= str.charCodeAt(i);
        h = Math.imul(h, 0x01000193) >>> 0;
    }
    return h >>> 0;
}

/**
 * FNV-1a over PCM samples, decimated so a six-minute track hashes in milliseconds.
 * This is what makes a given audio file always produce the same look: the seed is a
 * property of the content, not of the filename or the wall clock.
 */
export function hashSamples(float32, stride = 997) {
    let h = 0x811c9dc5 >>> 0;
    for (let i = 0; i < float32.length; i += stride) {
        // Quantize so imperceptible float noise can't change the seed.
        const q = Math.round(float32[i] * 32767) & 0xffff;
        h ^= q & 0xff;
        h = Math.imul(h, 0x01000193) >>> 0;
        h ^= (q >>> 8) & 0xff;
        h = Math.imul(h, 0x01000193) >>> 0;
    }
    return h >>> 0;
}
