using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Pixiview.Illust;
using Xamarin.Forms;
using System.Linq;
#if __IOS__
using AVFoundation;
using CoreGraphics;
using CoreMedia;
using CoreVideo;
using Foundation;
using UIKit;
#endif

namespace Pixiview.Utils
{
    public class Ugoira : IDisposable
    {
        private const int DELAY = 200;
        private const int BUFFER_SIZE = 300000;
        private const int BUFFER_TABLE = 30000;
        private readonly object sync = new object();

        private readonly IllustUgoiraBody ugoira;
        private readonly IllustDetailItem detailItem;
        private readonly ImageSource[] frames;
        private Timer timer;
        private int index = 0;
        private ParallelTask downloadTask;

        public bool IsPlaying { get; private set; }
        public readonly int FrameCount;
        public event EventHandler<UgoiraEventArgs> FrameChanged;

        public Ugoira(IllustUgoiraData illust, IllustDetailItem item)
        {
            ugoira = illust.body;
            detailItem = item;
            frames = new ImageSource[ugoira.frames.Length];
            FrameCount = frames.Length;

            Task.Run(LoadFrames);
        }

        public void Dispose()
        {
            if (IsPlaying)
            {
                TogglePlay(false);
            }
            if (downloadTask != null)
            {
                downloadTask.Dispose();
                downloadTask = null;
            }
            ClearTimer();
        }

        private void ClearTimer()
        {
            lock (sync)
            {
                if (timer != null)
                {
                    timer.Dispose();
                    timer = null;
                }
            }
        }

        public void TogglePlay(bool flag)
        {
            if (IsPlaying == flag)
            {
                return;
            }
            ClearTimer();
            if (flag)
            {
                timer = new Timer(OnTimerCallback, null, 0, Timeout.Infinite);
                IsPlaying = true;
            }
            else
            {
                IsPlaying = false;
            }
        }

        public void ToggleFrame(int frame)
        {
            if (IsPlaying)
            {
                // TODO: doesn't support change current frame when playing
                return;
            }
            if (frame < 0 || frame >= frames.Length)
            {
                return;
            }
            var image = frames[frame];
            if (image != null)
            {
                index = frame;
                FrameChanged?.Invoke(this, new UgoiraEventArgs
                {
                    DetailItem = detailItem,
                    Image = image,
                    FrameIndex = frame
                });
            }
        }

        private void OnTimerCallback(object state)
        {
            lock (sync)
            {
                if (!IsPlaying)
                {
                    return;
                }
            }

            ImageSource frame;
            var i = index;
            var info = ugoira.frames[i];
            while ((frame = frames[i]) == null)
            {
                lock (sync)
                {
                    if (timer == null)
                    {
                        return;
                    }
                }
                // not downloaded yet, waiting...
                Thread.Sleep(DELAY);
            }
            FrameChanged?.Invoke(this, new UgoiraEventArgs
            {
                DetailItem = detailItem,
                Image = frame,
                FrameIndex = i
            });
            i++;
            if (i >= frames.Length)
            {
                i = 0;
            }
            index = i;
            if (timer != null && IsPlaying)
            {
                timer.Change(info.delay, Timeout.Infinite);
            }
        }

        private void LoadFrames()
        {
            var zip = Path.GetFileName(ugoira.originalSrc);
            bool download = false;
            var uframes = ugoira.frames;
            for (var i = 0; i < uframes.Length; i++)
            {
                var frame = uframes[i];
                var image = Stores.LoadUgoiraImage(zip, frame.file);
                if (image != null)
                {
                    frame.FilePath = image;
                    frames[i] = image;
                }
                else
                {
                    frame.Incompleted = true;
                    download = true;
                }
            }

            if (download)
            {
                // need download
                var url = ugoira.originalSrc;
                var id = detailItem.Id;
                var (size, lastModified, client) = HttpUtility.GetUgoiraHeader(url, id);
#if LOG
                App.DebugPrint($"starting download ugoira: {size} bytes, last modified: {lastModified}");
#endif

                var data = new byte[size];

                Segment[] segs;
                var length = (int)Math.Ceiling((double)(size - BUFFER_TABLE) / BUFFER_SIZE) + 1;
                segs = new Segment[length];

                var tableOffset = size - BUFFER_TABLE;
                segs[length - 1] = new Segment(length - 1, tableOffset, size - 1);
                segs[length - 2] = new Segment(length - 2, (length - 2) * BUFFER_SIZE, tableOffset - 1);
                for (var i = 0; i < length - 2; i++)
                {
                    long from = i * BUFFER_SIZE;
                    segs[i] = new Segment(i, from, from + BUFFER_SIZE - 1);
                }

                // table
                var segTable = segs[length - 1];
                using (var ms = new MemoryStream(data, (int)segTable.From, BUFFER_TABLE))
                {
                    HttpUtility.DownloadUgoiraImage(client, url, id, lastModified, segTable.From, segTable.To, ms);
                    if (timer == null)
                    {
                        return;
                    }
                }
                segTable.Done = true;
                for (var i = tableOffset; i < size - 4; i++)
                {
                    if (data[i + 0] == 0x50 && data[i + 1] == 0x4b &&
                        data[i + 2] == 0x01 && data[i + 3] == 0x02)
                    {
                        tableOffset = i;
                        break;
                    }
                }
                if (tableOffset > size - 31)
                {
                    App.DebugError("find.table", $"failed to find table offset, id: {id}, url: {url}");
                    return;
                }
                for (var n = 0; n < uframes.Length; n++)
                {
                    var frame = uframes[n];
                    var i = (int)tableOffset;
                    i += 10; // signature(4) & version(2) & version_need(2) & flags(2)
                    if (data[i] != 0 || data[i + 1] != 0)
                    {
                        App.DebugError("extract.image", $"doesn't support compressed data: {BitConverter.ToInt16(data, i):x4}");
                        return;
                    }
                    i += 10; // compression(2) & mod-time(2) & mod-date(2) & crc-32(4)
                    int entrySize = BitConverter.ToInt32(data, i);
                    i += 4; // size(4)
                    int rawSize = BitConverter.ToInt32(data, i);
                    if (entrySize != rawSize)
                    {
                        App.DebugError("find.table", $"data seems to be compressed: {entrySize:x8} ({rawSize:x8}) bytes");
                        return;
                    }
                    i += 4;  // rawSize(4)
                    int filenameLength = BitConverter.ToInt16(data, i);
                    i += 2;  // filename length(2)
                    int extraLength = BitConverter.ToInt16(data, i);
                    i += 2;  // extra length(2)
                    int commentLength = BitConverter.ToInt16(data, i);
                    i += 10; // comment length(2) & disk start(2) & internal attr(2) & external attr(4)
                    int entryOffset = BitConverter.ToInt32(data, i);
                    i += 4;  // offset(4)
                    var filename = Encoding.UTF8.GetString(data, i, filenameLength);
                    tableOffset = i + filenameLength + extraLength + commentLength;  // filename & extra & comment

                    if (frame.file != filename)
                    {
                        App.DebugError("find.table", $"error when fill entry information, read name: {filename}, frame name: {frame.file}");
                        return;
                    }
                    frame.Offset = entryOffset;
                    frame.Length = entrySize;
                }

                // only download needed
                var inSegs = new List<Segment>();
                for (var i = 0; i < uframes.Length; i++)
                {
                    var frame = uframes[i];
                    if (frame.Incompleted)
                    {
                        var (first, last) = QueryRange(segs, frame);
                        frame.First = first;
                        frame.Last = last;
                        for (var n = first; n <= last; n++)
                        {
                            var seg = segs[n];
                            if (!seg.Done && !inSegs.Contains(seg))
                            {
                                inSegs.Add(seg);
                            }
                        }
                    }
                }

                if (downloadTask != null)
                {
                    downloadTask.Dispose();
                    downloadTask = null;
                }
                downloadTask = ParallelTask.Start("ugoira.download", 0, inSegs.Count, 2, i =>
                {
                    var seg = inSegs[i];
#if DEBUG
                    App.DebugPrint($"start to download segment #{seg.Index}, from {seg.From} to {seg.To} / {size}");
#endif
                    using (var ms = new MemoryStream(data, (int)seg.From, seg.Count))
                    {
                        HttpUtility.DownloadUgoiraImage(client, url, id, lastModified, seg.From, seg.To, ms);
                    }
                    seg.Done = true;
                    return timer != null;
                });

                for (var i = 0; i < uframes.Length; i++)
                {
                    var frame = uframes[i];
                    if (frame.Incompleted)
                    {
                        var first = frame.First;
                        var last = frame.Last;
                        while (segs.AnyFor(first, last, s => !s.Done))
                        {
                            lock (sync)
                            {
                                if (timer == null)
                                {
                                    return;
                                }
                            }
                            Thread.Sleep(DELAY);
                        }

                        var file = ExtractImage(zip, data, frame.Offset);
                        if (file != null)
                        {
                            frame.FilePath = file;
                            frames[i] = ImageSource.FromFile(file);
                            frame.Incompleted = false;
                        }
                    }
                }
#if LOG
                App.DebugPrint("load frames over");
#endif
            }
        }

        private (int first, int last) QueryRange(Segment[] segs, IllustUgoiraBody.Frame frame)
        {
            var start = frame.Offset;
            var end = start + frame.Length;
            var first = segs.LastIndexOf(s => start >= s.From);
            var last = segs.IndexOf(s => end <= s.To);
            return (first, last);
        }

        private string ExtractImage(string zip, byte[] data, int index)
        {
            var i = index;
            if (i + 30 > data.Length - 1)   // last
            {
                return null;
            }
            if (data[i] != 0x50 || data[i + 1] != 0x4b ||
                data[i + 2] != 0x03 || data[i + 3] != 0x04)
            {
                App.DebugPrint($"extract complete, header: {BitConverter.ToInt32(data, i):x8}");
                return null;
            }
            i += 8; // signature(4) & version(2) & flags(2)
            if (data[i] != 0 || data[i + 1] != 0)
            {
                App.DebugError("extract.image", $"doesn't support compressed data: {BitConverter.ToInt16(data, i):x4}");
                return null;
            }
            i += 10; // compression(2) & mod-time(2) & mod-date(2) & crc-32(4)
            int size = BitConverter.ToInt32(data, i);
            i += 4; // size(4)
            int rawSize = BitConverter.ToInt32(data, i);
            if (size != rawSize)
            {
                App.DebugError("extract.image", $"data seems to be compressed: {size:x8} ({rawSize:x8}) bytes");
                return null;
            }
            i += 4; // rawSize(4)
            int filenameLength = BitConverter.ToInt16(data, i);
            i += 2; // filename length(2)
            int extraLength = BitConverter.ToInt16(data, i);
            i += 2; // extra length(2)

            if (i + filenameLength + extraLength + size > data.Length - 1)  // last
            {
                App.DebugPrint($"download is not completed, index: {index}, size: {size}, length: {data.Length}"); // , last: {last}
                return null;
            }

            var filename = Encoding.UTF8.GetString(data, i, filenameLength);
            i += filenameLength + extraLength;  // filename & extra

            // content
            var content = new byte[size];
            Array.Copy(data, i, content, 0, size);
            //i += size;

            var file = Stores.SaveUgoiraImage(zip, filename, content);
            return file;
        }

        class Segment
        {
            public int Index;
            public long From;
            public long To;
            public bool Done;

            public int Count => (int)(To - From + 1);

            public Segment(int index, long from, long to)
            {
                Index = index;
                From = from;
                To = to;
                Done = false;
            }
        }

#if __IOS__
        public async Task<string> ExportVideo()
        {
            if (ugoira == null || ugoira.frames.Any(f => f.Incompleted))
            {
                return null;
            }

            var file = Stores.GetUgoiraPath(ugoira.originalSrc, ".mp4");
            if (File.Exists(file))
            {
                File.Delete(file);
            }
            var fileURL = NSUrl.FromFilename(file);
            var images = ugoira.frames.Select(f => UIImage.FromFile(f.FilePath)).ToArray();

            var videoSettings = new NSMutableDictionary
            {
                { AVVideo.CodecKey, AVVideo.CodecH264 },
                { AVVideo.WidthKey, NSNumber.FromNFloat(images[0].Size.Width) },
                { AVVideo.HeightKey, NSNumber.FromNFloat(images[0].Size.Height) }
            };
            var videoWriter = new AVAssetWriter(fileURL, AVFileType.Mpeg4, out var err);
            if (err != null)
            {
                App.DebugError("export.video", $"failed to create an AVAssetWriter: {err}");
                return null;
            }
            var writerInput = new AVAssetWriterInput(AVMediaType.Video, new AVVideoSettingsCompressed(videoSettings));
            var sourcePixelBufferAttributes = new NSMutableDictionary
            {
                { CVPixelBuffer.PixelFormatTypeKey, NSNumber.FromInt32((int)CVPixelFormatType.CV32ARGB) }
            };
            var pixelBufferAdaptor = new AVAssetWriterInputPixelBufferAdaptor(writerInput, sourcePixelBufferAttributes);
            videoWriter.AddInput(writerInput);
            if (videoWriter.StartWriting())
            {
                videoWriter.StartSessionAtSourceTime(CMTime.Zero);
                var lastTime = CMTime.Zero;
#if DEBUG
                bool log = false;
#endif
                for (int i = 0; i < images.Length; i++)
                {
                    while (!writerInput.ReadyForMoreMediaData)
                    {
                        lock (sync)
                        {
                            if (timer == null)
                            {
                                return null;
                            }
                        }
                        Thread.Sleep(50);
                    }
                    // get pixel buffer and fill it with the image
                    using (CVPixelBuffer pixelBufferImage = pixelBufferAdaptor.PixelBufferPool.CreatePixelBuffer())
                    {
                        pixelBufferImage.Lock(CVPixelBufferLock.None);

                        try
                        {
                            IntPtr pxdata = pixelBufferImage.BaseAddress;

                            if (pxdata != IntPtr.Zero)
                            {
                                using (var rgbColorSpace = CGColorSpace.CreateDeviceRGB())
                                {
                                    var cgImage = images[i].CGImage;
                                    var width = cgImage.Width;
                                    var height = cgImage.Height;
                                    var bitsPerComponent = cgImage.BitsPerComponent;
                                    var bytesPerRow = cgImage.BytesPerRow;
                                    // padding to 64
                                    var bytes = bytesPerRow >> 6 << 6;
                                    if (bytes < bytesPerRow)
                                    {
                                        bytes += 64;
                                    }
#if DEBUG
                                    if (!log)
                                    {
                                        log = true;
                                        App.DebugPrint($"animation, width: {width}, height: {height}, type: {cgImage.UTType}\n" +
                                            $"bitmapInfo: {cgImage.BitmapInfo}\n" +
                                            $"bpc: {bitsPerComponent}\n" +
                                            $"bpp: {cgImage.BitsPerPixel}\n" +
                                            $"calculated: {bytesPerRow} => {bytes}");
                                    }
#endif
                                    using (CGBitmapContext bitmapContext = new CGBitmapContext(
                                        pxdata, width, height,
                                        bitsPerComponent,
                                        bytes,
                                        rgbColorSpace,
                                        CGImageAlphaInfo.NoneSkipFirst))
                                    {
                                        if (bitmapContext != null)
                                        {
                                            bitmapContext.DrawImage(new CGRect(0, 0, width, height), cgImage);
                                        }
                                    }
                                }
                            }
                        }
                        finally
                        {
                            pixelBufferImage.Unlock(CVPixelBufferLock.None);
                        }

                        // and finally append buffer to adapter
                        if (pixelBufferAdaptor.AssetWriterInput.ReadyForMoreMediaData && pixelBufferImage != null)
                        {
                            pixelBufferAdaptor.AppendPixelBufferWithPresentationTime(pixelBufferImage, lastTime);
                        }
                    }

                    var frameTime = new CMTime(ugoira.frames[i].delay, 1000);
                    lastTime = CMTime.Add(lastTime, frameTime);
                }
                writerInput.MarkAsFinished();
                await videoWriter.FinishWritingAsync();
                return file;
            }
            return null;
        }

        public Task<string> ExportGif()
        {
            if (ugoira == null || ugoira.frames.Any(f => f.Incompleted))
            {
                return null;
            }

            var file = Stores.GetUgoiraPath(ugoira.originalSrc, ".gif");
            if (File.Exists(file))
            {
                File.Delete(file);
            }
            var fileURL = NSUrl.FromFilename(file);

            var dictFile = new NSMutableDictionary();
            var gifDictionaryFile = new NSMutableDictionary
            {
                { ImageIO.CGImageProperties.GIFLoopCount, NSNumber.FromFloat(0f) }
            };
            dictFile.Add(ImageIO.CGImageProperties.GIFDictionary, gifDictionaryFile);
            var dictFrame = new NSMutableDictionary();
            var gifDictionaryFrame = new NSMutableDictionary
            {
                { ImageIO.CGImageProperties.GIFDelayTime, NSNumber.FromFloat(ugoira.frames[0].delay / 1000f) }
            };
            dictFrame.Add(ImageIO.CGImageProperties.GIFDictionary, gifDictionaryFrame);

            var task = new TaskCompletionSource<string>();
            Xamarin.Essentials.MainThread.BeginInvokeOnMainThread(() =>
            {
                var images = ugoira.frames.Select(f => UIImage.FromFile(f.FilePath)).ToArray();

                var imageDestination = ImageIO.CGImageDestination.Create(fileURL, MobileCoreServices.UTType.GIF, images.Length);
                imageDestination.SetProperties(dictFile);
                for (int i = 0; i < images.Length; i++)
                {
                    imageDestination.AddImage(images[i].CGImage, dictFrame);
                }
                imageDestination.Close();

                task.SetResult(file);
            });
            return task.Task;
        }
#endif
    }

    public class UgoiraEventArgs : EventArgs
    {
        public IllustDetailItem DetailItem { get; set; }
        public ImageSource Image { get; set; }
        public int FrameIndex { get; set; }
    }
}

