#!/usr/bin/perl

# Copyright (c) 2024-2026 Philipp Schafft

# licensed under Artistic License 2.0 (see LICENSE file)

# ABSTRACT: Script used to render a HTML page with font information

use strict;
use warnings;
use v5.16;

use Carp;
use Fcntl qw(SEEK_SET);
use Getopt::Long;
use Unicode::UCD;
use SIRTX::Font;
use Template;
use URI;
use Encode ();
use charnames ();

my %_attribute_names = (
);

my %config = (
    output          => undef,
    font            => undef,
    default_aliases => undef,
    gc              => undef,
    analyse         => undef,
    show_found      => 1,
    show_missing    => [],
    also_missing    => [],
    eigen_missing   => undef,
);

{
    my %opts;

    $opts{'output|o=s'}         = \$config{output};
    $opts{'font=s'}             = \$config{font};
    $opts{'default-aliases=s'}  = \$config{default_aliases};
    $opts{'gc!'}                = \$config{gc};
    $opts{'analyse!'}           = \$config{analyse};
    $opts{'show-found!'}        = \$config{show_found};
    $opts{'show-missing=s@'}    = \$config{show_missing};
    $opts{'also-missing=s@'}    = \$config{also_missing};
    $opts{'eigen-missing!'}     = \$config{eigen_missing};

    $opts{'help|h'} = sub {
        printf("Usage: %s [OPTIONS] -o output.html --font=input.sf\n", $0);
        say '';
        printf("OPTIONS:\n");
        printf(" %s\n", $_) foreach sort keys %opts;
        exit(0);
    };

    Getopt::Long::Configure('bundling');
    GetOptions(%opts);
}

my $font = SIRTX::Font->new;

if (defined($config{font})) {
    open(my $in, '<:raw', $config{font}) or croak 'Cannot open: '.$config{font};
    $font->read($in);
}

$font->add_default_aliases($config{default_aliases}) if defined $config{default_aliases};
$font->gc if $config{gc};
$font->analyse if $config{analyse};

if (defined $config{output}) {
    my $tt = Template->new;
    my %rows;
    my %missing_counts;
    my %extra_attributes;
    my $example;

    eval {
        my $img = $font->render("The quick brown fox jumps over the lazy dog!\nFranz jagt im komplett verwahrlosten Taxi quer durch Bayern.");
        my $uri = URI->new('data:');

        $uri->media_type('image/png');
        $uri->data($img->ImageToBlob(magick => 'png'));

        $example = $uri;
    };

    if ($config{show_found}) {
        foreach my $codepoint (keys %{$font->{chars}}) { # TODO: FIXME! Internal API!
            my $cp = build_codepoint($codepoint => \%rows);
            my $img = $font->export_glyph_as_image_magick($font->glyph_for($codepoint));
            my $uri = URI->new('data:');

            $uri->media_type('image/png');
            $uri->data($img->ImageToBlob(magick => 'png'));

            $cp->{uri} = $uri;
        }
    }

    foreach my $list (@{$config{show_missing}}) {
        $missing_counts{$list} //= $font->list_info($list);
        $missing_counts{$list}{missing} //= 0;

        foreach my $codepoint ($font->missing_codepoints_from($list)) {
            my $cp = build_codepoint($codepoint => \%rows);
            $cp->{classes} = 'missing';
            push(@{$cp->{missing_in} //= []}, $list);
            $missing_counts{$list}{missing}++;
        }
    }
    foreach my $list (@{$config{also_missing}}) {
        $missing_counts{'also-missing'} //= {characters => 0, missing => 0};
        foreach my $codepoint_pair (split /(?:\s*,\s*|\s+)/, $list) {
            my ($start, $end);

            if ($codepoint_pair =~ /^[Uu]\+([0-9a-fA-F]{4,6})(?:-[Uu]\+([0-9a-fA-F]{4,6}))?$/) {
                $start = hex $1;
                $end   = hex $2 if defined $2;
            } else {
                croak 'Bad code point: '.$codepoint_pair;
            }
            $end //= $start;
            for (my $codepoint = $start; $codepoint <= $end; $codepoint++) {
                my $cp = build_codepoint($codepoint => \%rows);
                $missing_counts{'also-missing'}{characters}++;
                unless (defined $cp->{uri}) {
                    $cp->{classes} = 'missing';
                    $missing_counts{'also-missing'}{missing}++;
                }
            }
        }
    }
    if ($config{eigen_missing}) {
        my $counts = $missing_counts{'eigen-missing'} //= {characters => 0, missing => 0};
        my $str = '';
        my %chars;

        eval { $str .= $font->get_attribute('font_name') };
        eval { $str .= $font->get_attribute('font_tag')->uuid };

        eval { $chars{$font->get_attribute('icontext')} = undef };

        $chars{ord $_} = undef foreach split //, $str;

        foreach my $codepoint (keys %chars) {
            my $cp = build_codepoint($codepoint => \%rows);
            $counts->{characters}++;
            unless (defined $cp->{uri}) {
                $cp->{classes} = 'missing';
                $counts->{missing}++;
            }
        }
    }
    foreach my $list (values %missing_counts) {
        $list->{found}      = $list->{characters} - $list->{missing};
        $list->{found_pc}   = sprintf('%.1f%%', 100 * $list->{found}   / $list->{characters});
        $list->{missing_pc} = sprintf('%.1f%%', 100 * $list->{missing} / $list->{characters});
    }

    foreach my $key ($font->list_attributes) {
        my $v = eval {$font->get_attribute($key)} // next;
        my $as_string;
        my $entry;

        next if $key eq 'height' || $key eq 'width' || $key eq 'bits'; # handled specifically

        $entry = $extra_attributes{$key} = {
            value => $v,
        };

        if ($key eq 'reverse_slant') {
            $as_string = $v ? 'true' : 'false';
        } elsif ($key eq 'icontext') {
            $as_string = sprintf('U+%04X "%s"', $v, chr($v));
            $entry->{anchor} = sprintf('#U+%04X', $v);
        } elsif (ref $v) {
            if (eval {$v->isa('SIRTX::Datecode')}) {
                $as_string = $v->iso8601;
            } elsif (eval {$v->isa('Data::Identifier')}) {
                $as_string = $v->ise;

                if (defined(my $generator = $v->generator(default => undef, no_defaults => 1)) && defined(my $request = $v->request(default => undef, no_defaults => 1))) {
                    if ($generator->eq('55febcc4-6655-4397-ae3d-2353b5856b34')) {
                        $entry->{rgb} = $request;
                    }
                }
            }
        }

        eval {
            my $img = $font->render($as_string // $v);
            my $uri = URI->new('data:');

            $uri->media_type('image/png');
            $uri->data($img->ImageToBlob(magick => 'png'));

            $entry->{as_img} = $uri;
        };

        $entry->{as_string} = $as_string // $v;

    }

    open(my $out, '>:utf8', $config{output}) or croak 'Cannot open output: '.$config{output};

    $tt->process(\*DATA, {
            info => {
                width => $font->width,
                height => $font->height,
                bits => $font->bits,
                codepoints => scalar($font->codepoints),
                glyphs => scalar($font->glyphs),
                scale => $font->height <= 16 ? 2 : undef,
            },
            example => $example,
            missing_counts => \%missing_counts,
            missing_counts_keys => [sort keys %missing_counts],
            rows => [sort {$a->{first} <=> $b->{first}} values %rows],
            extra_attributes => \%extra_attributes,
            extra_attributes_keys => [sort keys %extra_attributes],
            attribute_names => \%_attribute_names,
            ucd => {
                version => scalar(Unicode::UCD::UnicodeVersion()),
            },
    }, $out);
}

sub build_codepoint {
    my ($codepoint, $rows) = @_;
    my $row = $rows->{int($codepoint / 16)} //= {first => ($codepoint & 0xFFFFF0), chars => []};
    return $row->{chars}[$codepoint & 0xF] //= {
        codepoint   => $codepoint,
        is_print    => ($codepoint > 0x20 && ($codepoint < 0x7F || $codepoint > 0x9F) && scalar(chr($codepoint) !~ /^\s$/) && $codepoint != 0x00AD && $codepoint != 0x00A0),
        is_space    => ($codepoint == 0x00A0 || $codepoint == 0x20 || scalar(chr($codepoint) =~ /^\s$/)),
        as_utf8     => join('', map {sprintf('\\x%02X', ord($_))} split //, Encode::encode('UTF-8', chr($codepoint))),
        name        => scalar(charnames::viacode($codepoint)),
    };
}

#ll
__DATA__
<!DOCTYPE html>
<html>
    <head>
        <title>Font Information[% IF extra_attributes.font_name %] for "[% extra_attributes.font_name.as_string | html %]"[% END %]</title>
        <meta charset="utf-8">
        <style>
:root {
    --pattern-colour: #CCCCCC;
}

body {
    background-color: white;
    color: black;
}

a:link, a:visited {
    color: unset;
    text-decoration: unset;
}
a {
    user-select: text;
}

table, tr, td, th {
    border: 1px solid black;
    border-collapse: collapse;
    vertical-align: top;
    background-color: white;
}
td, th {
    padding: 5px;
}
td, th:not([colspan]) {
    text-align: left;
}

#character-table td:hover {
    scale: 2;
    outline: 1px solid black;
}

.numbercell {
    text-align: right;
}

.sf img {
[% IF info.scale %]
    scale: [% info.scale | html %];
[% END %]
    image-rendering: crisp-edges;
    border: 1px solid black;
    padding: 1px;
    margin: 3px;
}
.example img {
    scale: 1 !important;
}
span.example img {
    margin: 0 3px;
[% IF extra_attributes.baseline %]
    vertical-align: baseline;
    margin-bottom: -[% (info.height - extra_attributes.baseline.value + 1) | html %]px;
[% ELSE %]
    vertical-align: bottom;
[% END %]
}

.colour-chip {
    display: inline-block;
    border: 2px outset black;
    border-radius: 5px;
    width: 1em;
    height: 1em;
}

.char {
    font-size: 150%;
}

.name, .utf8, .missing-in {
    font-size: 50%;
}

.nobr {
    white-space: pre;
}

.charinfo {
    text-align: center;
}

.undef {
    background-color: #CCCCCC;
}
.ascii {
    background-color: #C0FFC0;
}
.missing {
    background-color: #FFC0C0;
}
#character-table :target {
    background-color: #C0C0FF;
}
.nonprint {
    background-size: 20px 20px;
    background-image: repeating-linear-gradient(45deg, var(--pattern-colour) 0, var(--pattern-colour) 2px, transparent 0, transparent 50%);
}
.space {
    background-size: 20px 20px;
    background-image: radial-gradient(var(--pattern-colour) 2px, transparent 2px);
}
        </style>
    </head>
    <body>
        <h1 id="top">Font Information[% IF extra_attributes.font_name %] for "[% extra_attributes.font_name.as_string | html %]"[% END %]</h1>
        <table>
            <tr>
                <th>Key</th>
                <th>Value</th>
            </tr>
            <tr>
                <th>Cell size</th>
                <td>[% info.width | html %]x[% info.height | html %]</td>
            </tr>
            <tr>
                <th>Bits per pixel</th>
                <td>[% info.bits | html %]</td>
            </tr>
            <tr>
                <th>Codepoints</th>
                <td>[% info.codepoints | html %]</td>
            </tr>
            <tr>
                <th>Glyphs</th>
                <td>[% info.glyphs | html %]</td>
            </tr>
            [% IF extra_attributes_keys.0 %]
            <tr>
                <th colspan="2">Extra attributes</th>
            </tr>
            [% FOREACH key IN extra_attributes_keys %]
            [% attr = extra_attributes.$key %]
            <tr>
                <th>[% IF attribute_names.$key %][% attribute_names.$key | html %][% ELSE %]<code>[% key | html %]</code>[% END %]</th>
                <td>
                [% attr.as_string | html %]
                [% IF attr.as_img %]<span class="sf example"><img src="[% attr.as_img | html %]"></span>[% END %]
                [% IF attr.rgb %][<span class="colour-chip" style="background-color: [% attr.rgb | html %]">&nbsp;</span> [% attr.rgb | html %]][% END %]
                [% IF attr.anchor %]<a href="[% attr.anchor | html %]">&#10515;</a>[% END %]
                </td>
            </tr>
            [% END %]
            [% END %]
        </table>
        [% IF missing_counts_keys.0 %]
        <h2 id="missing">Missing counts</h2>
        <table>
            <tr>
                <th>List</th>
                <th>Total</th>
                <th>Found</th>
                <th>Missing</th>
                <th>Progress</th>
            </tr>
            [% FOREACH key IN missing_counts_keys %]
            [% list = missing_counts.$key %]
            <tr>
                <th>[% key | html %]</th>
                <td class="numbercell">[% list.characters | html %]</td>
                <td class="numbercell">[% list.found | html %] ([% list.found_pc | html %])</td>
                <td class="numbercell">[% list.missing | html %] ([% list.missing_pc | html %])</td>
                <td><progress value="[% list.found | html %]" max="[% list.characters | html %]"></td>
            </tr>
            [% END %]
        </table>
        [% END %]
        [% IF example %]
        <h2 id="example">Example</h2>
        <div class="sf example"><img src="[% example | html %]"></div>
        [% END %]
        <h2 id="characters">Character list</h2>
        <table id="character-table">
            <tr>
                <th>&nbsp;</th>
                [% FOREACH x IN [0..15] %]
                <th>[% x | format('_%X') | html %]</th>
                [% END %]
            </tr>
            [% FOREACH row IN rows %]
            <tr>
                <th>[% (row.first / 16) | format('%X_') | html %]</th>
                [% FOREACH x IN [0..15] %]
                [% cp = row.chars.$x %]
                [% IF cp %]
                <td id="[% cp.codepoint | format('U+%04X') | html %]" class="charinfo [% cp.classes | html %] [% IF cp.codepoint < 128 %]ascii[% END %] [% IF cp.is_space %]space[% ELSIF cp.is_print %]print[% ELSE %]nonprint[% END %]">
                    <a href="#[% cp.codepoint | format('U+%04X') | html %]" draggable="false">
                        <div class="char">[% IF cp.is_print %]&#[% cp.codepoint | html %];[% ELSE %]&nbsp;[% END %]</div>
                        <div class="cp">[% cp.codepoint | format('U+%04X') | html %]</div>
                        [% IF cp.uri %]<div class="sf"><img src="[% cp.uri | html %]"></div>[% END %]
                        <div class="utf8">"[% cp.as_utf8 | html %]"</div>
                        <div class="name">[% cp.name | html %]</div>
                        [% IF cp.missing_in %]<div class="missing-in">[% FOREACH list IN cp.missing_in %]<span class="nobr">[% list | html %]</span> [% END %]</div>[% END %]
                    </a>
                </td>
                [% ELSE %]
                <td class="undef"></td>
                [% END %]
                [% END %]
            </tr>
            [% END %]
        </table>
        <p>Generated using UCD version <code>[% ucd.version | html %]</code>.</p>
    </body>
</html>
