// Copyright 2023 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package rand implements a cryptographically secure pseudorandom number
// generator.
package rand

import (
	"encoding/binary"
	"fmt"
	"io"
)

// RNG exposes convenience functions based on a cryptographically secure
// io.Reader.
type RNG struct {
	Reader io.Reader
}

// RNGFrom returns a new RNG. r must be a cryptographically secure io.Reader.
func RNGFrom(r io.Reader) RNG {
	return RNG{Reader: r}
}

// Uint16 is analogous to the standard library's math/rand.Uint16.
func (rg *RNG) Uint16() uint16 {
	var data [2]byte
	if _, err := rg.Reader.Read(data[:]); err != nil {
		panic(fmt.Sprintf("Read() failed: %v", err))
	}
	// The endianness doesn't matter here as it's random bytes either way.
	return binary.LittleEndian.Uint16(data[:])
}

// Uint32 is analogous to the standard library's math/rand.Uint32.
func (rg *RNG) Uint32() uint32 {
	var data [4]byte
	if _, err := rg.Reader.Read(data[:]); err != nil {
		panic(fmt.Sprintf("Read() failed: %v", err))
	}
	// The endianness doesn't matter here as it's random bytes either way.
	return binary.LittleEndian.Uint32(data[:])
}

// Int63n is analogous to the standard library's math/rand.Int63n.
func (rg *RNG) Int63n(n int64) int64 {
	// Based on Go's rand package implementation, but using
	// cryptographically secure random numbers.
	if n <= 0 {
		panic(fmt.Sprintf("n must be positive, but got %d", n))
	}

	// This can be done quickly when n is a power of 2.
	if n&(n-1) == 0 {
		return int64(rg.Uint64()) & (n - 1)
	}

	// The naive approach would be to return rg.Int63()%n, but we need the
	// random number to be fair. It shouldn't be biased towards certain
	// results, but simple modular math can be very biased. For example, if
	// n is 40% of the maximum int64, then the output values of rg.Int63
	// map to return values as follows:
	//
	//  - The first 40% of values map to themselves.
	//  - The second 40% map to themselves - maximum int64.
	//  - The remaining 20% map to the themselves - 2 * (maximum int64),
	//    i.e. the first half of possible output values.
	//
	// And thus 60% of results map the first half of possible output
	// values, and 40% map the second half. Oops!
	//
	// We use the same trick as Go to deal with this: shave off the last
	// segment (the 20% in our example) to make the RNG more fair.
	//
	// In the worst case, n is just over half of maximum int64, meaning
	// that the upper half of rg.Int63 return values are bad. So each call
	// to rg.Int63 has, at worst, a 50% chance of needing a retry.
	maximum := int64((1 << 63) - 1 - (1<<63)%uint64(n))
	ret := rg.Int63()
	for ret > maximum {
		ret = rg.Int63()
	}
	return ret % n
}

// Int63 is analogous to the standard library's math/rand.Int63.
func (rg *RNG) Int63() int64 {
	return ((1 << 63) - 1) & int64(rg.Uint64())
}

// Uint64 is analogous to the standard library's math/rand.Uint64.
func (rg *RNG) Uint64() uint64 {
	var data [8]byte
	if _, err := rg.Reader.Read(data[:]); err != nil {
		panic(fmt.Sprintf("Read() failed: %v", err))
	}
	// The endianness doesn't matter here as it's random bytes either way.
	return binary.LittleEndian.Uint64(data[:])
}

// Uint32 is analogous to the standard library's math/rand.Uint32.
func Uint32() uint32 {
	rng := RNG{Reader: Reader}
	return rng.Uint32()
}

// Int63n is analogous to the standard library's math/rand.Int63n.
func Int63n(n int64) int64 {
	rng := RNG{Reader: Reader}
	return rng.Int63n(n)
}

// Int63 is analogous to the standard library's math/rand.Int63.
func Int63() int64 {
	rng := RNG{Reader: Reader}
	return rng.Int63()
}

// Uint64 is analogous to the standard library's math/rand.Uint64.
func Uint64() uint64 {
	rng := RNG{Reader: Reader}
	return rng.Uint64()
}
