use OpenMPT::Bindings::Module;
use OpenMPT::Bindings::Util;

constant BUF-FRAMES = 4096;

# ============================================================================
# MAIN
# ============================================================================

multi sub MAIN(Bool :$list-extensions!) {
    .say for OpenMPT::Bindings::Module.supported-extensions.sort;
}

multi sub MAIN(
    *@files,
    Str  :$format     = 'wav',
    Int  :$rate       = 48000,
    Int  :$subsong    = 0,
    Int  :$repeat     = 0,
    Numeric :$seconds = 0,
    Numeric :$seek    = 0,
    Numeric :$gain    = 0,
    Numeric :$tempo   = 1.0,
    Str     :$output,
    Str     :$output-dir,
    Bool    :$quiet,
    Bool    :$force,
) {
    die "No input files" unless @files;
    die "Unsupported format: $format"
        unless $format eq any(<wav mp3 flac ogg opus>);
    sub has-ffmpeg {
        state $has;
        return $has with $has;
        my $sep = $*DISTRO.is-win ?? ';' !! ':';
        my $ext = $*DISTRO.is-win ?? '.exe' !! '';
        $has = so %*ENV<PATH>.split($sep).first(-> $dir { "$dir/ffmpeg$ext".IO.x });
    }
    die "ffmpeg required for $format format; install it or use --format=wav"
        if $format ne 'wav' && !has-ffmpeg();
    die "Cannot use both --output and --output-dir" if $output.defined && $output-dir.defined;

    for @files -> $path {
        next unless $path.IO.e;

        my $mod = try OpenMPT::Bindings::Module.from-file($path);
        if $! {
            note "Skipping $path: { $!.message }";
            next;
        }

        my $outpath = resolve-output($path, $format, $output, $output-dir, :multi(@files > 1));
        die "Output $outpath already exists. Use --force to overwrite"
            if $outpath.IO.e && !$force;
        note "--- { $path.IO.basename } → { $outpath.IO.basename } ---" unless $quiet;

        $mod.select-subsong($subsong);
        $mod.set-repeat-count($repeat);
        $mod.set-position($seek) if $seek > 0;
        $mod.set-mastergain(($gain * 100).Int) if $gain != 0;
        $mod.set-ctl('play.tempo_factor', $tempo.Num) if $tempo != 1.0;

        my $max-frames = $seconds > 0 ?? ($rate * $seconds).Int !! 0;
        my $total = render-wrapper($mod, $rate, $max-frames, $outpath, $format, $quiet);
        next if $total < 0;

        unless $quiet {
            my $dur = fmt-time($total / $rate);
            note "Wrote {$outpath.IO.basename}: {$dur}, {$total} frames";
        }
    }
}

# ============================================================================
# Render dispatcher
# ============================================================================

sub render-wrapper(
    OpenMPT::Bindings::Module $mod, int32 $rate, Int $max-frames,
    Str $outpath, Str $format, Bool $quiet
    --> Int
) {
    my $t0 = now;
    if $format eq 'wav' {
        render-wav($mod, $rate, $outpath, $max-frames, $quiet, $t0)
    } else {
        render-ffmpeg($mod, $rate, $outpath, $max-frames, $format, $quiet, $t0)
    }
}

# ============================================================================
# WAV rendering (native)
# ============================================================================

sub render-wav(
    OpenMPT::Bindings::Module $mod, int32 $rate, Str $outpath,
    Int $max-frames, Bool $quiet, Numeric $t0
    --> Int
) {
    $outpath.IO.parent.mkdir unless $outpath.IO.parent.d;
    my $progress-last = 0;
    my $rendered = 0;
    my $fh = $outpath.IO.open(:w, :bin);
    LEAVE .close with $fh;

    $fh.write: wav-header($rate, 0);

    while True {
        my $remain = $max-frames > 0 ?? $max-frames - $rendered !! BUF-FRAMES;
        last if $remain <= 0;
        my $count = min(BUF-FRAMES, $remain);
        my $got = $mod.write-interleaved-stereo-s16-to-handle($fh, $rate, $count);
        last unless $got;
        $rendered += $got;
        $progress-last = show-progress($outpath.IO.basename, $rendered, $max-frames, $rate, $t0, $progress-last)
            unless $quiet;
    }

    my $data-size = $rendered * 4;
    $fh.seek(0, SeekFromBeginning);
    $fh.write(wav-header($rate, $data-size));
    $rendered
}

# ============================================================================
# ffmpeg pipe rendering
# ============================================================================

sub ffmpeg-args(Str $outpath, int32 $rate, Str $format --> List) {
    my @base = < -y -f s16le -ar>;
    my @codec = do given $format {
        when 'mp3'  { < -codec:a libmp3lame -b:a 320k > }
        when 'flac' { < -codec:a flac > }
        when 'ogg'  { < -codec:a libvorbis -q:a 5 > }
        when 'opus' { < -codec:a libopus -b:a 128k > }
        default     { die "unreachable: $format" }
    }
    flat(@base, $rate.Str, < -ac 2 -i pipe:0 >, @codec, $outpath).List
}

sub render-ffmpeg(
    OpenMPT::Bindings::Module $mod, int32 $rate, Str $outpath,
    Int $max-frames, Str $format, Bool $quiet, Numeric $t0
    --> Int
) {
    $outpath.IO.parent.mkdir unless $outpath.IO.parent.d;
    my $progress-last = 0;
    my $rendered = 0;
    my @args = ffmpeg-args($outpath, $rate, $format);
    my $proc = run 'ffmpeg', |@args, :in, :err($*ERR);

    while True {
        my $remain = $max-frames > 0 ?? $max-frames - $rendered !! BUF-FRAMES;
        last if $remain <= 0;
        my $count = min(BUF-FRAMES, $remain);
        my $got = $mod.write-interleaved-stereo-s16-to-handle($proc.in, $rate, $count);
        last unless $got;
        $rendered += $got;
        $progress-last = show-progress($outpath.IO.basename, $rendered, $max-frames, $rate, $t0, $progress-last)
            unless $quiet;
    }

    $proc.in.close;
    my $ec = $proc.exitcode;
    if $ec != 0 {
        note "\nffmpeg exited with code $ec" unless $quiet;
        $outpath.IO.unlink;
        -1
    } else {
        $rendered
    }
}

# ============================================================================
# Progress display
# ============================================================================

sub show-progress(Str $name, Int $rendered, Int $max-frames, int32 $rate, Numeric $t0, Int $progress-last --> Int) {
    return $progress-last unless $rendered - $progress-last >= $rate div 4;

    my $elapsed = now - $t0;
    my $elapsed-str = fmt-time($elapsed);

    if $max-frames > 0 {
        my $pct = 100 * $rendered / $max-frames;
        my $bar-width = 24;
        my $filled = ($pct * $bar-width / 100).Int;
        my $bar = '#' x $filled ~ '-' x ($bar-width - $filled);
        my $total-str = fmt-time($max-frames / $rate);

        $*ERR.print("\r$name  {$pct.fmt('%3.0f')}% [$bar] {$elapsed-str} / $total-str  ");
    } else {
        my $len = fmt-time($rendered / $rate);
        $*ERR.print("\r$name  $len  elapsed {$elapsed-str}  ");
    }
    $*ERR.flush;
    $rendered
}

# ============================================================================
# Helpers
# ============================================================================

# $output/$output-dir may be Str:U when not provided on CLI
sub resolve-output(Str $path, Str $format, Str $output, Str $output-dir, Bool :$multi --> Str) {
    if $output.defined {
        die "Multiple input files with --output will overwrite each other; use --output-dir instead" if $multi;
        return $output;
    }
    my $stem = $path.IO.extension('').basename;
    my $out = $output-dir.defined ?? $output-dir.IO.add("$stem.$format").Str !! "$stem.$format";
    $out
}


