#!/usr/bin/env raku
use v6.d;
use JSON::Fast;

=begin pod

=head1 NAME

generate-flags.raku - Generate FFmpegFlags from ffmpeg help output

=head1 DESCRIPTION

Automatically generates FFmpegFlags.rakumod from ffmpeg -h full output.

=head1 USAGE

    raku tools/generate-flags.raku

=end pod

my constant $DEFAULT-OUTPUT = 'lib/FFmpegProgressBar/FFmpegFlags.rakumod';

sub get-help-text(Str $ffmpeg = 'ffmpeg' --> Str) {
    my $proc = try run $ffmpeg, '-hide_banner', '-h', 'full', :out, :merge;
    die "Could not run ffmpeg — is it installed and in PATH?" unless $proc.defined;
    die "ffmpeg exited unexpectedly (code { $proc.exitcode })" 
        unless $proc.exitcode == 0 || $proc.exitcode == 1;
    my $text = $proc.out.slurp(:close);
    die "Got empty output from ffmpeg -h full" if $text.trim eq '';
    return $text;
}

sub get-version(Str $ffmpeg = 'ffmpeg' --> Str) {
    my $proc = try run $ffmpeg, '-version', :out, :err;
    return 'unknown' unless $proc.defined;
    my $first-line = try { $proc.out.slurp(:close) } // '';
    try { $proc.err.slurp(:close) };  # Drain stderr
    if $first-line ~~ / 'ffmpeg version' \s+ (\S+) / {
        return ~$0;
    }
    return 'unknown';
}

sub extract-flags(Str $text) {
    my @flags;
    for $text.lines -> $line {
        next unless $line.trim;
        # Format: "-flag[<stream_spec>] <arg> description" - capture full flag and rest
        # NOTE: We use \S+ to capture flags like -ac[:<stream_spec>] which contain []
        if $line ~~ / ^ \s* '-' (\S+) \s+ (.*) $ / {
            my $flag-part = ~$0 // '';
            my $rest-m = $1;
            my $rest = $rest-m.defined ?? ~$rest-m !! '';
            my $flag = '-' ~ $flag-part;
            @flags.push: { :$flag, :$rest };
        }
    }
    return @flags;
}

sub takes-value(Str $flag, Str $rest --> Bool) {
    # Has <arg> placeholder = value-taking (most reliable indicator)
    return True if $rest ~~ / '<' .* '>' /;
    # Has stream specifier like -c:v = value-taking
    return True if $flag ~~ /^ '-' <[\w\-]>+ ':'/;
    # Description starts with a number default value followed by 'set' keyword
    # Pattern: digit(s) followed by whitespace then a word containing 'set'
    # This avoids false positives like "0 disable something" where flag is a toggle
    return True if $rest ~~ / ^ \d+ \s+ <[a..zA..Z]>+ / && $rest.lc.contains('set');
    return False;
}

sub normalize-flag(Str $flag --> Str) {
    my $f = $flag.subst(/ \[ .*? \] $/, '', :g);  # strip all [:<stream_spec>] at end-of-string
    if $f ~~ /^ ('-' <[\w\-]>+ ':') .* $/ {
        return ~$0;
    }
    return $f;
}

sub MAIN(Str :$ffmpeg = 'ffmpeg', Str :$output = $DEFAULT-OUTPUT, Str :$module-version) {
    note "Generating FFmpegFlags from $ffmpeg...";
    
    my $text = get-help-text($ffmpeg);
    note "Got { $text.lines.elems } lines of help output";

    my $version = get-version($ffmpeg);
    note "FFmpeg version: $version";

    my $mod-ver = $module-version // try {
        from-json(slurp($*PROGRAM.parent.parent.add('META6.json')))<version>
    } // '0.0.1';

    # Extract flags
    my @flags = extract-flags($text);
    note "Extracted { @flags.elems } flag lines";

    # Known standalone flags (no value) - these override auto-detection
    my $known-no-value = set <
        -y -n -an -vn -sn -dn -hide_banner -re -copyts -start_at_zero 
        -accurate_seek -noaccurate_seek -xerror -fix_sub_duration -autorotate 
        -autoscale -nostdin -nostats -shortest -bitexact -benchmark
        --help -? -h -help -stats
    >;

    # Flags that take values but lack <arg> in help text
    my $known-value-taking = set <
        -i
        -thread_queue_size
        -stats_enc_post_fmt
        -stats_enc_pre_fmt
        -stats_mux_pre_fmt
    >;

    # Build sets: classify each flag
    my %value-taking;
    my %no-value;

    for @flags -> %f {
        my $flag = normalize-flag(%f<flag>);
        
        # Known no-value takes priority
        if $flag ∈ $known-no-value {
            %no-value{$flag} = True;
            %value-taking{$flag}:delete;  # Remove from value-taking if present
            next;
        }
        
        my $takes = takes-value($flag, %f<rest>);
        
        if $takes {
            %value-taking{$flag} = True;
        } else {
            %no-value{$flag} = True;
        }
    }

    # Final: value-taking wins (flags seen with <arg> anywhere override no-value)
    for %value-taking.keys -> $k {
        %no-value{$k}:delete if %no-value{$k}:exists;
    }

    # Force-add all known-no-value flags (handles undocumented flags like -nostats)
    for $known-no-value.keys -> $flag {
        %no-value{$flag} = True;
        %value-taking{$flag}:delete;
    }

    # Force-add known-value-taking flags (overrides misclassification)
    for $known-value-taking.keys -> $flag {
        %value-taking{$flag} = True;
        %no-value{$flag}:delete;
    }

    note "Classified: { %value-taking.elems } value-taking, { %no-value.elems } no-value";

    # Extract prefixes (flags ending with : or patterns like -c:v, -b:a)
    my %prefixes;
    for %value-taking.keys -> $flag {
        # Existing: flags ending with : (e.g., -vf:)
        if $flag ~~ / ':' $/ {
            %prefixes{$flag} = True;
        }
        # Auto-detect -X:Y patterns like -c:v, -b:a, -filter:v (not ending with :)
        if $flag ~~ /^ '-' <[\w\-]>+ ':' <[\w\-]>+ $/ {
            %prefixes{$flag} = True;
        }
    }

    # Add essential prefixes that are commonly used (including ones not ending with :)
    my $essential-prefixes = set <
        -vf: -filter:v: -af: -filter:a: -c: -b: -codec: 
        -filter_complex: -filter_complex_script:
        -c:v -c:a -c:s -c:d
        -b:a -b:v
        -filter:v -filter:a
        -r: -s:
        -map:
    >;
    for $essential-prefixes.keys -> $p {
        %prefixes{$p} = True;
    }

    # Duration-affecting options (excluding -t -to -ss which are already in essential-prefixes)
    my $duration-affecting = set <
        -sseof -itsoffset -itoffset -itsscale -stream_loop -timestamp -avoid_negative_ts
    >;

    # Validation
    die "Empty value-taking set!" if %value-taking.elems == 0;
    if %value-taking.elems < 200 {
        die "Fatal: Only { %value-taking.elems } value-taking flags parsed (expected ~500) - ffmpeg version may have changed significantly";
    }
    note "Final: { %value-taking.elems } value-taking, { %no-value.elems } no-value, { %prefixes.elems } prefixes";

    # Render output
    my @value-sorted = %value-taking.keys.sort;
    my $value-rendered = @value-sorted.map({ "    '$_'," }).join("\n");

    my @no-value-sorted = %no-value.keys.sort;
    my $no-value-rendered = @no-value-sorted.map({ "    '$_'," }).join("\n");

    # Sort prefixes by length descending to match runtime behavior (longest-match-first)
    my @prefix-sorted = %prefixes.keys.sort({ -$_.chars });
    my $prefixes-rendered = @prefix-sorted.map({ "    '$_' => True," }).join("\n");

    my @duration-sorted = $duration-affecting.keys.sort;
    my $duration-rendered = @duration-sorted.map({ "    '$_'," }).join("\n");

    my $output-text = 'use v6.d;

unit class FFmpegProgressBar::FFmpegFlags:ver<PLACEHOLDER_MODULE_VER>:auth<zef:sasha>;

=begin pod

=head1 NAME

FFmpegProgressBar::FFmpegFlags - FFmpeg flag constants extracted from ffmpeg -h full

=head1 DESCRIPTION

Contains sets of ffmpeg flags extracted from `ffmpeg -h full` output.
Generated from ffmpeg version PLACEHOLDER_VERSION.

This module provides comprehensive flag lists to help the parser correctly
identify value-taking flags vs standalone flags.

=end pod

our constant $value-taking-opts = set
PLACEHOLDER_VALUE
;

our constant $no-value-flags = set
PLACEHOLDER_NO_VALUE
;

our constant $duration-affecting-opts = set
PLACEHOLDER_DURATION
;

our constant $value-taking-prefixes = hash
PLACEHOLDER_PREFIXES
;

our sub prefix-takes-value(Str $arg --> Bool) is export {
    return False unless $arg.starts-with("-");
    return True if $arg ∈ $value-taking-opts;
    return True if $value-taking-prefixes{$arg}:exists;
    return True if $arg ∈ $duration-affecting-opts;
    for $value-taking-prefixes.keys.sort(-*.chars) -> $p {
        return True if $arg.starts-with($p);
    }
    if $arg ~~ /^ "-" <[\w\-]>+ ":" \d+ $/ {
        return True;
    }
    return False;
}

our sub is-ffmpeg-flag(Str $arg --> Bool) is export {
    return False unless $arg.starts-with("-");
    return True if prefix-takes-value($arg);
    return True if $arg ∈ $no-value-flags;
    return False;
}
';

    $output-text = $output-text.subst('PLACEHOLDER_VERSION', $version);
    $output-text = $output-text.subst('PLACEHOLDER_MODULE_VER', $mod-ver);
    $output-text = $output-text.subst('PLACEHOLDER_VALUE', $value-rendered);
    $output-text = $output-text.subst('PLACEHOLDER_NO_VALUE', $no-value-rendered);
    $output-text = $output-text.subst('PLACEHOLDER_DURATION', $duration-rendered);
    $output-text = $output-text.subst('PLACEHOLDER_PREFIXES', $prefixes-rendered);

    my $output-dir = $output.IO.dirname;
    die "Output directory does not exist: $output-dir" unless $output-dir.IO.d;

    # Atomic write: write to temp file, then rename to target
    # On Windows, rename with :overwrite is not guaranteed atomic, so unlink first
    my $temp-path = "$output.tmp";
    LEAVE try $temp-path.IO.unlink;  # clean up temp file after block exits
    $temp-path.IO.spurt($output-text);
    $output.IO.unlink if $output.IO.e;
    $temp-path.IO.rename($output);
    note "Written to $output";
}