package time;

use strict;
use warnings;
no warnings 'redefine';
use utf8;

use Time::Local();
use DateTime();
use Date::Calc();
use Date::Manip();
use POSIX qw(strftime);
use Data::Dumper;

use config();

#use base 'Exporter';
our @EXPORT_OK = qw(
  format_datetime format_time
  date_format time_format
  datetime_to_time time_to_datetime time_to_date
  datetime_to_date
  add_days_to_datetime add_hours_to_datetime add_minutes_to_datetime
  add_days_to_date
  datetime_to_array date_to_array array_to_date array_to_datetime array_to_time array_to_time_hm
  date_cond time_cond check_date check_time check_datetime check_year_month
  datetime_to_rfc822 get_datetime datetime_to_utc datetime_to_utc_datetime
  get_duration get_duration_seconds
  getDurations getWeekdayIndex getWeekdayNames getWeekdayNamesShort getMonthNames getMonthNamesShort
);

my $NAMES = {
    'de' => {
        months =>
          [ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' ],
        months_abbr => [ 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez' ],
        weekdays      => [ 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag' ],
        weekdays_abbr => [ 'Mo',     'Di',       'Mi',       'Do',         'Fr',      'Sa',      'So' ],
    },
    'en' => {
        months =>
          [ 'January', 'February', 'March', 'April', 'May', 'June', 'Jule', 'August', 'September', 'October', 'November', 'December' ],
        months_abbr   => [ 'Jan',    'Feb',     'Mar',       'Apr',      'May',    'Jun',      'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ],
        weekdays      => [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ],
        weekdays_abbr => [ 'Mo',     'Tu',      'We',        'Th',       'Fr',     'Sa',       'Su' ],
    },
};


# map starting with monday=0
my $WEEKDAY_INDEX = {
    '0'  => 0,
    '1'  => 1,
    '2'  => 2,
    '3'  => 3,
    '4'  => 4,
    '5'  => 5,
    '6'  => 6,
    'Mo' => 0,
    'Tu' => 1,
    'Di' => 1,
    'We' => 2,
    'Mi' => 2,
    'Th' => 3,
    'Do' => 3,
    'Fr' => 4,
    'Sa' => 5,
    'Su' => 6,
    'So' => 6
};

my $DURATIONS = [
    0,   5,   10,  15,  20,  30,  40,  45,  50,  60,  70,  75,  80,  90,  100, 105, 110, 115,
    120, 135, 150, 165, 180, 195, 210, 225, 240, 300, 360, 420, 480, 540, 600, 660, 720, 1440
];

sub getDurations() {
    return $DURATIONS;
}

sub getWeekdayNames(;$) {
    my $language = shift || 'en';
    return $NAMES->{$language}->{weekdays};
}

sub getWeekdayNamesShort(;$) {
    my $language = shift || 'en';
    return $NAMES->{$language}->{weekdays_abbr};
}

sub getMonthNames(;$) {
    my $language = shift || 'en';
    return $NAMES->{$language}->{months};
}

sub getMonthNamesShort(;$) {
    my $language = shift || 'en';
    return $NAMES->{$language}->{months_abbr};
}


sub getWeekdayIndex(;$) {
    my $weekday = shift || '';
    return $WEEKDAY_INDEX->{$weekday};
}

# map starting with monday=1
sub getWeekdays {
    return {
        1    => 1,
        2    => 2,
        3    => 3,
        4    => 4,
        5    => 5,
        6    => 6,
        7    => 7,
        'Mo' => 1,
        'Tu' => 2,
        'Di' => 2,
        'We' => 3,
        'Mi' => 3,
        'Th' => 4,
        'Do' => 4,
        'Fr' => 5,
        'Sa' => 6,
        'Su' => 7,
        'So' => 7
    };
}

#deprecated, for wordpress sync
sub format_datetime(;$) {
    my $datetime = shift;
    return $datetime if ( $datetime eq '' );
    return add_hours_to_datetime( $datetime, 0 );
}

#deprecated
sub format_time($) {
    my $t = $_[0];

    my $year  = $t->[5] + 1900;
    my $month = $t->[4] + 1;
    $month = '0' . $month if ( length($month) == 1 );

    my $day = $t->[3];
    $day = '0' . $day if ( length($day) == 1 );

    my $hour = $t->[2];
    $hour = '0' . $hour if ( length($hour) == 1 );

    my $minute = $t->[1];
    $minute = '0' . $minute if ( length($minute) == 1 );

    return [ $day, $month, $year, $hour, $minute ];
}

# convert datetime to unix time
sub datetime_to_time ($){
    my $datetime = $_[0];

    #	print $datetime."\n";
    if ( $datetime =~ /(\d\d\d\d)\-(\d+)\-(\d+)[T\s](\d+)\:(\d+)(\:(\d+))?/ ) {
        my $year   = $1;
        my $month  = $2 - 1;
        my $day    = $3;
        my $hour   = $4;
        my $minute = $5;
        my $second = $8 || 0;
        return Time::Local::timelocal( $second, $minute, $hour, $day, $month, $year ) || print STDERR "datetime_to_time: no valid date time found! ($datetime)\n";

    } else {
        print STDERR "datetime_to_time: no valid date time found! ($datetime)\n";
        return -1;
    }
}

#get rfc822 datetime string from datetime string
sub datetime_to_rfc822($) {
    my $datetime = $_[0];
    my $time     = datetime_to_time($datetime);
    return POSIX::strftime( "%a, %d %b %Y %H:%M:%S %z", localtime($time) );
}

#get seconds from epoch
sub datetime_to_utc($$) {
    my $datetime  = shift;
    my $time_zone = shift;
    $datetime = get_datetime( $datetime, $time_zone );
    return $datetime->epoch();
}

# get full utc datetime including timezone offset
sub datetime_to_utc_datetime($$) {
    my $datetime  = shift;
    my $time_zone = shift;
    $datetime = get_datetime( $datetime, $time_zone );
    return $datetime->format_cldr("yyyy-MM-ddTHH:mm:ssZZZZZ");
}

#add hours to datetime string
sub add_hours_to_datetime($;$) {
    my $datetime = shift;
    my $hours    = shift;
    $hours = 0 unless defined $hours;
    return time_to_datetime( datetime_to_time($datetime) + ( 3600 * $hours ) );
}

#add minutes to datetime string
sub add_minutes_to_datetime($;$) {
    my $datetime = shift;
    my $minutes  = shift;
    $minutes = 0 unless defined $minutes;
    return time_to_datetime( datetime_to_time($datetime) + ( 60 * $minutes ) );
}

#add days to datetime string
sub add_days_to_datetime($;$) {
    my $datetime = shift;
    my $days     = shift;
    $days = 0 unless defined $days;
    my $time = datetime_to_array($datetime);

    ( $time->[0], $time->[1], $time->[2] ) = Date::Calc::Add_Delta_Days( $time->[0] + 0, $time->[1] + 0, $time->[2] + 0, $days );
    return array_to_datetime($time);
}

sub add_days_to_date($;$) {
    my $datetime = shift;
    my $days     = shift;
    $days = 0 unless defined $days;
    my $date = date_to_array($datetime);
    ( $date->[0], $date->[1], $date->[2] ) = Date::Calc::Add_Delta_Days( $date->[0] + 0, $date->[1] + 0, $date->[2] + 0, $days );
    return array_to_date($date);
}

# convert unix time to datetime format
sub time_to_datetime(;$) {
    my $time = shift;
    $time = time() unless ( defined $time ) && ( $time ne '' );
    my @t = localtime($time);
    return sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0] );
}

# convert unix time to date format
sub time_to_date(;$) {
    my $time = shift;
    $time = time() unless ( defined $time ) && ( $time ne '' );
    my @t = localtime($time);
    return sprintf( '%04d-%02d-%02d', $t[5] + 1900, $t[4] + 1, $t[3] );
}

# convert datetime to a array of date/time values
sub datetime_to_array(;$) {
    my $datetime = $_[0] || '';
    if ( $datetime =~ /(\d\d\d\d)\-(\d+)\-(\d+)([T\s]+(\d+)\:(\d+)(\:(\d+))?)?/ ) {
        my $year   = $1;
        my $month  = $2;
        my $day    = $3;
        my $hour   = $5 || '00';
        my $minute = $6 || '00';
        my $second = $8 || '00';
        return [ $year, $month, $day, $hour, $minute, $second ];
    }
    return undef;
}

# convert datetime to date
sub datetime_to_date(;$) {
    my $datetime = $_[0] || '';
    if ( $datetime =~ /(\d\d\d\d)\-(\d+)\-(\d+)/ ) {
        my $year  = $1;
        my $month = $2;
        my $day   = $3;
        return sprintf( "%04d-%02d-%02d", $year, $month, $day );
    }
    return undef;
}

#convert datetime array or single value to datetime string
sub array_to_datetime(;$) {
    my $date = shift;
    if ( ref($date) eq 'ARRAY' ) {
        return sprintf( "%04d-%02d-%02d %02d:%02d:%02d", $date->[0], $date->[1], $date->[2], $date->[3], $date->[4], $date->[5] );
    }
    my $month  = shift;
    my $day    = shift;
    my $hour   = shift || '0';
    my $minute = shift || '0';
    my $second = shift || '0';
    return sprintf( "%04d-%02d-%02d %02d:%02d:%02d", $date, $month, $day, $hour, $minute, $second );
}

#convert date array or single values to date string
sub array_to_date($;$$) {
    my $date = shift;
    if ( ref($date) eq 'ARRAY' ) {
        return sprintf( "%04d-%02d-%02d", $date->[0], $date->[1], $date->[2] );
    }
    my $month = shift;
    my $day   = shift;
    return sprintf( "%04d-%02d-%02d", $date, $month, $day );
}

sub array_to_time(;$) {
    my $date = shift;
    if ( ref($date) eq 'ARRAY' ) {
        return sprintf( "%02d:%02d:%02d", $date->[3], $date->[4], $date->[5] );
    }
    my $minute = shift || 0;
    my $second = shift || 0;
    return sprintf( "%02d:%02d:%02d", $date, $minute, $second );
}

sub array_to_time_hm(;$) {
    my $date = shift;
    if ( ref($date) eq 'ARRAY' ) {
        return sprintf( "%02d:%02d", $date->[3], $date->[4] );
    }
    my $minute = shift || 0;
    return sprintf( "%02d:%02d", $date, $minute );
}

# get number of days between two days
sub days_between($$) {
    my $today      = $_[0];
    my $date       = $_[1];
    my $delta_days = eval { Date::Calc::Delta_Days( $today->[0], $today->[1], $today->[2], $date->[0], $date->[1], $date->[2] ) };
    return $delta_days;
}

sub dayOfYear($) {
    my $datetime = $_[0];
    if ( $datetime =~ /(\d\d\d\d)\-(\d+)\-(\d+)/ ) {
        my $year  = $1;
        my $month = $2;
        my $day   = $3;
        return Date::Calc::Day_of_Year( $year, $month, $day );
    }
    return undef;
}

# get duration in minutes
sub get_duration($$$) {
    my $start    = shift;
    my $end      = shift;
    my $timezone = shift;
    $start = time::get_datetime( $start, $timezone );
    $end   = time::get_datetime( $end,   $timezone );
    my $duration = $end->epoch() - $start->epoch();
    return $duration / 60;
}

# get duration in seconds
sub get_duration_seconds($$;$) {
    my $start    = shift;
    my $end      = shift;
    my $timezone = shift || 'UTC';

    unless ( defined $start ) {
        print STDERR "time::get_duration_seconds(): start is missing\n";
        return 0;
    }
    unless ( defined $end ) {
        print STDERR "time::get_duration_seconds(): end is missing\n";
        return 0;
    }

    $start = time::get_datetime( $start, $timezone );
    $end   = time::get_datetime( $end,   $timezone );
    unless ( defined $start ) {
        print STDERR "time::get_duration_seconds(): invalid start\n";
        return 0;
    }
    unless ( defined $end ) {
        print STDERR "time::get_duration_seconds(): invalid end\n";
        return 0;
    }
    my $duration = $end->epoch() - $start->epoch();
    return $duration;
}

# convert date string to a array of date values
sub date_to_array($) {
    my $datetime = $_[0];
    if ( $datetime =~ /(\d\d\d\d)\-(\d+)\-(\d+)/ ) {
        my $year  = $1;
        my $month = $2;
        my $day   = $3;
        return [ $year, $month, $day ];
    }
    return undef;
}

# parse date string and return date string
# pass 'today', return '' on parse error
sub date_cond($) {
    my $date = shift;

    return '' if ( $date eq '' );
    if ( $date =~ /(\d\d\d\d)\-(\d\d?)\-(\d\d?)/ ) {
        my $year  = $1;
        my $month = $2;
        my $day   = $3;
        return sprintf( "%04d-%02d-%02d", $year, $month, $day );
    }
    return 'today' if ( $date eq 'today' );
    return '';
}

#parse time and return time string hh:mm:ss
#return hh:00 if time is 'now'
sub time_cond($) {
    my $time = shift;

    return '' if ( $time eq '' );
    if ( $time =~ /(\d\d?)\:(\d\d?)(\:(\d\d))?/ ) {
        my $hour   = $1;
        my $minute = $2;
        my $second = $4 || '00';
        return sprintf( "%02d:%02d:%02d", $hour, $minute, $second );
    }
    if ( $time eq 'now' ) {
        my $date = datetime_to_array( time_to_datetime( time() ) );
        my $hour = $date->[3] - 2;
        $hour = 0 if ( $hour < 0 );
        $time = sprintf( "%02d:00", $hour );
        return $time;
    }
    return '';
}

#parse date and time string and return yyyy-mm-ddThh:mm:ss
sub datetime_cond($) {
    my $datetime = shift;

    return '' if ( $datetime eq '' );
    ( my $date, my $time ) = split /[ T]/, $datetime;
    $date = time::date_cond($date);
    return '' if ( $date eq '' );
    $time = time::time_cond($time);
    return '' if ( $time eq '' );

    return $date . 'T' . $time;
}

sub check_date($) {
    my $date = shift;

    return "" if ( !defined $date ) || ( $date eq '' );
    if ( $date =~ /(\d\d\d\d)\-(\d\d?)\-(\d\d?)/ ) {
        return $1 . '-' . $2 . '-' . $3;
    } elsif ( $date =~ /(\d\d?)\.(\d\d?)\.(\d\d\d\d)/ ) {
        return $3 . '-' . $2 . '-' . $1;
    }
    return $date if ( $date eq 'today' || $date eq 'tomorrow' || $date eq 'yesterday' );
    return -1;

    #error("no valid date format given!");
}

sub check_time($) {
    my $time = shift;
    return "" if ( !defined $time ) || ( $time eq '' );
    return $time if ( $time eq 'now' ) || ( $time eq 'future' );
    if ( $time =~ /(\d\d?)\:(\d\d?)/ ) {
        return $1 . ':' . $2;
    }
    return -1;
}

sub check_datetime($) {
    my $date = shift;

    return "" if ( !defined $date ) || ( $date eq '' );
    if ( $date =~ /(\d\d\d\d)\-(\d\d?)\-(\d\d?)[T ](\d\d?)\:(\d\d?)/ ) {
        return sprintf( "%04d-%02d-%02dT%02d:%02d", $1, $2, $3, $4, $5 );
    }
    return -1;
}

sub check_year_month($) {
    my $date = shift;
    return -1 unless defined $date;
    return $date if ( $date eq '' );
    if ( $date =~ /(\d\d\d\d)\-(\d\d?)/ ) {
        return $1 . '-' . $2 . '-' . $3;
    }
    return -1;
}

#TODO: remove config dependency
sub date_time_format($$;$) {
    my $config   = shift;
    my $datetime = shift;
    my $language = shift || $config->{date}->{language} || 'en';
    if ( defined $datetime && $datetime =~ /(\d\d\d\d)\-(\d\d?)\-(\d\d?)[\sT](\d\d?\:\d\d?)/ ) {
        my $time  = $4;
        my $day   = $3;
        my $month = $2;
        my $year  = $1;

        $month = time::getMonthNamesShort($language)->[ $month - 1 ] || '';
        return "$day. $month $year $time";
    }
    return $datetime;
}

#format datetime to date string
#TODO: remove config dependency
sub date_format($$;$) {
    my $config   = shift;
    my $datetime = shift;
    my $language = shift || $config->{date}->{language} || 'en';

    if ( defined $datetime && $datetime =~ /(\d\d\d\d)\-(\d\d?)\-(\d\d?)/ ) {
        my $day   = $3;
        my $month = $2;
        my $year  = $1;
        $month = time::getMonthNamesShort($language)->[ $month - 1 ] || '';
        return "$day. $month $year";
    }
    return $datetime;
}

#format datetime to time string
sub time_format($) {
    my $datetime = shift;
    if ( defined $datetime && $datetime =~ /(\d\d?\:\d\d?)/ ) {
        return $1;
    }
    return $datetime;
}

#get offset from given time_zone
sub utc_offset($) {
    my $time_zone = shift;

    my $datetime = DateTime->now();
    $datetime->set_time_zone($time_zone);
    return $datetime->strftime("%z");
}

#get weekday from (yyyy,mm,dd)
sub weekday($$$) {
    my ( $year, $month, $day ) = @_;
    my $time = Time::Local::timelocal( 0, 0, 0, $day, $month - 1, $year );
    return ( localtime($time) )[6];
}

#get current date, related to starting day_starting_hour
#TODO: remove config dependency
sub get_event_date($) {
    my $config = shift;

    my $datetime = time::time_to_datetime( time() );
    my $hour     = ( time::datetime_to_array($datetime) )->[3];

    #print STDERR "datetime=$datetime hour=$hour\n";
    #today: between 0:00 and starting_hour show last day
    if ( $hour < $config->{date}->{day_starting_hour} ) {
        my $date = time::datetime_to_array( time::add_days_to_datetime( $datetime, -1 ) );
        return join( '-', ( $date->[0], $date->[1], $date->[2] ) );
    } else {

        #today: between starting_hour and end of day show current day
        my $date = time::datetime_to_array( time::time_to_datetime( time() ) );
        return join( '-', ( $date->[0], $date->[1], $date->[2] ) );
    }
}

#get datetime object from datetime string
sub get_datetime(;$$) {
    my $datetime = shift;
    my $timezone = shift;

    return unless defined $datetime;
    return if $datetime eq '';
    my @l = @{ time::datetime_to_array($datetime) };
    return undef if scalar(@l) == 0;

    # catch invalid datees
    $datetime = undef;
    eval {
        $datetime = DateTime->new(
            year      => $l[0],
            month     => $l[1],
            day       => $l[2],
            hour      => $l[3],
            minute    => $l[4],
            second    => $l[5],
            time_zone => $timezone
        );
    };
    return undef unless defined $datetime;
    $datetime->set_locale('de_DE');
    return $datetime;
}

#get list of nth weekday in month from start to end
sub get_nth_weekday_in_month(;$$$$) {
    my $start   = shift;    # datetime string
    my $end     = shift;    # datetime string
    my $nth     = shift;    # every nth week of month
    my $weekday = shift;    # weekday [1..7,'Mo'-'Su','Mo'-'Fr']

    return [] unless defined $start;
    return [] unless defined $end;
    return [] unless defined $nth;
    return [] unless defined $weekday;

    my $weekdays = time::getWeekdays();
    return [] unless defined $weekdays->{$weekday};
    $weekday = $weekdays->{$weekday};

    my $dates = [];

    if ( $start =~ /(\d\d\d\d)-(\d\d)-(\d\d)[ T](\d\d)\:(\d\d)/ ) {
        my $hour = int($4);
        my $min  = int($5);
        my $sec  = 0;
        my @date = Date::Manip::ParseRecur( "0:1*$nth:$weekday:$hour:$min:$sec", "", $start, $end );
        for my $date (@date) {
            if ( $date =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)\:(\d\d)\:(\d\d)/ ) {
                push @$dates, "$1-$2-$3 $4:$5:$6";
            }
        }
    }
    return $dates;
}

#do not delete last line!
1;
