# --
# Copyright (C) 2001-2021 OTRS AG, https://otrs.com/
# Copyright (C) 2021 Znuny GmbH, https://znuny.org/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --

package Kernel::System::DateTime;
## nofilter(TidyAll::Plugin::Znuny::Perl::Time)
## nofilter(TidyAll::Plugin::Znuny::Perl::Translatable)
## nofilter(TidyAll::Plugin::Znuny::Perl::IsIntegerVariableCheck)

use strict;
use warnings;

use Exporter qw(import);
our %EXPORT_TAGS = (    ## no critic
    all => [
        'OTRSTimeZoneGet',
        'SystemTimeZoneGet',
        'TimeZoneList',
        'UserDefaultTimeZoneGet',
    ],
);
Exporter::export_ok_tags('all');

use DateTime;
use DateTime::TimeZone;
use Scalar::Util                  qw( looks_like_number );
use Kernel::System::VariableCheck qw( IsArrayRefWithData IsHashRefWithData );

our %ObjectManagerFlags = (
    NonSingleton            => 1,
    AllowConstructorFailure => 1,
);

our @ObjectDependencies = (
    'Kernel::Config',
    'Kernel::System::Main',
    'Kernel::System::Log',
);

our $Locale = DateTime::Locale->load('en_US');

use overload
    '>'        => \&_OpIsNewerThan,
    '<'        => \&_OpIsOlderThan,
    '>='       => \&_OpIsNewerThanOrEquals,
    '<='       => \&_OpIsOlderThanOrEquals,
    '=='       => \&_OpEquals,
    '!='       => \&_OpNotEquals,
    'fallback' => 1;

=head1 NAME

Kernel::System::DateTime - Handles date and time calculations.

=head1 DESCRIPTION

Handles date and time calculations.

=head1 PUBLIC INTERFACE

=head2 new()

Creates a DateTime object. Do not use new() directly, instead use the object manager:

    # Create an object with current date and time
    # within time zone set in SysConfig OTRSTimeZone:
    my $DateTimeObject = $Kernel::OM->Create(
        'Kernel::System::DateTime'
    );

    # Create an object with current date and time
    # within a certain time zone:
    my $DateTimeObject = $Kernel::OM->Create(
        'Kernel::System::DateTime',
        ObjectParams => {
            TimeZone => 'Europe/Berlin',        # optional, TimeZone name.
        }
    );

    # Create an object with a specific date and time:
    my $DateTimeObject = $Kernel::OM->Create(
        'Kernel::System::DateTime',
        ObjectParams => {
            Year     => 2016,
            Month    => 1,
            Day      => 22,
            Hour     => 12,                     # optional, defaults to 0
            Minute   => 35,                     # optional, defaults to 0
            Second   => 59,                     # optional, defaults to 0
            TimeZone => 'Europe/Berlin',        # optional, defaults to setting of SysConfig OTRSTimeZone
        }
    );

    # Create an object from an epoch timestamp. These timestamps are always UTC/GMT,
    # hence time zone will automatically be set to UTC.
    #
    # If parameter Epoch is present, all other parameters will be ignored.
    my $DateTimeObject = $Kernel::OM->Create(
        'Kernel::System::DateTime',
        ObjectParams => {
            Epoch => 1453911685,
        }
    );

    # Create an object from a date/time string.
    #
    # If parameter String is given, Year, Month, Day, Hour, Minute and Second will be ignored
    my $DateTimeObject = $Kernel::OM->Create(
        'Kernel::System::DateTime',
        ObjectParams => {
            String   => '2016-08-14 22:45:00',
            TimeZone => 'Europe/Berlin',        # optional, defaults to setting of SysConfig OTRSTimeZone
        }
    );

    # Following formats for parameter String are supported:
    #
    #   yyyy-mm-dd hh:mm:ss
    #   yyyy-mm-dd hh:mm                # sets second to 0
    #   yyyy-mm-dd                      # sets hour, minute and second to 0
    #   yyyy-mm-ddThh:mm:ss+tt:zz
    #   yyyy-mm-ddThh:mm:ss+ttzz
    #   yyyy-mm-ddThh:mm:ss-tt:zz
    #   yyyy-mm-ddThh:mm:ss-ttzz
    #   yyyy-mm-ddThh:mm:ss [timezone]  # time zone will be deduced from an optional string
    #   yyyy-mm-ddThh:mm:ss[timezone]   # i.e. 2018-04-20T07:37:10UTC

=cut

sub new {
    my ( $Type, %Param ) = @_;

    # allocate new hash for object
    my $Self = {};
    bless( $Self, $Type );

    # CPAN DateTime: only use English descriptions and abbreviations internally.
    #   This has nothing to do with the user's locale settings in OTRS.
    $Self->{Locale} = $Locale;

    # Use private parameter to pass in an already created CPANDateTimeObject (used)
    #   by the Clone() function).
    if ( $Param{_CPANDateTimeObject} ) {
        $Self->{CPANDateTimeObject} = $Param{_CPANDateTimeObject};
        return $Self;
    }

    # Create the CPAN/Perl DateTime object.
    my $CPANDateTimeObject = $Self->_CPANDateTimeObjectCreate(%Param);

    if ( ref $CPANDateTimeObject ne 'DateTime' ) {

        # Add debugging information.
        my $Parameters = $Kernel::OM->Get('Kernel::System::Main')->Dump(
            \%Param,
        );

        # Remove $VAR1 =
        $Parameters =~ s{ \s* \$VAR1 \s* = \s* \{}{}xms;

        # Remove closing brackets.
        $Parameters =~ s{\}\s+\{}{\{}xms;
        $Parameters =~ s{\};\s*$}{}xms;

        # Replace new lines with spaces.
        $Parameters =~ s{\n}{ }gsmx;

        # Replace multiple spaces with one.
        $Parameters =~ s{\s+}{ }gsmx;

        $Kernel::OM->Get('Kernel::System::Log')->Log(
            'Priority' => 'Error',
            'Message'  => "Error creating DateTime object ($Parameters).",
        );

        return;
    }

    $Self->{CPANDateTimeObject} = $CPANDateTimeObject;
    return $Self;
}

=head2 Get()

Returns hash ref with the date, time and time zone values of this object.

    my $DateTimeSettings = $DateTimeObject->Get();

Returns:

    my $DateTimeSettings = {
        Year      => 2016,
        Month     => 1,                 # starting at 1
        Day       => 22,
        Hour      => 16,
        Minute    => 35,
        Second    => 59,
        DayOfWeek => 5,                 # starting with 1 for Monday, ending with 7 for Sunday
        TimeZone  => 'Europe/Berlin',
    };

=cut

sub Get {
    my ( $Self, %Param ) = @_;

    my $Values = {
        Year      => $Self->{CPANDateTimeObject}->year(),
        Month     => $Self->{CPANDateTimeObject}->month(),
        MonthAbbr => $Self->{CPANDateTimeObject}->month_abbr(),
        Day       => $Self->{CPANDateTimeObject}->day(),
        Hour      => $Self->{CPANDateTimeObject}->hour(),
        Minute    => $Self->{CPANDateTimeObject}->minute(),
        Second    => $Self->{CPANDateTimeObject}->second(),
        DayOfWeek => $Self->{CPANDateTimeObject}->day_of_week(),
        DayAbbr   => $Self->{CPANDateTimeObject}->day_abbr(),
        TimeZone  => $Self->{CPANDateTimeObject}->time_zone_long_name(),
    };

    return $Values;
}

=head2 Set()

Sets date and time values of this object. You have to give at least one parameter. Only given values will be changed.
Note that the resulting date and time have to be valid. On validation error, the current date and time of the object
won't be changed.

Note that in order to change the time zone, you have to use function C<L</ToTimeZone()>>.

    # Setting values by hash:
    my $Success = $DateTimeObject->Set(
        Year     => 2016,
        Month    => 1,
        Day      => 22,
        Hour     => 16,
        Minute   => 35,
        Second   => 59,
    );

    # Settings values by date/time string:
    my $Success = $DateTimeObject->Set( String => '2016-02-25 20:34:01' );

If parameter C<String> is present, all other parameters will be ignored. Please see C<L</new()>> for the list of
supported string formats.

Returns:

   $Success = 1;    # On success, or false otherwise.

=cut

sub Set {
    my ( $Self, %Param ) = @_;

    if ( defined $Param{String} ) {
        my $DateTimeHash = $Self->_StringToHash( String => $Param{String} );
        return if !$DateTimeHash;

        %Param = %{$DateTimeHash};
    }

    my @DateTimeParams = qw ( Year Month Day Hour Minute Second );

    # Check given parameters
    my $ParamGiven;
    DATETIMEPARAM:
    for my $DateTimeParam (@DateTimeParams) {
        next DATETIMEPARAM if !defined $Param{$DateTimeParam};

        $ParamGiven = 1;
        last DATETIMEPARAM;
    }

    if ( !$ParamGiven ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            'Priority' => 'Error',
            'Message'  => 'Missing at least one parameter.',
        );
        return;
    }

    # Validate given values by using the current settings + the given ones.
    my $CurrentValues = $Self->Get();
    DATETIMEPARAM:
    for my $DateTimeParam (@DateTimeParams) {
        next DATETIMEPARAM if !defined $Param{$DateTimeParam};

        $CurrentValues->{$DateTimeParam} = $Param{$DateTimeParam};
    }

    # Create a new DateTime object with the new/added values
    my $CPANDateTimeParams = $Self->_ToCPANDateTimeParamNames( %{$CurrentValues} );

    # Delete parameters that are not allowed for set function
    delete $CPANDateTimeParams->{time_zone};

    my $Result;
    eval {
        $Result = $Self->{CPANDateTimeObject}->set( %{$CPANDateTimeParams} );
    };

    return $Result;
}

=head2 Add()

Adds duration or working time to date and time of this object. You have to give at least one of the valid parameters.
On error, the current date and time of this object won't be changed.

    my $Success = $DateTimeObject->Add(
        Years         => 1,
        Months        => 2,
        Weeks         => 4,
        Days          => 34,
        Hours         => 2,
        Minutes       => 5,
        Seconds       => 459,

        # Calculate "destination date" by adding given time values as
        # working time. Note that for adding working time,
        # only parameters Seconds, Minutes, Hours and Days are allowed.
        AsWorkingTime => 0, # set to 1 to add given values as working time

        # Calendar to use for working time calculations, optional
        Calendar => 9,
    );

Returns:

    $Success = 1;    # On success, or false otherwise.

=cut

sub Add {
    my ( $Self, %Param ) = @_;

    #
    # Check parameters
    #
    my @DateTimeParams = qw ( Years Months Weeks Days Hours Minutes Seconds );
    @DateTimeParams = qw( Days Hours Minutes Seconds ) if $Param{AsWorkingTime};

    # Check for needed parameters
    my $ParamsGiven = 0;
    my $ParamsValid = 1;
    DATETIMEPARAM:
    for my $DateTimeParam (@DateTimeParams) {
        next DATETIMEPARAM if !defined $Param{$DateTimeParam};

        if ( !looks_like_number( $Param{$DateTimeParam} ) ) {
            $ParamsValid = 0;
            last DATETIMEPARAM;
        }

        # negative values are not allowed when calculating working time
        if ( int $Param{$DateTimeParam} < 0 && $Param{AsWorkingTime} ) {
            $ParamsValid = 0;
            last DATETIMEPARAM;
        }

        $ParamsGiven = 1;
    }

    if ( !$ParamsGiven || !$ParamsValid ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            'Priority' => 'Error',
            'Message'  => 'Missing or invalid date/time parameter(s).',
        );
        return;
    }

    # Check for not allowed parameters
    my %AllowedParams = map { $_ => 1 } @DateTimeParams;
    $AllowedParams{AsWorkingTime} = 1;
    if ( $Param{AsWorkingTime} ) {
        $AllowedParams{Calendar} = 1;
    }

    for my $Param ( sort keys %Param ) {
        if ( !$AllowedParams{$Param} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                'Priority' => 'Error',
                'Message'  => "Parameter $Param is not allowed.",
            );
            return;
        }
    }

    # NOTE: For performance reasons, the following code for calculating date and time
    # works directly with the CPAN DateTime object instead of functions of Kernel::System::DateTime.

    #
    # Working time calculation
    #
    if ( $Param{AsWorkingTime} ) {

        # Combine time parameters to seconds
        my $RemainingSeconds = 0;
        if ( defined $Param{Seconds} ) {
            $RemainingSeconds += int $Param{Seconds};
        }
        if ( defined $Param{Minutes} ) {
            $RemainingSeconds += int $Param{Minutes} * 60;
        }
        if ( defined $Param{Hours} ) {
            $RemainingSeconds += int $Param{Hours} * 60 * 60;
        }
        if ( defined $Param{Days} ) {
            $RemainingSeconds += int $Param{Days} * 60 * 60 * 24;
        }

        return if !$RemainingSeconds;

        # Backup current date/time to be able to revert to it in case of failure
        my $OriginalDateTimeObject = $Self->{CPANDateTimeObject}->clone();

        my $TimeZone = $OriginalDateTimeObject->time_zone();

        # Get working and vacation times, use calendar if given
        my $ConfigObject            = $Kernel::OM->Get('Kernel::Config');
        my $TimeWorkingHours        = $ConfigObject->Get('TimeWorkingHours');
        my $TimeVacationDays        = $ConfigObject->Get('TimeVacationDays');
        my $TimeVacationDaysOneTime = $ConfigObject->Get('TimeVacationDaysOneTime');
        if (
            $Param{Calendar}
            && $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} . "Name" )
            )
        {
            $TimeWorkingHours        = $ConfigObject->Get( "TimeWorkingHours::Calendar" . $Param{Calendar} );
            $TimeVacationDays        = $ConfigObject->Get( "TimeVacationDays::Calendar" . $Param{Calendar} );
            $TimeVacationDaysOneTime = $ConfigObject->Get(
                "TimeVacationDaysOneTime::Calendar" . $Param{Calendar}
            );

            # Switch to time zone of calendar
            $TimeZone = $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} )
                || $Self->OTRSTimeZoneGet();

            # Use Kernel::System::DateTime's ToTimeZone() here because of error handling
            # and because performance is irrelevant at this point.
            if ( !$Self->ToTimeZone( TimeZone => $TimeZone ) ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => "Error setting time zone $TimeZone.",
                );

                return;
            }
        }

        # If there are for some reason no working hours configured, stop here
        # to prevent failing via loop protection below.
        my $WorkingHoursConfigured;
        WORKINGHOURCONFIGDAY:
        for my $WorkingHourConfigDay ( sort keys %{$TimeWorkingHours} ) {
            if ( IsArrayRefWithData( $TimeWorkingHours->{$WorkingHourConfigDay} ) ) {
                $WorkingHoursConfigured = 1;
                last WORKINGHOURCONFIGDAY;
            }
        }
        return 1 if !$WorkingHoursConfigured;

        # Convert $TimeWorkingHours into Hash
        my %TimeWorkingHours;
        for my $DayName ( sort keys %{$TimeWorkingHours} ) {
            $TimeWorkingHours{$DayName} = { map { $_ => 1 } @{ $TimeWorkingHours->{$DayName} } };
        }

        # Protection for endless loop
        my $LoopStartTime = time();
        SECOND:
        while ( $RemainingSeconds > 0 ) {

            # Fail if this loop takes longer than 5 seconds
            if ( time() - $LoopStartTime > 5 ) {

                # Reset this object to original date/time.
                $Self->{CPANDateTimeObject} = $OriginalDateTimeObject->clone();

                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => 'Adding working time took too long, aborting.',
                );

                return;
            }

            my $Year    = $Self->{CPANDateTimeObject}->year();
            my $Month   = $Self->{CPANDateTimeObject}->month();
            my $Day     = $Self->{CPANDateTimeObject}->day();
            my $DayName = $Self->{CPANDateTimeObject}->day_abbr();
            my $Hour    = $Self->{CPANDateTimeObject}->hour();
            my $Minute  = $Self->{CPANDateTimeObject}->minute();
            my $Second  = $Self->{CPANDateTimeObject}->second();

            # Check working times and vacation days
            my $IsWorkingDay = !$TimeVacationDays->{$Month}->{$Day}
                && !$TimeVacationDaysOneTime->{$Year}->{$Month}->{$Day}
                && exists $TimeWorkingHours->{$DayName}
                && keys %{ $TimeWorkingHours{$DayName} };

            # On start of day check if whole day can be processed in one chunk
            # instead of hour by hour (performance reasons).
            if ( !$Hour && !$Minute && !$Second ) {

                # The following code is slightly faster than using CPAN DateTime's add(),
                # presumably because add() always creates a DateTime::Duration object.
                my $Epoch = $Self->{CPANDateTimeObject}->epoch();
                $Epoch += 60 * 60 * 24;

                my $NextDayDateTimeObject = DateTime->from_epoch(
                    epoch     => $Epoch,
                    time_zone => $TimeZone,
                    locale    => $Self->{Locale},
                );

                # Only handle days with exactly 24 hours here
                if (
                    !$NextDayDateTimeObject->hour()
                    && !$NextDayDateTimeObject->minute()
                    && !$NextDayDateTimeObject->second()
                    && $NextDayDateTimeObject->day() != $Day
                    )
                {
                    my $FullDayProcessed = 1;

                    if ($IsWorkingDay) {
                        my $WorkingHours   = keys %{ $TimeWorkingHours{$DayName} };
                        my $WorkingSeconds = $WorkingHours * 60 * 60;

                        if ( $RemainingSeconds > $WorkingSeconds ) {
                            $RemainingSeconds -= $WorkingSeconds;
                        }
                        else {
                            $FullDayProcessed = 0;
                        }
                    }

                    # Move forward 24 hours if full day has been processed
                    if ($FullDayProcessed) {

                        # Time implicitly set to 0
                        $Self->{CPANDateTimeObject}->set(
                            year  => $NextDayDateTimeObject->year(),
                            month => $NextDayDateTimeObject->month(),
                            day   => $NextDayDateTimeObject->day(),
                        );

                        next SECOND;
                    }
                }
            }

            # Calculate remaining seconds of the current hour
            my $SecondsOfCurrentHour = ( $Minute * 60 ) + $Second;
            my $SecondsToAdd         = ( 60 * 60 ) - $SecondsOfCurrentHour;

            if ( $IsWorkingDay && $TimeWorkingHours{$DayName}->{$Hour} ) {
                $SecondsToAdd = $RemainingSeconds if $SecondsToAdd > $RemainingSeconds;
                $RemainingSeconds -= $SecondsToAdd;
            }

            # The following code is slightly faster than using CPAN DateTime's add(),
            # presumably because add() always creates a DateTime::Duration object.
            my $Epoch = $Self->{CPANDateTimeObject}->epoch();
            $Epoch += $SecondsToAdd;

            $Self->{CPANDateTimeObject} = DateTime->from_epoch(
                epoch     => $Epoch,
                time_zone => $TimeZone,
                locale    => $Self->{Locale},
            );
        }

        # Return to original time zone, might have been changed by calendar
        $Self->{CPANDateTimeObject}->set_time_zone( $OriginalDateTimeObject->time_zone() );

        return 1;
    }

    #
    # "Normal" date/time calculation
    #

    # Calculations are only made in UTC/floating time zone to prevent errors with times that
    # would not exist in the given time zone (e. g. on/around daylight saving time switch).
    # CPAN DateTime fails if adding days, months or years which would result in a non-existing
    # time in the given time zone. Converting it to UTC and back has the desired effect.
    #
    # Also see http://stackoverflow.com/questions/18489927/a-day-without-midnight
    my $TimeZone = $Self->{CPANDateTimeObject}->time_zone();
    $Self->{CPANDateTimeObject}->set_time_zone('UTC');

    # Convert to floating time zone to get rid of leap seconds which can lead to times like 23:59:61
    $Self->{CPANDateTimeObject}->set_time_zone('floating');

    # Add duration
    my $DurationParameters = $Self->_ToCPANDateTimeParamNames(%Param);
    eval {
        $Self->{CPANDateTimeObject}->add( %{$DurationParameters} );
    };

    # Store possible error before it might get lost by call to ToTimeZone
    my $Error = $@;

    # First convert floating time zone back to UTC and from there to the original time zone
    $Self->{CPANDateTimeObject}->set_time_zone('UTC');
    $Self->{CPANDateTimeObject}->set_time_zone($TimeZone);

    return if $Error;

    return 1;
}

=head2 Subtract()

Subtracts duration from date and time of this object. You have to give at least one of the valid parameters. On
validation error, the current date and time of this object won't be changed.

    my $Success = $DateTimeObject->Subtract(
        Years     => 1,
        Months    => 2,
        Weeks     => 4,
        Days      => 34,
        Hours     => 2,
        Minutes   => 5,
        Seconds   => 459,
    );

Returns:

    $Success =  1;  # On success, or false otherwise.

=cut

sub Subtract {
    my ( $Self, %Param ) = @_;

    my @DateTimeParams = qw ( Years Months Weeks Days Hours Minutes Seconds );

    # Check for needed parameters
    my $ParamsGiven = 0;
    my $ParamsValid = 1;
    DATETIMEPARAM:
    for my $DateTimeParam (@DateTimeParams) {
        next DATETIMEPARAM if !defined $Param{$DateTimeParam};

        if ( !looks_like_number( $Param{$DateTimeParam} ) ) {
            $ParamsValid = 0;
            last DATETIMEPARAM;
        }

        # negative values are not allowed when calculating working time
        if ( int $Param{$DateTimeParam} < 0 && $Param{AsWorkingTime} ) {
            $ParamsValid = 0;
            last DATETIMEPARAM;
        }

        $ParamsGiven = 1;
    }

    if ( !$ParamsGiven || !$ParamsValid ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            'Priority' => 'Error',
            'Message'  => 'Missing or invalid date/time parameter(s).',
        );
        return;
    }

    # Check for not allowed parameters
    my %AllowedParams = map { $_ => 1 } @DateTimeParams;
    $AllowedParams{AsWorkingTime} = 1;
    if ( $Param{AsWorkingTime} ) {
        $AllowedParams{Calendar} = 1;
    }

    for my $Param ( sort keys %Param ) {
        if ( !$AllowedParams{$Param} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                'Priority' => 'Error',
                'Message'  => "Parameter $Param is not allowed.",
            );
            return;
        }
    }

    # Calculations are only made in UTC/floating time zone to prevent errors with times that
    # would not exist in the given time zone (e. g. on/around daylight saving time switch).
    my $DateTimeValues = $Self->Get();
    $Self->ToTimeZone( TimeZone => 'UTC' );

    # Convert to floating time zone to get rid of leap seconds which can lead to times like 23:59:61
    $Self->{CPANDateTimeObject}->set_time_zone('floating');

    # Subtract duration
    my $DurationParameters = $Self->_ToCPANDateTimeParamNames(%Param);
    eval {
        $Self->{CPANDateTimeObject}->subtract( %{$DurationParameters} );
    };

    # Store possible error before it might get lost by call to ToTimeZone
    my $Error = $@;

    # First convert floating time zone back to UTC and from there to the original time zone
    $Self->{CPANDateTimeObject}->set_time_zone('UTC');
    $Self->ToTimeZone( TimeZone => $DateTimeValues->{TimeZone} );

    return if $@;

    return 1;
}

=head2 Delta()

Calculates delta between this and another DateTime object. Optionally calculates the working time between the two.

    my $Delta = $DateTimeObject->Delta( DateTimeObject => $AnotherDateTimeObject );

Note that the returned values are always positive. Use the comparison functions to see if a date is newer/older/equal.

    # Calculate "working time"
    ForWorkingTime => 0, # set to 1 to calculate working time between the two DateTime objects

    # Calendar to use for working time calculations, optional
    Calendar => 9,

Returns:

    my $Delta = {
        Years           => 1,           # Set to 0 if working time was calculated
        Months          => 2,           # Set to 0 if working time was calculated
        Weeks           => 4,           # Set to 0 if working time was calculated
        Days            => 34,          # Set to 0 if working time was calculated
        Hours           => 2,
        Minutes         => 5,
        Seconds         => 459,
        AbsoluteSeconds => 42084759,    # complete delta in seconds
    };

=cut

sub Delta {
    my ( $Self, %Param ) = @_;

    if (
        !defined $Param{DateTimeObject}
        || ref $Param{DateTimeObject} ne ref $Self
        )
    {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            'Priority' => 'Error',
            'Message'  => "Missing or invalid parameter DateTimeObject.",
        );
        return;
    }

    my $Delta = {
        Years           => 0,
        Months          => 0,
        Weeks           => 0,
        Days            => 0,
        Hours           => 0,
        Minutes         => 0,
        Seconds         => 0,
        AbsoluteSeconds => 0,
    };

    #
    # Calculate delta for working time
    #
    if ( $Param{ForWorkingTime} ) {

        # NOTE: For performance reasons, the following code for calculating the working time
        # works directly with the CPAN DateTime object instead of Kernel::System::DateTime.

        # Clone StartDateTime object because it will be changed while calculating
        # but the original object must not be changed.
        my $StartDateTimeObject = $Self->{CPANDateTimeObject}->clone();
        my $TimeZone            = $StartDateTimeObject->time_zone();

        # Get working and vacation times, use calendar if given
        my $ConfigObject            = $Kernel::OM->Get('Kernel::Config');
        my $TimeWorkingHours        = $ConfigObject->Get('TimeWorkingHours');
        my $TimeVacationDays        = $ConfigObject->Get('TimeVacationDays');
        my $TimeVacationDaysOneTime = $ConfigObject->Get('TimeVacationDaysOneTime');
        if (
            $Param{Calendar}
            && $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} . "Name" )
            )
        {
            $TimeWorkingHours        = $ConfigObject->Get( "TimeWorkingHours::Calendar" . $Param{Calendar} );
            $TimeVacationDays        = $ConfigObject->Get( "TimeVacationDays::Calendar" . $Param{Calendar} );
            $TimeVacationDaysOneTime = $ConfigObject->Get(
                "TimeVacationDaysOneTime::Calendar" . $Param{Calendar}
            );

            # switch to time zone of calendar
            $TimeZone = $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} )
                || $Self->OTRSTimeZoneGet();

            eval {
                $StartDateTimeObject->set_time_zone($TimeZone);
            };

            if ($@) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => "Error setting time zone $TimeZone for start DateTime object.",
                );

                return;
            }
        }

        # If there are for some reason no working hours configured, stop here
        # to prevent failing via loop protection below.
        my $WorkingHoursConfigured;
        WORKINGHOURCONFIGDAY:
        for my $WorkingHourConfigDay ( sort keys %{$TimeWorkingHours} ) {
            if ( IsArrayRefWithData( $TimeWorkingHours->{$WorkingHourConfigDay} ) ) {
                $WorkingHoursConfigured = 1;
                last WORKINGHOURCONFIGDAY;
            }
        }
        return $Delta if !$WorkingHoursConfigured;

        # Convert $TimeWorkingHours into Hash
        my %TimeWorkingHours;
        for my $DayName ( sort keys %{$TimeWorkingHours} ) {
            $TimeWorkingHours{$DayName} = { map { $_ => 1 } @{ $TimeWorkingHours->{$DayName} } };
        }

        my $StartTime   = $StartDateTimeObject->epoch();
        my $StopTime    = $Param{DateTimeObject}->{CPANDateTimeObject}->epoch();
        my $WorkingTime = 0;

        # Protection for endless loop
        my $LoopStartTime = time();
        EPOCH:
        while ( $StartTime < $StopTime ) {

            # Fail if this loop takes longer than 5 seconds
            if ( time() - $LoopStartTime > 5 ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => 'Delta calculation of working time took too long, aborting.',
                );

                return;
            }

            my $RemainingSeconds = $StopTime - $StartTime;

            my $Year    = $StartDateTimeObject->year();
            my $Month   = $StartDateTimeObject->month();
            my $Day     = $StartDateTimeObject->day();
            my $DayName = $StartDateTimeObject->day_abbr();
            my $Hour    = $StartDateTimeObject->hour();
            my $Minute  = $StartDateTimeObject->minute();
            my $Second  = $StartDateTimeObject->second();

            # Check working times and vacation days
            my $IsWorkingDay = !$TimeVacationDays->{$Month}->{$Day}
                && !$TimeVacationDaysOneTime->{$Year}->{$Month}->{$Day}
                && exists $TimeWorkingHours->{$DayName}
                && keys %{ $TimeWorkingHours{$DayName} };

            # On start of day check if whole day can be processed in one chunk
            # instead of hour by hour (performance reasons).
            if ( !$Hour && !$Minute && !$Second ) {

                # The following code is slightly faster than using CPAN DateTime's add(),
                # presumably because add() always creates a DateTime::Duration object.
                my $Epoch = $StartDateTimeObject->epoch();
                $Epoch += 60 * 60 * 24;

                my $NextDayDateTimeObject = DateTime->from_epoch(
                    epoch     => $Epoch,
                    time_zone => $TimeZone,
                    locale    => $Self->{Locale},
                );

                # Only handle days with exactly 24 hours here
                if (
                    !$NextDayDateTimeObject->hour()
                    && !$NextDayDateTimeObject->minute()
                    && !$NextDayDateTimeObject->second()
                    && $NextDayDateTimeObject->day() != $Day
                    && $RemainingSeconds > 60 * 60 * 24
                    )
                {
                    my $FullDayProcessed = 1;

                    if ($IsWorkingDay) {
                        my $WorkingHours   = keys %{ $TimeWorkingHours{$DayName} };
                        my $WorkingSeconds = $WorkingHours * 60 * 60;

                        if ( $RemainingSeconds > $WorkingSeconds ) {
                            $WorkingTime += $WorkingSeconds;
                        }
                        else {
                            $FullDayProcessed = 0;
                        }
                    }

                    # Move forward 24 hours if full day has been processed
                    if ($FullDayProcessed) {

                        # Time implicitly set to 0
                        $StartDateTimeObject->set(
                            year  => $NextDayDateTimeObject->year(),
                            month => $NextDayDateTimeObject->month(),
                            day   => $NextDayDateTimeObject->day(),
                        );

                        $StartTime = $Epoch;

                        next EPOCH;
                    }
                }
            }

            # Calculate remaining seconds of the current hour
            my $SecondsOfCurrentHour = ( $Minute * 60 ) + $Second;
            my $SecondsToAdd         = ( 60 * 60 ) - $SecondsOfCurrentHour;

            if ( $IsWorkingDay && $TimeWorkingHours{$DayName}->{$Hour} ) {
                $SecondsToAdd = $RemainingSeconds if $SecondsToAdd > $RemainingSeconds;
                $WorkingTime += $SecondsToAdd;
            }

            # The following code is slightly faster than using CPAN DateTime's add(),
            # presumably because add() always creates a DateTime::Duration object.
            my $Epoch = $StartDateTimeObject->epoch();
            $Epoch += $SecondsToAdd;

            $StartDateTimeObject = DateTime->from_epoch(
                epoch     => $Epoch,
                time_zone => $TimeZone,
                locale    => $Self->{Locale},
            );

            $StartTime = $Epoch;
        }

        # Set values for delta
        my $RemainingWorkingTime = $WorkingTime;

        $Delta->{Hours} = int $RemainingWorkingTime / ( 60 * 60 );
        $RemainingWorkingTime -= $Delta->{Hours} * 60 * 60;

        $Delta->{Minutes} = int $RemainingWorkingTime / 60;
        $RemainingWorkingTime -= $Delta->{Minutes} * 60;

        $Delta->{Seconds} = $RemainingWorkingTime;
        $RemainingWorkingTime = 0;

        $Delta->{AbsoluteSeconds} = $WorkingTime;

        return $Delta;
    }

    #
    # Calculate delta for "normal" date/time
    #
    my $DeltaDuration = $Self->{CPANDateTimeObject}->subtract_datetime(
        $Param{DateTimeObject}->{CPANDateTimeObject}
    );

    $Delta->{Years}   = $DeltaDuration->years();
    $Delta->{Months}  = $DeltaDuration->months();
    $Delta->{Weeks}   = $DeltaDuration->weeks();
    $Delta->{Days}    = $DeltaDuration->days();
    $Delta->{Hours}   = $DeltaDuration->hours();
    $Delta->{Minutes} = $DeltaDuration->minutes();
    $Delta->{Seconds} = $DeltaDuration->seconds();

    # Absolute seconds
    $DeltaDuration = $Self->{CPANDateTimeObject}->subtract_datetime_absolute(
        $Param{DateTimeObject}->{CPANDateTimeObject}
    );

    $Delta->{AbsoluteSeconds} = $DeltaDuration->seconds();

    return $Delta;
}

=head2 Compare()

Compares dates and returns a value suitable for using Perl's sort function (-1, 0, 1).

    my $Result = $DateTimeObject->Compare( DateTimeObject => $AnotherDateTimeObject );

You can also use this as a function for Perl's sort:
    my @SortedDateTimeObjects = sort { $a->Compare( DateTimeObject => $b ) } @UnsortedDateTimeObjects;

Returns:

    my $Result = -1;       # if date/time of $DateTimeObject < date/time of $AnotherDateTimeObject
    my $Result = 0;        # if date/time are equal
    my $Result = 1;        # if date/time of $DateTimeObject > date/time of $AnotherDateTimeObject

=cut

sub Compare {
    my ( $Self, %Param ) = @_;

    if (
        !defined $Param{DateTimeObject}
        || ref $Param{DateTimeObject} ne ref $Self
        )
    {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            'Priority' => 'Error',
            'Message'  => "Missing or invalid parameter DateTimeObject.",
        );
        return;
    }

    my $Result;
    eval {
        $Result = DateTime->compare(
            $Self->{CPANDateTimeObject},
            $Param{DateTimeObject}->{CPANDateTimeObject}
        );
    };

    return $Result;
}

=head2 ToTimeZone()

Converts the date and time of this object to the given time zone.

    my $Success = $DateTimeObject->ToTimeZone(
        TimeZone => 'Europe/Berlin',
    );

Returns:

    $Success = 1;   # success, or false otherwise.

=cut

sub ToTimeZone {
    my ( $Self, %Param ) = @_;

    for my $RequiredParam (qw( TimeZone )) {
        if ( !defined $Param{$RequiredParam} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                'Priority' => 'Error',
                'Message'  => "Missing parameter $RequiredParam.",
            );
            return;
        }
    }

    eval {
        $Self->{CPANDateTimeObject}->set_time_zone( $Param{TimeZone} );
    };

    return 1 if !$@;

    # Certain times do not exist at certain dates in certain time zones, e.g.
    # 2:00 AM on the day of the DST switch from winter to summer time in time zone
    # Europe/Berlin (1:59 AM switches to 3:00 AM).
    #
    # In these cases, try to add an hour or a few to get a valid date/time.
    #
    # The autocorrection is only relevant for DateTime objects in time zone 'floating'
    # because setting the time zone above always works for other time zones.
    my $AutocorrectedCPANDateTimeObject = $Self->_AutocorrectNonExistingDateTimeForTimeZone(
        CPANDateTimeObject  => $Self->{CPANDateTimeObject},
        DestinationTimeZone => $Param{TimeZone},
    );
    return if !$AutocorrectedCPANDateTimeObject;

    $Self->{CPANDateTimeObject} = $AutocorrectedCPANDateTimeObject;

    return 1;
}

=head2 ToOTRSTimeZone()

Converts the date and time of this object to the data storage time zone.

    my $Success = $DateTimeObject->ToOTRSTimeZone();

Returns:

    $Success = 1;   # success, or false otherwise.

=cut

sub ToOTRSTimeZone {
    my ( $Self, %Param ) = @_;

    return $Self->ToTimeZone( TimeZone => $Self->OTRSTimeZoneGet() );
}

=head2 Validate()

Checks if given date, time and time zone would result in a valid date.

    my $IsValid = $DateTimeObject->Validate(
        Year     => 2016,
        Month    => 1,
        Day      => 22,
        Hour     => 16,
        Minute   => 35,
        Second   => 59,
        TimeZone => 'Europe/Berlin',
    );

Returns:

    $IsValid = 1;   # if date/time is valid, or false otherwise.

=cut

sub Validate {
    my ( $Self, %Param ) = @_;

    my @DateTimeParams = qw ( Year Month Day Hour Minute Second TimeZone );
    for my $RequiredDateTimeParam (@DateTimeParams) {
        if ( !defined $Param{$RequiredDateTimeParam} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                'Priority' => 'Error',
                'Message'  => "Missing parameter $RequiredDateTimeParam.",
            );
            return;
        }
    }

    my $DateTimeObject = $Self->_CPANDateTimeObjectCreate(%Param);
    return if !$DateTimeObject;

    return 1;
}

=head2 Format()

Returns the date/time as string formatted according to format given.

See L<http://search.cpan.org/~drolsky/DateTime-1.21/lib/DateTime.pm#strftime_Patterns> for supported formats.

Short overview of essential formatting options:

    %Y or %{year}: four digit year

    %m: month with leading zero
    %{month}: month without leading zero

    %d: day of month with leading zero
    %{day}: day of month without leading zero

    %H: 24 hour with leading zero
    %{hour}: 24 hour without leading zero

    %l: 12 hour with leading zero
    %{hour_12}: 12 hour without leading zero

    %M: minute with leading zero
    %{minute}: minute without leading zero

    %S: second with leading zero
    %{second}: second without leading zero

    %{time_zone_long_name}: Time zone, e. g. 'Europe/Berlin'

    %{epoch}: Seconds since the epoch (OS specific)
    %{offset}: Offset in seconds to GMT/UTC

    my $DateTimeString = $DateTimeObject->Format( Format => '%Y-%m-%d %H:%M:%S' );

Returns:

    my $String = '2016-01-22 18:07:23';

=cut

sub Format {
    my ( $Self, %Param ) = @_;

    for my $RequiredParam (qw( Format )) {
        if ( !defined $Param{$RequiredParam} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                'Priority' => 'Error',
                'Message'  => "Missing parameter $RequiredParam.",
            );

            return;
        }
    }

    return $Self->{CPANDateTimeObject}->strftime( $Param{Format} );
}

=head2 ToEpoch()

Returns date/time as seconds since the epoch.

    my $Epoch = $DateTimeObject->ToEpoch();

Returns e. g.:

    my $Epoch = 1454420017;

=cut

sub ToEpoch {
    my ( $Self, %Param ) = @_;

    return $Self->{CPANDateTimeObject}->epoch();
}

=head2 ToString()

Returns date/time as string.

    my $DateTimeString = $DateTimeObject->ToString();

Returns e. g.:

    $DateTimeString = '2016-01-31 14:05:45'

=cut

sub ToString {
    my ( $Self, %Param ) = @_;

    return $Self->Format( Format => '%Y-%m-%d %H:%M:%S' );
}

=head2 ToEmailTimeStamp()

Returns the date/time of this object as time stamp in RFC 2822 format to be used in email headers.

    my $MailTimeStamp = $DateTimeObject->ToEmailTimeStamp();

    # Typical usage:
    # You want to have the date/time of OTRS + its UTC offset, so:
    my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime');
    my $MailTimeStamp = $DateTimeObject->ToEmailTimeStamp();

    # If you already have a DateTime object, possibly in another time zone:
    $DateTimeObject->ToOTRSTimeZone();
    my $MailTimeStamp = $DateTimeObject->ToEmailTimeStamp();

Returns:

    my $String = 'Wed, 2 Sep 2014 16:30:57 +0200';

=cut

sub ToEmailTimeStamp {
    my ( $Self, %Param ) = @_;

    # According to RFC 2822, section 3.3

    # The date and time-of-day SHOULD express local time.
    #
    # The zone specifies the offset from Coordinated Universal Time (UTC,
    # formerly referred to as "Greenwich Mean Time") that the date and
    # time-of-day represent.  The "+" or "-" indicates whether the
    # time-of-day is ahead of (i.e., east of) or behind (i.e., west of)
    # Universal Time.  The first two digits indicate the number of hours
    # difference from Universal Time, and the last two digits indicate the
    # number of minutes difference from Universal Time.  (Hence, +hhmm
    # means +(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm)
    # minutes).  The form "+0000" SHOULD be used to indicate a time zone at
    # Universal Time.  Though "-0000" also indicates Universal Time, it is
    # used to indicate that the time was generated on a system that may be
    # in a local time zone other than Universal Time and therefore
    # indicates that the date-time contains no information about the local
    # time zone.

    my $EmailTimeStamp = $Self->Format(
        Format => '%a, %{day} %b %Y %H:%M:%S %z',
    );

    return $EmailTimeStamp;
}

=head2 ToCTimeString()

Returns date and time as ctime string, as for example returned by Perl's C<localtime> and C<gmtime> in scalar context.

    my $CTimeString = $DateTimeObject->ToCTimeString();

Returns:

    my $String = 'Fri Feb 19 16:07:31 2016';

=cut

sub ToCTimeString {
    my ( $Self, %Param ) = @_;

    my $LocalTimeString = $Self->Format(
        Format => '%a %b %{day} %H:%M:%S %Y',
    );

    return $LocalTimeString;
}

=head2 IsVacationDay()

Checks if date/time of this object is a vacation day.

    my $IsVacationDay = $DateTimeObject->IsVacationDay(
        Calendar => 9, # optional, OTRS vacation days otherwise
    );

Returns:

    my $IsVacationDay = 'some vacation day',    # description of vacation day or 0 if no vacation day.

=cut

sub IsVacationDay {
    my ( $Self, %Param ) = @_;

    my $OriginalDateTimeValues = $Self->Get();

    # Get configured vacation days
    my $ConfigObject            = $Kernel::OM->Get('Kernel::Config');
    my $TimeVacationDays        = $ConfigObject->Get('TimeVacationDays');
    my $TimeVacationDaysOneTime = $ConfigObject->Get('TimeVacationDaysOneTime');
    if ( $Param{Calendar} ) {
        if ( $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} . "Name" ) ) {
            $TimeVacationDays        = $ConfigObject->Get( "TimeVacationDays::Calendar" . $Param{Calendar} );
            $TimeVacationDaysOneTime = $ConfigObject->Get(
                "TimeVacationDaysOneTime::Calendar" . $Param{Calendar}
            );

            # Switch to time zone of calendar
            my $TimeZone = $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} )
                || $Self->OTRSTimeZoneGet();

            if ( defined $TimeZone ) {
                $Self->ToTimeZone( TimeZone => $TimeZone );
            }
        }
    }

    my $DateTimeValues = $Self->Get();

    my $VacationDay        = $TimeVacationDays->{ $DateTimeValues->{Month} }->{ $DateTimeValues->{Day} };
    my $VacationDayOneTime = $TimeVacationDaysOneTime->{ $DateTimeValues->{Year} }->{ $DateTimeValues->{Month} }
        ->{ $DateTimeValues->{Day} };

    # Switch back to original time zone
    $Self->ToTimeZone( TimeZone => $OriginalDateTimeValues->{TimeZone} );

    return $VacationDay        if defined $VacationDay;
    return $VacationDayOneTime if defined $VacationDayOneTime;

    return 0;
}

=head2 LastDayOfMonthGet()

Returns the last day of the month.

    $LastDayOfMonth = $DateTimeObject->LastDayOfMonthGet();

Returns:

    my $LastDayOfMonth = {
        Day       => 31,
        DayOfWeek => 5,
        DayAbbr   => 'Fri',
    };

=cut

sub LastDayOfMonthGet {
    my ( $Self, %Param ) = @_;

    my $DateTimeValues = $Self->Get();

    my $TempCPANDateTimeObject;
    eval {
        $TempCPANDateTimeObject = DateTime->last_day_of_month(
            year  => $DateTimeValues->{Year},
            month => $DateTimeValues->{Month},
        );
    };

    return if !$TempCPANDateTimeObject;

    my $Result = {
        Day       => $TempCPANDateTimeObject->day(),
        DayOfWeek => $TempCPANDateTimeObject->day_of_week(),
        DayAbbr   => $TempCPANDateTimeObject->day_abbr(),
    };

    return $Result;
}

=head2 Clone()

Clones the DateTime object.

    my $ClonedDateTimeObject = $DateTimeObject->Clone();

=cut

sub Clone {
    my ( $Self, %Param ) = @_;

    return __PACKAGE__->new(
        _CPANDateTimeObject => $Self->{CPANDateTimeObject}->clone()
    );
}

=head2 TimeZoneList()

Returns an array ref of available time zones.

    my $TimeZones = $DateTimeObject->TimeZoneList();

    # You can add an obsolete time zone to the list if the time zone is valid.
    # This is useful to keep the obsolete time zone from a stored setting
    # so that it will e.g. be shown as selected when showing a selection list.
    # Otherwise the user would have to select a new time zone.

    my $TimeZones = $DateTimeObject->TimeZoneList(
        # Africa/Kinshasa has become obsolete and has been replaced by Africa/Lagos.
        # This option will add Africa/Kinshasa to the list of time zones nonetheless.
        # The given time zone must be valid, so 'Some/InvalidTimeZone' will not be added.
        IncludeTimeZone => 'Africa/Kinshasa',
    );

You can also call this function without an object:

    my $TimeZones = Kernel::System::DateTime->TimeZoneList();

Returns:

    my $TimeZoneList = [
        # ...
        'Europe/Amsterdam',
        'Europe/Andorra',
        'Europe/Athens',
        # ...
    ];

=cut

sub TimeZoneList {
    my ( $Self, %Param ) = @_;

    my @TimeZones = @{ DateTime::TimeZone->all_names() };
    my %TimeZones = map { $_ => 1 } @TimeZones;

    # add missing UTC time zone for certain DateTime versions
    if ( !exists $TimeZones{UTC} ) {
        push @TimeZones, 'UTC';
    }

    # Add missing obsolete (but valid) time zone if given.
    # This ensures that selected time zones in drop-downs will always
    # also show an obsolete time zone that was previously selected.
    if (
        $Param{IncludeTimeZone}
        && !exists $TimeZones{ $Param{IncludeTimeZone} }
        && $Self->IsTimeZoneValid( TimeZone => $Param{IncludeTimeZone} )
        )
    {
        push @TimeZones, $Param{IncludeTimeZone};
    }

    @TimeZones = sort @TimeZones;

    return \@TimeZones;
}

=head2 TimeZoneByOffsetList()

Returns a list of time zones by offset in hours. Of course, the resulting list depends on the date/time set within this
DateTime object.

    my %TimeZoneByOffset = $DateTimeObject->TimeZoneByOffsetList();

Returns:

    my $TimeZoneByOffsetList = {
        # ...
        -9 => [ 'America/Adak', 'Pacific/Gambier', ],
        # ...
        2  => [
            # ...
            'Europe/Berlin',
            # ...
        ],
        # ...
        8.75 => [ 'Australia/Eucla', ],
        # ...
    };

=cut

sub TimeZoneByOffsetList {
    my ( $Self, %Param ) = @_;

    my $DateTimeObject = $Self->Clone();

    my $TimeZones = $Self->TimeZoneList();

    my %TimeZoneByOffset;
    for my $TimeZone ( sort @{$TimeZones} ) {
        $DateTimeObject->ToTimeZone( TimeZone => $TimeZone );
        my $TimeZoneOffset = $DateTimeObject->Format( Format => '%{offset}' ) / 60 / 60;

        if ( exists $TimeZoneByOffset{$TimeZoneOffset} ) {
            push @{ $TimeZoneByOffset{$TimeZoneOffset} }, $TimeZone;
        }
        else {
            $TimeZoneByOffset{$TimeZoneOffset} = [ $TimeZone, ];
        }
    }

    return \%TimeZoneByOffset;
}

=head2 IsTimeZoneValid()

Checks if the given time zone is valid.

    my $Valid = $DateTimeObject->IsTimeZoneValid( TimeZone => 'Europe/Berlin' );

    # You can also call this function without an object:
    my $Valid = Kernel::System::DateTime->IsTimeZoneValid( TimeZone => 'Europe/Berlin' );

Returns:

    $Valid = 1;    # if given time zone is valid, 0 otherwise.

=cut

sub IsTimeZoneValid {
    my ( $Self, %Param ) = @_;

    for my $RequiredParam (qw( TimeZone )) {
        if ( !defined $Param{$RequiredParam} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                'Priority' => 'Error',
                'Message'  => "Missing parameter $RequiredParam.",
            );
            return;
        }
    }

    # Special case: Somehow is_valid_name below returns true value for time zone '0'.
    return 0 if $Param{TimeZone} eq '0';

    # allow DateTime internal time zone in 'floating'
    return 1 if $Param{TimeZone} eq 'floating';

    return 1 if DateTime::TimeZone->is_valid_name( $Param{TimeZone} );

    return 0;
}

=head2 GetRealTimeZone()

Returns the real time zone for the given one.
Some time zones become obsolete/invalid over time but must still be mapped to the current/valid ones.
DateTime::TimeZone calls this "linked" time zones. For current/valid time zones this function just
returns the given time zone.

    my $TimeZone = $DateTimeObject->GetRealTimeZone( TimeZone => 'Africa/Addis_Ababa' );

    # You can also call this function without an object:
    my $TimeZone = Kernel::System::DateTime->GetRealTimeZone( TimeZone => 'Africa/Addis_Ababa' );

Returns:
    my $TimeZone = 'Africa/Nairobi';

=cut

sub GetRealTimeZone {
    my ( $Self, %Param ) = @_;

    my $LogObject = $Kernel::OM->Get('Kernel::System::Log');

    NEEDED:
    for my $Needed (qw(TimeZone)) {
        next NEEDED if defined $Param{$Needed};

        $LogObject->Log(
            Priority => 'error',
            Message  => "Parameter '$Needed' is needed!",
        );
        return;
    }

    my %RealByLinkedTimeZones = DateTime::TimeZone->links();
    my $RealTimeZone          = $RealByLinkedTimeZones{ $Param{TimeZone} };
    return $RealTimeZone if $RealTimeZone;

    my $TimeZoneIsValid = $Self->IsTimeZoneValid( TimeZone => $Param{TimeZone} );
    return $Param{TimeZone} if $TimeZoneIsValid;

    return;
}

=head2 OTRSTimeZoneGet()

Returns the time zone set for OTRS.

    my $OTRSTimeZone = $DateTimeObject->OTRSTimeZoneGet();

    # You can also call this function without an object:
    my $OTRSTimeZone = Kernel::System::DateTime->OTRSTimeZoneGet();

Returns:

    my $OTRSTimeZone = 'Europe/Berlin';

=cut

sub OTRSTimeZoneGet {
    return $Kernel::OM->Get('Kernel::Config')->Get('OTRSTimeZone') || 'UTC';
}

=head2 UserDefaultTimeZoneGet()

Returns the time zone set as default in SysConfig UserDefaultTimeZone for newly created users or existing users without
time zone setting.

    my $UserDefaultTimeZoneGet = $DateTimeObject->UserDefaultTimeZoneGet();

You can also call this function without an object:

    my $UserDefaultTimeZoneGet = Kernel::System::DateTime->UserDefaultTimeZoneGet();

Returns:

    my $UserDefaultTimeZone = 'Europe/Berlin';

=cut

sub UserDefaultTimeZoneGet {
    return $Kernel::OM->Get('Kernel::Config')->Get('UserDefaultTimeZone') || 'UTC';
}

=head2 SystemTimeZoneGet()

Returns the time zone of the system.

    my $SystemTimeZone = $DateTimeObject->SystemTimeZoneGet();

You can also call this function without an object:

    my $SystemTimeZone = Kernel::System::DateTime->SystemTimeZoneGet();

Returns:

    my $SystemTimeZone = 'Europe/Berlin';

=cut

sub SystemTimeZoneGet {
    return DateTime::TimeZone->new( name => 'local' )->name();
}

=begin Internal:

Private functions used by this package (not part of the documented public API).

=end Internal:

=head2 _ToCPANDateTimeParamNames()

Maps date/time parameter names expected by the functions of this package to the ones expected by CPAN/Perl DateTime
package.

    my $DateTimeParams = $DateTimeObject->_ToCPANDateTimeParamNames(
        Year     => 2016,
        Month    => 1,
        Day      => 22,
        Hour     => 17,
        Minute   => 20,
        Second   => 2,
        TimeZone => 'Europe/Berlin',
    );

Returns:

    my $CPANDateTimeParamNames = {
        year      => 2016,
        month     => 1,
        day       => 22,
        hour      => 17,
        minute    => 20,
        second    => 2,
        time_zone => 'Europe/Berlin',
    };

=cut

sub _ToCPANDateTimeParamNames {
    my ( $Self, %Param ) = @_;

    my %ParamNameMapping = (
        Year     => 'year',
        Month    => 'month',
        Day      => 'day',
        Hour     => 'hour',
        Minute   => 'minute',
        Second   => 'second',
        TimeZone => 'time_zone',

        Years   => 'years',
        Months  => 'months',
        Weeks   => 'weeks',
        Days    => 'days',
        Hours   => 'hours',
        Minutes => 'minutes',
        Seconds => 'seconds',
    );

    my $DateTimeParams;

    PARAMNAME:
    for my $ParamName ( sort keys %ParamNameMapping ) {
        next PARAMNAME if !exists $Param{$ParamName};

        $DateTimeParams->{ $ParamNameMapping{$ParamName} } = $Param{$ParamName};
    }

    return $DateTimeParams;
}

=head2 _StringToHash()

Parses a date/time string and returns a hash ref.

    my $DateTimeHash = $DateTimeObject->_StringToHash( String => '2016-08-14 22:45:00' );

    # Sets second to 0:
    my $DateTimeHash = $DateTimeObject->_StringToHash( String => '2016-08-14 22:45' );

    # Sets hour, minute and second to 0:
    my $DateTimeHash = $DateTimeObject->_StringToHash( String => '2016-08-14' );

    # Format with time zone
    my $DateTimeHash = $DateTimeObject->_StringToHash(
        String   => '2023-02-17T11:00:00+03:00'
        TimeZone => 'Europe/Berlin', # desired time zone of the created DateTime object, optional
    );


Please see C<L</new()>> for the list of supported string formats.

Returns:

    my $DateTimeHash = {
        Year   => 2016,
        Month  => 8,
        Day    => 14,
        Hour   => 22,
        Minute => 45,
        Second => 0,
    };

=cut

sub _StringToHash {
    my ( $Self, %Param ) = @_;

    for my $RequiredParam (qw( String )) {
        if ( !defined $Param{$RequiredParam} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                'Priority' => 'Error',
                'Message'  => "Missing parameter $RequiredParam.",
            );

            return;
        }
    }

    if ( $Param{String} =~ m{\A(\d{4})-(\d{1,2})-(\d{1,2})(\s(\d{1,2}):(\d{1,2})(:(\d{1,2}))?)?\z} ) {

        my $DateTimeHash = {
            Year   => int $1,
            Month  => int $2,
            Day    => int $3,
            Hour   => defined $5 ? int $5 : 0,
            Minute => defined $6 ? int $6 : 0,
            Second => defined $8 ? int $8 : 0,
        };

        return $DateTimeHash;
    }

    # Match the following formats:
    #   - yyyy-mm-ddThh:mm:ss+tt:zz
    #   - yyyy-mm-ddThh:mm:ss+ttzz
    #   - yyyy-mm-ddThh:mm:ss-tt:zz
    #   - yyyy-mm-ddThh:mm:ss-ttzz
    #   - yyyy-mm-ddThh:mm:ss [timezone]
    #   - yyyy-mm-ddThh:mm:ss[timezone]
    if ( $Param{String} =~ /^\d{4}-\d{1,2}-\d{1,2}T\d{1,2}:\d{1,2}:\d{1,2}(.+)$/i ) {
        my ( $Year, $Month, $Day, $Hour, $Minute, $Second, $OffsetOrTZ ) =
            ( $Param{String} =~ m/^(\d{4})-(\d{2})-(\d{2})T(\d{1,2}):(\d{1,2}):(\d{1,2})\s*(.+)$/i );

        my $DateTimeHash = {
            Year   => int $Year,
            Month  => int $Month,
            Day    => int $Day,
            Hour   => int $Hour,
            Minute => int $Minute,
            Second => int $Second,
        };

        # Check if the rest 'OffsetOrTZ' is an offset or timezone.
        #   If it isn't an offset consider it a timezone
        if ( $OffsetOrTZ !~ m/(\+|\-)\d{2}:?\d{2}/i ) {

            # Make sure the time zone is valid. If it is an alias, replace it
            # with the real name, otherwise assume UTC.
            $OffsetOrTZ = $Self->GetRealTimeZone( TimeZone => $OffsetOrTZ ) // 'UTC';

            return {
                %{$DateTimeHash},
                TimeZone => $OffsetOrTZ,
            };
        }

        # It's an offset, get the time in GMT/UTC.
        $OffsetOrTZ =~ s/://i;    # Remove the ':'

        my $DT;
        eval {
            $DT = DateTime->new(
                ( map { lcfirst $_ => $DateTimeHash->{$_} } keys %{$DateTimeHash} ),
                time_zone => $OffsetOrTZ,
            );
        };
        return if !$DT || ref $DT ne 'DateTime';

        $DT->set_time_zone('UTC');

        my $TimeZone = $Param{TimeZone} // $Self->OTRSTimeZoneGet();
        if (
            $TimeZone ne 'UTC'    # because it's already UTC
            && $Self->IsTimeZoneValid( TimeZone => $TimeZone )
            )
        {
            $TimeZone = $Self->GetRealTimeZone( TimeZone => $TimeZone );
            $DT->set_time_zone($TimeZone);
        }

        return {
            ( map { ucfirst $_ => $DT->$_() } qw(year month day hour minute second) ),
            TimeZone => $TimeZone,
        };
    }

    $Kernel::OM->Get('Kernel::System::Log')->Log(
        'Priority' => 'Error',
        'Message'  => "Invalid date/time string $Param{String}.",
    );

    return;
}

=head2 _CPANDateTimeObjectCreate()

Creates a CPAN DateTime object which will be stored within this object and used for date/time calculations.

    # Create an object with current date and time
    # within time zone set in SysConfig OTRSTimeZone:
    my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate();

    # Create an object with current date and time
    # within a certain time zone:
    my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate(
        TimeZone => 'Europe/Berlin',
    );

    # Create an object with a specific date and time:
    my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate(
        Year     => 2016,
        Month    => 1,
        Day      => 22,
        Hour     => 12,                 # optional, defaults to 0
        Minute   => 35,                 # optional, defaults to 0
        Second   => 59,                 # optional, defaults to 0
        TimeZone => 'Europe/Berlin',    # optional, defaults to setting of SysConfig OTRSTimeZone
    );

    # Create an object from an epoch timestamp. These timestamps are always UTC/GMT,
    # hence time zone will automatically be set to UTC.
    #
    # If parameter Epoch is present, all other parameters except TimeZone will be ignored.
    my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate(
        Epoch => 1453911685,
    );

    # Create an object from a date/time string.
    #
    # If parameter String is given, Year, Month, Day, Hour, Minute and Second will be ignored. Please see C<L</new()>>
    # for the list of supported string formats.
    my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate(
        String   => '2016-08-14 22:45:00',
        TimeZone => 'Europe/Berlin',        # optional, defaults to setting of SysConfig OTRSTimeZone
    );

=cut

sub _CPANDateTimeObjectCreate {
    my ( $Self, %Param ) = @_;

    # Create object from string
    if ( defined $Param{String} ) {
        my $DateTimeHash = $Self->_StringToHash(
            String   => $Param{String},
            TimeZone => $Param{TimeZone},
        );
        if ( !IsHashRefWithData($DateTimeHash) ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                'Priority' => 'Error',
                'Message'  => "Invalid value for String: $Param{String}.",
            );

            return;
        }

        %Param = (

            # put time zone first because $DateTimeHash might contain one that has to be used
            # and overwrites the one here
            TimeZone => $Param{TimeZone},

            %{$DateTimeHash},
        );
    }

    my $CPANDateTimeObject;
    my $TimeZone = $Param{TimeZone} || $Self->OTRSTimeZoneGet();

    if ( !$Self->IsTimeZoneValid( TimeZone => $TimeZone ) ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            'Priority' => 'Error',
            'Message'  => "Invalid value for TimeZone: $TimeZone.",
        );

        return;
    }

    # Create object from epoch
    if ( defined $Param{Epoch} ) {

        if ( $Param{Epoch} !~ m{\A[+-]?\d+\z}sm ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                'Priority' => 'Error',
                'Message'  => "Invalid value for Epoch: $Param{Epoch}.",
            );

            return;
        }

        eval {
            $CPANDateTimeObject = DateTime->from_epoch(
                epoch     => $Param{Epoch},
                time_zone => $TimeZone,
                locale    => $Self->{Locale},
            );
        };

        return $CPANDateTimeObject;
    }

    $Param{TimeZone} = $TimeZone;

    # Check if date/time params were given, excluding time zone
    my $DateTimeParamsGiven = %Param && ( !defined $Param{TimeZone} || keys %Param > 1 );

    # Create object from date/time parameters
    if ($DateTimeParamsGiven) {
        for my $RequiredParam (qw( Year Month Day )) {
            if ( !$Param{$RequiredParam} ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    'Priority' => 'Error',
                    'Message'  => "Missing parameter $RequiredParam.",
                );
                return;
            }
        }

        # Create DateTime object
        my $DateTimeParams = $Self->_ToCPANDateTimeParamNames(%Param);

        eval {
            $CPANDateTimeObject = DateTime->new(
                %{$DateTimeParams},
                locale => $Self->{Locale},
            );
        };

        # Maybe the DateTime object could not be created because of a non-existing date/time, e.g.
        # 2:00 AM on the day of the DST switch from winter to summer time in time zone
        # Europe/Berlin (1:59 AM switches to 3:00 AM).
        #
        # In this case try to add an hour or a few to get a valid date/time.
        if (
            ref $CPANDateTimeObject ne 'DateTime'
            && $TimeZone ne 'UTC'
            && $TimeZone ne 'floating'
            )
        {
            my $FloatingCPANDateTimeObject;
            eval {
                $FloatingCPANDateTimeObject = DateTime->new(
                    %{$DateTimeParams},
                    time_zone => 'floating',
                    locale    => $Self->{Locale},
                );
            };
            return if ref $FloatingCPANDateTimeObject ne 'DateTime';

            $CPANDateTimeObject = $Self->_AutocorrectNonExistingDateTimeForTimeZone(
                CPANDateTimeObject  => $FloatingCPANDateTimeObject,
                DestinationTimeZone => $TimeZone,
            );
        }

        return $CPANDateTimeObject;
    }

    # Create object with current date/time.
    eval {
        $CPANDateTimeObject = DateTime->now(
            time_zone => $TimeZone,
            locale    => $Self->{Locale},
        );
    };

    return $CPANDateTimeObject;
}

=head2 _AutocorrectNonExistingDateTimeForTimeZone()

Certain times do not exist at certain dates in certain time zones, e.g.
2:00 AM on the day of the DST switch from winter to summer time in time zone
Europe/Berlin (1:59 AM switches to 3:00 AM).

In these cases, this function tries to add an hour or a few to get a valid date/time.

    my $AutocorrectedCPANDateTimeObject = $DateTimeObject->_AutocorrectNonExistingDateTimeForTimeZone(
        CPANDateTimeObject  => $CPANDateTimeObject, # must be in time zone 'floating'
        DestinationTimeZone => 'Europe/Berlin',
    );

    Returns a new CPAN DateTime object with the autocorrected time on success.
    Note that it's possible that also the day changes, not only the hour.

=cut

sub _AutocorrectNonExistingDateTimeForTimeZone {
    my ( $Self, %Param ) = @_;

    my $LogObject = $Kernel::OM->Get('Kernel::System::Log');

    NEEDED:
    for my $Needed (qw(CPANDateTimeObject)) {
        next NEEDED if defined $Param{$Needed};

        $LogObject->Log(
            'Priority' => 'Error',
            'Message'  => "Missing parameter $Needed.",
        );

        return;
    }

    if ( ref $Param{CPANDateTimeObject} ne 'DateTime' ) {
        $LogObject->Log(
            'Priority' => 'Error',
            'Message'  => 'Parameter CPANDateTimeObject has to be an object of type DateTime.',
        );
        return;
    }

    # Create a clone because the tests with added hours must not change the given CPAN DateTime
    # object if autocorrection fails.
    my $TempCPANDateTimeObject = $Param{CPANDateTimeObject}->clone();

    # Add one hour at a time until the creation of the DateTime object
    # with the desired time zone succeeds (max. 3 tries).
    #
    # Note that hours only will be added (and not subtracted) because the relevant
    # days without certain times are those with less than 24 hours.
    # This might lead to a date for the following day.

    # First try is with original date/time in case it works and wasn't checked before.
    # Don't change date/time in this case.
    eval {
        $TempCPANDateTimeObject->set_time_zone( $Param{DestinationTimeZone} );
    };

    return $TempCPANDateTimeObject if !$@;

    my $RetrySuccessful;
    RETRY:
    for my $Retry ( 1 .. 3 ) {
        $TempCPANDateTimeObject->add( hours => 1 );

        eval {
            $TempCPANDateTimeObject->set_time_zone( $Param{DestinationTimeZone} );
        };

        next RETRY if $@;

        $RetrySuccessful = 1;
        last RETRY;
    }

    return if !$RetrySuccessful;

    return $TempCPANDateTimeObject->clone();
}

=head2 _OpIsNewerThan()

Operator overloading for >

=cut

sub _OpIsNewerThan {
    my ( $Self, $OtherDateTimeObject ) = @_;

    my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject );
    return if !defined $Result;

    $Result = $Result == 1 ? 1 : 0;

    return $Result;
}

=head2 _OpIsOlderThan()

Operator overloading for <

=cut

sub _OpIsOlderThan {
    my ( $Self, $OtherDateTimeObject ) = @_;

    my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject );
    return if !defined $Result;

    $Result = $Result == -1 ? 1 : 0;

    return $Result;
}

=head2 _OpIsNewerThanOrEquals()

Operator overloading for >=

=cut

sub _OpIsNewerThanOrEquals {
    my ( $Self, $OtherDateTimeObject ) = @_;

    my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject );
    return if !defined $Result;

    $Result = $Result >= 0 ? 1 : 0;

    return $Result;
}

=head2 _OpIsOlderThanOrEquals()

Operator overloading for <=

=cut

sub _OpIsOlderThanOrEquals {
    my ( $Self, $OtherDateTimeObject ) = @_;

    my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject );
    return if !defined $Result;

    $Result = $Result <= 0 ? 1 : 0;

    return $Result;
}

=head2 _OpEquals()

Operator overloading for ==

=cut

sub _OpEquals {
    my ( $Self, $OtherDateTimeObject ) = @_;

    my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject );
    return if !defined $Result;

    $Result = !$Result ? 1 : 0;

    return $Result;
}

=head2 _OpNotEquals()

Operator overloading for !=

=cut

sub _OpNotEquals {
    my ( $Self, $OtherDateTimeObject ) = @_;

    my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject );
    return if !defined $Result;

    $Result = $Result != 0 ? 1 : 0;

    return $Result;
}

1;

=head1 TERMS AND CONDITIONS

This software is part of the OTRS project (L<https://otrs.org/>).

This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.

=cut
