package DateTime::Event::DayOfWeek;

use strict;
use warnings;
use vars qw(
    $VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS
);

$VERSION = '0.01';

BEGIN {
	sub SUNDAY()   { 0 }
	sub MONDAY()   { 1 }
	sub TUESDAY()  { 2 }
	sub WEDNESDAY(){ 3 }
	sub THURSDAY() { 4 }
	sub FRIDAY()   { 5 }
	sub SATURDAY() { 6 }
};

use Params::Validate qw/validate OBJECT SCALAR/;
use DateTime::Set;
use Carp;

require Exporter;
@ISA = qw(Exporter);
@EXPORT_OK   = qw(SUNDAY MONDAY TUESDAY WEDNESDAY THURSDAY);
%EXPORT_TAGS = (
	'daynames_en' => [qw(SUNDAY MONDAY TUESDAY WEDNESDAY THURSDAY)],
);

sub new {
	my $class = shift;
	my $current_dow = shift;

	return bless { dow => $current_dow || 0 }, $class;
}

sub Sunday    { __PACKAGE__->new(0) }
sub Monday    { __PACKAGE__->new(1) }
sub Tuesday   { __PACKAGE__->new(2) }
sub Wednesday { __PACKAGE__->new(3) }
sub Thursday  { __PACKAGE__->new(4) }
sub Friday    { __PACKAGE__->new(5) }
sub Saturday  { __PACKAGE__->new(6) }

sub _calculate_dow {
	my $self = shift;
	my %arg = @_;

	my $current_dow = $arg{dt}->day_of_week();

	return $arg{dt}->clone->truncate( to => 'day' )
		if $current_dow == $arg{dow} and not $arg{always_differ};

	my $delta = ( $arg{dow} - $current_dow + 7 ) % 7;
	$delta -= 7 if $arg{past};

	$delta += ($arg{past} ? -7 : 7) if ($delta == 0 && $arg{always_differ});

	return $arg{dt}->clone->add( days => $delta )->truncate( to => 'day' );
};

sub next   {
	my ($self, $dt) = @_;

	if (ref($dt) ne 'DateTime') {
		croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
		$dt = DateTime->from_object(object=>$dt);
	}

	return $self->_calculate_dow(
		dt => $dt,
		dow => $self->{dow}
	)
}
sub last   {
	my ($self, $dt) = @_;

	if (ref($dt) ne 'DateTime') {
		croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
		$dt = DateTime->from_object(object=>$dt);
	}

	return $self->_calculate_dow(
		dt => $dt,
		dow => $self->{dow},
		past => 1
	)
}
sub following {
	my ($self, $dt) = @_;

	if (ref($dt) ne 'DateTime') {
		croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
		$dt = DateTime->from_object(object=>$dt);
	}

	return $self->_calculate_dow(
		dt => $dt,
		dow => $self->{dow},
		always_differ => 1
	)
}
sub previous {
	my ($self, $dt) = @_;

	if (ref($dt) ne 'DateTime') {
		croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
		$dt = DateTime->from_object(object=>$dt);
	}

	return $self->_calculate_dow(
		dt => $dt,
		dow => $self->{dow},
		always_differ => 1,
		past => 1
	)
}

sub is {
	my ($self, $dt) = @_;

	if (ref($dt) ne 'DateTime') {
		croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
		$dt = DateTime->from_object(object=>$dt);
	}

	return ($self->{dow} % 7 == $dt->day_of_week() % 7) ? 1 : 0;
}

sub closest {
	my ($self, $dt) = @_;

	if (ref($dt) ne 'DateTime') {
		croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
		$dt = DateTime->from_object(object=>$dt);
	}

	if( $self->is( $dt ) ){
		return $dt->clone->truncate( to => 'day' );
	}

	my $following = $self->following( $dt );
	my $previous  = $self->previous( $dt );

	return ( abs($dt->epoch - $following->epoch) < abs($dt->epoch - $previous->epoch) )
		? $following
		: $previous
}

sub as_list {
	my $self = shift;
	my %args  = validate( @_,
		{   from        => { type => OBJECT },
			to          => { type => OBJECT },
			inclusive   => { type => SCALAR, default=>0 },
		}
	);

	# Make sure our args are in the right order
	($args{from}, $args{to}) = sort ($args{from}, $args{to});

	my @set = ();

	if ($args{inclusive}) {
		if ($self->is($args{from})) {
			push(@set,$args{from});
		}
		if ($self->is($args{to})) {
			push(@set,$args{to});
		}
	}

	my $checkdate = $args{from};

	while ($checkdate < $args{to}) {
		$checkdate = $self->following($checkdate);
		push(@set,$checkdate) if ($checkdate < $args{to});
	}

	return sort @set;
}

sub as_set {
	my $self = shift;
	my %args = @_;
	if (exists $args{inclusive}) {
		croak("You must specify both a 'from' and a 'to' datetime") unless
			ref($args{to})=~/DateTime/ and
			ref($args{from})=~/DateTime/;
		if ($args{inclusive}) {
			$args{start} = delete $args{from};
			$args{end} = delete $args{to};
		} else {
			$args{after} = delete $args{from};
			$args{before} = delete $args{to};
		}
		delete $args{inclusive};
	} elsif (exists $args{from} or exists $args{to}) {
		croak("You must specify both a 'from' and a 'to' datetime") unless
			ref($args{to})=~/DateTime/ and
			ref($args{from})=~/DateTime/;
			$args{after} = delete $args{from};
			$args{before} = delete $args{to};
	}
	return DateTime::Set->from_recurrence(
		next		=> sub { return $_[0] if $_[0]->is_infinite; $self->following( $_[0] ) },
		previous	=> sub { return $_[0] if $_[0]->is_infinite; $self->previous(  $_[0] ) },
		%args
	);
}

1;
__END__

=head1 NAME

DateTime::Event::DayOfWeek - Returns Day-of-Week events for DateTime objects

=head1 SYNOPSIS

  use DateTime::Event::DayOfWeek;

  $dt = DateTime->new( year   => 2008,
                       month  => 4,
                       day    => 13,
                     );


  $wednesday = DateTime::Event::DayOfWeek->wednesday();
  # or $wednesday = new DateTime::Event::DayOfWeek( WEDNESDAY );

  $previous_wednesday = $wednesday->previous($dt);
  # Wed, 9 Apr 2008 00:00:00

  $following_wednesday = $wednesday->following($dt);
  # Wed, 16 Apr 2008 00:00:00

  $closest_wednesday = $wednesday->closest($dt);
  # Wed, 16 Apr 2008 00:00:00

  $is_wednesday = $wednesday->is($dt);
  # 0

  $dt2 = $dt->clone->add( months => 1 );

  @set = $wednesday->as_list(from=>$dt, to=>$dt2);
  # Wed, 16 Apr 2008 00:00:00
  # Wed, 23 Apr 2008 00:00:00
  # Wed, 30 Apr 2008 00:00:00
  # Sun, 07 May 2008 00:00:00

  $every_wednesday = $wednesday->as_set;
  # A set of every wednesday ever. See C<DateTime::Set> for more information.

=head1 DESCRIPTION

The DateTime::Event::DayOfWeek module returns events that occur on the
day-of-week required where an event is the occurrence of that day of
the week.

=head1 CONSTRUCTORS

The main 'new' constructor takes one argument: A day of the week
expressed as an integer where Sunday is zero (0).

  $wednesday = new DateTime::Event::DayOfWeek( 3 );

Constants are available for SUNDAY, MONDAY, .., SATURDAY

  $wednesday = new DateTime::Event::DayOfWeek( WEDNESDAY );

You can also use the English day names as constructors:

  $wednesday = DateTime::Event::DayOfWeek::Wednesday;
  # or
  $wednesday = DateTime::Event::DayOfWeek->Wednesday;


=head1 METHODS

For all these methods, unless otherwise noted, $dt is a plain vanila
DateTime object or a DateTime object from any DateTime::Calendar module
that can handle calls to from_object and utc_rd_values (which should be
all of them, but there's nothing stopping someone making a bad egg).

This class offers the following methods.

=over 4

=item * following($dt)

Returns the DateTime object for the Day of the Week after $dt. This will
not return $dt.

=item * previous($dt)

Returns the DateTime object for the Day of the Week before $dt. This will
not return $dt.

=item * closest($dt)

Returns the DateTime object for the Day of the Week closest to $dt. This
will return midnight of $dt if $dt is the Day of the Week.

=item * is($dt)

Return positive (1) if $dt is the Day of the Week, otherwise returns false
(0)

=item * as_list(from => $dt, to => $dt2, inclusive=>I<([0]|1)>)

Returns a list of Day-of-the-Weeks between I<to> and I<from>.

If the optional I<inclusive> parameter is true (non-zero), the to and
from dates will be included if they are the Day of the Week.

If you do not include an I<inclusive> parameter, we assume you do not
want to include these dates (the same behaviour as supplying a false
value)


=item * as_set()

Returns a DateTime::Set of Day-of-the-Weeks.

In the past this method used the same syntax as 'as_list' above. However
we now allow both the above syntax as well as the full options allowable
when creating sets with C<DateTime::Set>. This means you can call
C<$datetime_set = $sunday->as_set;> and it will return a
C<DateTime::Set> of all Sundays. See C<DateTime::Set> for more information.

=back

=head1 EXPORTS

This class does not export anything by default, however the following
exports are supported.

=over 4

=item * SUNDAY, MONDAY, .., SATURDAY

These constants map to the integer value of that day of the week.

=item * :daynames_en

Exports all the day names at once

=back

=head1 THE SMALL PRINT

=head2 REFERENCES

=over 4

=item * http://datetime.perl.org - The official home of the DateTime
project

=back

=head2 SUPPORT

Support for this module, and for all DateTime modules will be given
through the DateTime mailing list - datetime@perl.org.

Bugs should be reported through rt.cpan.org.

=head2 AUTHOR

Rick Measham <rickm@cpan.org>

Aristotle Pagaltzis

=head2 CREDITS

B<Aristotle Pagaltzis> - whose journal post
(http://use.perl.org/~Aristotle/journal/36022) inspired the module and
whose code started the ball rolling

=head2 COPYRIGHT

(c) Copyright  2008 Rick Measham. All rights reserved. This program is
free software; you can redistribute it and/or modify it under the same
terms as Perl itself.

The full text of the license can be found in the LICENSE file included
with this module.

=head2 SEE ALSO

L<DateTime>, L<DateTime::Set>, perl(1),
http://datetime.perl.org.
