#!perl
use 5.010;
use strict;
use warnings;
use JSON::PP;
use Getopt::Long;

# Writes include/tk/tk_cal_eras.h from cldr-core's calendarData.json and
# calendarPreferenceData.json: the eras of every calendar (code and first
# day, as days since 1970 in proleptic Gregorian) and the calendars each
# territory prefers. Dates in CLDR are proleptic Gregorian.

my ($cldr, $version, $out, $names_out, $en_out) = (undef, '48.2.3', 'include/tk/tk_cal_eras.h', undef, 'include/tk/tk_cal_en.h');
GetOptions('cldr=s' => \$cldr, 'cldr-version=s' => \$version, 'out=s' => \$out, 'names=s' => \$names_out, 'en=s' => \$en_out)
    or die "usage: calcompile --cldr DIR [--cldr-version V] [--out FILE] [--names share/calendars.tkd [--en FILE]]\n";
die "--cldr DIR is required\n" unless $cldr;

sub slurp { my ($f) = @_; local $/; open my $h, '<', $f or die "$f: $!"; my $s = <$h>; close $h; decode_json($s) }
my $data = slurp("$cldr/cldr-core/supplemental/calendarData.json")->{supplemental}{calendarData};
my $pref = slurp("$cldr/cldr-core/supplemental/calendarPreferenceData.json")->{supplemental}{calendarPreferenceData};
my $cal_parents = slurp("$cldr/cldr-core/supplemental/parentLocales.json")->{supplemental}{parentLocales}{parentLocale};
my $names = slurp("$cldr/cldr-localenames-full/main/en/localeDisplayNames.json")->{main}{en}{localeDisplayNames}{types}{calendar};

sub days_from_civil {
    my ($y, $m, $d) = @_;
    $y -= 1 if $m <= 2;
    my $era = int(($y >= 0 ? $y : $y - 399) / 400);
    my $yoe = $y - $era * 400;
    my $doy = int((153 * ($m + ($m > 2 ? -3 : 9)) + 2) / 5) + $d - 1;
    my $doe = $yoe * 365 + int($yoe / 4) - int($yoe / 100) + $doy;
    return $era * 146097 + $doe - 719468;
}
sub days_of {
    my ($s) = @_;
    my ($neg, $y, $m, $d) = $s =~ /^(-?)(\d+)-(\d+)-(\d+)$/ or die "date '$s'";
    return days_from_civil(($neg ? -$y : $y), $m, $d);
}

# CLDR's keys to the ids the engine uses.
my %id_of = ('gregorian' => 'gregory', 'ethiopic-amete-alem' => 'ethioaa');
my @order = qw(gregory buddhist japanese roc coptic ethiopic ethioaa hebrew islamic-civil islamic-tbla islamic-umalqura islamic islamic-rgsa persian indian chinese dangi);
my %eras;
for my $key (sort keys %$data) {
    next if $key eq 'generic';
    my $id = $id_of{$key} // $key;
    my $e = $data->{$key}{eras} or next;
    my @rows;
    for my $i (sort { $a <=> $b } keys %$e) {
        my $x = $e->{$i};
        my $code = $x->{_code} // "era$i";
        my $start = exists $x->{_start} ? days_of($x->{_start}) : undef;
        push @rows, { code => $code, start => $start, index => $i };
    }
    # an era given only by its end comes first
    @rows = sort { (defined $a->{start} ? $a->{start} : -1e18) <=> (defined $b->{start} ? $b->{start} : -1e18) } @rows;
    # ICU's readings, which CLDR's names are drawn for: the Hijri calendars have
    # one era and count the years before it as 0 and less; the Coptic has an
    # era before the Martyrs that counts backwards
    @rows = grep { $_->{code} ne 'bh' } @rows if $id =~ /^islamic/;
    unshift @rows, { code => 'before-am', start => undef } if $id eq 'coptic';
    $eras{$id} = \@rows;
}
for (@order) { die "no eras for $_" unless $eras{$_} }

open my $o, '>', $out or die "$out: $!";
print $o <<"C";
/* Generated by tools/calcompile from CLDR $version: the eras of every
 * calendar, first day as days since 1970 in proleptic Gregorian (the
 * least value for an era known only by its end), and the calendars each
 * territory prefers. Do not edit. */
#ifndef TK_CAL_ERAS_H
#define TK_CAL_ERAS_H

typedef struct tk_era {
    const char *code;
    int64_t     start;
} tk_era;

#define TK_ERA_FROM_THE_START (-INT64_C(0x7fffffffffffffff) - 1)

C
for my $id (@order) {
    my $c = $id; $c =~ s/-/_/g;
    printf $o "static const tk_era tk_eras_%s[%d] = {\n", $c, scalar @{ $eras{$id} };
    for my $r (@{ $eras{$id} }) {
        printf $o "    { \"%s\", %s },\n", $r->{code}, defined $r->{start} ? sprintf('INT64_C(%d)', $r->{start}) : 'TK_ERA_FROM_THE_START';
    }
    print $o "};\n";
}
my %name_key = ('gregory' => 'gregorian', 'ethioaa' => 'ethiopic-amete-alem');
print $o "\n/* English display names, from cldr-localenames-full */\n";
printf $o "static const char *const tk_cal_english[%d] = {\n", scalar @order;
for my $id (@order) {
    my $n = $names->{ $name_key{$id} // $id } or die "no English name for $id";
    $n =~ s/ Calendar\b//;
    printf $o "    \"%s\",\n", $n;
}
print $o "};\n";
print $o "\n#define TK_ERA_TABLE(id) { tk_eras_##id, (unsigned)(sizeof(tk_eras_##id) / sizeof(tk_era)) }\n\n";
print $o "typedef struct tk_cal_pref {\n    const char *territory;\n    const char *ids;    /* space separated, most preferred first */\n} tk_cal_pref;\n\n";
my %pref_id = ('gregorian' => 'gregory', 'ethiopic-amete-alem' => 'ethioaa', 'islamicc' => 'islamic-civil');
# the territory a locale code without one is likely in (likelySubtags), for Calendar->preferred
{
    my $likely = slurp("$cldr/cldr-core/supplemental/likelySubtags.json")->{supplemental}{likelySubtags};
    my @codes = grep { $_ ne 'und' } @{ slurp("$cldr/cldr-core/availableLocales.json")->{availableLocales}{full} };
    my %t;
    for my $code (@codes) {
        next if grep { /^(?:[A-Z]{2}|\d{3})$/ } split /-/, $code;
        my $full = $likely->{$code} // $likely->{ (split /-/, $code)[0] } or next;
        my ($terr) = grep { /^(?:[A-Z]{2}|\d{3})$/ } split /-/, $full;
        $t{$code} = $terr if $terr;
    }
    print $o "typedef struct tk_cal_likely {\n    const char *code;         /* a locale code with no territory */\n    const char *territory;    /* the one it is likely in */\n} tk_cal_likely;\n\n";
    printf $o "static const tk_cal_likely tk_cal_likelies[%d] = {\n", scalar keys %t;
    printf $o "    { \"%s\", \"%s\" },\n", $_, $t{$_} for sort keys %t;
    print $o "};\n\n";
}

printf $o "static const tk_cal_pref tk_cal_prefs[%d] = {\n", scalar keys %$pref;
for my $t (sort keys %$pref) {
    printf $o "    { \"%s\", \"%s\" },\n", $t, join ' ', map { $pref_id{$_} // $_ } @{ $pref->{$t} };
}
print $o "};\n\n#endif\n";
close $o;
printf "%s: %d calendars, %d territories\n", $out, scalar @order, scalar keys %$pref;

sub same { my ($x, $y) = @_; (defined $x ? $x : "\x{FFFF}") eq (defined $y ? $y : "\x{FFFF}") }
sub c_str {
    my ($s) = @_;
    my $b = $s;
    utf8::encode($b);
    $b =~ s/([\\"])/\\$1/g;
    $b =~ s/([^\x20-\x7e])/sprintf('\\%03o', ord $1)/ge;
    return "\"$b\"";
}

# ---- the calendars' names and formats -------------------------------------
# For every locale and every calendar but the Gregorian: CLDR's month names
# (by slot: the Hebrew Nisan is 8 in every year, with the leap-year name of
# slot 7 kept apart), era names, the leap-month patterns and cyclic year
# names of the Chinese calendars, the date and date-time formats and the
# available formats. Days, quarters, day periods and time formats are the
# Gregorian's in every calendar (CLDR aliases them) and are not stored.
#
# A set is a fixed array of string slots (tk_cal_names in C, the layout
# below) and a list of skeleton patterns. share/calendars.tkd holds, for each
# (locale, calendar), only the slots and skeletons that differ from the
# parent locale's set for the same calendar, the parents ending at und, so
# the file is small; the loader resolves the chain. English and und are
# compiled in (tk_cal_en.h) so English never opens the file.

my @names_cals = qw(buddhist japanese roc coptic ethiopic ethioaa hebrew islamic-civil islamic-tbla islamic-umalqura islamic islamic-rgsa persian indian chinese dangi);
my %cal_file = ('ethioaa' => 'ethiopic-amete-alem');
my %cal_pkg = map { $_ => ($_ =~ /^islamic/ ? 'islamic' : $_ eq 'ethioaa' ? 'ethiopic' : $_) } @names_cals;
my @ctx = qw(format stand-alone);
my @wid = qw(wide abbreviated narrow);
my %era_key = (wide => 'eraNames', abbreviated => 'eraAbbr', narrow => 'eraNarrow');
my @lens = qw(full long medium short);
# the fixed layout
use constant { CN_MONTHS => 0, CN_MONTHS_LEAP => 78, CN_LEAP_PATTERN => 84, CN_LEAP_NUMERIC => 90, CN_CYCLIC => 91, CN_DATE_FORMATS => 271, CN_DATETIME_FORMATS => 275, CN_FIXED => 279 };

my %numsys_known = map { $_ => 1 } qw(hanidec hanidays romanlow jpanyear hebr);
# a pattern with a numbers attribute travels as "\001spec\001pattern"; the engine writes these five systems
sub pattern_value {
    my ($v) = @_;
    return $v unless ref $v eq 'HASH';
    my $n = $v->{_numbers};
    return $v->{_value} unless defined $n;
    for (split /;/, $n) {
        my ($sys) = /^(?:[A-Za-z]=)?(\w+)$/ or die "numbers attribute '$n'\n";
        die "the numbering system $sys is not one the engine writes\n" unless $numsys_known{$sys};
    }
    return "\x01$n\x01$v->{_value}";
}

my %nameset_memo;
sub nameset {
    my ($code, $cal) = @_;
    return $nameset_memo{"$code $cal"} if exists $nameset_memo{"$code $cal"};
    my $file = "$cldr/cldr-cal-$cal_pkg{$cal}-full/main/$code/ca-" . ($cal_file{$cal} // $cal) . ".json";
    return $nameset_memo{"$code $cal"} = undef unless -e $file;
    my $d = slurp($file)->{main}{$code}{dates}{calendars}{ $cal_file{$cal} // $cal } or die "$code $cal: no data";
    my $n_eras = scalar @{ $eras{$cal} };
    my @slot = (undef) x (CN_FIXED + 3 * $n_eras);
    for my $ci (0 .. 1) {
        for my $wi (0 .. 2) {
            my $m = $d->{months}{ $ctx[$ci] }{ $wid[$wi] } || {};
            for my $k (keys %$m) {
                if ($k =~ /^(\d+)$/ && $1 >= 1 && $1 <= 13) { $slot[CN_MONTHS + ($ci * 3 + $wi) * 13 + $1 - 1] = $m->{$k} }
                elsif ($k =~ /^(\d+)-yeartype-leap$/) { $slot[CN_MONTHS_LEAP + $ci * 3 + $wi] = $m->{$k} }
            }
            my $lp = $d->{monthPatterns}{ $ctx[$ci] }{ $wid[$wi] }{leap};
            $slot[CN_LEAP_PATTERN + $ci * 3 + $wi] = $lp if defined $lp;
        }
    }
    $slot[CN_LEAP_NUMERIC] = $d->{monthPatterns}{numeric}{all}{leap} if $d->{monthPatterns}{numeric};
    for my $wi (0 .. 2) {
        my $cy = $d->{cyclicNameSets}{years}{format}{ $wid[$wi] };
        if ($cy) { $slot[CN_CYCLIC + $wi * 60 + $_ - 1] = $cy->{$_} for 1 .. 60 }
        my $e = $d->{eras}{ $era_key{ $wid[$wi] } } || {};
        for my $k (0 .. $n_eras - 1) {
            # CLDR's era index is the engine's, save that the Hijri before-era and the Coptic order are the engine's own
            my $ck = $k;
            $slot[CN_FIXED + $wi * $n_eras + $k] = $e->{$ck} if defined $e->{$ck};
        }
    }
    for my $li (0 .. 3) {
        $slot[CN_DATE_FORMATS + $li] = pattern_value($d->{dateFormats}{ $lens[$li] });
        $slot[CN_DATETIME_FORMATS + $li] = pattern_value($d->{dateTimeFormats}{ $lens[$li] });
    }
    my %skel = map { $_ => pattern_value($d->{dateTimeFormats}{availableFormats}{$_}) } keys %{ $d->{dateTimeFormats}{availableFormats} || {} };
    return $nameset_memo{"$code $cal"} = { slot => \@slot, skel => \%skel, days => $d->{days}, quarters => $d->{quarters}, time => $d->{timeFormats} };
}

# the locale's Gregorian names, as the main archive has them, resolved down the chain
my %greg_memo;
sub gregorian_set {
    my ($code) = @_;
    return $greg_memo{$code} if $greg_memo{$code};
    my $f = "$cldr/cldr-dates-full/main/$code/ca-gregorian.json";
    my $p = cal_parent_of($code);
    my $base = $p ? gregorian_set($p) : { months => [ map { [ map { [] } 0 .. 2 ] } 0 .. 1 ], date => [], datetime => [], skel => {} };
    my $r = { months => [ map { my $c = $_; [ map { [ @{ $base->{months}[$c][$_] } ] } 0 .. 2 ] } 0 .. 1 ], date => [ @{ $base->{date} } ], datetime => [ @{ $base->{datetime} } ], skel => { %{ $base->{skel} } } };
    if (-e $f) {
        my $g = slurp($f)->{main}{$code}{dates}{calendars}{gregorian};
        for my $ci (0 .. 1) { for my $wi (0 .. 2) { my $m = $g->{months}{ $ctx[$ci] }{ $wid[$wi] } || {}; for my $k (1 .. 12) { $r->{months}[$ci][$wi][$k - 1] = $m->{$k} if defined $m->{$k} } } }
        for my $li (0 .. 3) {
            $r->{date}[$li] = pattern_value($g->{dateFormats}{ $lens[$li] }) if defined $g->{dateFormats}{ $lens[$li] };
            $r->{datetime}[$li] = pattern_value($g->{dateTimeFormats}{ $lens[$li] }) if defined $g->{dateTimeFormats}{ $lens[$li] };
        }
        $r->{skel}{$_} = pattern_value($g->{dateTimeFormats}{availableFormats}{$_}) for keys %{ $g->{dateTimeFormats}{availableFormats} || {} };
    }
    return $greg_memo{$code} = $r;
}

sub cal_parent_of {
    my ($code) = @_;
    return undef if $code eq 'und';
    my $p = $cal_parents->{$code};
    unless ($p) {
        return 'und' unless $code =~ /-/;
        ($p = $code) =~ s/-[^-]+\z//;
    }
    return $p eq 'root' ? 'und' : $p;
}

# a code's set, resolved down the parent chain
my %resolved_memo;
sub resolved {
    my ($code, $cal) = @_;
    return $resolved_memo{"$code $cal"} if $resolved_memo{"$code $cal"};
    my $mine = nameset($code, $cal);
    my $p = cal_parent_of($code);
    my $base = $p ? resolved($p, $cal) : { slot => [], skel => {} };
    my @slot = @{ $base->{slot} };
    my %skel = %{ $base->{skel} };
    if ($mine) {
        for my $i (0 .. $#{ $mine->{slot} }) { $slot[$i] = $mine->{slot}[$i] if defined $mine->{slot}[$i] }
        $skel{$_} = $mine->{skel}{$_} for keys %{ $mine->{skel} };
    }
    # what equals the locale's Gregorian is not stored: the engine falls back to the Gregorian set for a missing slot or skeleton
    my $g = gregorian_set($code);
    for my $ci (0 .. 1) {
        for my $wi (0 .. 2) {
            for my $k (1 .. 12) {
                my $i = CN_MONTHS + ($ci * 3 + $wi) * 13 + $k - 1;
                $slot[$i] = undef if defined $slot[$i] && defined $g->{months}[$ci][$wi][$k - 1] && $slot[$i] eq $g->{months}[$ci][$wi][$k - 1];
            }
        }
    }
    for my $li (0 .. 3) {
        $slot[CN_DATE_FORMATS + $li] = undef if defined $slot[CN_DATE_FORMATS + $li] && $slot[CN_DATE_FORMATS + $li] eq ($g->{date}[$li] // '');
        $slot[CN_DATETIME_FORMATS + $li] = undef if defined $slot[CN_DATETIME_FORMATS + $li] && $slot[CN_DATETIME_FORMATS + $li] eq ($g->{datetime}[$li] // '');
    }
    for my $k (keys %skel) { delete $skel{$k} if defined $g->{skel}{$k} && $g->{skel}{$k} eq $skel{$k} }
    return $resolved_memo{"$code $cal"} = { slot => \@slot, skel => \%skel };
}

sub cal_section {
    my ($code, $cal) = @_;
    my $r = resolved($code, $cal);
    my $p = cal_parent_of($code);
    my $pr = $p ? resolved($p, $cal) : { slot => [], skel => {} };
    my (@pool, %at);
    my $pool_len = 0;
    my $str = sub {
        my ($s) = @_;
        my $b = $s;
        utf8::encode($b);
        unless (exists $at{$b}) {
            push @pool, $b . "\0";
            $at{$b} = $pool_len;
            $pool_len += length($b) + 1;
        }
        die "$code $cal: the pool is over 64 KB\n" if $at{$b} >= 0xFFFE;
        return $at{$b};
    };
    my @pairs;
    for my $i (0 .. ($#{ $r->{slot} } > $#{ $pr->{slot} } ? $#{ $r->{slot} } : $#{ $pr->{slot} })) {
        my ($mine, $theirs) = ($r->{slot}[$i], $pr->{slot}[$i]);
        if (!defined $mine) {
            push @pairs, [ $i, 0xFFFE ] if defined $theirs;   # the parent's name is not this locale's: back to the Gregorian
            next;
        }
        next if defined $theirs && $mine eq $theirs;
        push @pairs, [ $i, $str->($mine) ];
    }
    my @skel;
    for my $k (sort keys %{ { %{ $r->{skel} }, %{ $pr->{skel} } } }) {
        if (!defined $r->{skel}{$k}) {
            push @skel, [ $str->($k), 0xFFFE ] if defined $pr->{skel}{$k};
            next;
        }
        next if defined $pr->{skel}{$k} && $pr->{skel}{$k} eq $r->{skel}{$k};
        push @skel, [ $str->($k), $str->($r->{skel}{$k}) ];
    }
    return undef unless @pairs || @skel;
    return 'TKS1' . pack('v*', scalar(@pairs), scalar(@skel), map({ @$_ } @pairs), map({ @$_ } @skel)) . join('', @pool);
}

sub fnv { my $h = 0x811c9dc5; for (unpack 'C*', $_[0]) { $h ^= $_; $h = ($h * 0x01000193) & 0xFFFFFFFF } $h }

if ($names_out) {
    my @codes = sort grep { $_ ne 'und' } @{ slurp("$cldr/cldr-core/availableLocales.json")->{availableLocales}{full} };
    my (%same_as, %differs);
    # calendars whose names equal another's in every locale are aliases
    my %alias_of = ('islamic-civil' => 'islamic', 'islamic-tbla' => 'islamic', 'islamic-umalqura' => 'islamic', 'islamic-rgsa' => 'islamic', 'dangi' => 'chinese', 'ethioaa' => 'ethiopic');
    for my $cal (sort keys %alias_of) {
        my $diff = 0;
        for my $code ('und', @codes) {
            my ($a, $b) = (resolved($code, $cal), resolved($code, $alias_of{$cal}));
            my $same = @{ $a->{slot} } == @{ $b->{slot} } && !grep { !same($a->{slot}[$_], $b->{slot}[$_]) } 0 .. $#{ $a->{slot} };
            $same &&= join("\0", map { "$_=$a->{skel}{$_}" } sort keys %{ $a->{skel} }) eq join("\0", map { "$_=$b->{skel}{$_}" } sort keys %{ $b->{skel} });
            $diff++ unless $same;
        }
        $differs{$cal} = $diff;
        $same_as{$cal} = $alias_of{$cal} if $diff == 0;
    }
    my @stored = grep { !$same_as{$_} } @names_cals;
    my (@index, $body, %per_cal);
    $body = '';
    my $at = 0;
    my $gregorian_alias_fail = 0;
    for my $cal (@stored) {
        for my $code ('und', @codes) {
            my $s = cal_section($code, $cal);
            next unless defined $s;
            push @index, [ fnv("$code\0$cal"), $at, length $s ];
            $body .= $s;
            $at += length $s;
            $per_cal{$cal} += length $s;
            my $ns = nameset($code, $cal);
            if ($ns && $code ne 'und') {
                my $g = slurp("$cldr/cldr-dates-full/main/$code/ca-gregorian.json")->{main}{$code}{dates}{calendars}{gregorian};
                unless (JSON::PP->new->canonical->encode($ns->{days}) eq JSON::PP->new->canonical->encode($g->{days})) { $gregorian_alias_fail++; print "  days differ from the Gregorian: $code $cal\n" }
            }
        }
    }
    @index = sort { $a->[0] <=> $b->[0] } @index;
    my %seen; $seen{$_->[0]}++ && die "hash clash\n" for @index;
    my @parents = map { [ $_, cal_parent_of($_) // '' ] } sort ('und', @codes);
    die "a code is over 15 bytes\n" if grep { length($_->[0]) > 15 || length($_->[1]) > 15 } @parents;
    my $ptable = join '', map { pack 'Z16Z16', @$_ } @parents;
    my $index_len = 32 + 12 * @index + length $ptable;
    my $index = join '', map { pack 'VVV', $_->[0], $_->[1] + $index_len, $_->[2] } @index;
    my $stamp = fnv($index . $ptable . $body);
    my $archive = 'TKC1' . pack('VV', scalar(@index), $stamp) . pack('a16', $version) . pack('V', scalar @parents) . $index . $ptable . $body;
    open my $o, '>:raw', $names_out or die "$names_out: $!";
    print $o $archive;
    close $o;
    printf "%s: %d sections, %d bytes (%.2f MB), stamp %u\n", $names_out, scalar(@index), length($archive), length($archive) / 1048576, $stamp;
    printf "  %-17s %7d bytes\n", $_, $per_cal{$_} for @stored;
    printf "  aliases: %s\n", join(', ', map { "$_ = $same_as{$_}" } sort keys %same_as) || 'none';
    printf "  differ from their would-be alias: %s\n", join(', ', map { "$_ in $differs{$_} locales" } grep { !$same_as{$_} } sort keys %differs) || 'none';
    printf "  locales whose calendar days differ from the Gregorian: %d\n", $gregorian_alias_fail;
    # English and und, resolved, compiled in
    my @h = ("/* Generated by tools/calcompile from CLDR $version: the calendars' names in und and en,\n * resolved, and the layout of a set of names. Do not edit. */",
             '#ifndef TK_CAL_EN_H', '#define TK_CAL_EN_H', '',
             "#define TK_CN_MONTHS " . CN_MONTHS, "#define TK_CN_MONTHS_LEAP " . CN_MONTHS_LEAP, "#define TK_CN_LEAP_PATTERN " . CN_LEAP_PATTERN,
             "#define TK_CN_LEAP_NUMERIC " . CN_LEAP_NUMERIC, "#define TK_CN_CYCLIC " . CN_CYCLIC, "#define TK_CN_DATE_FORMATS " . CN_DATE_FORMATS,
             "#define TK_CN_DATETIME_FORMATS " . CN_DATETIME_FORMATS, "#define TK_CN_FIXED " . CN_FIXED, "#define TK_CAL_NAMES_STAMP ${stamp}u", '',
             '/* the calendar whose names a calendar shares, or itself */',
             'static const char *const tk_cal_names_alias[' . scalar(@names_cals) . '][2] = {',
             (map { "    { \"$_\", \"" . ($same_as{$_} // $_) . "\" }," } @names_cals), '};', '');
    for my $who ('und', 'en') {
        for my $cal (@stored) {
            my $r = resolved($who, $cal);
            (my $c = "${who}_$cal") =~ s/-/_/g;
            push @h, "static const char *const tk_cn_${c}_slots[" . scalar(@{ $r->{slot} }) . "] = {";
            push @h, join('', map { '    ' . (defined $_ ? c_str($_) : 'NULL') . ",\n" } @{ $r->{slot} });
            push @h, '};';
            my @sk = sort keys %{ $r->{skel} };
            push @h, "static const tk_pair tk_cn_${c}_skel[" . (scalar(@sk) || 1) . "] = {";
            push @h, @sk ? join('', map { '    { ' . c_str($_) . ', ' . c_str($r->{skel}{$_}) . " },\n" } @sk) : "    { NULL, NULL },\n";
            push @h, '};';
        }
    }
    push @h, '', 'typedef struct tk_cn_builtin {', '    const char *code;', '    const char *calendar;', '    const char *const *slots;', '    unsigned n_slots;', '    const tk_pair *skeletons;', '    unsigned n_skeletons;', '} tk_cn_builtin;', '';
    push @h, 'static const tk_cn_builtin tk_cn_builtins[' . (2 * @stored) . '] = {';
    for my $who ('und', 'en') {
        for my $cal (@stored) {
            my $r = resolved($who, $cal);
            (my $c = "${who}_$cal") =~ s/-/_/g;
            push @h, sprintf('    { "%s", "%s", tk_cn_%s_slots, %d, tk_cn_%s_skel, %d },', $who, $cal, $c, scalar(@{ $r->{slot} }), $c, scalar(keys %{ $r->{skel} }));
        }
    }
    push @h, '};', '', '#endif';
    open my $eh, '>:utf8', $en_out or die "$en_out: $!";
    print $eh join("\n", @h), "\n";
    close $eh;
    printf "%s: und and en for %d calendars\n", $en_out, scalar @stored;
}
