// Package fixed provides deterministic Q32.32 fixed-point arithmetic.
//
// No floating-point operation appears anywhere in this package. Results must be
// bit-identical across architectures and between native and WASM builds, which
// is what allows a player's browser to independently replay a round and reach
// exactly the same outcome as the server.
package fixed

import (
	"math/bits"
	"strconv"
)

// F is a Q32.32 fixed-point number: an int64 with 32 fractional bits.
type F int64

// One is the fixed-point representation of 1.0.
const One F = 1 << 32

const fracBits = 32

// MaxInt is the largest whole number representable in Q32.32. Values beyond
// it cannot be held in the 32 integer bits.
const MaxInt int64 = 1<<31 - 1

// MinInt is the smallest whole number representable in Q32.32.
const MinInt int64 = -(1 << 31)

// FromInt converts a whole number to fixed-point.
//
// It panics outside [MinInt, MaxInt] rather than wrapping. A silent wrap here
// produced a negative multiplier from a positive input, which is exactly the
// kind of fault that is invisible until it corrupts a payout.
func FromInt(v int64) F {
	if v > MaxInt || v < MinInt {
		panic("fixed: " + strconv.FormatInt(v, 10) + " is outside the Q32.32 integer range")
	}
	return F(v << fracBits)
}

// Int truncates toward negative infinity and returns the whole part.
func (a F) Int() int64 { return int64(a) >> fracBits }

func (a F) Add(b F) F { return a + b }
func (a F) Sub(b F) F { return a - b }

// Mul multiplies via a 128-bit intermediate so no precision is lost before the
// shift back down. A naive (a*b)>>32 overflows for operands above roughly 2^15.
func (a F) Mul(b F) F {
	neg := false
	x, y := int64(a), int64(b)
	if x < 0 {
		x, neg = -x, !neg
	}
	if y < 0 {
		y, neg = -y, !neg
	}
	hi, lo := bits.Mul64(uint64(x), uint64(y))
	res := int64(lo>>fracBits | hi<<(64-fracBits))
	if neg {
		res = -res
	}
	return F(res)
}

// Div divides via a 128-bit intermediate for the same reason as Mul.
func (a F) Div(b F) F {
	if b == 0 {
		panic("fixed: division by zero")
	}
	neg := false
	x, y := int64(a), int64(b)
	if x < 0 {
		x, neg = -x, !neg
	}
	if y < 0 {
		y, neg = -y, !neg
	}
	hi := uint64(x) >> (64 - fracBits)
	lo := uint64(x) << fracBits
	q, _ := bits.Div64(hi, lo, uint64(y))
	res := int64(q)
	if neg {
		res = -res
	}
	return F(res)
}

// Sqrt returns the fixed-point square root using integer Newton iteration.
// It converges in well under the iteration cap for the full int64 range.
func Sqrt(a F) F {
	if a < 0 {
		panic("fixed: sqrt of negative")
	}
	if a == 0 {
		return 0
	}
	// Initial guess: half the bit length puts us within a factor of two.
	shift := uint(bits.Len64(uint64(a))+fracBits) / 2
	x := F(1) << shift
	for i := 0; i < 64; i++ {
		next := (x + a.Div(x)) / 2
		if next == x || next == x-1 {
			x = next
			break
		}
		x = next
	}
	// Newton can land one ulp high; step down while the square exceeds the input.
	for x > 0 && x.Mul(x) > a {
		x--
	}
	return x
}

// String renders the value with six fractional digits, using integer math only.
func (a F) String() string {
	neg := a < 0
	if neg {
		a = -a
	}
	whole := int64(a) >> fracBits
	frac := int64(a) & (int64(One) - 1)
	micros := (frac * 1_000_000) >> fracBits
	s := strconv.FormatInt(whole, 10) + "." + pad6(micros)
	if neg {
		return "-" + s
	}
	return s
}

func pad6(v int64) string {
	s := strconv.FormatInt(v, 10)
	for len(s) < 6 {
		s = "0" + s
	}
	return s
}
