using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.OutputFormat;
using ErsatzTV.FFmpeg.Pipeline;
using ErsatzTV.FFmpeg.Preset;
using ErsatzTV.FFmpeg.State;
using LanguageExt;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using static LanguageExt.Prelude;

namespace ErsatzTV.FFmpeg.Tests.Pipeline;

[TestFixture]
public class QsvPipelineBuilderTests
{
    private readonly ILogger _logger = Substitute.For<ILogger>();

    [Test]
    public void Qsv_PreferNativeDecoder_Should_Decode_Via_Vaapi_To_Software_Then_Qsv_Encode()
    {
        string command = BuildAndPrint(preferNativeDecoder: true);

        // VA-API decode, frames downloaded to software (NO hwaccel_output_format)
        command.ShouldContain("-hwaccel vaapi");
        command.ShouldNotContain("-hwaccel_output_format");
        command.ShouldNotContain("-hwaccel qsv");
        // no QSV *decoder* input option (decoder input options sit directly before "-readrate"/"-i";
        // "-c:v h264_qsv -" alone would also match the encoder's "-c:v h264_qsv -low_power ..." output option)
        command.ShouldNotContain("-c:v h264_qsv -readrate");
        // derived-device chain retained for the QSV encoder
        command.ShouldContain("-init_hw_device vaapi=va:/dev/dri/renderD128");
        command.ShouldContain("-init_hw_device qsv=hw@va");
        // software frames re-uploaded before QSV filters/encoder (proves NO bare vpp_qsv on VA-API frames)
        command.ShouldContain("hwupload=extra_hw_frames");
        // QSV encoder still used
        command.ShouldContain("h264_qsv");
    }

    [Test]
    public void Qsv_Default_Should_Decode_And_Encode_With_Qsv()
    {
        string command = BuildAndPrint(preferNativeDecoder: false);

        command.ShouldContain("-hwaccel qsv");
        command.ShouldContain("-hwaccel_output_format qsv");
        command.ShouldContain("h264_qsv");
        command.ShouldNotContain("-hwaccel vaapi");
    }

    [Test]
    public void Qsv_PreferNativeDecoder_Interlaced_Should_Hwupload_Before_Deinterlace_Qsv()
    {
        string command = BuildInterlacedAndPrint();

        // VA-API decode, software frames
        command.ShouldContain("-hwaccel vaapi");
        command.ShouldNotContain("-hwaccel_output_format");

        // software frames re-uploaded BEFORE deinterlace_qsv (never a bare deinterlace_qsv on VA-API frames)
        command.ShouldContain("hwupload=extra_hw_frames");
        command.ShouldContain("hwupload=extra_hw_frames=64,deinterlace_qsv");
        // exactly one deinterlace_qsv, and (assertion above) it is preceded by hwupload — so
        // there is no second, bare deinterlace_qsv running on VA-API frames
        (command.Split("deinterlace_qsv").Length - 1).ShouldBe(1);

        command.ShouldContain("h264_qsv");
    }

    // ersatztv#529: a stored qsvExtraHardwareFrames of 0 produced hwupload=extra_hw_frames=0, which
    // leaves the QSV pool no headroom. Measured on the deployed FFmpeg 8.1.2: with 0 the filter graph
    // fails with -12 and writes zero segments as soon as the input is not throttled (a work-ahead
    // start, or #350's cold-start burst); with 64 the same command writes segments either way.
    [TestCase(-1)]
    [TestCase(0)]
    [TestCase(1)]
    [TestCase(63)]
    public void Qsv_Should_Never_Upload_With_Less_Than_Minimum_Extra_Hardware_Frames(int configured)
    {
        string command = BuildAndPrint(preferNativeDecoder: true, maybeExtraHardwareFrames: configured);

        // pinned to the literal value measured against the deployed FFmpeg, not to the constant, so
        // that lowering the floor in code cannot quietly satisfy this test
        command.ShouldContain("hwupload=extra_hw_frames=64");

        // every upload site in the whole command, not just the one this pipeline happens to emit
        ShouldNeverUploadBelowMinimum(command);
    }

    [Test]
    public void Qsv_Interlaced_Should_Never_Deinterlace_Upload_Below_Minimum_Extra_Hardware_Frames()
    {
        // the deinterlace upload is a separate formatter fed from QsvPipelineBuilder, so it needs
        // its own guard (ersatztv#529)
        string command = BuildInterlacedAndPrint(maybeExtraHardwareFrames: 0);

        command.ShouldContain("hwupload=extra_hw_frames=64,deinterlace_qsv");
        ShouldNeverUploadBelowMinimum(command);
    }

    private static void ShouldNeverUploadBelowMinimum(string command)
    {
        MatchCollection matches = Regex.Matches(command, @"extra_hw_frames=(-?\d+)");
        matches.Count.ShouldBeGreaterThan(0, "expected the pipeline to upload to QSV at all");
        foreach (Match match in matches)
        {
            int.Parse(match.Groups[1].Value)
                .ShouldBeGreaterThanOrEqualTo(FFmpegState.MinimumQsvExtraHardwareFrames, command);
        }
    }

    [Test]
    public void Qsv_Should_Honor_Extra_Hardware_Frames_Above_The_Minimum()
    {
        string command = BuildAndPrint(preferNativeDecoder: true, maybeExtraHardwareFrames: 128);

        command.ShouldContain("hwupload=extra_hw_frames=128");
    }

    private string BuildInterlacedAndPrint(Option<int> maybeExtraHardwareFrames = default) =>
        BuildAndPrint(
            preferNativeDecoder: true,
            maybeExtraHardwareFrames,
            ScanKind.Interlaced,
            deinterlace: true);

    private string BuildAndPrint(
        bool preferNativeDecoder,
        Option<int> maybeExtraHardwareFrames = default,
        ScanKind scanKind = ScanKind.Progressive,
        bool deinterlace = false)
    {
        (VideoInputFile videoInputFile, AudioInputFile audioInputFile, FFmpegState ffmpegState, FrameState desiredState) =
            BuildQsvH264Pipeline(preferNativeDecoder, scanKind, deinterlace);

        ffmpegState = ffmpegState with { MaybeQsvExtraHardwareFrames = maybeExtraHardwareFrames };

        var builder = new QsvPipelineBuilder(
            new DefaultFFmpegCapabilities(),
            new DefaultHardwareCapabilities(),
            HardwareAccelerationMode.Qsv,
            videoInputFile,
            audioInputFile,
            None,
            None,
            None,
            Option<GraphicsEngineInput>.None,
            "",
            "",
            _logger);

        FFmpegPipeline result = builder.Build(ffmpegState, desiredState);

        return PrintCommand(videoInputFile, audioInputFile, None, None, None, result);
    }

    private static (VideoInputFile, AudioInputFile, FFmpegState, FrameState) BuildQsvH264Pipeline(
        bool preferNativeDecoder,
        ScanKind scanKind,
        bool deinterlace)
    {
        var videoInputFile = new VideoInputFile(
            "/tmp/whatever.mkv",
            new List<VideoStream>
            {
                new(
                    0,
                    VideoFormat.H264,
                    VideoProfile.Main,
                    new PixelFormatYuv420P(),
                    ColorParams.Default,
                    new FrameSize(1920, 1080),
                    "1:1",
                    "16:9",
                    FrameRate.DefaultFrameRate,
                    false,
                    scanKind)
            });

        var audioInputFile = new AudioInputFile(
            "/tmp/whatever.mkv",
            new List<AudioStream> { new(1, AudioFormat.Aac, 2) },
            new AudioState(
                AudioFormat.Aac,
                2,
                320,
                640,
                48,
                false,
                AudioFilter.None,
                Option<double>.None));

        var desiredState = new FrameState(
            true,
            false,
            VideoFormat.H264,
            VideoProfile.Main,
            VideoPreset.Unset,
            false,
            new PixelFormatYuv420P(),
            new FrameSize(1280, 720),
            new FrameSize(1280, 720),
            Option<FrameSize>.None,
            FFmpegFilterMode.Software,
            false,
            Option<FrameRate>.None,
            2000,
            4000,
            90_000,
            false,
            deinterlace);

        var ffmpegState = new FFmpegState(
            false,
            HardwareAccelerationMode.Qsv,
            HardwareAccelerationMode.Qsv,
            Option<string>.None,
            "/dev/dri/renderD128",
            Option<TimeSpan>.None,
            Option<TimeSpan>.None,
            false,
            Option<string>.None,
            Option<string>.None,
            Option<string>.None,
            Option<string>.None,
            Option<string>.None,
            OutputFormatKind.MpegTs,
            Option<string>.None,
            Option<string>.None,
            Option<string>.None,
            Option<string>.None,
            TimeSpan.Zero,
            Option<int>.None,
            Option<int>.None,
            false,
            false,
            "linear",
            false,
            preferNativeDecoder);

        return (videoInputFile, audioInputFile, ffmpegState, desiredState);
    }

    private static string PrintCommand(
        Option<VideoInputFile> videoInputFile,
        Option<AudioInputFile> audioInputFile,
        Option<WatermarkInputFile> watermarkInputFile,
        Option<ConcatInputFile> concatInputFile,
        Option<GraphicsEngineInput> graphicsEngineInput,
        FFmpegPipeline pipeline)
    {
        IList<string> arguments = CommandGenerator.GenerateArguments(
            videoInputFile,
            audioInputFile,
            watermarkInputFile,
            concatInputFile,
            graphicsEngineInput,
            pipeline.PipelineSteps,
            pipeline.IsIntelVaapiOrQsv);

        var command = string.Join(" ", arguments);

        Console.WriteLine($"Generated command: ffmpeg {string.Join(" ", arguments)}");

        return command;
    }

    public class DefaultFFmpegCapabilities() : FFmpegCapabilities(
        string.Empty,
        new System.Collections.Generic.HashSet<string>(),
        new System.Collections.Generic.HashSet<string>(),
        new System.Collections.Generic.HashSet<string>(),
        new System.Collections.Generic.HashSet<string>(),
        new System.Collections.Generic.HashSet<string>(),
        new System.Collections.Generic.HashSet<string>());
}
