package DateTime::Calendar::Mayan;

use strict;

use vars qw( $VERSION );
$VERSION = '0.0601';

use DateTime;
use Params::Validate qw( validate SCALAR OBJECT );

use constant MAYAN_EPOCH => -1137142;
use constant MAYAN_HAAB_EPOCH => MAYAN_EPOCH - 348;
use constant MAYAN_HAAB_MONTH => qw( Pop Uo Zip Zotz Tzec Xul Yaxkin Mol Chen
    Yax Zac Ceh Mac Kankin Muan Pax Kayab Cumku Uayeb );
use constant MAYAN_TZOLKIN_EPOCH => MAYAN_EPOCH - 159;
use constant MAYAN_TZOLKIN_NAME => qw( Imix Ik Akbal Kan Chicchan Cimi Manik
    Lamat Muluc Oc Chuen Eb Ben Ix Men Cib Caban Etznab Cauac Ahau );

sub new {
    my( $class ) = shift;

    my %args = validate( @_,
        {
            baktun  => { type => SCALAR, default => 0 },
            katun   => { type => SCALAR, default => 0 },
            tun     => { type => SCALAR, default => 0 },
            uinal   => { type => SCALAR, default => 0 },
            kin     => { type => SCALAR, default => 0 },
            epoch   => {
                type        => OBJECT,
                can         => 'utc_rd_values',
                optional    => 1,
            },
        }
    );

    $class = ref( $class ) || $class;

    my $alt_epoch;
    if ( exists $args{ epoch } ) {
        my $object = $args{ epoch };
        delete $args{ epoch };
        $object = $object->clone->set_time_zone( 'floating' )
            if $object->can( 'set_time_zone' );

        $alt_epoch = ( $object->utc_rd_values )[ 0 ];
    }

    my $self = {
        epoch       => $alt_epoch || MAYAN_EPOCH,
        rd_secs     => 0,
        rd_nanos    => 0,
    };

    $self->{ rd } = _long_count2rd( $self, \%args );

    return( bless( $self, $class ) );
}

sub now {
    my( $class ) = shift;

    $class = ref( $class ) || $class;

    my $dt = DateTime->now;
    my $dtcm = $class->from_object( object => $dt );

    return( $dtcm );
}

sub today {
    my( $class ) = shift;

    $class = ref( $class ) || $class;

    my $dt = DateTime->today;
    my $dtcm = $class->from_object( object => $dt );

    return( $dtcm );
}

# lifted from DateTime
sub clone { bless { %{ $_[0] } }, ref $_[0] }

sub _long_count2rd {
    my( $self, $lc ) = @_;

    my $rd = $self->{ epoch }
    + $lc->{ baktun } * 144000
    + $lc->{ katun }  * 7200
    + $lc->{ tun }    * 360
    + $lc->{ uinal }  * 20
    + $lc->{ kin };

    return( $rd );
}

sub _rd2long_count {
    my( $self ) = shift;

    my %lc;
    my $long_count  = $self->{ rd } - $self->{ epoch };
    $lc{ baktun }   = _floor( $long_count / 144000 );
    my $day_baktun  = $long_count % 144000;
    $lc{ katun }    = _floor( $day_baktun / 7200 );
    my $day_katun   = $day_baktun % 7200;
    $lc{ tun }      = _floor( $day_katun / 360 );
    my $day_tun     = $day_katun % 360;
    $lc{ uinal }    = _floor( $day_tun / 20 );
    $lc{ kin }      = _floor( $day_tun % 20 );

    return( \%lc );
}

sub  _rd2haab {
    my( $self ) = shift;

    my %haab;
    my $count = ( $self->{ rd } - MAYAN_HAAB_EPOCH ) % 365;
    $haab{ day }    = $count % 20;
    $haab{ month }  = _floor( $count / 20 ) + 1;

    return( \%haab );
}

sub _haab2rd {
    my( $month, $day ) = @_;
    
    return( ( $month - 1 ) * 20 + $day );
}

sub _rd2tzolkin {
    my( $self ) = shift;

    my %tzolkin;
    my $count = $self->{ rd } - MAYAN_TZOLKIN_EPOCH + 1;
    $tzolkin{ number }  = _amod( $count, 13 );
    $tzolkin{ name }    = _amod( $count, 20 );

    return( \%tzolkin );
}

sub _tzolkin2rd {
    my( $number, $name ) = shift;

    return( ( $number - 1 + 39 x ( $number - $name ) ) % 260 );
}

sub from_object {
    my( $class ) = shift;
    my %args = validate( @_,
        {
            object => {
                type    => OBJECT,
                can     => 'utc_rd_values',
            },
        },
    );

    $class = ref( $class ) || $class;

    my $object = $args{ object };
    $object = $object->clone->set_time_zone( 'floating' )
            if $object->can( 'set_time_zone' );

    my( $rd, $rd_secs, $rd_nanos ) = $object->utc_rd_values();

    my $dtcm_epoch = $object->mayan_epoch
            if $object->can( 'mayan_epoch' );

    my $self = {
        rd          => $rd,
        rd_secs     => $rd_secs,
        rd_nanos    => $rd_nanos || 0,
        epoch       => $dtcm_epoch->{ rd } || MAYAN_EPOCH,
    };

    return( bless( $self, $class ) );
}

sub utc_rd_values {
    my( $self ) = shift;

    # days utc, seconds utc,
    return( $self->{ rd }, $self->{ rd_secs }, $self->{ rd_nanos } || 0 );
}

sub from_epoch {
    my( $class ) = shift;
    my %args = validate( @_,
        {
            epoch => { type => SCALAR },
        }
    );

    $class = ref( $class ) || $class;

    my $dt = DateTime->from_epoch( epoch => $args{ epoch } );

    my $self = $class->from_object( object => $dt );

    return( $self );
}

sub epoch {
    my( $self ) = shift;

    my $dt = DateTime->from_object( object => $self );

    return( $dt->epoch );
}

sub set_mayan_epoch {
    my( $self ) = shift;

    my %args = validate( @_,
        {
            object => {
                type    => OBJECT,
                can     => 'utc_rd_values',
            },
        },
    );

    my $object = $args{ object };
    $object = $object->clone->set_time_zone( 'floating' )
            if $object->can( 'set_time_zone' );

    # this can not handle rd values larger then a Mayan year
    # $self->{ rd } = _long_count2rd( $self, _rd2long_count( $self ) );

    $self->{ epoch } = ( $object->utc_rd_values )[ 0 ];
    if ( $self->{ epoch } > MAYAN_EPOCH ) {
        $self->{ rd } += abs( $self->{ epoch } - MAYAN_EPOCH );
    } else {
        $self->{ rd } -= abs( $self->{ epoch } - MAYAN_EPOCH );
    }

    return( $self );
}

sub mayan_epoch {
    my( $self ) = shift;

    my $new_self = $self->clone();

    $new_self->{ rd }       = $self->{ epoch };
    $new_self->{ rd_secs }  = 0;
    $new_self->{ epoch }    = MAYAN_EPOCH;

    # calling from_object causes a method loop

    my $class = ref( $self );
    my $dtcm = bless( $new_self, $class );

    return( $dtcm );
}

sub set {
    my( $self ) = shift;

    my %args = validate( @_,
        {
            baktun  => { type => SCALAR, optional => 1 },
            katun   => { type => SCALAR, optional => 1 },
            tun     => { type => SCALAR, optional => 1 },
            uinal   => { type => SCALAR, optional => 1 },
            kin     => { type => SCALAR, optional => 1 },
        }
    );

    my $lc = _rd2long_count( $self );

    $lc->{ baktun } = $args{ baktun } if defined $args{ baktun };
    $lc->{ katun }  = $args{ katun } if defined $args{ katun };
    $lc->{ tun }    = $args{ tun } if defined $args{ tun };
    $lc->{ uinal }  = $args{ uinal } if defined $args{ uinal };
    $lc->{ kin }    = $args{ kin } if defined $args{ kin };

    $self->{ rd } = _long_count2rd( $self, $lc ); 

    return( $self );
}

sub add {
    my( $self ) = shift;

    my %args = validate( @_,
        {
            baktun  => { type => SCALAR, optional => 1 },
            katun   => { type => SCALAR, optional => 1 },
            tun     => { type => SCALAR, optional => 1 },
            uinal   => { type => SCALAR, optional => 1 },
            kin     => { type => SCALAR, optional => 1 },
        }
    );

    my $lc = _rd2long_count( $self );

    $lc->{ baktun } += $args{ baktun } if defined $args{ baktun };
    $lc->{ katun }  += $args{ katun } if defined $args{ katun };
    $lc->{ tun }    += $args{ tun } if defined $args{ tun };
    $lc->{ uinal }  += $args{ uinal } if defined $args{ uinal };
    $lc->{ kin }    += $args{ kin } if defined $args{ kin };

    $self->{ rd } = _long_count2rd( $self, $lc ); 
    
    return( $self );
}

sub subtract {
    my( $self ) = shift;

    my %args = validate( @_,
        {
            baktun  => { type => SCALAR, optional => 1 },
            katun   => { type => SCALAR, optional => 1 },
            tun     => { type => SCALAR, optional => 1 },
            uinal   => { type => SCALAR, optional => 1 },
            kin     => { type => SCALAR, optional => 1 },
        }
    );

    my $lc = _rd2long_count( $self );

    $lc->{ baktun } -= $args{ baktun } if defined $args{ baktun };
    $lc->{ katun }  -= $args{ katun } if defined $args{ katun };
    $lc->{ tun }    -= $args{ tun } if defined $args{ tun };
    $lc->{ uinal }  -= $args{ uinal } if defined $args{ uinal };
    $lc->{ kin }    -= $args{ kin } if defined $args{ kin };

    $self->{ rd } = _long_count2rd( $self, $lc ); 

    return( $self );
}

sub add_duration {
    my( $self, $duration ) = @_;

    my $dt = DateTime->from_object( object => $self );
    $dt->add_duration( $duration );

    my $new_self = $self->from_object( object => $dt );

    # if there is an alternate epoch defined don't touch it
    $self->{ rd }       = $new_self->{ rd };
    $self->{ rd_secs }  = $new_self->{ rd_secs };

    return( $self );
}

sub subtract_duration {
    my( $self, $duration ) = @_;

    my $dt = DateTime->from_object( object => $self );
    $dt->subtract_duration( $duration );

    my $new_self = $self->from_object( object => $dt );

    # if there is an alternate epoch defined don't touch it
    $self->{ rd }       = $new_self->{ rd };
    $self->{ rd_secs }  = $new_self->{ rd_secs };

    return( $self );
}

sub baktun {
    my( $self, $arg ) = @_;

    my $lc = _rd2long_count( $self );

    if ( defined $arg ) {
        $lc->{ baktun } = $arg;
        $self->{ rd }   = _long_count2rd( $self, $lc ); 

        return( $self );
    }

    # conversion from Date::Maya
    # set baktun to [1-13]
    $lc->{ baktun } %= 13;
    $lc->{ baktun } = 13 if $lc->{ baktun } == 0;

    return( $lc->{ baktun } );
}

*set_baktun = \&baktun;

sub katun {
    my( $self, $arg ) = @_;

    my $lc = _rd2long_count( $self );

    if ( defined $arg ) {
        $lc->{ katun }  = $arg;
        $self->{ rd }   = _long_count2rd( $self, $lc ); 

        return( $self );
    }

    return( $lc->{ katun } );
}

*set_katun= \&katun;

sub tun {
    my( $self, $arg ) = @_;

    my $lc = _rd2long_count( $self );

    if ( defined $arg ) {
        $lc->{ tun }    = $arg;
        $self->{ rd }   = _long_count2rd( $self, $lc ); 

        return( $self );
    }

    return( $lc->{ tun } );
}

*set_tun= \&tun;

sub uinal {
    my( $self, $arg ) = @_;

    my $lc = _rd2long_count( $self );

    if ( defined $arg ) {
        $lc->{ uinal }  = $arg;
        $self->{ rd }   = _long_count2rd( $self, $lc ); 

        return( $self );
    }

    return( $lc->{ uinal } );
}

*set_uinal= \&uinal;

sub kin {
    my( $self, $arg ) = @_;

    my $lc = _rd2long_count( $self );

    if ( defined $arg ) {
        $lc->{ kin }    = $arg;
        $self->{ rd }   = _long_count2rd( $self, $lc ); 

        return( $self );
    }

    return( $lc->{ kin } );
}

*set_kin= \&kin;

sub bktuk {
    my( $self, $sep ) = @_;
    $sep = '.' unless defined $sep;

    my $lc = _rd2long_count( $self ); 

    $lc->{ baktun } %= 13;
    $lc->{ baktun } = 13 if $lc->{ baktun } == 0;

    return(
        $lc->{ baktun } . $sep .
        $lc->{ katun }  . $sep .
        $lc->{ tun }    . $sep .
        $lc->{ uinal }  . $sep .
        $lc->{ kin }
    );
}

*date = \&bktuk;
*long_count = \&bktuk;

sub haab {
    my( $self, $sep ) = @_;
    $sep = ' ' unless defined $sep;

    my $haab = _rd2haab( $self );

    return( $haab->{ day } . $sep . (MAYAN_HAAB_MONTH)[ $haab->{ month } - 1 ] );
}

sub tzolkin {
    my( $self, $sep ) = @_;
    $sep = ' ' unless defined $sep;

    my $tzolkin = _rd2tzolkin( $self );

    return( $tzolkin->{ number } . $sep . (MAYAN_TZOLKIN_NAME )[ $tzolkin->{ name } - 1 ] );
}

# lifted from DateTime::Calendar::Julian;
sub _floor {
    my $x  = shift;
    my $ix = int $x;
    if ($ix <= $x) {
        return $ix;
    } else {
        return $ix - 1;
    }
}

sub _amod {
    my( $x, $y ) = @_;

    return( $y + $x % ( -$y ) );
}

1;

__END__