# Copyright 2012 Kevin Ryde

# This file is part of Math-NumSeq.
#
# Math-NumSeq is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3, or (at your option) any later
# version.
#
# Math-NumSeq is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with Math-NumSeq.  If not, see <http://www.gnu.org/licenses/>.

# Mark Renault
# http://www.math.temple.edu/~renault/fibonacci/fib.html
# http://www.math.temple.edu/~renault/fibonacci/thesis.ps

# On Arithmetical Functions Related to the Fibonacci Numbers, Fulton and Morris
# aa1621.pdf
#
# period(m)=m iff m=24*5^(l-1) for some l
# l = Leonardo logarithm
# A001179 leonardo logarithm
#
# K.S.Brown a(n)/n <= 6 for all n, a(n)=6n iff n=2*5^k.
#

package Math::NumSeq::PisanoPeriod;
use 5.004;
use strict;
use Math::Factor::XS 0.39 'prime_factors'; # version 0.39 for prime_factors()

use vars '$VERSION', '@ISA';
$VERSION = 52;

use Math::NumSeq;
use Math::NumSeq::Base::IterateIth;
@ISA = ('Math::NumSeq::Base::IterateIth',
        'Math::NumSeq');

*_is_infinite = \&Math::NumSeq::_is_infinite;

# uncomment this to run the ### lines
#use Smart::Comments;


# use constant name => Math::NumSeq::__('...');
use constant description => Math::NumSeq::__('The cycle length of the Fibonacci numbers modulo i.');
use constant i_start => 1;
use constant characteristic_smaller => 1;
use constant characteristic_integer => 1;
use constant characteristic_count => 1;

use constant values_min => 1;

#------------------------------------------------------------------------------
# cf A071774 n for which period(n)==2n+1
#    A060305 period mod nthprime
#    A003893 fib mod 10
#    A105955 fib mod 11
#    A089911 fib mod 12
#    A064737 fib mod 10 with carry
#    A001176 how many zeros
#    A001177 least k where n divides F[k]

use constant oeis_anum => 'A001175';

#------------------------------------------------------------------------------

use vars '%cache';
my $tempdir;
use constant::defer _cache => sub {
  require SDBM_File;
  require File::Temp;
  $tempdir = File::Temp->newdir;
  ### $tempdir
  ### tempdir: $tempdir->dirname
  tie (%cache, 'SDBM_File',
       File::Spec->catfile ($tempdir->dirname, "cache"),
       Fcntl::O_RDWR()|Fcntl::O_CREAT(),
       0666)
    or die "Couldn't tie SDBM file 'filename': $!; aborting";

  END {
    if ($tempdir) {
      ### unlink cache ...
      untie %cache;
      my $dirname = $tempdir->dirname;
      unlink File::Spec->catfile ($dirname, "cache.pag");
      unlink File::Spec->catfile ($dirname, "cache.dir");
    }
  }
  # END {
  #   if ($tempdir) {
  #     ### cache diagnostics ...
  #     my $count = 0;
  #     while (each %cache) {
  #       $count++;
  #     }
  #     untie %cache;
  #     my $dirname = $tempdir->dirname;
  #     print "cache final $count file sizes cache.pag ",
  #       (-s File::Spec->catfile($dirname,"cache.pag")),
  #         " cache.dir ",
  #           (-s File::Spec->catfile($dirname,"cache.dir")),
  #             "\n";
  #   }
  # }
  return \%cache;
};

sub ith {
  my ($self, $i) = @_;
  ### PisanoPeriod ith(): "$i"

  if ($i < 1) {
    return undef;
  }
  if (_is_infinite($i)) {
    return $i;
  }
  unless ($i <= 0xFFFF_FFFF) { # too big
    return undef;
  }

  my $lcm = Math::NumSeq::_to_bigint(1);
  my @ret;
  my @primes = prime_factors($i);
  while (@primes) {
    my $prime = shift @primes;

    my $period = 1;
    my $power = 1;
    my $modulus;

    if ($prime < 1e14) {
      # period(p^e) = period(p) * p^(e-1)
      while (@primes && $primes[0] == $prime) {
        shift @primes;
        $period *= $prime;
      }
      $modulus = $prime;
    } else {
      # full period(p^e)
      while (@primes && $primes[0] == $prime) {
        shift @primes;
        $power++;
      }
      $modulus = $prime ** $power;
    }

    $period *= (_cache()->{"$prime,$power"} ||= do {
      my $f0 = 0;
      my $f1 = 1;
      my $period = 1;
      for ( ; ; $period++) {
        ### at: "f0=$f0 f1=$f1"
        ($f0,$f1) = ($f1, ($f0+$f1) % $modulus);
        if ($f0 == 0 && $f1 == 1) {
          last;
        }
      }
      ### period calcuated: "prime=$prime power=$power  period=$period"
      $period;
    });

    $lcm /= Math::BigInt::bgcd($period,$lcm);
    $lcm *= $period;
  }

  if ($lcm <= 0xFFFF_FFFF) {
    return $lcm->numify;
  } else {
    return $lcm;
  }
}

1;
__END__



  # prime_factors($i);
  # my $past_f0 = 1;
  # my $past_f1 = 1;
  # my $f0 = 1;
  # my $f1 = 1;
  # for (;;) {
  #   if ($f0 == $past_f0 && $f1 == $past_f1) {
  #     last;
  #   }
  #   ($past_f0,$past_f1) = ($past_f1, ($past_f0+$past_f1) % $i);
  # 
  #   $f0 += $f1;
  #   $f1 += $f0;
  #   $f0 %= $i;
  #   $f1 %= $i;
  # }
  # 
  # my $pos = 1;
  # for (;;) {
  #   ($f0,$f1) = ($f1, ($f0+$f1) % $i);
  #   if ($f0 == $past_f0 && $f1 == $past_f1) {
  #     return $pos;
  #   }
  #   $pos++;
  # }


=for stopwords Ryde Math-NumSeq

=head1 NAME

Math::NumSeq::PisanoPeriod -- cycle length of Fibonacci numbers mod i

=head1 SYNOPSIS

 use Math::NumSeq::PisanoPeriod;
 my $seq = Math::NumSeq::PisanoPeriod->new;
 my ($i, $value) = $seq->next;

=head1 DESCRIPTION

This is the length cycle of Fibonacci numbers modulo i.

    starting i=1
    1, 3, 8, 6, 20, 24, 16, 12, 24, 60, 10, 24, 28, 48, 40, ...

For example Fibonacci numbers modulo 4 repeat in a cycle of 6 numbers, so
value=6.

   Fibonacci  0, 1, 1, 2, 3, 5, 8,13,21,34,55,89,144,...
   mod 4      0, 1, 1, 2, 3, 1, 0, 1, 1, 2, 3, 1, 0,...
              \--------------/  \--------------/  \---
            repeating cycle of 6

The Fibonaccis are determined by a pair F[i],F[i+1] and there can be at most
i*i many different pairs mod i, so there's always a finite repeating period.
Since the Fibonaccis can go backwards as F[i-1]=F[i+1]-F[i] the modulo
sequence is purely periodic, so the initial 0,1 is always part of the cycle.

=head1 FUNCTIONS

See L<Math::NumSeq/FUNCTIONS> for behaviour common to all sequence classes.

=over 4

=item C<$seq = Math::NumSeq::PisanoPeriod-E<gt>new ()>

Create and return a new sequence object.

=back

=head2 Random Access

=over

=item C<$value = $seq-E<gt>ith($i)>

Return the Pisano period of C<$i>.

=cut

=back

=head1 SEE ALSO

L<Math::NumSeq>,
L<Math::NumSeq::Fibonacci>,
L<Math::NumSeq::FibonacciWord>

=head1 HOME PAGE

http://user42.tuxfamily.org/math-numseq/index.html

=head1 LICENSE

Copyright 2012 Kevin Ryde

Math-NumSeq is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 3, or (at your option) any later
version.

Math-NumSeq is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
more details.

You should have received a copy of the GNU General Public License along with
Math-NumSeq.  If not, see <http://www.gnu.org/licenses/>.

=cut
