The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Schedule::Week;

use 5.008008;
use strict;
use warnings;

use Readonly;
use Carp;

require Exporter;

our @ISA = qw(Exporter);

our %EXPORT_TAGS = ( 'days' => [ qw(
   $MONDAY $TUESDAY $WEDNESDAY $THURSDAY $FRIDAY $SATURDAY $SUNDAY
) ] );

our @EXPORT_OK = ( @{ $EXPORT_TAGS{'days'} } );

our $VERSION = '1.0';

our Readonly::Scalar $SUNDAY    = 0;
our Readonly::Scalar $MONDAY    = 1;
our Readonly::Scalar $TUESDAY   = 2;
our Readonly::Scalar $WEDNESDAY = 3;
our Readonly::Scalar $THURSDAY  = 4;
our Readonly::Scalar $FRIDAY    = 5;
our Readonly::Scalar $SATURDAY  = 6;

sub new {

    my $class = shift;

    my $self = {
        'ts' => _initialize_schedule()
    };

    return bless $self, $class;
}

#  Day convenience accessors
sub sunday { 
    my ($self, $hours_array_ref, $state) = @_; 
    $self->hours_for_day($SUNDAY, $hours_array_ref, $state); 
}

sub monday { 
    my ($self, $hours_array_ref, $state) = @_;
    $self->hours_for_day($MONDAY, $hours_array_ref, $state); 
}

sub tuesday { 
    my ($self, $hours_array_ref, $state) = @_;
    $self->hours_for_day($TUESDAY, $hours_array_ref, $state); 
}

sub wednesday { 
    my ($self, $hours_array_ref, $state) = @_;
    $self->hours_for_day($WEDNESDAY, $hours_array_ref, $state); 
}

sub thursday { 
    my ($self, $hours_array_ref, $state) = @_;
    $self->hours_for_day($THURSDAY, $hours_array_ref, $state); 
}

sub friday { 
    my ($self, $hours_array_ref, $state) = @_;
    $self->hours_for_day($FRIDAY, $hours_array_ref, $state); 
}

sub saturday { 
    my ($self, $hours_array_ref, $state) = @_;
    $self->hours_for_day($SATURDAY, $hours_array_ref, $state); 
}

#  set or get hours across all days
sub hours {

    my ($self, $hours_array_ref, $state) = @_;

    my @hour_states;

    if (! defined $hours_array_ref) {
        croak 'Missing reference to array of hours to get/set!';
    }

    if (ref($hours_array_ref) ne 'ARRAY') {
        croak "$hours_array_ref is not a reference to an array!";
    }

    if (defined $state) {
        $state = ($state == 0) ? 0 : 1;
        for my $day ($SUNDAY ... $SATURDAY) {
            $hour_states[$day] = [];
            for my $hour (@{$hours_array_ref}) {
                $self->{'ts'}->[$day]->[$hour] = $state;
                push(@{$hour_states[$day]}, $state);
            }
        }
    }


    if (scalar(@hour_states) == 0) {
        for my $day ($SUNDAY ... $SATURDAY) {
            $hour_states[$day] = [];
            for my $hour (@{$hours_array_ref}) {
                push(@{$hour_states[$day]},
                     $self->{'ts'}->[$day]->[$hour]);
            }
        }
    }

    return @hour_states;
}

#  Reset all bits in arrays to 0
sub reset {
    my $self = shift;
    $self->{'ts'} = _initialize_schedule();
}

#  Does this day have any hours scheduled?
sub has_hours {

    my ($self, $day) = @_;

    if (($day < $SUNDAY) || ($day > $SATURDAY)) {
        croak "Day number $day is invalid";
    }

    return grep(/1/, @{$self->{'ts'}->[$day]});
}

sub hours_for_day {
    my ($self, $day, $hours_ref, $state) = @_;

    if (($day < $SUNDAY) || ($day > $SATURDAY)) {
        croak "Day number $day is invalid";
    }

    if ((defined $hours_ref) && (ref($hours_ref) ne 'ARRAY')) {
        croak "$hours_ref is not a reference to an array!";
    }

    my @hour_states = ();

    #  Want all hour states for the day
    if (! defined $hours_ref) {
        return @{$self->{'ts'}->[$day]};
    }

    if (defined($state)) {
        $state = ($state == 0) ? 0 : 1;
        for my $h (@{$hours_ref}) {
            $self->{'ts'}->[$day]->[$h] = $state;
            push(@hour_states, $self->{'ts'}->[$day]->[$h]);
        }
    }

    if (scalar(@hour_states) == 0) {
        for my $h (@{$hours_ref}) {
            push(@hour_states, $self->{'ts'}->[$day]->[$h]);
        }
    }

    return @hour_states;
}

#  Return 1 or 0 to indicate if the passed in time falls in the
#  active periods of this schedule.  If no time stamp is passed
#  in, defaults to now.

sub is_active {

    my ($self, $ts) = @_;

    #  Default to now
    $ts = time() if not defined $ts;
    my ($hr, $day) = (localtime())[2, 6];

    return ($self->{'ts'}->[$day]->[$hr] == 1) ? 1 : 0;
}

#  Serialize a schedule into a string of 168 1s and 0s

sub serialize {
    my $self = shift;

    my $serialized = "";

    for my $day ($SUNDAY ... $SATURDAY) {
        $serialized .= join('', @{$self->{'ts'}->[$day]});
    }

    return $serialized;
}

#  Return an instantiated schedule made from the passed in
#  serialized string

sub deserialize {
    my $serialized = shift;

    if (! defined $serialized) {
        croak "Must pass in a serialized schedule!";
    }

    if (length($serialized) != 168) {
        croak "Passed in serialized schedule is not 168 hours (chars) long!";
    }

    if ($serialized =~ m/[^01]+/) {
        croak "Passed in serialized schedule must be all 1s and 0s";
    }

    my $offset = 0;

    my $hours_in_day = 24;

    my $schedule = Schedule::Week->new();

    #  Localize for map() in loop below
    local $_;

    for my $day ($SUNDAY ... $SATURDAY) {
        my $day_template = substr($serialized, $offset, $hours_in_day);

        #  Explicitly convert to integers to keep perl from guessing that
        #  values are 1 character strings
        my @hours = map { int($_); } (split('', $day_template));

        $schedule->{'ts'}->[$day] = \@hours;
        $offset += $hours_in_day;
    }

    return $schedule;
}

#  Set hours on or off for all weekdays in one shot or retrieve values
#  for same.
sub weekday_hours {
    my ($self, $hours_ref, $state) = @_;

    my @results;

    for my $day ($MONDAY ... $FRIDAY) {
        push(@results, [$self->hours_for_day($day, $hours_ref, $state)]);
    }

    return @results;
}

#  Set hours on or off for all weekends in one shot or retrieve values for
#  same.
sub weekend_hours {
    my ($self, $hours_ref, $state) = @_;

    my @results;

    for my $day ($SATURDAY, $SUNDAY) {
        push(@results, [$self->hours_for_day($day, $hours_ref, $state)]);
    }

    return @results;
}

### Private methods
sub _initialize_schedule {

    my @schedule = ();
    $schedule[$SUNDAY]    = _initialize_hours(0);
    $schedule[$MONDAY]    = _initialize_hours(0);
    $schedule[$TUESDAY]   = _initialize_hours(0);
    $schedule[$WEDNESDAY] = _initialize_hours(0);
    $schedule[$THURSDAY]  = _initialize_hours(0);
    $schedule[$FRIDAY]    = _initialize_hours(0);
    $schedule[$SATURDAY]  = _initialize_hours(0);

    return \@schedule;
}

sub _initialize_hours {

    my $v = shift;

    return [
        # Hours of day, starting with 12 AM (hour 00 aka midnight)
        $v, $v, $v, $v, $v, $v, $v, $v,
        $v, $v, $v, $v, $v, $v, $v, $v,
        $v, $v, $v, $v, $v, $v, $v, $v,
    ];

}

1;