package Gtk2::Ex::TimeEntry;
$Gtk2::Ex::TimeEntry::VERSION = 0.02;
use strict;
use warnings;
use Carp;


use Gtk2;

use Glib qw(TRUE FALSE);
our $VERSION = 3;

use constant DEBUG => 0;

use Glib::Object::Subclass
    Gtk2::Entry::,
    interfaces  => [ 'Gtk2::CellEditable' ],
    properties  => [
        Glib::ParamSpec->string(
            'value'                                  ,
            'value'                                  ,
            'ISO format time string like 13:00'      ,
            ''                                       , #default value
            Glib::G_PARAM_READWRITE
        ),
    ],
    signals => {
        value_changed => {
           class_closure => \&_do_value_changed,
           flags         => ['run-first']     ,
           return_type   => undef             ,
           param_types   => []                ,
        },
    }
;

sub INIT_INSTANCE {
    my $self = shift;
    $self->signal_connect('key-press-event' => \&_do_key_press_event);
    $self->signal_connect('focus-out-event' => \&_do_focus_out_event);
}

sub SET_PROPERTY {
    my ($self, $pspec, $newval) = @_;
    
    my $pname = $pspec->get_name;
    
    # handle changes to the value parameter (emit a signal on change)
    if ($pname eq 'value') {
        $newval = $self->_parse_input($newval);
        # set the new value if it is different
        if (!defined $self->{$pname} && defined $newval || $self->{$pname} ne $newval) {
            $self->{$pname} = $newval;
            $self->signal_emit('value-changed');
        }
        else {
            # update the display
            $self->set_text($self->_format_output($self->get_value));
        }
    }
    else {
        $self->{$pname} = $newval;
    }
}

sub get_value {
    my $self = shift;
    $self->get('value');
}

sub set_value {
    my $self = shift;
    my $new_value = shift;
    $self->set('value', $new_value);
}

sub set_now {
    my $self = shift;
    my ($hour, $minute) = (localtime time)[2,1];
    
    my $value = sprintf '%02d:%02d', $hour, $minute;
    
    $self->set_value( $value );
}

sub _do_value_changed {
    my $self = shift;
    my $value = $self->get_value;
    $self->set_text($self->_format_output($self->get_value));
}

sub _do_focus_out_event {
    my $self = shift;
    $self->set_value($self->get_text);
    return FALSE;
}

sub _do_key_press_event {
    my $self = shift;
    my $key  = shift;
    my $key_val = $key->keyval;
    
    # entry pressed, parse input
    if ($key_val == 65293) {
        $self->set_value($self->get_text);
        return FALSE;
    }
    # laft arrow key pressed
    elsif ($key_val >= 65361 && $key_val <= 65364) {
        $self->set_value($self->get_text);
        return $self->_do_key_left  if $key_val == 65361;
        return $self->_do_key_right if $key_val == 65363;
        return $self->_do_key_up    if $key_val == 65362;
        return $self->_do_key_down  if $key_val == 65364;
    }
    # pass everything else on
    else {
        return FALSE;
    }
}

sub _do_key_left {
    my $self = shift;
    my $selected = $self->get_selected_component;
    $self->_select_closest_component('left') and return TRUE if !$selected;
    
    if ($selected =~ /all|minutes/) {
        $self->set_selected_component('hours');
    }
    elsif ($selected eq 'meridiem') {
        $self->set_selected_component('minutes');
    }
    return TRUE;
}

sub _do_key_right {
    my $self = shift;
    my $selected = $self->get_selected_component;
    $self->_select_closest_component('right') and return TRUE if !$selected;
    
    if ($selected =~ /all|minutes/) {
        $self->set_selected_component('meridiem');
    }
    elsif ($selected eq 'hours') {
        $self->set_selected_component('minutes');
    }
    return TRUE;
}

sub _do_key_up {
    my $self = shift;
    my $selected = $self->get_selected_component;
    $self->_select_closest_component('up') and return TRUE unless $selected;
    
    my ($hours, $minutes) = split /:/, $self->get_value;
    
    if ($selected eq 'hours') {
        $hours++;
    }
    elsif ($selected eq 'minutes') {
        $minutes++;
    }
    elsif ($selected eq 'meridiem') {
        $hours+=12;
    }
    elsif ($selected eq 'all') {
        $minutes+=15;
    }
    
    my $new_value = join ':', $hours, $minutes;
    $self->set_value($new_value);
    $self->set_selected_component($selected);
    
    return TRUE;
}

sub _do_key_down {
    my $self = shift;
    my $selected = $self->get_selected_component;
    $self->_select_closest_component('down') and return TRUE unless $selected;
    
    my ($hours, $minutes, $seconds) = split /:/, $self->get_value;
    
    if ($selected eq 'hours') {
        $hours--;
    }
    elsif ($selected eq 'minutes') {
        $minutes--;
    }
    elsif ($selected eq 'meridiem') {
        $hours-=12;
    }
    elsif ($selected eq 'all') {
        $minutes-=15;
    }

    if ($minutes < 0) {        
        # get the absolute value
        my $absolute_minutes = $minutes * -1;
        
        # hours to subtract
        my $delta_hours = int ($absolute_minutes / 60) + 1;
        $hours -= $delta_hours;
        
        # minutes to subtract (from 60)
        my $delta_minutes = $absolute_minutes % 60;
        $minutes = 60 - $delta_minutes;
    }
    if ($hours < 0) {
        # get the absolute value
        my $absolute_hours = $hours * -1;
        my $delta_hours = $absolute_hours % 24;
        $hours = 24 - $delta_hours;
    }
    
    my $new_value = sprintf '%02d:%02d', $hours, $minutes;
    $self->set_value($new_value);
    $self->set_selected_component($selected);
    return TRUE;
}

sub _format_output {
    my $self  = shift;
    my $value = shift;
    
    # throw exception unless passed a time in a valid format
    carp  q[Usage is $self->_format_output($value) where ] .
          q[$value is a valid time in the format HH:MM:SS: ] .
         qq[you supplied ($value)]
         unless defined $value && $value !~ /^\d\d:\d\d:\s\s$/;
    
    # return empty string if no value
    return '' if $value eq '';
    
    # format the value if there is one
    my ($hours, $minutes, $seconds) = split /:/, $value;
    my $ampm;
    
    if ($hours == 0) {
        $hours = 12;
        $ampm  = 'AM';
    }
    elsif ($hours == 12) {
        $ampm = 'PM';
    }
    elsif ($hours > 12) {
        $hours -= 12;
        $ampm = 'PM';
    }
    else {
        $ampm = 'AM';
    }
    
    
    return sprintf qq[%02d:%02d $ampm], $hours, $minutes;

}

sub get_selected_component {
    my $self = shift;
    
    my ($select_start, $select_end) = $self->get_selection_bounds;
    $select_start = 0 unless $select_start;
    $select_end = 0 unless $select_end;
    
    my $selected = $select_end - $select_start;
    return unless $selected == 2 || 8;
    
    if ($select_start == 0 && $select_end == 2) {
        return 'hours';
    }
    elsif ($select_start == 3 && $select_end == 5) {
        return 'minutes';
    }
    elsif ($select_start == 6 && $select_end == 8) {
        return 'meridiem';
    }
    elsif ($select_start == 0 && $select_end == 8) {
        return 'all';
    }
    else {
        return undef;
    }
}

sub set_selected_component {
    my $self = shift;
    my $field = shift;
    
    # throw exception if $field var is not valid
    confess  q[Usage is $self->set_selected_component($field) where field is one of ] .
             q[hours, minutes, ampm, meridiem, all, or an empty string: ] .
            qq[you supplied($field)]
            and return unless $field =~ /(hours|minutes|ampm|meridiem|all|none)/
            || $field eq '';
    
    if ($field eq 'hours') {
        $self->select_region(0,2);
    }
    elsif ($field eq 'minutes') {
        $self->select_region(3,5);
    }
    elsif ($field =~ /ampm|meridiem/) {
        $self->select_region(6,8);
    }
    elsif ($field eq 'all') {
        $self->select_region(0,8);
    }
    elsif ($field eq 'none' || $field eq '') {
        $self->select_region(0,0);
    }
    else {
        confess qq[Invalid argument passed: $field];
    }
}

sub _select_closest_component {
    my $self      = shift;
    my $direction = shift;
    my $cursor = $self->get_position;
    
    if ($cursor == 0 || $cursor == 1) {
        $self->set_selected_component('hours');
    }
    elsif ($cursor == 2 && $direction ne 'right') {
        $self->set_selected_component('hours');
    }
    elsif ($cursor == 2 && $direction eq 'right') {
        $self->set_selected_component('minutes');
    }
    elsif ($cursor == 3 && $direction eq 'left') {
        $self->set_selected_component('hours');
    }
    elsif ($cursor == 3 && $direction ne 'left') {
        $self->set_selected_component('minutes');
    }
    elsif ($cursor == 4) {
        $self->set_selected_component('minutes');
    }
    elsif ($cursor == 5 && $direction ne 'right') {
        $self->set_selected_component('minutes');
    }
    elsif ($cursor == 5 && $direction eq 'right') {
        $self->set_selected_component('meridiem');
    }
    elsif ($cursor == 6 && $direction eq 'left') {
        $self->set_selected_component('minutes');
    }
    elsif ($cursor == 6 && $direction ne 'left') {
        $self->set_selected_component('meridiem');
    }
    elsif ($cursor == 7 || $cursor == 8) {
        $self->set_selected_component('meridiem');
    }
    
    return TRUE;
}

sub _parse_input {
    my $self  = shift;
    my $value = shift || '';
    $value =~ s/^\s+//;
    $value =~ s/\s+$//;
    return '' unless $value;
    
    my ($h, $m, $s);
    if ($value =~ /^(\d{1,2})$/) {
        $h = $1;
        $m = 0;
        $s = 0;
    }
    elsif ($value =~ /^([012]?[0-9]):?([0-5][0-9]):?([0-5][0-9])?$/) {
        $h = $1;
        $m = $2;
        $s = $3 || 0;
    }
    elsif ($value =~ /^([012]?[0-9]):?([0-9][0-9])?\s*(am|pm)?$/i) {
        $h = $1;
        $m = $2 || 0;
        $s = 0;
        $h -= 12 if $h == 12 && lc $3 eq 'am';
        $h += 12 if $h <12 && lc $3 eq 'pm';
    }
    elsif ($value eq '') {
        return '';
    }
    else {
        return $self->get_value;
    }
    
    if ($s > 59) {
        $m++;
        $s = $s % 60;
    }
    
    if ($m > 59) {
        $h++;
        $m = $m % 60;
    }
    
    # roll over 24 hours to the eqivelent time
    if ($h >= 24) {
        $h -= int($h / 24) * 24;
    }
    
    my $parsed = sprintf '%02d:%02d:%02d', $h, $m, $s;
    return $parsed;
}


1;


__END__

=head1 NAME

Gtk2::Ex::TimeEntry -- Widget for entering times

=head1 SYNOPSIS

 use Gtk2::Ex::TimeEntry;
 $te = Gtk2::Ex::TimeEntry->new (value => '13:00:00');
 $te->set_value('1pm');
 $te->get_value;

=head1 WIDGET HIERARCHY

    Gtk2::Widget
      Gtk2::Entry
        Gtk2::Ex::TimeEntry

=head1 DESCRIPTION

C<Gtk2::Ex::TimeEntry> displays and edits a time in HH::MM PM format with some
convienence functions.

Use the up and down keys to modify the invidual components of the value, and the
left and right keys to navigate between them. Pressing up or down while the
entire contents of the entry is selected (such as when you focus-in) modifies
the value in 15 minute increments.

The time is stored in HH:MM:SS format (but display in HH:MM PM format). If you
entry a value 24:00:00 or higher, it will loop back around t

You can also type a time into the entry into various formats, which will be
parsed and then displayed in the entry in HH:MM PM format. Here are some
examples of things you can enter into the widget and the resulting internal and
display values.

=over 4

INPUT       VALUE       DISPLAY
1           01:00:00    01:00 AM
10          10:00:00    10:00 AM
420         04:20:00    04:20 AM
4:20        04:20:00    04:20 AM
420pm       16:20:00    04:20 PM
04:20 PM    16:20:00    04:20 PM
30:20:00    04:20:00    04:20 AM

=back 4

=head1 FUNCTIONS

=over 4

=item C<< $te = Gtk2::Ex::TimeEntry->new (key=>value,...) >>

Create and return a new DateSpinner widget.  Optional key/value pairs set
initial properties per C<< Glib::Object->new >>.  Eg.

    my $te = Gtk2::Ex::TimeEntry->new (value => '16:00:00');

=item C<< $te->get_selected_component >>

Returns the currently selected component - any of hours, minutes, meridiem, all
or an emptry string. An emptry string will be returned if the selection bounds
contains more or less than 1 individual component, and will return all if all
componentes are selected.

=item C<< $te->set_selected_component($component) >>

Highlights the given component, which can then be edited by typing over it or
pressing the arrow keys up or down. You can pass the values all, hours, minutes,
meridiem, an emptry string, or undef.

=item C<< $te->set_now >>

Set the widget value to the current time. 

=back

=head1 PROPERTIES

=over 4

=item C<value> (string, default '')

The current time format in ISO format HH:MM:SS. Can be set to an empty string
for no time. When setting the value you, you may pass any acceptable value
outlined in the widget description, but the time will always be stored in
HH:MM:SS format.

=back

=head1 SIGNALS

=over 4

=item C<value-changed>)

Emitted after a succesful value change.

=back

=head1 SEE ALSO

L<Gtk2::Ex::TimeEntry::CellRenderer>

=head1 AUTHOR

Jeffrey Hallock <jeffrey dot hallock at gmail dot com>

=head1 BUGS

None known. Please send bugs to <jeffrey dot hallock at gmail dot org>.
Patches and suggestions welcome.

=head1 LICENSE

Gtk2-Ex-TimeEntry is Copyright 2009 Jeffrey Hallock

Gtk2-Ex-TimeEntry is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3, or (at your option) any later
version.

Gtk2-Ex-TimeEntry is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
more details.

You should have received a copy of the GNU General Public License along with
Gtk2-Ex-TimeEntry.  If not, see L<http://www.gnu.org/licenses/>.

=cut
