using Insight.Domain.Network;
using Insight.Domain.Network.Remote.Messages;
using Insight.Remote.Shared.Abstractions;
using Insight.Remote.Shared.Extensions;
using Insight.Remote.Shared.Models;
using Insight.Remote.Shared.Network;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
using SkiaSharp;
using System.Collections.Concurrent;
using System.Diagnostics;
using Vaitr.Bus;
using Vaitr.Network;

namespace Insight.Remote.Shared.Services;

public class Streamer : IDisposable
{
    public int ImageQuality { get; set; } = 50;
    public bool HasControl { get; set; } = true;
    public bool HasAudio { get; set; } = false;
    public bool IsResponsive { get; private set; } = true;
    public double CurrentFps { get; private set; }
    public double CurrentMbps { get; private set; }
    public TimeSpan? RoundTripLatency { get; private set; }

    private CancellationTokenSource? _cts;
    private volatile int _framesSentSinceLastReceipt;
    private DateTimeOffset _lastFrameReceived = DateTimeOffset.Now;
    private DateTimeOffset _lastFrameSent = DateTimeOffset.Now;
    private uint _pingFailures = 0;

    private readonly RecyclableMemoryStreamManager _recycleStreams = new();
    private readonly ConcurrentQueue<DateTimeOffset> _fpsQueue = new();
    private readonly ConcurrentQueue<SentFrame> _sentFrames = new();

    private readonly Bus _bus;
    private readonly ISessionPool<RemoteSession, IMessage> _remotePool;
    private readonly IFileProvider _fileProvider;
    private readonly IClipboardProvider _clipboardProvider;
    private readonly IAudioProvider _audioProvider;
    private readonly IInputProvider _inputProvider;
    private readonly ICursorProvider _cursorProvider;
    private readonly IScreenProvider _screenProvider;
    private readonly ILogger<Streamer> _logger;

    public Streamer(
        Bus bus,
        ISessionPool<RemoteSession, IMessage> remotePool,
        IFileProvider fileProvider,
        IClipboardProvider clipboardProvider,
        IAudioProvider audioProvider,
        IInputProvider inputProvider,
        ICursorProvider cursorProvider,
        IScreenProvider screenProvider,
        ILogger<Streamer> logger)
    {
        _bus = bus;
        _remotePool = remotePool;
        _fileProvider = fileProvider;
        _clipboardProvider = clipboardProvider;
        _audioProvider = audioProvider;
        _inputProvider = inputProvider;
        _cursorProvider = cursorProvider;
        _screenProvider = screenProvider;
        _logger = logger;
    }

    public async Task InitAsync(CastRequest request, CancellationToken cancellationToken)
    {
        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        if (_remotePool.FirstOrDefault().Value is not RemoteSession remoteSession) return;

        //using var audioToken = _bus.SubscribeAsync<AudioData>(async (message, token) => await OnAudioSampleReadyAsync(remoteSession, message, stop.Token), null);
        //using var clipboardToken = _bus.SubscribeAsync<ClipboardData>(async (message, token) => await OnClipboardChangedAsync(remoteSession, message, stop.Token), null);

        //using var sessionSwitchToken = _bus.Subscribe<WindowsSessionSwitched>((x) => stop?.Cancel(), null);
        //using var sessionEndingToken = _bus.Subscribe<WindowsSessionEnding>((x) => stop?.Cancel(), null);

        try
        {
            // send initial display informations
            var screenBounds = _screenProvider.CurrentScreenBounds;

            await remoteSession.SendAsync(new CastDisplay
            {
                Id = remoteSession.Id,
                DisplayNames = _screenProvider.GetDisplayNames(),
                SelectedDisplay = _screenProvider.SelectedScreen,
                ScreenWidth = screenBounds.Width,
                ScreenHeight = screenBounds.Height,
                MachineName = Environment.MachineName
            }, cancellationToken);

            //if (OperatingSystem.IsWindows())
            //{
            //    await SendChunkedAsync(new WindowsSessionsDto(Win32Interop.GetActiveSessions()), DtoType.WindowsSessions, stop.Token);
            //}

            // start => wait for async stream tasks
            await Task.WhenAll(
                StreamMetricAsync(remoteSession, _cts.Token),
                StreamScreenAsync(remoteSession, _cts.Token),
                StreamCursorAsync(remoteSession, _cts.Token)
                );
        }
        catch (OperationCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message);
        }
        finally
        {
            _logger.LogInformation("Viewer Disconnected ({responsive})", IsResponsive);
            _cts?.Cancel();

            IsResponsive = true;
        }
    }

    private async Task StreamMetricAsync(RemoteSession remoteSession, CancellationToken cancellationToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1000));

        while (await timer.WaitForNextTickAsync(cancellationToken) && IsResponsive)
        {
            // calculate mbps
            if (_sentFrames.IsEmpty) CurrentMbps = 0;
            else if (_sentFrames.Count == 1) CurrentMbps = _sentFrames.First().FrameSize / 1024 / 1024 * 8;
            else if (_sentFrames.Count > 1) CurrentMbps = (double)_sentFrames.Sum(x => x.FrameSize) / 1024 / 1024 * 8 / (_sentFrames.Last().Timestamp - _sentFrames.First().Timestamp).TotalSeconds;

            _sentFrames.Clear();

            // calculate fps
            if (_fpsQueue.IsEmpty) CurrentFps = 0;
            else CurrentFps = _fpsQueue.Count / (_fpsQueue.Last() - _fpsQueue.First()).TotalSeconds;

            _fpsQueue.Clear();

            // calculate latency
            try
            {
                var tick = Stopwatch.GetTimestamp();

                // new one
                //var result = await _client.Connection.InvokeAsync<PingViewerResult>(nameof(PingViewer), new PingViewer(ConnectionId), cancellationToken);

                RoundTripLatency = Stopwatch.GetElapsedTime(tick);
                IsResponsive = true;
                _pingFailures = 0;
            }
            catch (Exception)
            {
                RoundTripLatency = null;
                IsResponsive = _pingFailures > 3;
                _pingFailures++;
            }

            await remoteSession.SendAsync(new CastMetric
            {
                Id = remoteSession.Id,
                Timestamp = DateTimeOffset.Now,
                Mbps = CurrentMbps,
                Fps = CurrentFps,
                RTT = RoundTripLatency.HasValue ? RoundTripLatency.Value.TotalMilliseconds : 0,
            }, cancellationToken);
        }
    }

    private async Task StreamScreenAsync(RemoteSession remoteSession, CancellationToken cancellationToken)
    {
        var init = true;
        var errors = 0;

        while (cancellationToken.IsCancellationRequested is false && IsResponsive)
        {
            if (errors >= 3)
            {
                IsResponsive = false;
                break;
            }

            // Prevent publisher from overwhelming consumer bewteen receipts.
            var synced = await WaitForAsync(() => _framesSentSinceLastReceipt < 10, TimeSpan.FromSeconds(3), 10, cancellationToken);

            // Prevent viewer from getting too far behind.
            synced &= await WaitForAsync(() => _lastFrameSent - _lastFrameReceived < TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3), 10, cancellationToken);

            if (synced is false)
            {
                _logger.LogWarning("Viewer is behind on frames and did not catch up in time.");

                errors++;
                continue;
            }

            var frameResult = await _screenProvider.GetNextFrameAsync(cancellationToken);
            if (frameResult.Frame is null || frameResult.Error)
            {
                // consider message viewer to reconnect or doing screenProvider rewrite to dispatch on seperate thread
                await Task.Delay(1000, cancellationToken);
                continue;
            }
            else if (frameResult.Changed is false)
            {
                await Task.Delay(20, cancellationToken);
                continue;
            }

            var diffArea = await _screenProvider.GetDiffAreaAsync(init, cancellationToken);
            if (diffArea.IsEmpty)
            {
                await Task.Delay(20, cancellationToken);
                continue;
            }

            using var croppedFrame = frameResult.Frame.CropBitmap(diffArea);

            var encodedImageBytes = croppedFrame.EncodeBitmap(SKEncodedImageFormat.Jpeg, ImageQuality);
            if (encodedImageBytes.Length == 0)
            {
                await Task.Delay(20, cancellationToken);
                continue;
            }

            Interlocked.Increment(ref _framesSentSinceLastReceipt);

            var frame = new SentFrame(encodedImageBytes.Length, DateTimeOffset.Now);
            _lastFrameSent = frame.Timestamp;
            _sentFrames.Enqueue(frame);
            _fpsQueue.Enqueue(DateTimeOffset.Now);

            init = false;

            var screenBounds = _screenProvider.CurrentScreenBounds;

            await remoteSession.SendAsync(new CastScreen
            {
                Id = remoteSession.Id,
                Timestamp = _lastFrameSent,
                ViewWidth = screenBounds.Width,
                ViewHeight = screenBounds.Height,
                Left = diffArea.Left,
                Top = diffArea.Top,
                Width = diffArea.Width,
                Height = diffArea.Height,
                Image = encodedImageBytes,
            }, cancellationToken);
        }
    }

    private async Task StreamCursorAsync(RemoteSession remoteSession, CancellationToken cancellationToken)
    {
        CastCursor? cacheCursor = null;

        while (cancellationToken.IsCancellationRequested is false && IsResponsive)
        {
            try
            {
                var cursor = await _cursorProvider.GetAsync(cancellationToken);
                if (cursor is null || cursor == cacheCursor) continue;
                
                cursor.Id = remoteSession.Id;

                await remoteSession.SendAsync(cursor, cancellationToken);
                cacheCursor = cursor;
            }
            catch (Exception ex)
            {

            }
            finally
            {
                await Task.Delay(20, cancellationToken);
            }
        }
    }

    //public async Task SendFileAsync(FileUpload fileUpload, Action<double> progressUpdateCallback, CancellationToken cancellationToken)
    //{
    //    try
    //    {
    //        var messageId = Guid.NewGuid().ToString();
    //        var fileDto = new FileDto()
    //        {
    //            EndOfFile = false,
    //            FileName = fileUpload.DisplayName,
    //            MessageId = messageId,
    //            StartOfFile = true
    //        };

    //        await SendChunkedAsync(fileDto, DtoType.File, cancellationToken);

    //        using var fs = File.OpenRead(fileUpload.FilePath);
    //        using var br = new BinaryReader(fs);
    //        while (fs.Position < fs.Length)
    //        {
    //            if (cancellationToken.IsCancellationRequested)
    //            {
    //                return;
    //            }

    //            fileDto = new FileDto()
    //            {
    //                Buffer = br.ReadBytes(40_000),
    //                FileName = fileUpload.DisplayName,
    //                MessageId = messageId
    //            };

    //            await SendChunkedAsync(fileDto, DtoType.File, cancellationToken);

    //            progressUpdateCallback((double)fs.Position / fs.Length);
    //        }

    //        fileDto = new FileDto()
    //        {
    //            EndOfFile = true,
    //            FileName = fileUpload.DisplayName,
    //            MessageId = messageId,
    //            StartOfFile = false
    //        };

    //        await SendChunkedAsync(fileDto, DtoType.File, cancellationToken);

    //        progressUpdateCallback(1);
    //    }
    //    catch (Exception ex)
    //    {
    //        _logger.LogError(ex, "Error while sending file.");
    //    }
    //}

    public async Task CancelAsync(CastAbort castAbort, CancellationToken cancellationToken)
    {
        _cts?.Cancel();
    }

    public async Task FrameReceivedAsync(CastScreenReceived screenDataReceived, CancellationToken cancellationToken)
    {
        _lastFrameReceived = screenDataReceived.Timestamp.Value.ToLocalTime();
        _framesSentSinceLastReceipt = 0;
    }

    public async Task CursorReceivedAsync(CastCursorReceived cursorCallbackData, CancellationToken cancellationToken)
    {
        await _cursorProvider.SetAsync(cursorCallbackData.X, cursorCallbackData.Y, cancellationToken);
    }

    private static async Task<bool> WaitForAsync(Func<bool> condition, TimeSpan timeout, int pollingMs = 10, CancellationToken cancellationToken = default)
    {
        var ts = Stopwatch.GetTimestamp();

        while (!condition() && Stopwatch.GetElapsedTime(ts) < timeout) await Task.Delay(pollingMs, cancellationToken);
        return condition();
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);
    }
}