package sim

import (
	"math/bits"

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

// HouseEdgeBP is the house edge in basis points (100 = 1.00%).
//
// One percent is deliberately generous — better than almost anything
// commercial. This is a game among friends, not a revenue stream, and a
// thinner edge means the pot lasts the whole night instead of draining
// toward the house.
const HouseEdgeBP int64 = 100

// TickHz is the simulation rate. Rounds advance in whole ticks only.
const TickHz = 60

// RoundTicks is the hard ceiling on a round's length: 60 seconds at 60Hz.
//
// The multiplier follows a hyperbolic curve that diverges at exactly this
// tick, so no round can run longer no matter how extreme the crash point.
// An exponential curve has no such bound — a 275x round on one takes over two
// and a half minutes, which is unplayable when a dozen people are waiting.
const RoundTicks = 60 * TickHz

// MaxMultiplier is the largest value the curve expresses, reached on the final
// tick. Crash points at or above it settle when the round hits its ceiling.
func MaxMultiplier() fixed.F { return MultiplierAt(RoundTicks - 1) }

// CrashPoint derives the multiplier at which a round ends, as a pure function of
// the seed.
//
// The distribution is the inverse-uniform curve scaled by the house edge:
//
//	crash = (1 - edge) / u,  u uniform over (0, 1]
//
// which yields the same expected return of (1 - edge) at every cash-out target.
// No target is smarter than any other, so there is nothing to grind out.
func CrashPoint(seed [32]byte) fixed.F {
	r := NewRNG(seed)

	// u is uniform over [1, 2^32], giving a resolution of one part in 4 billion.
	u := (r.Uint64() >> 32) + 1

	// payoutRatio is (1 - edge) in Q32.32, e.g. 0.98.
	payoutRatio := uint64((10000 - HouseEdgeBP) << 32 / 10000)

	// crash = payoutRatio / (u / 2^32), computed as (payoutRatio * 2^32) / u
	// through a 128-bit intermediate so no precision is lost.
	hi, lo := bits.Mul64(payoutRatio, 1<<32)
	q, _ := bits.Div64(hi, lo, u)

	// The quotient can exceed int64 for the very smallest u — at u=1 it wraps
	// negative, which would silently turn the rarest and most valuable outcome
	// into an instant loss. Compare in unsigned space before converting.
	//
	// The cap is the largest multiplier the curve can express. Anything above
	// it is unreachable anyway: the round would hit its tick ceiling first.
	// It also bounds the maximum payout, so a single round cannot demand more
	// than the house can hold.
	maxCP := MaxMultiplier()
	if q >= uint64(maxCP) {
		return maxCP
	}

	cp := fixed.F(q)
	if cp < fixed.One {
		cp = fixed.One
	}
	return cp
}

// MultiplierAt returns the multiplier displayed at a given tick.
//
//	m(t) = 1 / (1 - t/T)^2
//
// It starts at 1.0, rises slowly at first, and accelerates without bound as t
// approaches T. That acceleration is the tension: the longer you hold, the
// faster the number moves away from you, and the less time you have to react.
// It is also O(1), so a long round costs no more per tick than a short one.
func MultiplierAt(tick int) fixed.F {
	if tick <= 0 {
		return fixed.One
	}
	// Clamp the tick, not the value: clamping the value would make the curve
	// step backwards at the boundary if rounding put the last computed point
	// above the nominal ceiling.
	if tick >= RoundTicks {
		tick = RoundTicks - 1
	}
	// remaining = 1 - tick/T, always in (0, 1].
	remaining := fixed.One - fixed.FromInt(int64(tick)).Div(fixed.FromInt(RoundTicks))
	return fixed.One.Div(remaining.Mul(remaining))
}

// TicksToMultiplier returns the first tick at which MultiplierAt reaches m,
// inverting the curve: t = T * (1 - 1/sqrt(m)).
func TicksToMultiplier(m fixed.F) int {
	if m <= fixed.One {
		return 0
	}
	if m >= MaxMultiplier() {
		return RoundTicks
	}
	inv := fixed.One.Div(fixed.Sqrt(m))
	t := fixed.FromInt(RoundTicks).Mul(fixed.One - inv).Int()

	// Rounding in fixed point can land a tick early; step forward to the first
	// tick that genuinely reaches the target.
	tick := int(t)
	for tick > 0 && MultiplierAt(tick-1) >= m {
		tick--
	}
	for tick < RoundTicks && MultiplierAt(tick) < m {
		tick++
	}
	return tick
}
