using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using FFmpeg.AutoGen;
using System.Windows;
using System.IO;

namespace SmartAquaViewer.Helper.FFHelper
{
    public sealed unsafe class StreamDecoder : IDisposable
    {
        public delegate void ThreadStoppedHandler(object obj);
        public static event ThreadStoppedHandler hThreadStoppedHandler;

        private readonly AVFormatContext* _pFormatContext;
        public readonly AVFrame* _pFrame;
        private readonly AVPacket* _pPacket;
        AVCodec* codec;

        public string CodecName { get; }
        public Size FrameSize { get; }
        public AVPixelFormat PixelFormat { get; }
        public int videoStreamIndex { get; }
        public AVCodecContext* _vcodecContext { get; }

        public bool _isContextNotInitialized = false;
        public double _fps;

        private static bool _ffInited;

        private static void EnsureFFmpegLoaded()
        {
            if (_ffInited) return;

            // 실행폴더\ffmpeg 강제 지정 (RootPath는 어떤 ffmpeg.* 호출보다 먼저!)
            var root = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg");
            ffmpeg.RootPath = root;

            // 네트워크 초기화 (중복 호출 무해)
            ffmpeg.avformat_network_init();

            // 여기서 한번 강제로 심볼 로딩 검증
            var ver = ffmpeg.av_version_info();

            _ffInited = true;
        }

        private static void SetLogLevelSafe(int level)
        {
            try { ffmpeg.av_log_set_level(level); }
            catch (NotSupportedException) { /* 일부 조합에서만 실패 → 그냥 무시 */ }
        }

        public StreamDecoder(string url)
        {

            //Application.Current.Dispatcher.Invoke(() =>
            //{
            //    ProgressWindow.GetInstance().Show();
            //});

            lock (this)
            {
                AVFormatContext* pFormatContext = null;
                AVCodecContext* vcodecContext = null;
                AVDictionary* options = null;
                AVStream* videoStream = null;

                try
                {
                    EnsureFFmpegLoaded();           // ★ 가장 첫 줄
                    SetLogLevelSafe(ffmpeg.AV_LOG_ERROR);

                    _pFormatContext = ffmpeg.avformat_alloc_context();
                    if (_pFormatContext == null)
                    {
                        throw new InvalidOperationException("Failed to allocate format context.");
                    }
                    pFormatContext = _pFormatContext;

                    // Get the codec context
                    _vcodecContext = ffmpeg.avcodec_alloc_context3(codec);
                    if (_vcodecContext == null)
                    {
                        // Out of memory
                        ffmpeg.avformat_close_input(&pFormatContext);
                    }

                    ffmpeg.av_dict_set(&options, "stimeout", "600000000", 0); // 60 seconds timeout
                    ffmpeg.av_dict_set(&options, "reconnect", "1", 0);
                    ffmpeg.av_dict_set(&options, "reconnect_at_eof", "1", 0);
                    ffmpeg.av_dict_set(&options, "reconnect_streamed", "1", 0);
                    ffmpeg.av_dict_set(&options, "rtsp_transport", "tcp", 0);

                    int ret = ffmpeg.avformat_open_input(&pFormatContext, url, null, &options).ThrowExceptionIfError();

                    if (options != null)
                    {
                        ffmpeg.av_dict_free(&options);
                    }

                    ret = ffmpeg.avformat_find_stream_info(_pFormatContext, null).ThrowExceptionIfError();

                    for (int i = 0; i < pFormatContext->nb_streams; i++)
                    {
                        if (pFormatContext->streams[i]->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
                        {
                            videoStream = pFormatContext->streams[i];
                            break;
                        }
                    }

                    if (videoStream == null)
                    {
                        Console.WriteLine("Failed to find video stream");
                        return;
                    }

                    AVRational frameRate = videoStream->avg_frame_rate;
                    _fps = (double)frameRate.num / frameRate.den;

                    videoStreamIndex = ffmpeg.av_find_best_stream(_pFormatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, null, 0);

                    int context = ffmpeg.avcodec_parameters_to_context(_vcodecContext, _pFormatContext->streams[videoStreamIndex]->codecpar).ThrowExceptionIfError();
                    if (context < 0)
                    {
                        //Log4NetManager.GetLog().Error("StreamDecoder() : avcodec_parameters_to_context Error");
                        return;
                    }

                    if (videoStreamIndex >= 0)
                    {
                        AVCodecContext* avctx = OpenStream(_vcodecContext);
                        if (avctx == null)
                        {
                            throw new InvalidOperationException("Failed to open codec context.");
                        }
                        FrameSize = new Size(avctx->width, avctx->height);
                        PixelFormat = avctx->pix_fmt;
                    }
                    else
                    {
                        throw new InvalidOperationException("No video stream found.");
                    }

                    _pPacket = ffmpeg.av_packet_alloc();
                    if (_pPacket == null)
                    {
                        throw new InvalidOperationException("Failed to allocate packet.");
                    }

                    _pFrame = ffmpeg.av_frame_alloc();
                    if (_pFrame == null)
                    {
                        throw new InvalidOperationException("Failed to allocate frame.");
                    }
                }
                catch (Exception ex)
                {
                    _isContextNotInitialized = true;

                    //Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
                    MessageBox.Show("StreamDecoder() : " + ex.Message);

                    if (_pFrame != null)
                    {
                        ffmpeg.av_frame_unref(_pFrame);
                        ffmpeg.av_free(_pFrame);
                    }
                    if (_pPacket != null)
                    {
                        ffmpeg.av_packet_unref(_pPacket);
                        ffmpeg.av_free(_pPacket);
                    }

                    if (_vcodecContext != null)
                    {
                        vcodecContext = _vcodecContext;
                        ffmpeg.avcodec_close(vcodecContext);
                        ffmpeg.avcodec_free_context(&vcodecContext);
                    }
                    if (_pFormatContext != null)
                    {
                        ffmpeg.avformat_close_input(&pFormatContext);
                        ffmpeg.avformat_free_context(pFormatContext);
                    }

                    //Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
                }
                finally
                {
                    if (options != null)
                    {
                        ffmpeg.av_dict_free(&options);
                    }

                    //Application.Current.Dispatcher.Invoke(() =>
                    //{
                    //    ProgressWindow.GetInstance().Close();
                    //});
                }
            }
        }

        private AVCodecContext* OpenStream(AVCodecContext* avctx)
        {
            AVCodec* codec = ffmpeg.avcodec_find_decoder(avctx->codec_id);
            if (codec == null) throw new InvalidOperationException("No codec could be found.");

            avctx->codec_id = codec->id;
            avctx->lowres = 0;
            if (avctx->lowres > codec->max_lowres)
                avctx->lowres = codec->max_lowres;

            avctx->idct_algo = ffmpeg.FF_IDCT_AUTO;
            avctx->error_concealment = 3;

            ffmpeg.avcodec_open2(avctx, codec, null).ThrowExceptionIfError();

            return avctx;
        }

        //20240326 LHB  - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
        //public void Dispose()
        //{
        //    ffmpeg.av_frame_unref(_pFrame);
        //    ffmpeg.av_free(_pFrame);

        //    ffmpeg.av_packet_unref(_pPacket);
        //    ffmpeg.av_free(_pPacket);

        //    ffmpeg.avcodec_close(_vcodecContext);
        //    //ffmpeg.avcodec_close(acodecContext);

        //    var pFormatContext = _pFormatContext;
        //    ffmpeg.avformat_close_input(&pFormatContext);
        //}

        public void Dispose()
        {
            lock (this)
            {
                if (_pFrame != null)
                {
                    ffmpeg.av_frame_unref(_pFrame);
                    ffmpeg.av_free(_pFrame);
                }
                if (_pPacket != null)
                {
                    ffmpeg.av_packet_unref(_pPacket);
                    ffmpeg.av_free(_pPacket);
                }

                if (_vcodecContext != null && !_isContextNotInitialized)
                {
                    ffmpeg.avcodec_close(_vcodecContext);
                }
                //ffmpeg.avcodec_close(acodecContext);

                if (_pFormatContext != null && !_isContextNotInitialized)
                {
                    var pFormatContext = _pFormatContext;
                    ffmpeg.avformat_close_input(&pFormatContext);
                    ffmpeg.avformat_free_context(pFormatContext);
                }
            }
        }


        //20240326 LHB  - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
        //public bool TryDecodeNextFrame(out AVFrame frame)
        //{
        //    ffmpeg.av_frame_unref(_pFrame);
        //    int error = 0;
        //    do
        //    {
        //        try
        //        {
        //            do
        //            {
        //                error = ffmpeg.av_read_frame(_pFormatContext, _pPacket);
        //                if (error == ffmpeg.AVERROR_EOF)
        //                {
        //                    frame = *_pFrame;
        //                    return false;
        //                }

        //                error.ThrowExceptionIfError();
        //            } while (_pPacket->stream_index != videoStreamIndex);

        //            ffmpeg.avcodec_send_packet(_vcodecContext, _pPacket).ThrowExceptionIfError();
        //        }
        //        catch (Exception ex)
        //        {
        //            Log4NetManager.GetLog().Error("TryDecodeNextFrame() : " + ex.Message);
        //            MessageBox.Show("TryDecodeNextFrame() : " + ex.Message);
        //        }
        //        finally
        //        {
        //            ffmpeg.av_packet_unref(_pPacket);
        //        }

        //        error = ffmpeg.avcodec_receive_frame(_vcodecContext, _pFrame);
        //    } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN));

        //    error.ThrowExceptionIfError();
        //    frame = *_pFrame;
        //    return true;
        //}


        public bool TryDecodeNextFrame(out AVFrame frame)
        {
            ffmpeg.av_frame_unref(this._pFrame);
            int error;

            // repeated-try avcodec_receive_frame until enough packets haven been send
            while (true)
            {
                // try read frame; maybe last avcodec_send_packet resulted in multiple frames to be read; will respond with EAGAIN if more packets are needed
                error = ffmpeg.avcodec_receive_frame(this._vcodecContext, this._pFrame);
                if (error != ffmpeg.AVERROR(ffmpeg.EAGAIN))
                {
                    frame = *this._pFrame;
                    if (error == ffmpeg.AVERROR_EOF)
                        return false;

                    error.ThrowExceptionIfError();
                    return true;
                }

                try
                {
                    // feed all stream-matching packets to decoder
                    while (true)
                    {
                        error = ffmpeg.av_read_frame(this._pFormatContext, this._pPacket);
                        if (error == ffmpeg.AVERROR_EOF)
                        {
                            // no more packets to read -> trigger draining/flushing of remaining frames
                            ffmpeg.avcodec_send_packet(this._vcodecContext, null).ThrowExceptionIfError();
                            break; // don't throw error, just exit packet-feed-loop
                        }

                        error.ThrowExceptionIfError();

                        if (this._pPacket->stream_index == this.videoStreamIndex)
                        {
                            var sendPacketResult = ffmpeg.avcodec_send_packet(this._vcodecContext, this._pPacket);
                            if (sendPacketResult == 0)
                                break;

                            // no reason to abort/crash (it was just an invalid packet from demuxer) -> just retrieve and feed next packet; most of time the codec will resume without problems
                            var errorMsg = Helper.av_strerror(sendPacketResult);
                            Trace.TraceError(errorMsg);
                            //Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : av_strerror 오류 " + errorMsg);
                        }

                        // unref all packets, no matter if sent to avcodec or ignored
                        ffmpeg.av_packet_unref(this._pPacket);
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("TryDecodeNextFrame()" + ex.Message);
                    if (ex.Message.Equals("End of file"))
                    {
                        //Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : " + ex.Message);
                    }
                    //쓰레드가 망함, 
                }
                finally
                {
                    ffmpeg.av_packet_unref(this._pPacket);
                }
            }
        }
        public IReadOnlyDictionary<string, string> GetContextInfo()
        {
            AVDictionaryEntry* tag = null;
            var result = new Dictionary<string, string>();
            while ((tag = ffmpeg.av_dict_get(_pFormatContext->metadata, "", tag, ffmpeg.AV_DICT_IGNORE_SUFFIX)) != null)
            {
                var key = Marshal.PtrToStringAnsi((IntPtr)tag->key);
                var value = Marshal.PtrToStringAnsi((IntPtr)tag->value);
                result.Add(key, value);
            }

            return result;
        }
    }
}
