using Avalonia.Animation;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using SkiaSharp;

namespace Avalonia.Gif;

public class WebpInstance : IGifInstance
{
    public IterationCount IterationCount { get; set; }
    public bool AutoStart { get; private set; } = true;

    private readonly WriteableBitmap? _targetBitmap;
    private TimeSpan _totalTime;
    private readonly List<TimeSpan> _frameTimes;
    private uint _iterationCount;
    private int _currentFrameIndex;

    private SKCodec? _codec;

    public CancellationTokenSource CurrentCts { get; }

    internal WebpInstance(object newValue)
        : this(
            newValue switch
            {
                Stream s => s,
                Uri u => GetStreamFromUri(u),
                string str => GetStreamFromString(str),
                _ => throw new InvalidDataException("Unsupported source object")
            }
        ) { }

    public WebpInstance(string uri)
        : this(GetStreamFromString(uri)) { }

    public WebpInstance(Uri uri)
        : this(GetStreamFromUri(uri)) { }

    public WebpInstance(Stream currentStream)
    {
        if (!currentStream.CanSeek)
            throw new InvalidDataException("The provided stream is not seekable.");

        if (!currentStream.CanRead)
            throw new InvalidOperationException("Can't read the stream provided.");

        currentStream.Seek(0, SeekOrigin.Begin);

        CurrentCts = new CancellationTokenSource();

        var managedStream = new SKManagedStream(currentStream);
        _codec = SKCodec.Create(managedStream);

        var pixSize = new PixelSize(_codec.Info.Width, _codec.Info.Height);

        // Different on os: https://github.com/mono/SkiaSharp/issues/1492#issuecomment-689015409
        // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault
        var format = SKImageInfo.PlatformColorType switch
        {
            SKColorType.Bgra8888 => PixelFormat.Bgra8888,
            SKColorType.Rgba8888 => PixelFormat.Rgba8888,
            _ => throw new NotSupportedException($"Unsupported color type: {SKImageInfo.PlatformColorType}")
        };

        _targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), format, AlphaFormat.Opaque);

        GifPixelSize = pixSize;

        _totalTime = TimeSpan.Zero;

        _frameTimes = _codec
            .FrameInfo.Select(frame =>
            {
                _totalTime = _totalTime.Add(TimeSpan.FromMilliseconds(frame.Duration));
                return _totalTime;
            })
            .ToList();

        RenderFrame(_codec, _targetBitmap, 0);
    }

    private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index)
    {
        codec.GetFrameInfo(index, out var frameInfo);

        var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
        var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);

        using var frameBuffer = targetBitmap.Lock();

        var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index));

        if (result != SKCodecResult.Success)
            throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
    }

    private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index, int priorIndex)
    {
        codec.GetFrameInfo(index, out var frameInfo);

        var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
        var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);

        using var frameBuffer = targetBitmap.Lock();

        var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index, priorIndex));

        if (result != SKCodecResult.Success)
            throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
    }

    private static Stream GetStreamFromString(string str)
    {
        if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res))
        {
            throw new InvalidCastException("The string provided can't be converted to URI.");
        }

        return GetStreamFromUri(res);
    }

    private static Stream GetStreamFromUri(Uri uri)
    {
        var uriString = uri.OriginalString.Trim();

        if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares"))
        {
            // Local file
            using var fs = new FileStream(uriString, FileMode.Open, FileAccess.Read);

            // Copy to memory stream then return
            var memoryStream = new MemoryStream();
            fs.CopyTo(memoryStream);
            memoryStream.Seek(0, SeekOrigin.Begin);

            return memoryStream;
        }

        // Internal Avalonia resources
        return AssetLoader.Open(uri);
    }

    public int GifFrameCount => _frameTimes.Count;

    public PixelSize GifPixelSize { get; }

    public void Dispose()
    {
        IsDisposed = true;
        CurrentCts.Cancel();
        _targetBitmap?.Dispose();
        _codec?.Dispose();
    }

    public bool IsDisposed { get; private set; }

    public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed)
    {
        if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value)
        {
            return null;
        }

        if (CurrentCts.IsCancellationRequested || _targetBitmap is null)
        {
            return null;
        }

        var elapsedTicks = stopwatchElapsed.Ticks;
        var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks);
        var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x);
        var currentFrame = _frameTimes.IndexOf(targetFrame);
        if (currentFrame == -1)
            currentFrame = 0;

        if (_currentFrameIndex == currentFrame)
            return _targetBitmap;

        _iterationCount = (uint)(elapsedTicks / _totalTime.Ticks);

        return ProcessFrameIndex(currentFrame);
    }

    internal WriteableBitmap ProcessFrameIndex(int frameIndex)
    {
        if (_codec is null)
            throw new InvalidOperationException("The codec is null.");

        if (_targetBitmap is null)
            throw new InvalidOperationException("The target bitmap is null.");

        RenderFrame(_codec, _targetBitmap, frameIndex, _currentFrameIndex);
        _currentFrameIndex = frameIndex;

        return _targetBitmap;
    }
}
