#!/usr/bin/env perl
use strict;
use warnings;

# Embedded kernels are private implementation details, not external crates.
# Preserve their Rust documentation examples as unit tests of that exact source.
sub extract {
    my ($source, $file) = @_;
    my ($fenced, $example, $no_run) = (0, 0, 0);
    my (@rewritten, @examples, @body);
    my $start = 0;
    my $line_number = 0;
    for my $line (split /(?<=\n)/, $source) {
        ++$line_number;
        if ($line =~ /^([ \t]*(?:\/\/\/|\/\/!)) ?([^\r\n]*)(?:\r?\n)?$/) {
            my ($prefix, $content) = ($1, $2);
            if ($content =~ /^```([^`]*)$/) {
                my $label = $1;
                if (!$fenced) {
                    $fenced = 1;
                    $example = $label =~ /^(?:|rust|no_run|text,xml-sec-example(?:,no_run)?)$/;
                    $no_run = $label =~ /no_run/;
                    die "$file:$line_number: unsupported executable example $label\n"
                        if $label =~ /compile_fail|ignore/;
                    if ($example) {
                        $start = $line_number;
                        @body = ();
                        push @rewritten, "$prefix ```text,xml-sec-example" . ($no_run ? ',no_run' : '') . "\n";
                        next;
                    }
                } else {
                    die "$file:$line_number: unterminated documentation fence\n" if $label ne '';
                    if ($example) {
                        push @examples, { line => $start, body => join("\n", @body), no_run => $no_run };
                    }
                    ($fenced, $example) = (0, 0);
                }
            } elsif ($fenced && $example) {
                $content =~ s/^# ?// if $content =~ /^#(?: |$)/;
                $content =~ s/^##/#/;
                die "$file:$line_number: explicit main needs a harness adapter\n" if $content =~ /\bfn\s+main\s*\(/;
                push @body, $content;
            }
        } elsif ($fenced) {
            die "$file:$line_number: documentation fence escaped its comment\n";
        }
        push @rewritten, $line;
    }
    die "$file: unclosed documentation fence\n" if $fenced;
    return (join('', @rewritten), \@examples);
}

if (@ARGV == 1 && $ARGV[0] eq '--self-test') {
    my $input = "/// ```\n/// # let a = 2;\n/// assert_eq!(a, 2);\n/// ```\n/// ```text\n/// not Rust\n/// ```\n/// ```no_run\n/// unreachable!();\n/// ```\n";
    my ($rewritten, $examples) = extract($input, 'sample.rs');
    die "lost examples\n" unless @$examples == 2;
    die "changed runnable semantics\n" unless $examples->[0]{body} eq "let a = 2;\nassert_eq!(a, 2);";
    die "lost compile-only semantics\n" unless $examples->[1]{no_run};
    my ($again, $roundtrip) = extract($rewritten, 'sample.rs');
    die "non-idempotent import\n" unless $again eq $rewritten && @$roundtrip == 2;
    print "XML kernel example extraction passed\n";
    exit 0;
}

my ($backend, $root, @files) = @ARGV;
die "usage: xml-kernel-examples.pl roxmltree|xmloxide ROOT SOURCE...\n"
    unless defined($root) && @files && $backend =~ /^(?:roxmltree|xmloxide)$/;
my $tests = "// Generated by scripts/xml-kernel-examples.pl; edit the source examples.\n";
# rustdoc itself permits unused example imports, bindings and mutability.
$tests .= "#![allow(unused_imports, unused_variables, unused_mut)]\n";
my $count = 0;
for my $file (@files) {
    open my $input, '<:raw', "$root/$file" or die "read $file: $!\n";
    local $/;
    my $original = <$input>;
    close $input or die "close $file: $!\n";
    my ($rewritten, $examples) = extract($original, $file);
    if ($rewritten ne $original) {
        open my $output, '>:raw', "$root/$file" or die "write $file: $!\n";
        print {$output} $rewritten or die "write $file: $!\n";
        close $output or die "close $file: $!\n";
    }
    my $ordinal = 0;
    for my $example (@$examples) {
        ++$count;
        ++$ordinal;
        (my $name = $file) =~ s/[^A-Za-z0-9]/_/g;
        $tests .= "\n// $file:$example->{line}; original " . ($example->{no_run} ? 'compile-only' : 'runnable') . " example.\n";
        $tests .= "#[test]\nfn ${name}_example_${ordinal}() -> impl std::process::Termination {\n";
        $tests .= "    use crate::parser_$backend as $backend;\n";
        $tests .= "    let _compile_only = || {\n" if $example->{no_run};
        # Leading bytes inside multiline Rust strings are data, not indentation.
        # rustfmt may format tokens afterwards but must see the original bytes.
        $tests .= $example->{body} . "\n";
        $tests .= $example->{no_run} ? "    };\n}\n" : "}\n";
    }
}
open my $output, '>:raw', "$root/documentation_tests.rs" or die "write examples: $!\n";
print {$output} $tests or die "write examples: $!\n";
close $output or die "close examples: $!\n";
print "Embedded $count $backend documentation examples\n";
