use strict;
use warnings;
package Time::R;
$Time::R::VERSION = '0.024';
# ABSTRACT: Handle recurrences.

use overload (
    '""' => sub { shift->to_string; },
    bool => sub { 1 },
    fallback => 1,
);

use Carp qw/ croak /;
use Time::C;
use Time::C::Sentinel;
use Function::Parameters qw/ :strict /;



method new ($c: $start, :$end = undef, :$years = 0, :$months = 0, :$weeks = 0, :$days = 0, :$hours = 0, :$minutes = 0, :$seconds = 0) {
    bless({
        start   => $start,
        end     => $end,
        years   => $years,
        months  => $months,
        weeks   => $weeks,
        days    => $days,
        hours   => $hours,
        minutes => $minutes,
        seconds => $seconds,
    }, $c)->_validate_start($start);
}

method _validate_start ($r: $new_start) {
    croak "->start(): Not a Time::C object: $new_start" unless ref $new_start and $new_start->isa('Time::C');

    return $r;
}



method start ($r: $new_start = undef) :lvalue {
    my $setter = sub {
        $r->_validate_start($_[0])->{start} = $_[0];

        return $r if defined $new_start;
        return $_[0];
    };

    return $setter->($new_start) if defined $new_start;

    sentinel value => $r->{start}, set => $setter;
}


method current ($r: $new_current = undef) :lvalue {
    my $current = $r->{current} // $r->start->clone();

    my $setter = sub {
        $r->{current} = $_[0];

        return $r if defined $new_current;
        return $_[0];
    };

    return $setter->($new_current) if defined $new_current;

    sentinel value => $current, set => $setter;
}


method end ($r: $new_end = undef) :lvalue {
    my $setter = sub {
        $r->{end} = $_[0];

        return $r if defined $new_end;
        return $_[0];
    };

    return $setter->($new_end) if defined $new_end;

    sentinel value => $r->{end}, set => $setter;
}


method years ($r: $new_years = undef) :lvalue {
    my $setter = sub {
        return $r->{years} = $_[0];
    };

    if (defined $new_years) { $setter->($new_years); return $r; }

    sentinel value => $r->{years}, set => $setter;
}


method months ($r: $new_months = undef) :lvalue {
    my $setter = sub {
        return $r->{months} = $_[0];
    };

    if (defined $new_months) { $setter->($new_months); return $r; }

    sentinel value => $r->{months}, set => $setter;
}


method weeks ($r: $new_weeks = undef) :lvalue {
    my $setter = sub {
        return $r->{weeks} = $_[0];
    };

    if (defined $new_weeks) { $setter->($new_weeks); return $r; }

    sentinel value => $r->{weeks}, set => $setter;
}


method days ($r: $new_days = undef) :lvalue {
    my $setter = sub {
        return $r->{days} = $_[0];
    };

    if (defined $new_days) { $setter->($new_days); return $r; }

    sentinel value => $r->{days}, set => $setter;
}


method hours ($r: $new_hours = undef) :lvalue {
    my $setter = sub {
        return $r->{hours} = $_[0];
    };

    if (defined $new_hours) { $setter->($new_hours); return $r; }

    sentinel value => $r->{hours}, set => $setter;
}


method minutes ($r: $new_minutes = undef) :lvalue {
    my $setter = sub {
        return $r->{minutes} = $_[0];
    };

    if (defined $new_minutes) { $setter->($new_minutes); return $r; }

    sentinel value => $r->{minutes}, set => $setter;
}


method seconds ($r: $new_seconds = undef) :lvalue {
    my $setter = sub {
        return $r->{seconds} = $_[0];
    };

    if (defined $new_seconds) { $setter->($new_seconds); return $r; }

    sentinel value => $r->{seconds}, set => $setter;
}



method next ($r:) {
    my $c = $r->current;
    my $n = $r->start->clone;

    if ($r->years) {
        my $i = 1;
        LOOP: {
            my $y = $n->clone;
            $y->year += ($r->years * $i++);
            redo LOOP if $y->epoch <= $c->epoch;

            $n = $y;
        }
    }
    if ($r->months) {
        my $i = 1;
        LOOP: {
            my $m = $n->clone;
            $m->month += ($r->months * $i++);
            redo LOOP if $m->epoch <= $c->epoch;

            $n = $m;
        }
    }
    if ($r->weeks)   { do { $n->week   += $r->weeks   } until $n->epoch > $c->epoch; }
    if ($r->days)    { do { $n->day    += $r->days    } until $n->epoch > $c->epoch; }
    if ($r->hours)   { do { $n->hour   += $r->hours   } until $n->epoch > $c->epoch; }
    if ($r->minutes) { do { $n->minute += $r->minutes } until $n->epoch > $c->epoch; }
    if ($r->seconds) { do { $n->second += $r->seconds } until $n->epoch > $c->epoch; }
    return undef if defined $r->end and $n->epoch > $r->end->epoch;

    $r->current = $n;
    return $n;
}


method upcoming ($r:) {
    my $c = $r->current->clone;
    $r->latest();
    my $n = $r->next();
    $r->current = $c if not defined $n;
    return $n;
}


method latest ($r:) {
    my @until = $r->reset->until(Time::C->now($r->start->tz));

    return $until[-1];
}


method until ($r: $end) {
    croak "\$end is not a Time::C object" unless ref $end and $end->isa('Time::C');
    $end = $r->end if defined $r->end and $r->end->epoch < $end->epoch;

    my @results = $r->current();
    push @results, $_ while ($_ = $r->next() and $_->epoch <= $end->epoch);
    $r->current = $results[-1] if @results;

    return @results;
}


method reset ($r:) {
    $r->current = $r->start->clone();
    return $r;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Time::R - Handle recurrences.

=head1 VERSION

version 0.024

=head1 SYNOPSIS

  use Time::C;
  use Time::R;

  my $start = Time::C->new(2016,1,31);
  my $r = Time::R->new($start, months => 1);

  # "2016-02-29T00:00:00Z"
  my $first = $r->next();

  # "2016-03-31T00:00:00Z
  my $second = $r->next();

  # resets $r->next()
  $r->reset();

  # 2016-10-31T00:00:00Z" (depending on current time)
  my $upcoming = $r->upcoming();

  # "2016-09-30T00:00:00Z" (depending on current time)
  my $latest = $r->latest();

  # ("2016-09-30T00:00:00Z", "2016-10-31T00:00:00Z", "2016-11-30T00:00:00Z",
  #  "2016-12-31T00:00:00Z")
  my @dates = $r->until(Time::C->new(2017,1,1));

  # "2017-01-31T00:00:00Z"
  my $next = $r->next();

  # print out the first thursday of every month until January 2017
  my $m = Time::R->new(Time::C->new(2016), months => 1, end => Time::C->new(2017));
  while (my $n = $m->next()) {
    $n->day++ while $n->day_of_week != 4;
    print $n->strftime('%A %d %B %Y') . "\n"; #ex: Thursday 4 February 2016
  }

=head1 DESCRIPTION

Convenient ways of handling recurrences.

=head1 CONSTRUCTORS

=head2 new

  my $r = Time::R->new($start);
  my $r = Time::R->new($start, end => $end, years => $year, months => $months,
    weeks => $weeks, days => $days, hours => $hours, minutes => $minutes,
    seconds => $seconds);

Creates a Time::R object starting at C<$start>, and optionally ending at C<$end>. Every argument except C<$start> is optional and can be in any order.

=over

=item C<$start>

This should be a L<Time::C> object representing the starting time.

=item C<< end => $end >>

This should be a L<Time::C> object optionally specifying the ending time. Defaults to C<undef>.

=item C<< years => $years >>

This should be the number of years between each recurrence. Defaults to C<0>.

=item C<< months => $months >>

This should be the number of months between each recurrence. Defaults to C<0>.

=item C<< weeks => $weeks >>

This should be the number of weeks between each recurrence. Defaults to C<0>.

=item C<< days => $days >>

This should be the number of days between each recurrence. Defaults to C<0>.

=item C<< hours => $hours >>

This should be the number of hours between each recurrence. Defaults to C<0>.

=item C<< minutes => $minutes >>

This should be the number of minutes between each recurrence. Defaults to C<0>.

=item C<< seconds => $seconds >>

This should be the number of seconds between each recurrence. Defaults to C<0>.

=back

=head1 ACCESSORS

=head2 start

  my $start = $r->start;
  $r->start = $start;

  $r = $r->start($new_start);

Returns or sets the L<Time::C> object representing the starting time of the recurrence. Setting this also calls C<< $r->reset() >>.

If the form C<< $r->start($new_start) >> is used, it likewise updates the start but returns the entire object.

=head2 current

  my $current = $r->current;
  $r->current = $current;

Returns or sets the L<Time::C> object representing the current time of the current recurrence.

This may get changed by C<< $r->next >>, C<< $r->upcoming >>, C<< $r->latest >>, C<< $r->until >>, and C<< $r->reset >>.

=head2 end

  my $end = $r->end;
  $r->end = $end;

Returns or sets the L<Time::C> object representing the end time of the recurrence.

=head2 years

  my $years = $r->years;
  $r->years = $years;
  $r->years += 10;
  $r->years++;
  $r->years--;

Returns or sets the number of years between each recurrence. Setting this also calls C<< $r->reset() >>.

=head2 months

  my $months = $r->months;
  $r->months = $months;
  $r->months += 12;
  $r->months++;
  $r->months--;

Returns or sets the number of months between each recurrence. Setting this also calls C<< $r->reset() >>.

=head2 weeks

  my $weeks = $r->weeks;
  $r->weeks = $weeks;
  $r->weeks += 52;
  $r->weeks++;
  $r->weeks--;

Returns or sets the number of weeks between each recurrence. Setting this also calls C<< $r->reset() >>.

=head2 days

  my $days = $r->days;
  $r->days = $days;
  $r->days += 7;
  $r->days++;
  $r->days--;

Returns or sets the number of days between each recurrence. Setting this also calls C<< $r->reset() >>.

=head2 hours

  my $hours = $r->hours;
  $r->hours = $hours;
  $r->hours += 24;
  $r->hours++;
  $r->hours--;

Returns or sets the number of hours between each recurrence. Setting this also calls C<< $r->reset() >>.

=head2 minutes

  my $minutes = $r->minutes;
  $r->minutes = $minutes;
  $r->minutes += 60;
  $r->minutes++;
  $r->minutes--;

Returns or sets the number of minutes between each recurrence. Setting this also calls C<< $r->reset() >>.

=head2 seconds

  my $seconds = $r->seconds;
  $r->seconds = $seconds;
  $r->seconds += 60;
  $r->seconds++;
  $r->seconds--;

Returns or sets the number of seconds between each recurrence. Setting this also calls C<< $r->reset() >>.

=head1 METHODS

=head2 next

  my $next = $r->next();

Sets C<< $r->current >> to and returns the next recurrence as a L<Time::C> object. If the next time would happen after the C<< $r->end >>, it instead returns C<undef> and leaves C<< $r->current >> alone.

=head2 upcoming

  my $upcoming = $r->upcoming();

Sets C<< $r->current >> to and returns the next time the recurrence occurs as a L<Time::C> object based on the current time. If the next time would happen after the C<< $r->end >>, it instead returns C<undef> and leaves C<< $r->current >> alone.

=head2 latest

  my $latest = $r->latest();

Sets C<< $r->current >> to and returns the latest time the recurrence occurs as a L<Time::C> object based on the current time.

=head2 until

  my @until = $r->until($end);

Returns all the recurrences that will happen from C<< $r->current >> until C<$end> (which should be a L<Time::C> object), and updates C<< $r->current >> to the last one returned if any. If C<< $r->end >> is defined and C<$end> is after, it will instead use C<< $r->end >> as the limit.

=head2 reset

  $r->reset();

Resets C<< $r->current >> to C<< $r->start >>.

=head1 SEE ALSO

=over

=item L<Time::C>

=item L<Time::D>

=back

=head1 AUTHOR

Andreas Guldstrand <andreas.guldstrand@gmail.com>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2016 by Andreas Guldstrand.

This is free software, licensed under:

  The MIT (X11) License

=cut