#!/usr/bin/env perl
use strict;
use warnings;

use Math::Prime::Util qw/prime_iterator is_prime
                         next_prime nth_prime_upper prime_precalc forprimes/;

my $count = shift || 20;
my $method = shift || 'forprimes';
my $precalc = 0;   # If set, precalc all the values we'll call is_prime on

# Find Sophie Germain primes (numbers where p and 2p+1 are both prime).

# Four methods are shown: forprimes, iter, iter2, and MNS.

# Times for 300k:
#
#                300k               1M
# precalc:
#   forprimes    1.3s   9.0MB       7.1s   21.6MB
#   iter         2.8s   8.7MB      12.6s   21.4MB
#   iter2        1.9s   8.7MB       9.4s   21.4MB
# no precalc:
#   forprimes    1.5s   4.5MB       5.6s   4.5MB
#   iter         9.5s   4.3MB      37.5s   4.3MB
#   iter2        8.5s   4.3MB      33.9s   4.3MB
#   MNS        254.3s  11.3MB   >1500s   >15 MB

if ($precalc) {
  prime_precalc(2 * sg_upper_bound($count));
}


if ($method eq 'forprimes') {

  my $estimate = sg_upper_bound($count);
  my $numfound = 0;
  forprimes {
    if ($numfound < $count && is_prime(2*$_+1)) {
      print "$_\n";
      $numfound++;
    }
  } $estimate;
  die "Estimate too low" unless $numfound >= $count;

} elsif ($method eq 'iter') {

  # Wrap the standard iterator
  sub get_sophie_germain_iterator {
    my $p = shift || 2;
    my $it = prime_iterator($p);
    return sub {
      do { $p = $it->() } while !is_prime(2*$p+1);
      $p;
    };
  }
  my $sgit = get_sophie_germain_iterator();
  print $sgit->(), "\n" for 1 .. $count;

} elsif ($method eq 'iter2') {

  # Iterate directly using next_prime
  my $prime = 2;
  for (1 .. $count) {
    $prime = next_prime($prime) while !is_prime(2*$prime+1);
    print "$prime\n";
    $prime = next_prime($prime);
  }

} elsif ($method eq 'MNS') {

  # Use Math::NumSeq
  require Math::NumSeq::SophieGermainPrimes;
  my $seq = Math::NumSeq::SophieGermainPrimes->new;
  for (1 .. $count) {
    print 0+($seq->next)[1];
  }

}

# Used for precalc and the forprimes example
sub sg_upper_bound {
  my $count = shift;
  my $nth = nth_prime_upper($count);
  # For lack of a better formula, do this step-wise estimate.
  my $estimate = ($count <   5000) ? 150 + int( $nth * log($nth) * 1.2 )
               : ($count <  19000) ? int( $nth * log($nth) * 1.135 )
               : ($count <  45000) ? int( $nth * log($nth) * 1.10 )
               : ($count < 100000) ? int( $nth * log($nth) * 1.08 )
               : ($count < 165000) ? int( $nth * log($nth) * 1.06 )
               : ($count < 360000) ? int( $nth * log($nth) * 1.05 )
               : ($count < 750000) ? int( $nth * log($nth) * 1.04 )
               : ($count <1700000) ? int( $nth * log($nth) * 1.03 )
               :                     int( $nth * log($nth) * 1.02 );

  return $estimate;
}
