using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using VisioForge.Core.MediaBlocks;
using VisioForge.Core.MediaBlocks.AudioRendering;
using VisioForge.Core.MediaBlocks.Sinks;
using VisioForge.Core.MediaBlocks.Sources;
using VisioForge.Core.Types.Events;
using VisioForge.Core.Types.X;
using VisioForge.Core.Types.X.Sources;
using VisioForge.Unity;

/// <summary>
/// Displays a live RTSP camera stream with the VisioForge MediaBlocks SDK, rendered into a
/// RawImage via <see cref="VisioForgeVideoView"/>:
///   RTSPSourceBlock → BufferSinkBlock(RGBA) → VisioForgeVideoView (Texture2D)
///                  \→ AudioRendererBlock (system default device)
/// Environment setup and texture plumbing live in the shared VisioForgeEnvironment /
/// VisioForgeVideoView helpers — this script is just the pipeline.
/// </summary>
[RequireComponent(typeof(RawImage))]
public class RTSPViewerPlayer : MonoBehaviour
{
    [SerializeField, Tooltip("RTSP URL, e.g. rtsp://192.168.1.10:554/stream")]
    private string _rtspUrl = "rtsp://192.168.1.10:554/stream";

    [SerializeField, Tooltip("RTSP username (leave empty if the stream needs no auth).")]
    private string _login = "";

    [SerializeField, Tooltip("RTSP password.")]
    private string _password = "";

    [SerializeField, Tooltip("Auto-connect in Start().")]
    private bool _autoPlayOnStart = true;

    [SerializeField, Tooltip("Render audio through the system default device.")]
    private bool _renderAudio = true;

    [SerializeField, Tooltip("How the video is fitted into the RawImage.")]
    private VideoViewAspectMode _aspectMode = VideoViewAspectMode.Letterbox;

    private VisioForgeVideoView _videoView;
    private MediaBlocksPipeline _pipeline;
    private RTSPSourceBlock _source;
    private BufferSinkBlock _videoSink;
    private AudioRendererBlock _audioRenderer;

    private volatile bool _playing;

    // Serializes PlayAsync/StopAsync so overlapping calls (a rapid reconnect, or a Stop racing a
    // Play) can never build two pipelines feeding the same view or tear one down mid-build.
    private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);

    // Teardown must not wait on the gate forever: a PlayAsync that hangs in the SDK (e.g. an
    // unreachable RTSP host) holds the gate until the connect attempt times out. StopAsync bounds
    // its wait and falls back to best-effort cleanup so OnDestroy can't stall indefinitely.
    private const int GateAcquireTimeoutMs = 10000;

    public bool IsPlaying => _playing;
    public string RtspUrl { get => _rtspUrl; set => _rtspUrl = value; }

    private void Awake()
    {
        _videoView = new VisioForgeVideoView(GetComponent<RawImage>(), _aspectMode);
    }

    private void Update()
    {
        _videoView?.Update();
    }

    private async void Start()
    {
        VisioForgeEnvironment.InitializeSdk();

        if (_autoPlayOnStart && !string.IsNullOrEmpty(_rtspUrl))
        {
            try
            {
                await PlayAsync(_rtspUrl, _login, _password);
            }
            catch (Exception ex)
            {
                Debug.LogError($"[RTSPViewer] Connect failed: {ex}");
            }
        }
    }

    public async Task PlayAsync(string rtspUrl, string login, string password)
    {
        if (string.IsNullOrEmpty(rtspUrl))
            throw new ArgumentException("RTSP URL is empty.", nameof(rtspUrl));

        // Serialize play/stop: an overlapping PlayAsync (rapid reconnect, or one racing a Stop)
        // must not build a second pipeline feeding the same view or tear one down mid-build.
        await _gate.WaitAsync();
        try
        {
            // Tear down any existing pipeline keyed on _pipeline, not _playing: a stream that ended
            // or disconnected clears _playing via OnPipelineStop but leaves _pipeline (and its
            // handlers / sink subscription) live, so a reconnect must still dispose it first.
            if (_pipeline != null)
                await StopCoreAsync();

            try
            {
                _pipeline = new MediaBlocksPipeline();
                _pipeline.OnError += OnPipelineError;
                _pipeline.OnStop += OnPipelineStop;

                // readInfo:false skips the gst-discoverer pre-probe (fails under this Unity runtime,
                // and probing a live stream adds connect latency); decodebin negotiates at PLAYING.
                var settings = await RTSPSourceSettings.CreateAsync(
                    new Uri(rtspUrl), login ?? string.Empty, password ?? string.Empty,
                    audioEnabled: _renderAudio, readInfo: false);

                // Defensive: a concurrent teardown nulled _pipeline, or this GameObject was destroyed
                // (OnDestroy's bounded gate-wait timed out) while we awaited — bail before building
                // blocks we'd orphan or starting a pipeline on a dead object.
                if (_pipeline == null || this == null) return;

                _source = new RTSPSourceBlock(settings);

                if (_source.VideoOutput == null)
                    throw new InvalidOperationException("RTSP source produced no video output.");

                _videoSink = new BufferSinkBlock(VideoFormatX.RGBA);
                _videoSink.OnVideoFrameBuffer += _videoView.OnFrameBuffer;
                _pipeline.Connect(_source.VideoOutput, _videoSink.Input);

                if (_renderAudio && _source.AudioOutput != null)
                {
                    _audioRenderer = new AudioRendererBlock();
                    _pipeline.Connect(_source.AudioOutput, _audioRenderer.Input);
                }

                if (!await _pipeline.StartAsync())
                    throw new InvalidOperationException("MediaBlocksPipeline failed to start.");

                _playing = true;
                Debug.Log($"[RTSPViewer] Streaming {rtspUrl}");
            }
            catch
            {
                // Setup failed mid-way. Tear down exactly like StopCoreAsync so we never orphan the
                // pipeline (with its event handlers) or leave a dangling OnVideoFrameBuffer
                // subscription that would double up on the next PlayAsync.
                if (_videoSink != null)
                    _videoSink.OnVideoFrameBuffer -= _videoView.OnFrameBuffer;

                if (_pipeline != null)
                {
                    _pipeline.OnError -= OnPipelineError;
                    _pipeline.OnStop -= OnPipelineStop;
                    try { await _pipeline.StopAsync(true); } catch { }
                    try { await _pipeline.DisposeAsync(); } catch { }
                }

                _pipeline = null;
                _source = null;
                _videoSink = null;
                _audioRenderer = null;
                _playing = false;
                throw;
            }
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task StopAsync()
    {
        // Bounded acquire: if a hung PlayAsync still holds the gate (e.g. connecting to a dead
        // RTSP host), don't block teardown forever.
        if (await _gate.WaitAsync(GateAcquireTimeoutMs))
        {
            try
            {
                await StopCoreAsync();
            }
            finally
            {
                _gate.Release();
            }
        }
        else
        {
            // Timed out: a PlayAsync is stuck mid-build still holding the gate. Do NOT tear the
            // in-flight pipeline down from here — that would operate on the SAME native pipeline
            // concurrently with the stuck StartAsync. Best-effort only: stop frames reaching the
            // view (OnDestroy is about to dispose it) and mark not-playing; the stuck PlayAsync
            // disposes its own pipeline in its catch when StartAsync finally faults/unwinds.
            _playing = false;
            var sink = _videoSink;
            var view = _videoView;
            if (sink != null && view != null)
                sink.OnVideoFrameBuffer -= view.OnFrameBuffer;
        }
    }

    // Core teardown. The caller (PlayAsync, or StopAsync's gated path) MUST hold _gate — it
    // disposes the native pipeline, so it must be serialized against a concurrent build.
    private async Task StopCoreAsync()
    {
        var pipeline = _pipeline;
        if (pipeline == null)
            return;

        _playing = false;

        if (_videoSink != null)
            _videoSink.OnVideoFrameBuffer -= _videoView.OnFrameBuffer;

        // Detach handlers and clear the fields up front so that a throw from StopAsync /
        // DisposeAsync can't leave the dead pipeline reachable via live subscriptions or a
        // non-null _pipeline (which would make the next PlayAsync orphan it).
        pipeline.OnError -= OnPipelineError;
        pipeline.OnStop -= OnPipelineStop;
        _pipeline = null;
        _source = null;
        _videoSink = null;
        _audioRenderer = null;

        try
        {
            await pipeline.StopAsync(true);
        }
        finally
        {
            await pipeline.DisposeAsync();
        }
    }

    private void OnPipelineError(object sender, ErrorsEventArgs e)
    {
        Debug.LogError($"[RTSPViewer] Pipeline error: {e.Message}");
    }

    private void OnPipelineStop(object sender, StopEventArgs e)
    {
        _playing = false;
    }

    private async void OnDestroy()
    {
        try
        {
            await StopAsync();
        }
        catch (Exception ex)
        {
            Debug.LogError($"[RTSPViewer] StopAsync on destroy failed: {ex}");
        }

        _videoView?.Dispose();
        _videoView = null;

        // Do NOT Dispose(_gate): a hung PlayAsync may still hold it and Release() later, which
        // would throw on a disposed semaphore. A SemaphoreSlim whose AvailableWaitHandle is never
        // accessed needs no explicit disposal — the GC reclaims it.

        // Do NOT DestroySDK here — the SDK is process-global and initialized once; tearing it
        // down on Stop and reusing it on the next Play crashes the native GStreamer registry.
    }
}
