package Orangutan::Date;

use strict;

use Data::Dumper;

our @weekdays = (
    'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
);

our @weekdays_short = (
    'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
);

our @months = (
    'January', 'February', 'March',     'April',   'May',      'June',
    'July',    'August',   'September', 'October', 'November', 'December'
);

our @months_short = (
    'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
);

my @holidays = (
    [ [  1,  1 ], 'New Year'         ],
    [ [  2,  1 ], 'New Year'         ],
    [ [  7,  1 ], 'Christmas'        ],
    [ [  8,  3 ], "Woman's Day"      ],
    [   \&Easter, 'Easter'           ],
    [ [  1,  5 ], 'Labor Day'        ],
    [ [  2,  5 ], 'Labor Day'        ],
    [ [  9,  5 ], 'Victory Day'      ],
    [  \&Trinity, 'The Trinity'      ],
    [ [ 28,  6 ], 'Constitution Day' ],
    [ [ 24,  8 ], 'Independence Day' ]
);

my %holidays_cache = ( );

sub Easter {
    my ($year) = @_;

    my @date = ( 0, 0 );

    my $a = (19 * ($year % 19) + 15) % 30;
    my $b = (2 * ($year % 4) + 4 * ($year % 7) + 6 * $a + 6) % 7;
    if (($a + $b) > 9) {
        $date[0] = $a + $b - 9;
        $date[1] = 4;
    } else {
        $date[0] = $a + $b + 22;
        $date[1] = 3;
    }

    @date = (localtime(POSIX::mktime(0, 0, 0, $date[0], $date[1] - 1, $year - 1900) + (13 * 86400)))[3..4];
    $date[1]++;

    return \@date;
}

sub Trinity {
    my ($year) = @_;

    my $easter = Easter($year);

    my @date = (localtime(POSIX::mktime(0, 0, 0, $easter->[0], $easter->[1] - 1, $year - 1900) + (49 * 86400)))[3..4];
    $date[1]++;

    return \@date;
}

sub new {
    my $class = shift;

    my $self = { };
    bless $self, $class;

    my $string = shift;
    if (defined($string)) {
        my $start = shift;
        if (!$self->Parse($string, $start)) {
            return undef;
        }
    } else {
        $self->{'date'} = int(time() / 60) * 60;
    }

    return $self;
}

sub Parse($$;$) {
    my ($self, $string, $start) = @_;

    if (ref $string eq 'Orangutan::Date') {
        $self->{'date'} = $string->Date;
        return $self->{'date'};
    } else {
        if ($string =~ /^[0-9]{3,}$/) {
            $self->{'date'} = $string;
            return $self->{'date'};
        } elsif ($string =~ /^([0-9]{1,2})(?:[:-]([0-9]{2})(?:[:-][0-9]{2})?)? ?((?:a\.?|p\.?)m\.?)?$/i) {
            my ($hour, $min, $meridiem) = ($1, $2, $3);

            if ($hour == 24) {
                $hour = 0;
            }

            if (!defined($min)) {
                $min = 0;
            }

            if (defined($meridiem)) {
                $meridiem =~ s!\.!!g;
                if ($hour == 12) {
                    if (lc($meridiem) eq 'am') {
                        $hour = 0;
                    }
                } else {
                    if (lc($meridiem) eq 'pm') {
                        $hour += 12;
                    }
                }
            }

            if (($hour < 24) && ($min < 60)) {
                my $time = time;
                if (defined($start)) {
                    my ($smin, $shour, $day, $mon, $year) = (localtime($start))[1..5];
                    if ((($hour * 60 + $min) < ($shour * 60 + $smin)) &&
                        (($start + ((24 - $shour - 1) + $hour) * 3600 + ((60 - $smin) + $min) * 60) < $time)) {
                        ($day, $mon, $year) = (localtime($start + 86400))[3..5];
                    }
                    $self->{'date'} = POSIX::mktime(0, $min, $hour, $day, $mon, $year);
                    return $self->{'date'};
                } else {
                    my ($cmin, $chour, $day, $mon, $year) = (localtime($time))[1..5];
                    if (($hour * 60 + $min) > ($chour * 60 + $cmin)) {
                        ($day, $mon, $year) = (localtime($time - 86400))[3..5];
                    }
                    $self->{'date'} = POSIX::mktime(0, $min, $hour, $day, $mon, $year);
                    return $self->{'date'};
                }
            }
        } elsif ($string =~ /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2})(?:[:-]([0-9]{2}))?$/i) {
            my ($year, $mon, $day, $hour, $min, $sec) = ($1, $2, $3, $4, $5, $6);

            if (!defined($sec)) {
                $sec = 0;
            }

            $self->{'date'} = POSIX::mktime($sec, $min, $hour, $day, $mon - 1, $year - 1900);
            return $self->{'date'};
        }
    }

    return undef;
}

sub ParseMonth {
    if (scalar @_ > 0) {
        if (ref $_[0] eq 'Orangutan::Date') {
            shift;
        }
        if (scalar @_ > 0) {
            my $month = shift;

            for (my $i = 0; $i < scalar @months; $i++) {
                if (lc($month) eq lc($months[$i])) {
                    return $i;
                }
            }

            for (my $i = 0; $i < scalar @months_short; $i++) {
                if (lc($month) eq lc($months_short[$i])) {
                    return $i;
                }
            }
        }
    }
    return undef;
}

sub ParseWeekDay {
    if (scalar @_ > 0) {
        if (ref $_[0] eq 'Orangutan::Date') {
            shift;
        }
        if (scalar @_ > 0) {
            my $weekday = shift;

            for (my $i = 0; $i < scalar @weekdays; $i++) {
                if (lc($weekday) eq lc($weekdays[$i])) {
                    return $i;
                }
            }

            for (my $i = 0; $i < scalar @weekdays_short; $i++) {
                if (lc($weekday) eq lc($weekdays_short[$i])) {
                    return $i;
                }
            }
        }
    }
    return undef;
}

sub Set($$) {
    my ($self, $date) = @_;
    $self->{'date'} = $date;
}

sub Date($) {
    my $self = shift;
    return $self->{'date'};
}

sub Elapsed($) {
    my $self = shift;
    my $elapsed = int(time() / 60) * 60 - $self->{'date'};
    if ($elapsed >= 0) {
        return $elapsed;
    } else {
        return 0;
    }
}

sub DayStart($) {
    my $self = shift;
    my ($day, $mon, $year) = (localtime($self->{'date'}))[3..5];
    return new Orangutan::Date(POSIX::mktime(0, 0, 0, $day, $mon, $year));
}

sub WorkDayStart($) {
    my $self = shift;
    my ($hour, $day, $mon, $year) = (localtime($self->{'date'}))[2..5];
    if ($hour < 6) {
        ($day, $mon, $year) = (localtime($self->{'date'} - 86400))[3..5];
    }
    return new Orangutan::Date(POSIX::mktime(0, 0, 6, $day, $mon, $year));
}

sub WeekStart($) {
    my $self = shift;
    my ($mday, $mon, $year, $wday) = (localtime($self->{'date'}))[3..6];
    if ($wday != 1) {
        if ($wday == 0) {
            $wday = 7;
        }
        ($mday, $mon, $year) = (localtime($self->{'date'} - (86400 * ($wday - 1))))[3..5];
    }
    return new Orangutan::Date(POSIX::mktime(0, 0, 0, $mday, $mon, $year));
}

sub MonthStart($) {
    my $self = shift;
    my ($mon, $year) = (localtime($self->{'date'}))[4..5];
    return new Orangutan::Date(POSIX::mktime(0, 0, 0, 1, $mon, $year));
}

sub IsFuture($) {
    my $self = shift;
    if ($self->{'date'} > time()) {
        return 1;
    } else {
        return 0;
    }
}

sub IsWorkDay($) {
    my $self = shift;
    my ($day, $mon, $year, $wday) = (localtime($self->{'date'}))[3..6];
    if (($wday > 5) || ($wday < 1)) {
        return 0;
    } else {
        my $time = POSIX::mktime(0, 0, 0, $day, $mon, $year);
        my $holidays = $self->GetHolidays;
        foreach my $holiday (@{$holidays}) {
            if (@{$holiday}[2] == $time) {
                return 0;
            } elsif (@{$holiday}[2] > $time) {
                last;
            }
        }
        return 1;
    }
}

sub DayEnd($) {
    my $self = shift;
    my ($day, $mon, $year) = (localtime($self->{'date'}))[3..5];
    return new Orangutan::Date(POSIX::mktime(59, 59, 23, $day, $mon, $year));
}

sub WeekEnd($) {
    my $self = shift;
    my ($mday, $mon, $year, $wday) = (localtime($self->{'date'}))[3..6];
    if ($wday != 0) {
        ($mday, $mon, $year) = (localtime($self->{'date'} + (86400 * (7 - $wday))))[3..5];
    }
    return new Orangutan::Date(POSIX::mktime(59, 59, 23, $mday, $mon, $year));
}

sub WorkDayEnd($) {
    my $self = shift;
    my ($hour, $day, $mon, $year) = (localtime($self->{'date'}))[2..5];
    if ($hour >= 6) {
        ($day, $mon, $year) = (localtime($self->{'date'} + 86400))[3..5];
    }
    return new Orangutan::Date(POSIX::mktime(59, 59, 5, $day, $mon, $year));
}

sub MonthEnd($) {
    my $self = shift;
    my ($day, $mon, $year) = (localtime($self->{'date'}))[3..5];
    ($day, $mon, $year) = (localtime($self->{'date'} + (86400 * ((31 - $day) + 5))))[3..5];
    return new Orangutan::Date(POSIX::mktime(0, 0, 0, 1, $mon, $year) - 1);
}

sub Previous($) {
    my $self = shift;
    return new Orangutan::Date($self->{'date'} - 86400);
}

sub PreviousWorkDay($) {
    my $self = shift;

    my $time = $self->WorkDayStart;
    do {
        my $wday = (localtime($time->Date))[6];
        if ($wday == 1) {
            $time->Set($time->Date - 3 * 86400);
        } elsif ($wday == 0) {
            $time->Set($time->Date - 2 * 86400);
        } else {
            $time->Set($time->Date - 86400);
        }
    } while (!$time->IsWorkDay);

    return $time;
}

sub Next($) {
    my $self = shift;
    return new Orangutan::Date($self->{'date'} + 86400);
}

sub NextWorkDay($) {
    my $self = shift;

    my $time = $self->WorkDayStart;
    do {
        my $wday = (localtime($time->Date))[6];
        if ($wday == 5) {
            $time->Set($time->Date + 3 * 86400);
        } elsif ($wday == 6) {
            $time->Set($time->Date + 2 * 86400);
        } else {
            $time->Set($time->Date + 86400);
        }
    } while (!$time->IsWorkDay);

    return $time;
}

sub DateTime($;$) {
    my $self = shift;
    my $format = shift;
    my ($sec, $min, $hour, $day, $mon, $year) = (localtime($self->{'date'}))[0..5];
    if (defined($format)) {
        if ($format eq 'date') {
            return sprintf("%04d-%02d-%02d", $year + 1900, $mon + 1, $day);
        } elsif ($format eq 'full') {
            return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year + 1900, $mon + 1, $day, $hour, $min, $sec);
        } else {
            return sprintf("%04d-%02d-%02d %02d:%02d", $year + 1900, $mon + 1, $day, $hour, $min);
        }
    } else {
        return sprintf("%04d-%02d-%02d %02d:%02d", $year + 1900, $mon + 1, $day, $hour, $min);
    }
}

sub GetDate($) {
    my $self = shift;
    my @date = (localtime($self->{'date'}))[3..5];
    $date[1] += 1;
    $date[2] += 1900;
    return @date;
}

sub GetWeek($) {
    my $self = shift;
    my ($wday, $yday) = (localtime($self->{'date'}))[6..7];
    if ($wday == 0) {
        $wday = 7;
    }
    return int(($yday + (7 - $wday)) / 7);
}

sub GetTime($) {
    my $self = shift;
    return reverse((localtime($self->{'date'}))[1..2]);
}

sub GetWorkDays {
    my @days = ( );
    if (scalar @_ == 2) {
        my ($start, $end) = @_;
        if (!$start->IsWorkDay) {
            $start = $start->NextWorkDay;
        }
        for (my $date = $start->WorkDayStart; $date->Date <= $end->WorkDayStart->Date; $date = $date->NextWorkDay) {
            push(@days, $date);
        }
    }
    return @days;
}

sub GetHolidays($) {
    my $self = shift;

    my $year = (localtime($self->{'date'}))[5] + 1900;
    if (defined($holidays_cache{$year})) {
        return $holidays_cache{$year};
    }

    my @days = ( );
    foreach my $holiday (@holidays) {
        if (ref @{$holiday}[0] eq 'ARRAY') {
            push(@days, [ @{$holiday}[0], @{$holiday}[1], 0 ]);
        } elsif (ref @{$holiday}[0] eq 'CODE') {
            push(@days, [ @{$holiday}[0]->($year), @{$holiday}[1], 0 ]);
        }

        my $time = POSIX::mktime(0, 0, 0, $days[$#days][0][0], $days[$#days][0][1] - 1, $year - 1900);
        $days[$#days][2] = $time;

        my $wday = (localtime($time))[6];
        if (($wday > 5) || ($wday < 1)) {
            my $backup = pop(@days);
            $time += (($wday == 6) ? 2 : 1) * 86400;
            if (($#days >= 0) && ($days[$#days][2] == $time)) {
                $time += 86400;
            }
            my @date = (localtime($time))[3..4];
            $date[1]++;
            my $description = sprintf("First working day after %s on %s %d",
                @{$holiday}[1], $months[@{$backup}[0]->[1]-1], @{$backup}[0]->[0]);
            push(@days, [ \@date, $description, $time ]);
        }
    }

    my $curyear = (localtime(time))[5] + 1900;
    if ($year == $curyear) {
        $holidays_cache{$year} = \@days;
    }

    return \@days
}

1;
