The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package perl5i::2::DateTime;

# A file to contain the Datetime work for perl5i to get it out of perl5i.pm

use 5.010;
use strict;
use warnings;

# Determine if we need Time::y2038 and only load if necessary.
# XXX This is a bit of a hack and should go into a config file.
use constant NEEDS_y2038 => (
    ((((CORE::gmtime(2**47-1))[5] || 0)      + 1900) != 4461763) ||
    ((((CORE::gmtime(-62135510400))[5] || 0) + 1900) != 1)
);

BEGIN {
    if( NEEDS_y2038 ) {
        require Time::y2038;
        Time::y2038->import;
    }
}

## no critic (Subroutines::ProhibitSubroutinePrototypes)
sub dt_gmtime (;$) {
    my $time = @_ ? shift : time;
    return gmtime($time) if wantarray;

    my($sec, $min, $hour, $mday, $mon, $year) = gmtime($time);
    $mon++;
    $year += 1900;

    require DateTime;
    return perl5i::2::DateTime::y2038->new(
        year            => $year,
        month           => $mon,
        day             => $mday,
        hour            => $hour,
        minute          => $min,
        second          => $sec,
        formatter       => "perl5i::2::DateTime::Format::CTime"
    );
}


sub _get_datetime_timezone {
    state $local_tzfile = "/etc/localtime";

    # Always be sure to honor the TZ environment var
    return "local" if $ENV{TZ};

    # Work around a bug in DateTime::TimeZone on FreeBSD where it
    # can't determine the time zone if /etc/localtime is not a link.
    # Tzfile is also faster to do localtime calculations.
    if( -e $local_tzfile ) {
        # Could go through more effort to figure it out.  Meh.
        my $tzname = "Local";
        if( -l $local_tzfile ) {
            if( my $real_tzfile = eval { readlink $local_tzfile } ) {
                $tzname = $real_tzfile;
            }
        }
        require DateTime::TimeZone::Tzfile;
        my $tz = DateTime::TimeZone::Tzfile->new(
            name     => $tzname,
            filename => $local_tzfile
        );
        return $tz if $tz;
    }

    return "local";
}

## no critic (Subroutines::ProhibitSubroutinePrototypes)
sub dt_localtime (;$) {
    my $time = @_ ? shift : time;
    return localtime($time) if wantarray;

    my($sec, $min, $hour, $mday, $mon, $year) = localtime($time);
    $mon++;
    $year += 1900;

    state $tz = _get_datetime_timezone();

    require DateTime;
    return perl5i::2::DateTime::y2038->new(
        year            => $year,
        month           => $mon,
        day             => $mday,
        hour            => $hour,
        minute          => $min,
        second          => $sec,
        time_zone       => $tz,
        formatter       => "perl5i::2::DateTime::Format::CTime"
    );
}


## no critic (Subroutines::ProhibitSubroutinePrototypes)
sub dt_time () {
    require DateTime::Format::Epoch;
    state $formatter = DateTime::Format::Epoch->new( epoch => DateTime->from_epoch( epoch => 0 ) );

    require DateTime;
    return perl5i::2::DateTime::time->from_epoch(
        epoch     => time,
        formatter => $formatter
    );
}


{
    package perl5i::2::DateTime::y2038;

    # Don't load DateTime until we need it.
    our @ISA = qw(DateTime);

    use overload
      "eq" => sub {
          my($dt1, $dt2) = @_;
          return "$dt1" eq "$dt2" if !eval { $dt2->isa("DateTime") };
          return $dt1 eq $dt2;
      };

    sub say {
        CORE::say("$_[0]");
    }

    sub print {
        CORE::print("$_[0]");
    }

    sub from_epoch {
        my $class = shift;

        if( perl5i::2::DateTime::NEEDS_y2038 ) {
            no warnings 'redefine';
            local *CORE::GLOBAL::gmtime    = \&Time::y2038::gmtime;
            local *CORE::GLOBAL::localtime = \&Time::y2038::localtime;

            return $class->SUPER::from_epoch(@_);
        }
        else {
            return $class->SUPER::from_epoch(@_);
        }
    }


    # Copy of DateTime's own epoch() function.
    if( perl5i::2::DateTime::NEEDS_y2038 ) {
        *epoch = sub {
            my $self = shift;

            my $zone = $self->time_zone;
            $self->set_time_zone("UTC");

            require Time::y2038;
            my $time = Time::y2038::timegm(
                $self->sec, $self->min, $self->hour, $self->mday,
                $self->mon - 1,
                $self->year - 1900,
            );

            $self->set_time_zone($zone);

            return $time;
        }
    }
}

{

    package perl5i::2::DateTime::time;

    use parent -norequire, qw(perl5i::2::DateTime::y2038);

    use overload
      "0+" => sub { $_[0]->epoch },
      "-"  => sub {
        my( $a, $b, $reverse ) = @_;

        if($reverse) {
            ( $b, $a ) = ( $a, $b );
        }

        my $time_a = eval { $a->isa("DateTime") } ? $a->epoch : $a;
        my $time_b = eval { $b->isa("DateTime") } ? $b->epoch : $b;

        return $time_a - $time_b;
      },

      "+" => sub {
        my( $a, $b, $reverse ) = @_;

        if($reverse) {
            ( $b, $a ) = ( $a, $b );
        }

        my $time_a = eval { $a->isa("DateTime") } ? $a->epoch : $a;
        my $time_b = eval { $b->isa("DateTime") } ? $b->epoch : $b;

        return $time_a + $time_b;
      },

      "==" => sub {
          my($a, $b) = @_;
          return $a+0 == $b+0 if !eval { $b->isa("DateTime") };
          return $a == $b;
      },

      fallback => 1;
}


{

    package perl5i::2::DateTime::Format::CTime;

    use CLASS;

    sub new {
        return bless {}, $CLASS;
    }

    sub format_datetime {
        my $self = shift;
        my $dt   = shift;

        # Straight from the Open Group asctime() docs.
        return sprintf "%.3s %.3s%3d %.2d:%.2d:%.2d %d",
          $dt->day_abbr,
          $dt->month_abbr,
          $dt->mday,
          $dt->hour,
          $dt->min,
          $dt->sec,
          $dt->year,
          ;
    }
}


1;