using System.Diagnostics;
using System.Text;
using System.IO;

namespace AmiReel.Services;

public sealed class ProcessRunner
{
    public async Task<string> RunAsync(
        string executable,
        IEnumerable<string> arguments,
        Action<string>? log,
        Action<TimeSpan>? position,
        CancellationToken token,
        bool allowFailure = false,
        Action<FfmpegOutputLine>? 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<FfmpegOutputLine>? outputHandler)
    {
        while (await reader.ReadLineAsync(token) is { } line)
        {
            output.AppendLine(line);
            FfmpegOutputLine parsed = FfmpegProgressParser.Parse(line);
            if (!IsNoise(parsed))
                log?.Invoke(line);
            if (parsed.Time is { } time)
                position?.Invoke(time);
            outputHandler?.Invoke(parsed);
        }
    }

    internal static bool IsNoise(FfmpegOutputLine 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 FfmpegOutputFilter.IsBoilerplateLine(line)
            || line.StartsWith("Side data:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("encoder         :", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("title           :", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("CPB properties:", StringComparison.OrdinalIgnoreCase)
            || line.StartsWith("[", StringComparison.OrdinalIgnoreCase);
    }
}
