using System.Diagnostics;
using System.Globalization;

namespace FileFlows.VideoNodes;

public class VideoInfoHelper
{
    private string ffMpegExe;
    private ILogger Logger;

    static Regex rgxTitle = new Regex(@"(?<=((^[\s]+title[\s]+:[\s])))(.*?)$", RegexOptions.Multiline);
    static Regex rgxDuration = new Regex(@"(?<=((^[\s]+DURATION(\-[\w]+)?[\s]+:[\s])))([\d]+:?)+\.[\d]{1,7}", RegexOptions.Multiline);
    static Regex rgxDuration2 = new Regex(@"(?<=((^[\s]+Duration:[\s])))([\d]+:?)+\.[\d]{1,7}", RegexOptions.Multiline);
    static Regex rgxAudioSampleRate = new Regex(@"(?<=((,|\s)))[\d]+(?=([\s]?hz))", RegexOptions.IgnoreCase);
    static Regex rgxAudioBitrate = new Regex(@"(?<=(, ))([\d]+)(?=( kb\/s))", RegexOptions.IgnoreCase);
    static Regex rgxAudioBitrateFull = new Regex(@"^[\s]+BPS[^:]+: ([\d]+)", RegexOptions.Multiline);
    static Regex rgxFilename = new Regex(@"(?<=((^[\s]+filename[\s]+:[\s])))(.*?)$", RegexOptions.Multiline);
    static Regex rgxMimeType = new Regex(@"(?<=((^[\s]+mimetype[\s]+:[\s])))(.*?)$", RegexOptions.Multiline);

    static int _ProbeSize = 25;
    
    /// <summary>
    /// Gets or set the probe size
    /// </summary>
    internal static int ProbeSize 
    { 
        get => _ProbeSize;
        set
        {
            if (value < 5)
                _ProbeSize = 5;
            else
                _ProbeSize = value;
        }
    }

    private static int _AnalyzeDuration = 5_000_000;

    /// <summary>
    /// Gets or sets the analyze duration
    /// </summary>
    public static int AnalyzeDuration
    {
        get => _AnalyzeDuration;
        set => _AnalyzeDuration = Math.Max(32, value);
    }

    public VideoInfoHelper(string ffMpegExe, ILogger logger)
    {
        this.ffMpegExe = ffMpegExe;
        this.Logger = logger;
    }

    public static string GetFFMpegPath(NodeParameters args) => args.GetToolPath("FFMpeg");

    public Result<VideoInfo> Read(string filename)
        => ReadStatic(Logger, ffMpegExe, filename);
    
    internal static Result<VideoInfo> ReadStatic(ILogger logger, string ffMpegExe, string filename)
    {
        #if(DEBUG) // UNIT TESTING
        filename = filename.Replace("~/", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/");
        #endif
        
        if (System.IO.File.Exists(filename) == false)
            return Result<VideoInfo>.Fail("File not found: " + filename);
        if (string.IsNullOrEmpty(ffMpegExe) || System.IO.File.Exists(ffMpegExe) == false)
            return Result<VideoInfo>.Fail("FFmpeg not found: " + (ffMpegExe ?? "not passed in"));

        try
        {
            using (var process = new Process())
            {
                process.StartInfo = new ProcessStartInfo();
                process.StartInfo.FileName = ffMpegExe;
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.CreateNoWindow = true;
                foreach (var arg in new[]
                {
                    "-hide_banner",
                    "-probesize", ProbeSize + "M",
                    "-analyzeduration", AnalyzeDuration.ToString(),
                    "-i",
                    filename,
                })
                {
                    process.StartInfo.ArgumentList.Add(arg);
                }
                process.Start();
                string output = process.StandardError.ReadToEnd();
                output = output.Replace("At least one output file must be specified", string.Empty).Trim();
                string error = process.StandardError.ReadToEnd();
                process.WaitForExit();

                if (string.IsNullOrEmpty(error) == false && error != "At least one output file must be specified")
                    return Result<VideoInfo>.Fail("Failed reading ffmpeg info: " + error);

                logger.ILog("Video Information:" + Environment.NewLine + output);

                if (process.ExitCode != 0)
                {
                    if (output.Contains("moov atom not found"))
                        return Result<VideoInfo>.Fail(
                            "The video file appears to be corrupted or incomplete. (moov atom not found)");
                }
                
                var vi = ParseOutput(logger, output);
                vi.FileName = filename;
                return vi;
            }
        }
        catch (Exception ex)
        {
            logger.ELog(ex.Message, ex.StackTrace);
            return Result<VideoInfo>.Fail("Failed reading video information: " + ex.Message);
        }
    }

    public static VideoInfo ParseOutput(ILogger logger, string output)
    {
        var vi = new VideoInfo();
        vi.AlreadyProcessed = output.Contains("Created by FileFlows");
        var rgxStreams = new Regex(@"Stream\s#[\d]+:[\d]+(.*?)(?=(Stream\s#[\d]|$))", RegexOptions.Singleline);
        var streamMatches = rgxStreams.Matches(output);
        int streamIndex = 0;


        // get a rough estimate, bitrate: 346 kb/s
        var rgxBitrate = new Regex(@"(?<=(bitrate: ))[\d\.]+(?!=( kb/s))");
        var brMatch = rgxBitrate.Match(output);
        if (brMatch.Success)
        {
            vi.Bitrate = float.Parse(brMatch.Value) * 1_000; // to convert to b/s
        }

        vi.Chapters = ParseChapters(output);

        int subtitleIndex = 0;
        int videoIndex = 0;
        int audioIndex = 0;
        int attachmentIndex = 0;
        bool webvtt = output.Contains("Unknown/unsupported AVCodecID S_TEXT/WEBVTT"); // FF-1534
        foreach (Match sm in streamMatches)
        {
            if (sm.Value.Contains(" Video: "))
            {
                var vs = ParseVideoStream(logger, sm.Value, output);
                if (vs != null)
                {
                    vs.Index = streamIndex;
                    vs.TypeIndex = videoIndex;
                    var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
                    if (match.Success)
                        vs.IndexString = match.Value;
                    vi.VideoStreams.Add(vs);
                }
                ++videoIndex;
            }
            else if (sm.Value.Contains(" Audio: "))
            {
                var audio = ParseAudioStream(logger, sm.Value);
                if (audio != null)
                {
                    audio.TypeIndex = audioIndex;
                    audio.Index = streamIndex;
                    var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
                    if (match.Success)
                        audio.IndexString = match.Value;
                    vi.AudioStreams.Add(audio);
                }
                ++audioIndex;
            }
            else if (sm.Value.Contains(" Subtitle: "))
            {
                var sub = ParseSubtitleStream(sm.Value);
                if (sub != null)
                {
                    sub.Index = streamIndex;
                    sub.TypeIndex = subtitleIndex;
                    var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
                    if (match.Success)
                        sub.IndexString = match.Value;
                    if ((string.IsNullOrEmpty(sub.Codec) || sub.Codec.ToLowerInvariant() == "none") && webvtt)
                        sub.Codec = "webvtt";
                        
                    vi.SubtitleStreams.Add(sub);
                }
                ++subtitleIndex;
            }
            else if (sm.Value.Contains(" Attachment: "))
            {
                AttachmentStream attachmentStream = ParseAttachmentStream(sm.Value);
                if (attachmentStream != null)
                {
                    attachmentStream.Index = streamIndex;
                    attachmentStream.TypeIndex = attachmentIndex;
                    var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
                    if (match.Success)
                        attachmentStream.IndexString = match.Value;
                    vi.Attachments.Add(attachmentStream);
                }
                ++subtitleIndex;
            }
            ++streamIndex;
        }
        return vi;
    }


    private static List<Chapter> ParseChapters(string output)
    {
        try
        {
            var rgxChatpers = new Regex("(?<=(Chapters:))(.*?)(?=(Stream))", RegexOptions.Singleline);
            string strChapters;
            if (rgxChatpers.TryMatch(output, out Match matchChapters))
                strChapters = matchChapters.Value.Trim();
            else
                return new List<Chapter>();

            var rgxChapter = new Regex("Chapter #(.*?)(?=(Chapter #|$))", RegexOptions.Singleline);
            var chapters = new List<Chapter>();

            var rgxTitle = new Regex(@"title[\s]*:[\s]*(.*?)$");
            var rgxStart = new Regex(@"(?<=(start[\s]))[\d]+\.[\d]+");
            var rgxEnd = new Regex(@"(?<=(end[\s]))[\d]+\.[\d]+");
            foreach (Match match in rgxChapter.Matches(strChapters))
            {
                try
                {
                    Chapter chapter = new Chapter();
                    if (rgxTitle.TryMatch(match.Value.Trim(), out Match title))
                        chapter.Title = title.Groups[1].Value;

                    if (rgxStart.TryMatch(match.Value, out Match start))
                    {
                        double startSeconds = double.Parse(start.Value);
                        chapter.Start = TimeSpan.FromSeconds(startSeconds);
                    }
                    if (rgxEnd.TryMatch(match.Value, out Match end))
                    {
                        double endSeconds = double.Parse(end.Value);
                        chapter.End = TimeSpan.FromSeconds(endSeconds);
                    }

                    if (chapter.Start > TimeSpan.Zero || chapter.End > TimeSpan.Zero)
                    {
                        chapters.Add(chapter);
                    }
                }
                catch (Exception ) { }
            }

            return chapters;
        }catch (Exception) { return new List<Chapter>(); }
    }

    public static VideoStream ParseVideoStream(ILogger logger, string info, string fullOutput)
    {
        // Stream #0:0(eng): Video: h264 (High), yuv420p(tv, bt709/unknown/unknown, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 23.98 fps, 23.98 tbr, 1k tbn (default)
        string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
        VideoStream vs = new VideoStream();
        vs.IsImage = info.Contains("(attached pic)");

        var matchCodecTag = new Regex(@": Video: [^(,]+\([^)]+\)[^(,]+\(([^)]+)\)").Match(line);
        if (matchCodecTag.Success)
        {
            vs.CodecTag = matchCodecTag.Groups[1].Value;
            if (vs.CodecTag.IndexOf(" /", StringComparison.Ordinal) > 0)
                vs.CodecTag = vs.CodecTag.Substring(0, vs.CodecTag.IndexOf(" /")).Trim();
        }

        vs.Codec = line.Substring(line.IndexOf("Video: ") + "Video: ".Length).Replace(",", "").Trim().Split(' ').First().ToLower();
        vs.PixelFormat = GetDecoderPixelFormat(line);
        vs.Bits = GetBitDepthFromStreamInfo(logger, info);
        if (rgxTitle.IsMatch(info))
            vs.Title = rgxTitle.Match(info).Value.Trim();

        var dimensions = Regex.Match(line, @"([\d]{3,})x([\d]{3,})");
        if (int.TryParse(dimensions.Groups[1].Value, out int width))
            vs.Width = width;
        if (int.TryParse(dimensions.Groups[2].Value, out int height))
            vs.Height = height;

        if (Regex.IsMatch(line, @"([\d]+(\.[\d]+)?)\sfps") &&
            float.TryParse(Regex.Match(line, @"([\d]+(\.[\d]+)?)\sfps").Groups[1].Value, out float fps))
        {
            logger?.ILog("Frames Per Second: " + fps);
            vs.FramesPerSecond = fps;
        }

        var rgxBps = new Regex(@"(?<=((BPS(\-[\w]+)?[\s]*:[\s])))([\d]+)");
        if (rgxBps.IsMatch(info) && float.TryParse(rgxBps.Match(info).Value, out float bps))
            vs.Bitrate = bps;

        if (rgxDuration.IsMatch(info) && TimeSpan.TryParse(rgxDuration.Match(info).Value, out TimeSpan duration) && duration.TotalSeconds > 0)
        {
            vs.Duration = duration;
            logger?.ILog("Video stream duration: " + vs.Duration);
        }
        else if (rgxDuration2.IsMatch(fullOutput) && TimeSpan.TryParse(rgxDuration2.Match(fullOutput).Value, out TimeSpan duration2) && duration2.TotalSeconds > 0)
        {
            vs.Duration = duration2;
            logger?.ILog("Video stream duration: " + vs.Duration);
        }
        else
        {
            logger?.ILog("Failed to read duration for VideoStream: " + info);
        }

        // As per https://video.stackexchange.com/a/33827
        // "HDR is only the new transfer function" (PQ or HLG)
        vs.HDR = info.Contains("arib-std-b67") || info.Contains("smpte2084");


        vs.DolbyVision = info.Contains("DOVI configuration record");
        
        return vs;
    }

    public static AudioStream ParseAudioStream(ILogger logger, string info)
    {
        // Stream #0:1(eng): Audio: dts (DTS), 48000 Hz, stereo, fltp, 1536 kb/s (default)
        string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
        var parts = line.Split(",").Select(x => x?.Trim() ?? "").ToArray();
        AudioStream audio = new AudioStream();
        audio.Title = "";
        // this isnt type index, this is overall index
        audio.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value) - 1;
        audio.Codec = parts[0].Substring(parts[0].IndexOf("Audio: ") + "Audio: ".Length).Trim().Split(' ').First().ToLower() ?? string.Empty;
        if (audio.Codec.EndsWith(","))
            audio.Codec = audio.Codec[..^1].Trim();

        audio.Language = GetLanguage(line);
        audio.Default = info.Contains("(default)");
        // if (info.IndexOf("0 channels", StringComparison.Ordinal) >= 0)
        // {
        //     logger?.WLog("Stream contained '0 Channels'");
        //     audio.Channels = 0;
        // }
        // else
        {
            try
            {
                //Logger.ILog("codec: " + vs.Codec);
                if (parts[2] == "stereo")
                    audio.Channels = 2;
                else if (parts[2] == "mono")
                    audio.Channels = 1;
                else if (Regex.IsMatch(parts[2], @"^[\d]+(\.[\d]+)?"))
                {
                    var matchValue = Regex.Match(parts[2], @"^[\d]+(\.[\d]+)?").Value;
                    
                    CultureInfo culture = CultureInfo.InvariantCulture; // Use invariant culture for consistent parsing
                    if (float.TryParse(matchValue, NumberStyles.Float, culture, out float channels))
                    {
                        audio.Channels = channels;
                        logger?.ILog("Detected audio channels: " + audio.Channels + ", from " + parts[2]);
                    }
                    else
                    {
                        logger?.WLog($"Failed to parse value '{matchValue}' as a float.");
                    }
                }
                else if (line.Contains(" 7.1"))
                    audio.Channels = 7.1f;
                else if (line.Contains(" 7.2"))
                    audio.Channels = 7.2f;
                else if (line.Contains(" 5.1"))
                    audio.Channels = 5.1f;
                else if (line.Contains(" 5.0"))
                    audio.Channels = 5f;
                else if (line.Contains(" 4.1"))
                    audio.Channels = 4.1f;
                else
                {
                    logger?.WLog("Unable to detect channels from: " + line);
                }

                logger?.ILog("Audio channels: " + audio.Channels + ", from " + parts[2]);
            }
            catch (Exception ex)
            {
                logger?.WLog("Failed to parse audio channels: " + ex.Message + "\n" + "From line: " + line);
            }
        }

        var match = rgxAudioSampleRate.Match(info);
        if (match.Success)
            audio.SampleRate = int.Parse(match.Value);

        if (rgxTitle.IsMatch(info))
            audio.Title = rgxTitle.Match(info).Value.Trim();


        if (rgxDuration.IsMatch(info))
            audio.Duration = TimeSpan.Parse(rgxDuration.Match(info).Value);

        try
        {
            if (rgxAudioBitrate.IsMatch(line))
            {
                audio.Bitrate = int.Parse(rgxAudioBitrate.Match(line).Value) * 1000; // this is NOT 1024
            }
            else if (rgxAudioBitrateFull.IsMatch(info))
            {
                audio.Bitrate = float.Parse(rgxAudioBitrateFull.Match(info).Groups[1].Value);
            }
        }
        catch(Exception) { }   


        return audio;
    }
    public static SubtitleStream ParseSubtitleStream(string info)
    {
        // Stream #0:1(eng): Audio: dts (DTS), 48000 Hz, stereo, fltp, 1536 kb/s (default)
        string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
        var parts = line.Split(",").Select(x => x?.Trim() ?? "").ToArray();
        SubtitleStream sub = new SubtitleStream();
        sub.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value);
        sub.Codec = line.Substring(line.IndexOf("Subtitle: ") + "Subtitle: ".Length).Trim().Split(' ').First().ToLower();
        if (sub.Codec.EndsWith(","))
            sub.Codec = sub.Codec[..^1].Trim();
        sub.Language = GetLanguage(line);
        sub.Default = info.Contains("(default)");

        if (rgxTitle.IsMatch(info))
            sub.Title = rgxTitle.Match(info).Value.Trim();

        sub.Forced = info.ToLower().Contains("(forced)");
        return sub;
    }
    
    private static AttachmentStream ParseAttachmentStream(string info)
    {
        // Stream #0:6: Attachment: ttf
        //   Metadata:
        //     filename        : FF Tisa OT XBold.ttf
        //     mimetype        : application/x-truetype-font
        string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
        var parts = line.Split(",").Select(x => x?.Trim() ?? "").ToArray();
        AttachmentStream stream = new AttachmentStream();
        stream.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value);
        stream.Codec = line.Substring(line.IndexOf("Attachment: ") + "Attachment: ".Length).Trim().Split(' ').First().ToLower();
        if (stream.Codec.EndsWith(","))
            stream.Codec = stream.Codec[..^1].Trim();

        if (rgxFilename.IsMatch(info))
            stream.FileName = rgxFilename.Match(info).Value.Trim();
        if (rgxMimeType.IsMatch(info))
            stream.MimeType = rgxMimeType.Match(info).Value.Trim();
        return stream;
    }

    private static string GetLanguage(string line)
    {
        var langSection = Regex.Match(line, @"(?<=(Stream\s#[\d]+:[\d]+))[^:]+");
        if (langSection.Success == false)
            return string.Empty;
        var lang = Regex.Match(langSection.Value, @"(?<=\()[^)]+").Value?.ToLower() ?? string.Empty;
        if (lang == "und")
            return string.Empty;
        return lang;
    }
    
    /// <summary>
    /// Extracts the supported pixel format from an FFmpeg output line for hardware decoding.
    /// </summary>
    /// <param name="line">The FFmpeg output line containing video stream information.</param>
    /// <returns>The supported pixel format (e.g., "yuv420p" or "p010le"), or an empty string if not found or not supported.</returns>
    /// <remarks>
    /// Supports "yuv420p" and "p010le" formats. Handles cases where "p010le" has no additional specifiers, defaulting to "yuv420p".
    /// Adjust the regular expression or default behavior based on specific hardware and FFmpeg output format.
    /// </remarks>
    static string GetDecoderPixelFormat(string line)
    {
        // only p010le confirmed working so far
        if(Regex.IsMatch(line, @"p(0)?10l(b)?e"))
           return "p010le";
        if(line.IndexOf("yuv420p", StringComparison.Ordinal) > 0)
            return "nv12"; // use nv12 instead of yuv420p
        // if (line.IndexOf("nv12", StringComparison.Ordinal) >= 0)
        //     return "nv12";
        // if (line.IndexOf("yuv444p", StringComparison.Ordinal) >= 0)
        //     return "yuv444p";
        return string.Empty;
    }

    /// <summary>
    /// Extracts the number of bits from the given video stream information.
    /// </summary>
    /// <param name="logger">the logger to use for logging</param>
    /// <param name="line">The video stream information string.</param>
    /// <returns>The number of bits, or 0 if unknown.</returns>
    static int GetBitDepthFromStreamInfo(ILogger logger, string line)
    {
        if (line.Contains("p10le") || line.Replace(" ", string.Empty).Contains("main10"))
        {
            logger?.ILog("10-Bit detected");
            return 10;
        }

        if (line.Contains("p12le"))
        {
            logger?.ILog("12-Bit detected");
            return 12;
        }

        if (line.Contains("yuv420p") || line.Contains("nv12"))
        {
            logger?.ILog("8-Bit detected");
            return 8;
        }

        logger?.ILog("Bits not detected in video");
        return 0; // Unknown
    }
}