#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use open ':locale';

use WWW::OrangeHRM::Client;
use Term::ReadKey;
use User::Utmp;
use DateTime;
use File::Spec;
use Config::Tiny;
use Getopt::Long;
use Pod::Usage;
use Encode::Locale;
use Encode;

=encoding utf-8

=head1 NAME

orangehrm - Command line client for OrangeHRM

=head1 SYNOPSYS

orangehrm [OPTION...]

=head1 DESCRIPTION

Set or show time sheet in OrangeHRM system.

=head1 OPTIONS

Without any options, it will show time sheet for current month.


=head2 Options for configuration

These options can be retrieved from configuration file.

=over 8

=item B<--url URL>

Base URL where OrangeHRM instance is running.

=item B<--username USER>

User name to log in as. If not specified, it will be asked when needed.

=item B<--password PASSWORD>

Password to use. If not specified, it will be asked when needed.

=back


=head2 Options for showing a time sheet

=over 8

=item B<--year NUMBER>

Year of Gregorian calendar, default is today.

=item B<--month NUMBER>

Month of year, counts from 1, default is today.

=back


=head2 Options for filling a time sheet

=over 8

=item B<--year NUMBER>

Year of Gregorian calendar, default is today.

=item B<--month NUMBER>

Month of year, counts from 1, default is today.

=item B<--day NUMBER>

Day of month, counts from 1, default is today.

=item B<--from HH::MM>

=item B<--from boot>

=item B<--from now>

Begining of working time. Use C<boot> for boot time. Use C<now> for current
time.

=item B<--to HH::MM>

=item B<--to boot>

=item B<--to now>

End of working time. Use C<boot> for boot time. Use C<now> for current time.

=item B<--break HH:MM>

Vacant part in the working time for purpose of working break.

=item B<--doctor HH:MM>

Vacant part in the working time for purpose of medical check.

=item B<--comment STRING>

Free comment to the day.

=item B<--trip>

Specify if you are on bussines trip on this day.

=item B<--work>

Negation of B<--trip> option. Working hours are filled as at-work by default.
This option is usefull only to flip the trip state to work when using
B<--amend>. 

=item B<--amend>

Partial modification. With this option, only records with explicit option will
be changed. By default, unspecified arguments reset corresponding records to
their default values.

=back


=head2 Options for submitting a time sheet

=over 8

=item B<--submit>

Submit current month's time sheet for review.

=item B<--year NUMBER>

Year of Gregorian calendar, default is today.

=item B<--month NUMBER>

Month of year, counts from 1, default is today.

=item B<--comment STRING>

Free comment to the submission.

=back


=head2 Options for debugging

=over 8

=item B<--debug>

Dump sent HTTP requests and other useful data.

=back


=head2 Other options

=over 8

=item B<--help>

Show program manual.

=item B<--version>

Show program version.

=back

=head1 FILES

=head2 F<~/.orangehrm>

User configuration:

    url = https://redhat.orangehrm.com/
    samlidp = https://saml.redhat.com/
    samlout = https://example.redhat.com/bye
    #username = ppisar
    #password = Foo

If L<LWP::Authen::Negotiate> Perl module is installed, and a valid Kerberos
ticket granting ticket is available, user name and password are not needed in
the configuration.

=cut
 
# Return boot HH:MM time of day specified as argument. Otherwise return undef.
sub get_boot_datetime {
    my $day = shift->clone()->truncate(to => 'day');
    my $boot;

    User::Utmp::utmpname(User::Utmp::WTMP_FILE);
    for my $entry (User::Utmp::getut()) {
        if ($entry->{'ut_type'} != User::Utmp::BOOT_TIME() or
            !exists $entry->{'ut_time'} or !defined $entry->{'ut_time'}) {
            next;
        }
        my $entry_time = DateTime->from_epoch(epoch => $entry->{'ut_time'});
        my $entry_day = $entry_time->clone()->truncate(to => 'day');
        if (!DateTime->compare($day, $entry_day)) {
            if (!defined $boot or 0 > DateTime->compare($entry_time, $boot)) {
                $boot = $entry_time;
            }
        }
    }
    if (defined $boot) {
        $boot = sprintf('%02d:%02d', (localtime($boot->epoch))[2,1]);
    }
    return $boot;
}


# Callback function for passing username and password;
sub ask_credentials {
    my ($prompt, $is_password) = @_;

    print "$prompt: ";
    if ($is_password) {
        ReadMode('noecho');
    }
    my $value = ReadLine(0);
    if ($is_password) {
        ReadMode('restore');
        print "\n";
    }
    chomp $value if (defined $value);
    return $value;
}


my ($now, $day, $month, $year);
# Translate time specification by word to HH::MM. It operates on $_.
sub translate_time {
    if (!defined) { return; }
    if ($_ eq 'now') {
        $_ = $now;
    } elsif ($_ eq 'boot') {
        $_ = get_boot_datetime(DateTime->new(
            year => $year, month => $month, day => $day
        ));
        if (!defined) {
            die "Could not determine boot time for $year-$month-$day.\n";
        }
    }
}

my $config_file = File::Spec->catfile($ENV{HOME}//'/', '.orangehrm');

my %configuration = (
    url => 'https://redhat.orangehrm.com/',
    username => undef,
    password => undef
);


# Main code
# Default arguments
my ($help, $debug, $version,
    $from, $to, $break, $doctor, $comment, $trip, $work, $amend,
    $submit);
my ($minutes, $hours);
($minutes, $hours, $day, $month, $year) = (localtime)[1, 2, 3, 4, 5];
$month += 1;
$year += 1900;
$now = WWW::OrangeHRM::Client::normalize_time($hours . ':' . $minutes);

# Load configuration
if (-f $config_file) {
    my $cfg = Config::Tiny->new->read($config_file);
    if (!defined $cfg) {
        print STDERR 'Could not parse `', $config_file,
            ' configuration file: ', $Config::Tiny::errstr, "\n";
        exit 1;
    }
    for (keys %{$cfg->{_}}) {
        $configuration{$_} = $cfg->{_}->{$_};
    }
}

GetOptions( 
    'url=s' => \$configuration{url},
    'username=s' => \$configuration{username},
    'password=s' => \$configuration{password},
    'year=i' => \$year,
    'month=i' => \$month,
    'day=i' => \$day,
    'from=s' => \$from,
    'to=s' => \$to,
    'break=s' => \$break,
    'doctor=s' => \$doctor,
    'comment=s' => \$comment,
    'trip' => \$trip,
    'work' => \$work,
    'amend' => \$amend,
    'submit' => \$submit,
    'help' => \$help,
    'version' => \$version,
    'debug' => \$debug
) or pod2usage(-exitstatus => 1, -verbose => 1);
if ($help) {
    pod2usage(-exitstatus => 0, -verbose => 2);
}
if ($version) {
    print $WWW::OrangeHRM::Client::VERSION, "\n";
    exit 0;
}
if (!defined $configuration{url}) {
    pod2usage(-exitstatus => 1, -verbose => 1,
        -message => "Missing configuration!\n");
}
if (!$amend and (!defined $from xor !defined $to)) {
    pod2usage(-exitstatus => 1, -verbose => 1,
        -message => "Missing options!\n");
}

if (((defined $from or defined $to or $amend) and defined $submit)
    or ($trip and $work)) {
    pod2usage(-exitstatus => 1, -verbose => 1,
        -message => "Conflicting options!\n");
}

if ($work) {
    $trip = 0;
}
map translate_time, ($from, $to);
if (defined $comment) {
    $comment = decode(locale => $comment);
}


my $automaton = WWW::Mechanize->new(
    'env_proxy' => 1,
    'keep_alive' => 4,
);
if ($debug) {
    WWW::OrangeHRM::Client::debug_http($automaton);
}

# Log-in
if (!WWW::OrangeHRM::Client::log_in($automaton, \%configuration,
        \&ask_credentials)) {
    WWW::OrangeHRM::Client::fatal_error($automaton, $debug,
        'Could not log in!');
}
print "Logged in.\n";

if (!WWW::OrangeHRM::Client::time_sheet($automaton)) {
    WWW::OrangeHRM::Client::fatal_error($automaton, $debug,
        'Could not get time sheet!');
}

my $date = WWW::OrangeHRM::Client::time_sheet_date($automaton);
if (!defined $date or !($date eq "$year-$month")) {
    if (!WWW::OrangeHRM::Client::time_sheet_change($automaton, $year, $month)) {
        WWW::OrangeHRM::Client::fatal_error($automaton, $debug,
            'Could not change time sheet!');
    }
}

if (defined $from or defined $to or $amend) {
    if (!WWW::OrangeHRM::Client::time_sheet_set_day($automaton, $day,
            $from, $to, $break, $doctor, $comment, $trip, $amend)) {
        WWW::OrangeHRM::Client::fatal_error($automaton, $debug,
            "Could not fill day #$day into time sheet!");
    }
    if (!WWW::OrangeHRM::Client::time_sheet_save($automaton)) { 
        WWW::OrangeHRM::Client::fatal_error($automaton, $debug,
            "Could not save time sheet!");
    }
} elsif (defined $submit) {
    if (!WWW::OrangeHRM::Client::time_sheet_submit($automaton, $comment)) {
        WWW::OrangeHRM::Client::fatal_error($automaton, $debug,
            "Could not submit time sheet!");
    }
} else {
    if (!WWW::OrangeHRM::Client::time_sheet_show($automaton)) {
        WWW::OrangeHRM::Client::fatal_error($automaton, $debug,
            "Could not parse time sheet!");
    }
}

# Log-out
if (!WWW::OrangeHRM::Client::log_out($automaton, \%configuration)) {
    WWW::OrangeHRM::Client::fatal_error($automaton, $debug,
        'Could not log out!');
}
print "Logged out.\n";

exit 0;

__END__

=head1 COPYRIGHT

Copyright © 2012, 2013, 2014, 2015, 2017, 2019  Petr Písař <ppisar@redhat.com>.

=head1 LICENSE

This is free software.  You may redistribute copies of it under the terms of
the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>.
There is NO WARRANTY, to the extent permitted by law.

=cut
