package HTML::FormFu::Element::Time;

use strict;
use base 'HTML::FormFu::Element::_Field', 'HTML::FormFu::Element::Multi';
use Class::C3;

use HTML::FormFu::Attribute qw/ mk_attrs mk_require_methods /;
use HTML::FormFu::Util qw/ _get_elements _parse_args /;
use DateTime;
use DateTime::Format::Builder;
use DateTime::Locale;
use Scalar::Util qw/ blessed /;
use Carp qw/ croak /;

__PACKAGE__->mk_attrs(
    qw/
        hour minute second
        /
);

__PACKAGE__->mk_accessors(
    qw/
        strftime auto_inflate renderhms
        /
);

# build get_Xs methods
for my $method (
    qw/
    deflator filter constraint inflator validator transformer /
    )
{
    my $sub = sub {
        my $self       = shift;
        my %args       = _parse_args(@_);
        my $get_method = "get_${method}s";

        my $accessor = "_${method}s";
        my @x        = @{ $self->$accessor };
        push @x, map { @{ $_->$get_method(@_) } } @{ $self->_elements };

        if ( exists $args{name} ) {
            @x = grep { $_->name eq $args{name} } @x;
        }

        if ( exists $args{type} ) {
            @x = grep { $_->type eq $args{type} } @x;
        }

        return \@x;
    };

    my $name = __PACKAGE__ . "::get_${method}s";

    no strict 'refs';

    *{$name} = $sub;
}

sub new {
    my $self = shift->next::method(@_);

    $self->is_field(0);
    $self->render_class_suffix('multi');
    $self->strftime("%H:%M:%S");
    $self->renderhms("hms");
    $self->hour( {
            type     => 'Select',
            prefix   => [],
            min      => 0,
            max      => 23,
        } );
    $self->minute( {
            type     => 'Select',
            prefix   => [],
            multiple => 5,
            min      => 0,
            max      => 59,
        } );
    $self->second( {
            type     => 'Select',
            prefix   => [],
            multiple => 1,
            min      => 0,
            max      => 59,
        } );

    return $self;
}

sub get_fields {
    my $self = shift;

    my $f = $self->HTML::FormFu::Element::Multi::get_fields(@_);

    unshift @$f, $self;

    return $f;
}

sub _add_elements {
    my $self = shift;

    $self->_elements( [] );

    _time_defaults($self);

    if ($self->renderhms =~ 'h') {
        _add_hour($self);
    }
    if ($self->renderhms =~ 'm') {
        _add_minute($self);
    }
    if ($self->renderhms =~ 's') {
        _add_second($self);
    }

    if ( $self->auto_inflate
        && !@{ $self->get_inflators( { type => "DateTime" } ) } )
    {
        _add_inflator($self);
    }

    return;
}

sub _time_defaults {
    my $self = shift;

    my $default = $self->default;

    if ( defined $default ) {

        if ( blessed($default) && $default->isa('DateTime') ) {
            $self->hour->{default}   = $default->hour;
            $self->minute->{default} = $default->minute;
            $self->second->{default} = $default->second;
        }
        else {
            my $builder = DateTime::Format::Builder->new;
            $builder->parser( { strptime => $self->strftime } );

            my $dt = $builder->parse_datetime($default);

            $self->hour->{default}   = $dt->hour;
            $self->minute->{default} = $dt->minute;
            $self->second->{default} = $dt->second;
        }

        # pad the string so that it matches
        if (length $self->minute->{default} == 1){
            $self->minute->{default} = '0' . $self->minute->{default};
        }
        if (length $self->second->{default} == 1){
            $self->second->{default} = '0' . $self->second->{default};
        }
    }

    return;
}

sub _add_hour {
    my ($self) = @_;

    my $hour = $self->hour;

    my $hour_name = _build_hour_name($self);

    my @hours = _build_hour_list($self);

    my @hour_prefix = map { [ '', $_ ] }
        ref $hour->{prefix} ? @{ $hour->{prefix} } : $hour->{prefix};

    $self->element( {
            type => $hour->{type},
            name => $hour_name,
            options =>
                [ @hour_prefix, map { [ $_ , $_ ] } @hours ],
            defined $hour->{default}
            ? ( default => $hour->{default} )
            : (),
        } );

    return;
}

sub _add_minute {
    my ($self) = @_;

    my $minute = $self->minute;

    my $minute_name = _build_minute_name($self);

    my @minutes = _build_minute_list($self);

    my @minute_prefix = map { [ '', $_ ] }
        ref $minute->{prefix} ? @{ $minute->{prefix} } : $minute->{prefix};

    $self->element( {
            type    => $minute->{type},
            name    => $minute_name,
            options => [ @minute_prefix, map { [ $_, $_ ] } @minutes ],
            defined $minute->{default}
            ? ( default => $minute->{default} )
            : (),
        } );

    return;
}

sub _add_second {
    my ($self) = @_;

    my $second = $self->second;

    my $second_name = _build_second_name($self);

    my @seconds = _build_second_list($self);

    my @second_prefix = map { [ '', $_ ] }
        ref $second->{prefix} ? @{ $second->{prefix} } : $second->{prefix};

    $self->element( {
            type    => $second->{type},
            name    => $second_name,
            options => [ @second_prefix, map { [ $_, $_ ] } @seconds ],
            defined $second->{default}
            ? ( default => $second->{default} )
            : (),
        } );

    return;
}

sub _build_hour_list {
    my ($self) = @_;

    my $hour = $self->hour;

    my @hours;
    if (defined $hour->{list}){
        @hours = $hour->{list};
    }
    else{
        @hours = $hour->{min} .. $hour->{max};
    }

    return @hours;
}

sub _build_minute_list {
    my ($self) = @_;

    my $minute = $self->minute;
    my @minutes;

    if (defined $minute->{list}){
        @minutes = $minute->{list};
    }
    else{
        for (my $x = $minute->{min}; $x <= $minute->{max}; $x += $minute->{multiple}) {
            if ((length $x) < 2){
                push @minutes, "0$x";
            }else{
                push @minutes, "$x";
            }
        }
    }

    return @minutes;
}

sub _build_second_list {
    my ($self) = @_;

    my $second = $self->second;
    my @seconds;

    if (defined $second->{list}){
        @seconds = $second->{list};
    }
    else{
        for (my $x = $second->{min}; $x <= $second->{max}; $x += $second->{multiple}) {
            if ((length $x) < 2){
                push @seconds, "0$x";
            }else{
                push @seconds, "$x";
            }
        }
    }

    return @seconds;
}

sub _build_hour_name {
    my ($self) = @_;

    my $hour_name
        = defined $self->hour->{name}
        ? $self->hour->{name}
        : sprintf "%s.hour", $self->name;

    return $hour_name;
}

sub _build_minute_name {
    my ($self) = @_;

    my $minute_name
        = defined $self->minute->{name}
        ? $self->minute->{name}
        : sprintf "%s.minute", $self->name;

    return $minute_name;
}

sub _build_second_name {
    my ($self) = @_;

    my $second_name
        = defined $self->second->{name}
        ? $self->second->{name}
        : sprintf "%s.second", $self->name;

    return $second_name;
}

sub _add_inflator {
    my ($self) = @_;

    $self->inflator( {
            type     => "DateTime",
            parser   => { strptime => $self->strftime, },
            strptime => $self->strftime,
        } );

    return;
}

sub process {
    my $self = shift;

    $self->_add_elements;

    return;
}

sub process_input {
    my ( $self, $input ) = @_;

    my $hour_name   = _build_hour_name($self);
    my $minute_name = _build_minute_name($self);
    my $second_name = _build_second_name($self);

    my $is_valid = 1;
    my $input_hour = 0;
    my $input_minute = 0;
    my $input_second = 0;

    if ($self->renderhms =~ 'h') {
        if (defined $input->{$hour_name}
            && length $input->{$hour_name}) {
            $input_hour = $input->{$hour_name};
        }
        else{
            $is_valid = 0;
        }
    }
    if ($self->renderhms =~ 'm') {
        if (defined $input->{$minute_name}
            && length $input->{$minute_name}) {
            $input_minute = $input->{$minute_name};
        }
        else{
            $is_valid = 0;
        }
    }
    if ($self->renderhms =~ 's') {
        if (defined $input->{$second_name}
            && length $input->{$second_name}) {
            $input_second = $input->{$second_name};
        }
        else{
            $is_valid = 0;
        }
    }

    if ( $is_valid )
    {
        my $dt;

        eval {
            $dt = DateTime->new(
                year   => 0, #The DateTime object requires a year
                hour   => $input_hour,
                minute => $input_minute,
                second => $input_second,
            );
        };

        if ($@) {
            $input->{ $self->name } = $self->strftime;
        }
        else {
            $input->{ $self->name } = $dt->strftime( $self->strftime );
        }
    }

    return $self->next::method($input);
}

sub render {
    my $self = shift;

    $self->_add_elements;

    my $render = $self->next::method( { @_ ? %{ $_[0] } : () } );

    return $render;
}

1;

__END__

=head1 NAME

HTML::FormFu::Element::Time - 3 select menu multi-field

=head1 SYNOPSIS

    ---
    elements:
      - type: Time
        name: foo
        auto_inflate: 1

=head1 DESCRIPTION

Creates a L<multi|HTML::FormFu::Element::Multi> element containing 3 select 
menus for the hour, minute and second.

A time element named C<foo> would result in 3 select menus with the names 
C<foo.hour>, C<foo.minute> and C<foo.second>. The names can instead be 
overridden by the C<name> value in L</hour>, L</minute> and L</second>.

This element automatically merges the input parameters from the select 
menu into a single time parameter (and doesn't delete the individual menu's 
parameters).

=head1 METHODS

=head2 default

Arguments: DateTime object

Arguments: $time_string

Accepts either a L<DateTime> object, or a string containing a time, matching 
the L</strftime> format. Overwrites any default value set in L</hour>, 
L</minute> or L</second>.

=head2 strftime

Default Value: "%H:%M:%S"

The format of the time as returned by L<HTML::FormFu/params>, if 
L</auto_inflate> is not set.

If L</auto_inflate> is used, this is still the format that the parameter 
will be in prior to the DateTime inflator being run; which is 
what any L<Filters|HTML::FormFu::Filter> and 
L<Constraints|HTML::FormFu::Constraint> will receive.

=head2 renderhms

Default Value: "hms"

The rendered output can be any or all of hours, minutes or seconds.

Setting renderhms to 'hm' will render only the hour and minute select elements.

Setting renderhms to 'ms' will render only the minute and second select elements.

=head2 hour

Arguments: \%setting

Set values effecting the C<hour> select menu. Known keys are:

=head3 name

Override the auto-generated name of the select menu.

=head3 default

Set the default value of the select menu

=head3 prefix

Arguments: $value

Arguments: \@values

A string or arrayref of strings to be inserted into the start of the select 
menu.

Each value is only used as the label for a select item - the value for each 
of these items is always the empty string C<''>.

If this is set, C<min> and C<max> are ignored.

If not set, the list of hours is calculated using the min and max values.

With C<list> un-set, C<min> with the default of 0 and C<max> with the default of 23, the created list will be:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

=head3 min

Arguments: $value

Default: 0

If C<list> is not set, the list is created from the range from 
C<min> to C<max>.

=head3 max

Arguments: $value

Default: 23

If C<list> is not set, the list is created from the range from 
C<min> to C<max>.

=head2 minute

Arguments: \%setting

Set values effecting the C<minute> select menu. Known keys are:

=head3 name

Override the auto-generated name of the select menu.

=head3 default

Set the default value of the select menu

=head3 prefix

Arguments: $value

Arguments: \@values

A string or arrayref of strings to be inserted into the start of the select 
menu.

Each value is only used as the label for a select item - the value for each 
of these items is always the empty string C<''>.

=head3 list

Arguments: \@minutes

A list of minutes used for the minute menu.

If this is set, C<min>, C<max> and C<multiple> are ignored.

If not set, the list of minutes is calculated using the min, max and multiple values.

With C<list> un-set, C<min> with the default of 0, C<max> with the default of 59
and C<multiple> with the default of 5, the created list will be:

0 5 10 15 20 25 30 35 40 45 50 55

Setting C<multiple> to 15 would create a list of:

0, 15, 30, 45

=head2 second

Arguments: \%setting

Set values effecting the C<second> select menu. Known keys are:

=head3 name

Override the auto-generated name of the select menu.

=head3 default

Set the default value of the select menu

=head3 prefix

Arguments: $value

Arguments: \@values

A string or arrayref of strings to be inserted into the start of the select 
menu.

Each value is only used as the label for a select item - the value for each 
of these items is always the empty string C<''>.

=head3 list

Arguments: \@seconds

A list of seconds used for the second menu.

If this is set, C<min>, C<max> and C<multiple> are ignored.

If not set, the list of seconds is calculated using the min, max and multiple values.

With C<list> un-set, C<min> with the default of 0, C<max> with the default of 59
and C<multiple> with the default of 5, the created list will be:

0 5 10 15 20 25 30 35 40 45 50 55

Setting C<multiple> to 15 would create a list of:

0, 15, 30, 45

=head3 min

Arguments: $value

Default: 0

If C<list> is not set, the list is created from the range from 
C<min> to C<max> in multiples of C<multiple>.

=head3 max

Arguments: $value

Default: 59

If C<list> is not set, the list is created from the range from 
C<min> to C<max> in multiples of C<multiple>.

=head3 multiple

Arguments: $value

Default: 5

If C<list> is not set, the list is created from the range from 
C<min> to C<max> in multiples of C<multiple>.

=head2 auto_inflate

If true, a L<DateTime Inflator|HTML::FormFu::Inflator::DateTime> will 
automatically be added to the element, and it will be given a formatter so 
that stringification will result in the format specified in L</strftime>.

If you require the DateTime Inflator to have a different stringification 
format to the format used internally by your Filters and Constraints, then 
you must explicitly add your own DateTime Inflator, rather than using 
L</auto_inflate>.

=head1 CAVEATS

Although this element inherits from L<HTML::FormFu::Element::Block>, it's 
behaviour for the methods 
L<filter/filters|HTML::FormFu/filters>, 
L<constraint/constraints|HTML::FormFu/constraints>, 
L<inflator/inflators|HTML::FormFu/inflators>, 
L<validator/validators|HTML::FormFu/validators> and 
L<transformer/transformers|HTML::FormFu/transformers> is more like that of 
a L<field element|HTML::FormFu::Element::_Field>, meaning all processors are 
added directly to the date element, not to it's select-menu child elements.

This element's L<get_elements|HTML::FormFu/get_elements> and 
L<get_all_elements|HTML::FormFu/get_all_elements> are inherited from 
L<HTML::FormFu::Element::Block>, and so have the same behaviour. However, it 
overrides the C<get_fields> method, such that it returns both itself and 
it's child elements.

=head1 SEE ALSO

Is a sub-class of, and inherits methods from 
L<HTML::FormFu::Element::_Field>, 
L<HTML::FormFu::Element::Multi>, 
L<HTML::FormFu::Element::Block>, 
L<HTML::FormFu::Element>

L<HTML::FormFu::FormFu>

=head1 AUTHOR

Carl Franks, C<cfranks.org>

=head1 LICENSE

This library is free software, you can redistribute it and/or modify it under
the same terms as Perl itself.
