package Random::Simple;

use strict;
use warnings;
use Time::HiRes;
use Carp qw(croak);

# https://pause.perl.org/pause/query?ACTION=pause_operating_model#3_5_factors_considering_in_the_indexing_phase
our $VERSION = '0.16';
our $debug   = 0;

#############################################################

require XSLoader;

XSLoader::load();

use Exporter 'import';
our @EXPORT = qw(random_int random_bytes random_float rand);

#############################################################

my $has_been_seeded = 0;

# Throw away the first batch to warm up the PRNG, this is helpful
# if a poor seed (lots of zero bits) was chosen
sub warmup {
	my $iter = $_[0];

	for (my $i = 0; $i < $iter; $i++) {
		Random::Simple::_rand64(); # C API
	}
}

# Manually seed the PRNG (no warmup)
sub seed {
	my ($seed1, $seed2) = @_;

	if ($debug) {
		print "SEEDING MANUALLY\n";
	}

	Random::Simple::_seed($seed1, $seed2); # C API

	$has_been_seeded = 1;
}

# Split a string into an array of smaller length strings
sub str_split {
    my ($string, $chunk_size) = @_;
	my $num_chunks = length($string) / $chunk_size;

    my @ret = ();
	for (my $i = 0; $i < $num_chunks; $i++) {
		my $str = substr($string, $i * $chunk_size, $chunk_size);
		push(@ret, $str);
	}

	return @ret;
}

# Binary to hex for human readability
sub bin2hex {
	my $bytes = shift();
	my $ret   = (unpack("h* ", $bytes));

	return $ret;
}

# Randomly seed the PRNG and warmup
sub seed_with_os_random {
	my ($high, $low, $seed1, $seed2);

	# PCG needs to be seeded with 2x 64bit unsigned integers
	# We fetch 16 bytes from the OS to create the two seeds
	# we need for proper seeding

	my $bytes = _get_os_random_bytes(16); # C API
	my @parts = str_split($bytes, 4);

	if (length($bytes) != 16) {
		my $size = length($bytes);
		die("Did not get enough entropy bytes from OS (got $size bytes)\n");
	}

	# Build the first 64bit seed from the random bytes
	# Cannot use Q because it doesn't exist on 32bit Perls
	$high  = unpack("L", $parts[0]);
	$low   = unpack("L", $parts[1]);
	$seed1 = ($high << 32) | $low;

	# Build the second 64bit seed
	$high  = unpack("L", $parts[2]);
	$low   = unpack("L", $parts[3]);
	$seed2 = ($high << 32) | $low;

	if ($debug) {
		print "RANDOM SEEDS: $seed1 / $seed2\n\n";
	}

	if ($seed1 == 0 && $seed2 == 0) {
		die("ERROR: Seeding from OS failed. Both zero? #91393\n");
	}

	# Seed the PRNG with the values we just created
	Random::Simple::_seed($seed1, $seed2); # C API

	$has_been_seeded = 1;

	warmup(1024);
}

######################################################################
# Below are the public user callable methods
######################################################################

# Get a string of random bytes
sub random_bytes {
	my $num = shift();

	if (!$has_been_seeded) { seed_with_os_random(); }

	my $octets_needed = $num / 4;

	my $ret = "";
	for (my $i = 0; $i < $octets_needed; $i++) {
		my $num = Random::Simple::_rand32(); # C API

		# Convert the integer into a 4 byte string
		$ret .= pack("L", $num);
	}

	$ret = substr($ret, 0, $num);

	return $ret;
}

# Get a random non-biased integer in a given range (inclusive)
# Note: Range must be no larger than 2^32 - 2
sub random_int {
	my ($min, $max) = @_;

	if (!$has_been_seeded) { seed_with_os_random(); }

	if ($max < $min) { die("Max can't be less than min"); }

	my $range = $max - $min + 1; # +1 makes it inclusive
	my $ret   = _bounded_rand($range);
	$ret      += $min;

	return $ret;
}

# Get a random float between 0 and 1 inclusive
sub random_float {
	if (!$has_been_seeded) { seed_with_os_random(); }

	my $max = 2**32 - 1;
	my $num = Random::Simple::_rand32(); # C API
	my $ret = $num / $max;

	#print "$num / $max = $ret\n";

	return $ret;
}

# Our rand() overrides CORE::rand()
# This is slightly different than random_float because it returns
# a number where: 0 <= x < 1
#
# This prototype is required so we can emulate CORE::rand(@array)
sub rand(;$) {
	my $mult = shift() || 1;

	if (!$has_been_seeded) { seed_with_os_random(); }

	my $max  = 2**32 - 2; # minus 2 so we're NOT inclusive
	my $rand = Random::Simple::_rand32(); # C API
	my $ret  = $rand / $max;

	$ret = $ret * $mult;

	return $ret;
}

#############################################################

=encoding utf8

=head1 NAME

Random::Simple - Generate good random numbers in a user consumable way.

=head1 SYNOPSIS

    use Random::Simple;

    my $coin_flip      = random_int(1, 2);
    my $die_roll       = random_int(1, 6);
    my $random_percent = random_float() * 100;
    my $buffer         = random_bytes(8);

    my @arr        = ('red', 'green', 'blue');
    my $rand_item  = $arr[random_int(0, @arr - 1)]; # Random array item

=head1 DESCRIPTION

Perl's internal C<rand()> function uses C<drand48> which is an older
pseudorandom number generator and may have limitations. C<Random::Simple> uses
PCG which is: modern, simple, well vetted, and fast. Using C<Random::Simple>
will automatically upgrade/override the core C<rand()> function to use a
better PRNG.

C<Random::Simple> is automatically seeded with entropy directly
from your OS. On Linux this is C</dev/urandom> and on Windows it uses
CryptGenRandom. You will get statistically unique random numbers
automatically.

=head1 METHODS

=over 4

=item B<random_int($min, $max)>

returns a non-biased integer between C<$min> and C<$max> (inclusive). Range must be no larger than 2**32 - 2.

=item B<random_float()>

returns a random floating point value between 0 and 1 (inclusive).

=item B<random_bytes($number)>

returns a string of random bytes with length of C<$number>.

=item B<rand()>

emulates C<CORE::rand()> using a better PRNG.

=back

=head1 BUGS

Submit issues on Github: L<https://github.com/scottchiefbaker/perl-Random-Simple/issues>

=head1 SEE ALSO

=over

=item *
L<Math::Random::PCG32>

=item *
L<Math::Random::ISAAC>

=item *
L<Math::Random::MT>

=item *
L<Math::Random::Secure>

=back

=head1 AUTHOR

Scott Baker - L<https://www.perturb.org/>

=cut

1;
