using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;

namespace AmiReel.Services;

internal sealed partial class FfmpegLibraryProbe : IDisposable
{
    private const int AvMediaTypeAudio = 1;
    private const int AvLogInfo = 32;

    private static readonly object ProbeLock = new();
    private static readonly AvLogCallback SharedLogCallback = HandleLogMessage;
    private static StringBuilder? s_activeLogBuffer;
    private static FfmpegBindings? s_activeBindings;

    private readonly IntPtr _avformatHandle;
    private readonly IntPtr _avutilHandle;
    private readonly FfmpegBindings _bindings;
    private readonly string _libraryDirectory;

    private FfmpegLibraryProbe(string libraryDirectory, IntPtr avformatHandle, IntPtr avutilHandle)
    {
        _libraryDirectory = libraryDirectory;
        _avformatHandle = avformatHandle;
        _avutilHandle = avutilHandle;
        _bindings = new FfmpegBindings(avformatHandle, avutilHandle);
    }

    public static bool TryCreate(string executablePath, out FfmpegLibraryProbe? probe)
    {
        probe = null;
        string? directory = Path.GetDirectoryName(executablePath);
        if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
            return false;

        string? avformatPath = FindLibrary(directory, "avformat");
        string? avutilPath = FindLibrary(directory, "avutil");
        if (avformatPath is null || avutilPath is null)
            return false;

        try
        {
            IntPtr avformatHandle = NativeLibrary.Load(avformatPath);
            IntPtr avutilHandle = NativeLibrary.Load(avutilPath);
            probe = new FfmpegLibraryProbe(directory, avformatHandle, avutilHandle);
            return true;
        }
        catch
        {
            probe?.Dispose();
            probe = null;
            return false;
        }
    }

    public double ProbeDuration(string inputPath)
    {
        string dump = ProbeFormatDump(inputPath);
        Match match = DurationRegex().Match(dump);
        if (!match.Success)
            throw new InvalidOperationException("FFmpeg library probing could not determine media duration.");

        int hours = int.Parse(match.Groups["hours"].Value, CultureInfo.InvariantCulture);
        int minutes = int.Parse(match.Groups["minutes"].Value, CultureInfo.InvariantCulture);
        double seconds = double.Parse(match.Groups["seconds"].Value, CultureInfo.InvariantCulture);
        return new TimeSpan(0, hours, minutes, 0).TotalSeconds + seconds;
    }

    public bool HasAudio(string inputPath)
    {
        IntPtr context = OpenInput(inputPath);
        try
        {
            int streamIndex = _bindings.AvFindBestStream(context, AvMediaTypeAudio, -1, -1, IntPtr.Zero, 0);
            return streamIndex >= 0;
        }
        finally
        {
            _bindings.AvFormatCloseInput(ref context);
        }
    }

    public void Dispose()
    {
        if (_avformatHandle != IntPtr.Zero) NativeLibrary.Free(_avformatHandle);
        if (_avutilHandle != IntPtr.Zero) NativeLibrary.Free(_avutilHandle);
    }

    private string ProbeFormatDump(string inputPath)
    {
        IntPtr context = OpenInput(inputPath);
        try
        {
            lock (ProbeLock)
            {
                StringBuilder buffer = new();
                s_activeBindings = _bindings;
                s_activeLogBuffer = buffer;
                _bindings.AvLogSetLevel(AvLogInfo);
                _bindings.AvLogSetCallback(SharedLogCallback);
                _bindings.AvDumpFormat(context, 0, inputPath, 0);
                s_activeLogBuffer = null;
                s_activeBindings = null;
                return buffer.ToString();
            }
        }
        finally
        {
            _bindings.AvFormatCloseInput(ref context);
        }
    }

    private IntPtr OpenInput(string inputPath)
    {
        IntPtr context = IntPtr.Zero;
        int openResult = _bindings.AvFormatOpenInput(ref context, inputPath, IntPtr.Zero, IntPtr.Zero);
        if (openResult < 0)
            throw new InvalidOperationException($"FFmpeg library probing failed to open '{inputPath}': {FormatError(openResult)}");

        int infoResult = _bindings.AvFormatFindStreamInfo(context, IntPtr.Zero);
        if (infoResult < 0)
        {
            _bindings.AvFormatCloseInput(ref context);
            throw new InvalidOperationException($"FFmpeg library probing failed to read stream info for '{inputPath}': {FormatError(infoResult)}");
        }

        return context;
    }

    private string FormatError(int errorCode)
    {
        IntPtr buffer = Marshal.AllocHGlobal(1024);
        try
        {
            int result = _bindings.AvStrError(errorCode, buffer, (UIntPtr)1024);
            if (result < 0)
                return $"error {errorCode}";
            return Marshal.PtrToStringUTF8(buffer) ?? $"error {errorCode}";
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }

    private static string? FindLibrary(string directory, string prefix) =>
        Directory.EnumerateFiles(directory, $"{prefix}*.dll", SearchOption.TopDirectoryOnly)
            .OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
            .FirstOrDefault();

    private static void HandleLogMessage(IntPtr avcl, int level, IntPtr format, IntPtr args)
    {
        StringBuilder? buffer = s_activeLogBuffer;
        FfmpegBindings? bindings = s_activeBindings;
        if (buffer is null || bindings is null)
            return;

        IntPtr lineBuffer = Marshal.AllocHGlobal(4096);
        try
        {
            Marshal.Copy(new byte[4096], 0, lineBuffer, 4096);
            int printPrefix = 1;
            bindings.AvLogFormatLine2(avcl, level, format, args, lineBuffer, 4096, ref printPrefix);
            string? line = Marshal.PtrToStringUTF8(lineBuffer);
            if (!string.IsNullOrWhiteSpace(line))
                buffer.Append(line);
        }
        finally
        {
            Marshal.FreeHGlobal(lineBuffer);
        }
    }

    [GeneratedRegex(@"Duration:\s(?<hours>\d{2}):(?<minutes>\d{2}):(?<seconds>\d{2}(?:\.\d+)?)", RegexOptions.CultureInvariant)]
    private static partial Regex DurationRegex();

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void AvLogCallback(IntPtr avcl, int level, IntPtr format, IntPtr args);

    private sealed class FfmpegBindings
    {
        public FfmpegBindings(IntPtr avformatHandle, IntPtr avutilHandle)
        {
            AvFormatOpenInput = GetDelegate<AvFormatOpenInputDelegate>(avformatHandle, "avformat_open_input");
            AvFormatFindStreamInfo = GetDelegate<AvFormatFindStreamInfoDelegate>(avformatHandle, "avformat_find_stream_info");
            AvFormatCloseInput = GetDelegate<AvFormatCloseInputDelegate>(avformatHandle, "avformat_close_input");
            AvFindBestStream = GetDelegate<AvFindBestStreamDelegate>(avformatHandle, "av_find_best_stream");
            AvDumpFormat = GetDelegate<AvDumpFormatDelegate>(avformatHandle, "av_dump_format");
            AvLogSetCallback = GetDelegate<AvLogSetCallbackDelegate>(avutilHandle, "av_log_set_callback");
            AvLogSetLevel = GetDelegate<AvLogSetLevelDelegate>(avutilHandle, "av_log_set_level");
            AvLogFormatLine2 = GetDelegate<AvLogFormatLine2Delegate>(avutilHandle, "av_log_format_line2");
            AvStrError = GetDelegate<AvStrErrorDelegate>(avutilHandle, "av_strerror");
        }

        public AvFormatOpenInputDelegate AvFormatOpenInput { get; }
        public AvFormatFindStreamInfoDelegate AvFormatFindStreamInfo { get; }
        public AvFormatCloseInputDelegate AvFormatCloseInput { get; }
        public AvFindBestStreamDelegate AvFindBestStream { get; }
        public AvDumpFormatDelegate AvDumpFormat { get; }
        public AvLogSetCallbackDelegate AvLogSetCallback { get; }
        public AvLogSetLevelDelegate AvLogSetLevel { get; }
        public AvLogFormatLine2Delegate AvLogFormatLine2 { get; }
        public AvStrErrorDelegate AvStrError { get; }

        private static T GetDelegate<T>(IntPtr handle, string exportName) where T : Delegate =>
            Marshal.GetDelegateForFunctionPointer<T>(NativeLibrary.GetExport(handle, exportName));
    }

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate int AvFormatOpenInputDelegate(ref IntPtr context, [MarshalAs(UnmanagedType.LPUTF8Str)] string url, IntPtr format, IntPtr options);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate int AvFormatFindStreamInfoDelegate(IntPtr context, IntPtr options);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void AvFormatCloseInputDelegate(ref IntPtr context);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate int AvFindBestStreamDelegate(IntPtr context, int mediaType, int wantedStream, int relatedStream, IntPtr decoder, int flags);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void AvDumpFormatDelegate(IntPtr context, int index, [MarshalAs(UnmanagedType.LPUTF8Str)] string url, int isOutput);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void AvLogSetCallbackDelegate(AvLogCallback callback);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void AvLogSetLevelDelegate(int level);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate int AvLogFormatLine2Delegate(IntPtr avcl, int level, IntPtr format, IntPtr args, IntPtr line, int lineSize, ref int printPrefix);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate int AvStrErrorDelegate(int errorCode, IntPtr errorBuffer, UIntPtr errorBufferSize);
}
