using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;

namespace AmiReel.Services;

public sealed partial class ProcessRunner
{
    public sealed record ProcessOutput(string Line, TimeSpan? Time, double? FramesPerSecond, double? Speed, int? Frame);

    public async Task<string> RunAsync(
        string executable,
        IEnumerable<string> arguments,
        Action<string>? log,
        Action<TimeSpan>? position,
        CancellationToken token,
        bool allowFailure = false,
        Action<ProcessOutput>? outputHandler = null)
    {
        ProcessStartInfo start = new()
        {
            FileName = executable,
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            StandardErrorEncoding = Encoding.UTF8,
            StandardOutputEncoding = Encoding.UTF8
        };
        foreach (string argument in arguments)
            start.ArgumentList.Add(argument);

        using Process process = new() { StartInfo = start, EnableRaisingEvents = true };
        StringBuilder output = new();
        process.Start();

        Task stdout = PumpAsync(process.StandardOutput, output, log, position, token, outputHandler);
        Task stderr = PumpAsync(process.StandardError, output, log, position, token, outputHandler);
        using CancellationTokenRegistration registration = token.Register(() =>
        {
            try { if (!process.HasExited) process.Kill(true); } catch { }
        });

        await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(token));
        if (process.ExitCode != 0 && !allowFailure)
            throw new InvalidOperationException($"Process exited with code {process.ExitCode}.\n{output}");
        return output.ToString();
    }

    private static async Task PumpAsync(
        StreamReader reader, StringBuilder output, Action<string>? log,
        Action<TimeSpan>? position, CancellationToken token, Action<ProcessOutput>? outputHandler)
    {
        while (await reader.ReadLineAsync(token) is { } line)
        {
            output.AppendLine(line);
            ProcessOutput parsed = ParseOutput(line);
            if (!IsNoise(parsed))
                log?.Invoke(line);
            if (parsed.Time is { } time)
                position?.Invoke(time);
            outputHandler?.Invoke(parsed);
        }
    }

    private static ProcessOutput ParseOutput(string line)
    {
        TimeSpan? time = null;
        double? fps = null;
        double? speed = null;
        int? frame = null;

        Match timeMatch = TimeRegex().Match(line);
        if (timeMatch.Success && TimeSpan.TryParse(timeMatch.Groups[1].Value, CultureInfo.InvariantCulture, out TimeSpan parsedTime))
            time = parsedTime;

        Match frameMatch = FrameRegex().Match(line);
        if (frameMatch.Success)
        {
            if (int.TryParse(frameMatch.Groups["frame"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedFrame))
                frame = parsedFrame;
            if (double.TryParse(frameMatch.Groups["fps"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedFps))
                fps = parsedFps;
            if (double.TryParse(frameMatch.Groups["speed"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedSpeed))
                speed = parsedSpeed;
        }

        return new ProcessOutput(line, time, fps, speed, frame);
    }

    private static bool IsNoise(ProcessOutput output)
    {
        string line = output.Line.TrimStart();
        if (string.IsNullOrWhiteSpace(line))
            return true;

        if (line.StartsWith("frame=", StringComparison.OrdinalIgnoreCase))
            return true;

        if (output.Frame is not null || output.Time is not null)
            return true;

        if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase)
            || line.Contains("skipped", StringComparison.OrdinalIgnoreCase)
            || line.Contains("could not", StringComparison.OrdinalIgnoreCase)
            || line.Contains("failed", StringComparison.OrdinalIgnoreCase)
            || line.Contains("not found", StringComparison.OrdinalIgnoreCase)
            || line.Contains("Output file is empty", StringComparison.OrdinalIgnoreCase))
            return false;

        return line.StartsWith("ffmpeg version ", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("built with ", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("configuration:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("libavutil", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("libavcodec", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("libavformat", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("libavdevice", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("libavfilter", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("libswscale", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("libswresample", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("Input #", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("Output #", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("Stream mapping:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("Stream #", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("Metadata:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("Side data:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("encoder         :", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("title           :", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("CPB properties:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("Press [q] to stop", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("[", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("Duration:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("video:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("audio:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("subtitle:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("other streams:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("global headers:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("muxing overhead:", StringComparison.OrdinalIgnoreCase);
    }

    [GeneratedRegex(@"time=(\d{2}:\d{2}:\d{2}(?:\.\d+)?)", RegexOptions.CultureInvariant)]
    private static partial Regex TimeRegex();

    [GeneratedRegex(@"frame=\s*(?<frame>\d+).*?fps=\s*(?<fps>\d+(?:\.\d+)?).*?speed=\s*(?<speed>\d+(?:\.\d+)?)x", RegexOptions.CultureInvariant)]
    private static partial Regex FrameRegex();
}
