#!/usr/bin/env perl
##----------------------------------------------------------------------------
## DateTime::Lite::TimeZone - ~/scripts/leap_seconds_header.pl
## Version v0.1.0
## Copyright(c) 2026 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2026/04/08
## Modified 2026/04/09
## All rights reserved
##
## This program is free software; you can redistribute  it  and/or  modify  it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
## Generates leap_seconds.h from an IERS leap-seconds.list file.
##
## The leap-seconds.list file is maintained by the IERS (International Earth
## Rotation and Reference Systems Service) and distributed as part of the
## IANA tzdata release. It is typically found at:
##
##     /usr/share/zoneinfo/leap-seconds.list
##
## or downloaded directly from:
##
##     https://data.iana.org/time-zones/data/leap-seconds.list
##
## Usage:
##     perl scripts/leap_seconds_header.pl [leap-seconds.list] [output.h]
##
##     Defaults:
##       input  = /usr/share/zoneinfo/leap-seconds.list
##       output = leap_seconds.h  (in the current directory)
##
## The generated header defines three macros used by DateTime-Lite.xs:
##
##     SET_LEAP_SECONDS( utc_rd, leaps )
##       Sets leaps to the number of leap seconds accumulated since 1972 for
##       the Rata Die day utc_rd. Uses a ternary chain for O(n) but
##       branch-prediction-friendly lookup.
##
##     SET_EXTRA_SECONDS( utc_rd, es )
##       Sets es to +1 if utc_rd is a leap-second day, 0 otherwise.
##       Uses a switch statement keyed on utc_rd+1.
##
##     SET_DAY_LENGTH( utc_rd, dl )
##       Sets dl to 86400 or 86401 (86400 + extra seconds).
##
## Rata Die convention:
##     RD day 1   = 0001-01-01
##     RD day 719163 = 1970-01-01 (Unix epoch)
##
## leap-seconds.list convention:
##     Timestamps are NTP seconds (since 1900-01-01 00:00:00 UTC).
##     Each data line: <ntp_ts> <tai_minus_utc> # <comment>
##     The TAI-UTC count applies from that timestamp onwards.
##     The baseline before 1972 is 10 (pre-leap-second era).
##     Accumulated since 1972 = tai_minus_utc - 10.
##
## The RD stored for each leap second is the first day of the new count,
## i.e. the day AFTER the 23:59:60 leap second. This matches the
## convention used in the original DateTime.pm leap_seconds.h.
##----------------------------------------------------------------------------
use v5.10.1;
use strict;
use warnings;
use File::Basename;
use POSIX;
our $VERSION = 'v0.1.0';

my $NTP_TO_UNIX    = 2_208_988_800;  # seconds between 1900-01-01 and 1970-01-01
my $UNIX_EPOCH_RD  = 719_163;        # Rata Die day of 1970-01-01
my $BASELINE_COUNT = 10;             # TAI-UTC before the first 1972 leap second

my $input  = $ARGV[0] // '/usr/share/zoneinfo/leap-seconds.list';
my $output = $ARGV[1] // 'leap_seconds.h';

die( "Cannot read '$input': $!\n" ) unless( -r $input );

# NOTE: Parse the leap-seconds.list file
my @leap_seconds;  # list of { rd => RD_day, accum => accumulated_since_1972 }

open( my $fh, '<', $input ) || die( "Cannot open '$input': $!\n" );
while( my $line = <$fh> )
{
    # Skip comments and blank lines
    next if( $line =~ /^\s*#/ || $line =~ /^\s*$/ );

    my( $ntp_ts, $tai_count ) = split( /\s+/, $line );
    next unless( defined( $ntp_ts ) && defined( $tai_count ) );
    next unless( $ntp_ts =~ /^\d+$/ && $tai_count =~ /^\d+$/ );

    # Only count leap seconds added since 1972 (baseline = 10)
    my $accum = $tai_count - $BASELINE_COUNT;
    next if( $accum <= 0 );

    # Convert NTP timestamp to Rata Die day
    my $unix_secs = $ntp_ts - $NTP_TO_UNIX;
    my $rd        = int( $unix_secs / 86_400 ) + $UNIX_EPOCH_RD;

    push( @leap_seconds, { rd => $rd, accum => $accum } );
}
close( $fh );

die( "No leap second data found in '$input'\n" ) unless( @leap_seconds );

# Sort by RD ascending, then deduplicate (keep highest accum per RD)
@leap_seconds = sort{ $a->{rd} <=> $b->{rd} } @leap_seconds;

# The list should already be sorted and deduplicated, but be safe
my %seen;
@leap_seconds = grep{ !$seen{ $_->{rd} }++ } @leap_seconds;

my $count  = scalar( @leap_seconds );
my $latest = $leap_seconds[-1]->{accum};
my $now    = POSIX::strftime( '%Y-%m-%dT%H:%M:%S+00:00', gmtime );
my $basename = File::Basename::basename( $output );

# NOTE: Emit the header
open( my $out, '>', $output ) || die( "Cannot write '$output': $!\n" );
print $out <<HEADER_TOP;
/*---------------------------------------------------------------------------
 * $basename - Leap seconds include for DateTime::Lite
 *
 * This file is auto-generated by scripts/leap_seconds_header.pl ($VERSION)
 * from DateTime::Lite distribution on $now.
 *
 * Do not edit this file directly. To regenerate, run:
 *
 *    perl scripts/leap_seconds_header.pl [leap-seconds.list] [leap_seconds.h]
 *
 * The leap-seconds.list source file is distributed with the IANA tzdata
 * release and is typically found at /usr/share/zoneinfo/leap-seconds.list, or
 * downloadedable from https://data.iana.org/time-zones/data/leap-seconds.list
 *
 * This IERS leap-seconds.list source file is in the public domain:
 *   "This file is in the public domain."
 *   (https://data.iana.org/time-zones/data/leap-seconds.list)
 * 
 * This program is free software; you can redistribute  it  and/or  modify  it
 * under the same terms as Perl itself.
 *---------------------------------------------------------------------------*/


/* utc_rd must be a simple variable */
HEADER_TOP

# SET_LEAP_SECONDS: ternary chain, highest RD first
print( $out "#define SET_LEAP_SECONDS(utc_rd, leaps)  \\\n" );
print( $out "  (leaps) =                              \\\n" );
for my $ls ( reverse( @leap_seconds ) )
{
    # Original format: "? NN :" with exactly one space before colon;
    # trailing spaces vary to keep the line length uniform.
    my $trail = $ls->{accum} < 10 ? "               " : "              ";
    printf( $out "    utc_rd >= %-6d ? %d :%s\\\n",
                 $ls->{rd}, $ls->{accum}, $trail );
}
print( $out "    0;                                   \\\n" );
print( $out "\n" );

# SET_EXTRA_SECONDS: switch on utc_rd+1 (the day after the leap second day)
print( $out "#define SET_EXTRA_SECONDS(utc_rd, es)    \\\n" );
print( $out "    switch (utc_rd +1) {                 \\\n" );
foreach my $ls ( @leap_seconds )
{
    printf( $out "    case %-6d: es = +1; break;         \\\n",
                      $ls->{rd} );
}
print( $out "    default:     es =  0; break;         \\\n" );
print( $out "    }\n" );
print( $out "\n" );

# SET_DAY_LENGTH: uses SET_EXTRA_SECONDS
print( $out <<'HEADER_BOTTOM' );
/* day length in seconds (86400 or 86401) */
#define SET_DAY_LENGTH(utc_rd, dl)       \
    {                                    \
        IV es;                           \
        SET_EXTRA_SECONDS(utc_rd, es);   \
        dl = SECONDS_PER_DAY + es;       \
    }
HEADER_BOTTOM

close( $out );

printf( "Generated %s: %d leap seconds, latest count = %d (as of RD %d)\n",
        $output, $count, $latest, $leap_seconds[-1]->{rd} );

__END__

=head1 NAME

leap_seconds_header.pl - Generate leap_seconds.h for DateTime::Lite XS

=head1 SYNOPSIS

    perl scripts/leap_seconds_header.pl [input] [output]

    # Using system leap-seconds.list (default):
    perl scripts/leap_seconds_header.pl

    # Explicit paths:
    perl scripts/leap_seconds_header.pl /usr/share/zoneinfo/leap-seconds.list leap_seconds.h

=head1 DESCRIPTION

Reads the IERS C<leap-seconds.list> file and generates a C header defining three macros:

=over 4

=item C<SET_LEAP_SECONDS(utc_rd, leaps)>

Sets C<leaps> to the count of leap seconds accumulated since 1972 for the given Rata Die day C<utc_rd>.

=item C<SET_EXTRA_SECONDS(utc_rd, es)>

Sets C<es> to C<+1> if C<utc_rd> is a leap second day, C<0> otherwise.

=item C<SET_DAY_LENGTH(utc_rd, dl)>

Sets C<dl> to C<86400> or C<86401> depending on whether C<utc_rd> is a leap second day.

=back

=head1 RATA DIE CONVENTION

Rata Die day 1 = 0001-01-01. Day 719163 = 1970-01-01 (Unix epoch).

=head1 LEAP-SECONDS.LIST FORMAT

Each data line contains an NTP timestamp (seconds since 1900-01-01) and the cumulative TAI-UTC count effective from that timestamp. The baseline before the first 1972 leap second is 10. Accumulated since 1972 = TAI-UTC - 10.

The RD stored for each entry is the first day of the new count (the day after C<23:59:60>).

=head1 AUTHOR

Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>

=head1 COPYRIGHT & LICENSE

Copyright(c) 2026 DEGUEST Pte. Ltd.

All rights reserved

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut
