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

use strict;
use warnings;
use autodie;

my $VERSION = 0.04;

use Dist::Zilla::File::InMemory;

use Moose;

has _leap_second_data => (
    is      => 'ro',
    isa     => 'HashRef',
    lazy    => 1,
    builder => '_build_leap_second_data',
);

with 'Dist::Zilla::Role::FileGatherer';

sub gather_files {
    my $self = shift;

    $self->add_file(
        Dist::Zilla::File::InMemory->new(
            name     => 'leap_seconds.h',
            encoding => 'bytes',
            content  => $self->_header,
        ),
    );
}

my $x = 1;
my %months = map { $_ => $x++ }
    qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );

sub _build_leap_second_data {
    my $self = shift;

    open my $fh, '<', 'leaptab.txt';

    my @leap_seconds;
    my @rd;
    my %rd_length;

    my $value = -1;
    while (<$fh>) {
        my ( $year, $mon, $day, $leap_seconds ) = split /\s+/;

        $mon =~ s/\W//;

        $leap_seconds =~ s/^([+-])//;
        my $mult = $1 eq '+' ? 1 : -1;

        my $utc_epoch = _ymd2rd( $year, $months{$mon}, $day );

        $value += $leap_seconds * $mult;

        push @leap_seconds, $value;
        push @rd,           $utc_epoch;

        $rd_length{ $utc_epoch - 1 } = $leap_seconds;
    }

    close $fh;

    push @leap_seconds, ++$value;

    return {
        leap_seconds => \@leap_seconds,
        rd           => \@rd,
        rd_length    => \%rd_length,
    };
}

sub _header {
    my $self = shift;

    my ( $leap_seconds, $rd, $rd_length )
        = @{ $self->_leap_second_data }{qw( leap_seconds rd rd_length )};

    my $set_leap_seconds = <<"EOF";

#define SET_LEAP_SECONDS(utc_rd, ls)  \\
{                                     \\
  {                                   \\
    if (utc_rd < $rd->[0]) {            \\
      ls = $leap_seconds->[0];           \\
EOF

    for ( my $x = 1; $x < @{$rd}; $x++ ) {
        my $condition
            = $x == @{$rd}
            ? "utc_rd < $rd->[$x]"
            : "utc_rd >= $rd->[$x - 1] && utc_rd < $rd->[$x]";

        $set_leap_seconds .= <<"EOF"
    } else if ($condition) {  \\
      ls = $leap_seconds->[$x];                      \\
EOF
    }

    $set_leap_seconds .= <<"EOF";
    } else {                         \\
      ls = $leap_seconds->[-1];       \\
    }                              \\
  }                                \\
}
EOF

    my $set_extra_seconds = <<"EOF";

#define SET_EXTRA_SECONDS(utc_rd, es)  \\
{                                      \\
  {                                    \\
    es = 0;                            \\
    switch (utc_rd) {                  \\
EOF

    my $set_day_length = <<"EOF";

#define SET_DAY_LENGTH(utc_rd, dl)     \\
{                                      \\
  {                                    \\
    dl = 86400;                        \\
    switch (utc_rd) {                  \\
EOF

    foreach my $utc_rd ( sort keys %{$rd_length} ) {
        $set_extra_seconds .= <<"EOF";
      case $utc_rd: es = $rd_length->{$utc_rd}; break;            \\
EOF

        $set_day_length .= <<"EOF";
      case $utc_rd: dl = 86400 + $rd_length->{$utc_rd}; break;    \\
EOF
    }

    $set_extra_seconds .= <<"EOF";
    }                                  \\
  }                                    \\
}
EOF

    $set_day_length .= <<"EOF";
    }                                  \\
  }                                    \\
}
EOF

    my $generator = ref $self;

    my $header = <<"EOF";
/*

This file is auto-generated by the leap second code generator ($VERSION). This
code generator comes with the DateTime.pm module distribution in the tools/
directory

Generated $generator.

Do not edit this file directly.

*/
EOF

    return join q{}, (
        $header,
        $set_leap_seconds,
        $set_extra_seconds,
        $set_day_length,
    );
}

# from lib/DateTimePP.pm
sub _ymd2rd {
    use integer;
    my ( $y, $m, $d ) = @_;
    my $adj;

    # make month in range 3..14 (treat Jan & Feb as months 13..14 of
    # prev year)
    if ( $m <= 2 ) {
        $y -= ( $adj = ( 14 - $m ) / 12 );
        $m += 12 * $adj;
    }
    elsif ( $m > 14 ) {
        $y += ( $adj = ( $m - 3 ) / 12 );
        $m -= 12 * $adj;
    }

    # make year positive (oh, for a use integer 'sane_div'!)
    if ( $y < 0 ) {
        $d -= 146097 * ( $adj = ( 399 - $y ) / 400 );
        $y += 400 * $adj;
    }

    # add: day of month, days of previous 0-11 month period that began
    # w/March, days of previous 0-399 year period that began w/March
    # of a 400-multiple year), days of any 400-year periods before
    # that, and 306 days to adjust from Mar 1, year 0-relative to Jan
    # 1, year 1-relative (whew)

    $d
        += ( $m * 367 - 1094 ) / 12
        + $y % 100 * 1461 / 4
        + ( $y / 100 * 36524 + $y / 400 )
        - 306;
}

1;