package sim

import (
	"encoding/binary"

	"github.com/drjones/quantum-arcade/pkg/fixed"
)

// RNG is a deterministic xoshiro256** generator seeded from 32 bytes.
// It uses only integer operations, so a browser replaying a round reproduces
// the server's stream exactly.
//
// This is the expansion function, not the entropy source: the seed itself comes
// from the commit-reveal protocol, which is what makes outcomes unriggable.
type RNG struct {
	state [4]uint64
}

// NewRNG creates a reproducible generator from a 32-byte seed.
//
// The seed is run through SplitMix64 rather than copied into the state
// directly. Copying directly leaves the first output depending only on
// state[1], so seeds differing in other bytes produce identical first draws —
// which would make CrashPoint blind to most of its own seed.
func NewRNG(seed [32]byte) *RNG {
	// Fold every seed byte into a single accumulator first, so all 32 bytes
	// influence all four state words.
	acc := uint64(0x9E3779B97F4A7C15)
	for i := 0; i < 4; i++ {
		acc ^= binary.LittleEndian.Uint64(seed[i*8 : i*8+8])
		acc = splitMix64(&acc)
	}
	r := &RNG{}
	for i := 0; i < 4; i++ {
		r.state[i] = splitMix64(&acc)
	}
	// An all-zero state is a fixed point of the recurrence. SplitMix64 makes
	// this vanishingly unlikely, but the guard costs nothing.
	if r.state[0]|r.state[1]|r.state[2]|r.state[3] == 0 {
		r.state[0] = 0x9E3779B97F4A7C15
	}
	return r
}

// splitMix64 advances x and returns a well-mixed 64-bit value. Every input bit
// affects every output bit, which is the property the state expansion needs.
func splitMix64(x *uint64) uint64 {
	*x += 0x9E3779B97F4A7C15
	z := *x
	z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9
	z = (z ^ (z >> 27)) * 0x94D049BB133111EB
	return z ^ (z >> 31)
}

// Uint64 returns the next 64 bits of the stream.
func (r *RNG) Uint64() uint64 {
	s := &r.state
	result := rotl(s[1]*5, 7) * 9
	t := s[1] << 17
	s[2] ^= s[0]
	s[3] ^= s[1]
	s[1] ^= s[2]
	s[0] ^= s[3]
	s[2] ^= t
	s[3] = rotl(s[3], 45)
	return result
}

func rotl(x uint64, k uint) uint64 { return (x << k) | (x >> (64 - k)) }

// Unit returns a fixed-point value uniformly distributed over [0, 1).
// Taking the top 32 bits places them exactly in the fractional field.
func (r *RNG) Unit() fixed.F {
	return fixed.F(r.Uint64() >> 32)
}
