using System;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using IPCamLib.Base;

// ReSharper disable UnusedMember.Global - public API

namespace IPCamLib.Concrete
{
    /// <summary>
    /// Implements the ICamera interface for IP cameras exposing an MJPEG stream over HTTP.
    /// </summary>
    public class HTTPMJPEGStreamCamera : BaseHTTPStreamCamera
    {
        // MJPEG decoding is a variation on https://github.com/arndre/MjpegDecoder
        private static readonly byte[] JpegSOI = { 0xff, 0xd8 }; // start of image bytes
        private static readonly byte[] JpegEOI = { 0xff, 0xd9 }; // end of image bytes

        private const int ChunkSize = 1024;
        private const int MaxBufferSize = 1024 * 1024 * 10;


        /// <summary>
        /// Creates a new instance for an IP camera exposing an MJPEG stream over HTTP.
        /// </summary>
        /// <inheritdoc />
        public HTTPMJPEGStreamCamera(Uri streamUri) : base(streamUri)
        {
        }


        /// <inheritdoc />
        protected override async Task ReadFrames(ICameraObserver observer, Stream stream, CancellationToken cancellationToken)
        {
            var buffer = new byte[ChunkSize];
            var bufferPosition = 0;
            int? startOfImage = null;
            int? lastEndOfSearch = null;


            void ExpandBuffer()
            {
                // Make sure we have at least ChunkSize remaining
                if (buffer.Length >= bufferPosition + ChunkSize)
                    return;

                // If we pass the MaxBufferSize (10mb unless changed above), this is likely not an MJPEG stream, abort
                if (bufferPosition + ChunkSize > MaxBufferSize)
                    throw new IOException("Buffer size exceeded before encountering JPEG image");

                Array.Resize(ref buffer, bufferPosition + ChunkSize);
            }


            void ResetBuffer(int untilPosition)
            {
                // Don't resize the buffer down, it is very likely the next image is of a similar size.
                // Instead move whatever's remaining to the start.
                if (untilPosition < buffer.Length - 1)
                    Array.Copy(buffer, untilPosition, buffer, 0, bufferPosition - untilPosition);

                bufferPosition = 0;
                startOfImage = null;
                lastEndOfSearch = null;
            }


            var frameTimeout = TimeSpan.FromMinutes(1);
            var frameTimeoutCancellationTokenSource = new CancellationTokenSource(frameTimeout);
            var combinedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, frameTimeoutCancellationTokenSource.Token);

            while (!combinedCancellation.IsCancellationRequested)
            {
                var bytesRead = await stream.ReadAsync(buffer, bufferPosition, ChunkSize, combinedCancellation.Token);
                if (bytesRead == 0)
                    throw new EndOfStreamException();

                bufferPosition += bytesRead;

                if (!startOfImage.HasValue)
                {
                    var index = buffer.IndexOf(JpegSOI, bufferPosition);
                    if (index == -1)
                    {
                        // No start of image yet, we need to buffer more
                        ExpandBuffer();
                        continue;
                    }

                    startOfImage = index;
                }

                var endOfImage = buffer.IndexOf(JpegEOI, bufferPosition, lastEndOfSearch.GetValueOrDefault(startOfImage.Value));
                if (endOfImage == -1)
                {
                    // No start of image yet, we need to buffer more. Keep track of where we were so we don't
                    // need to scan it all again
                    lastEndOfSearch = bufferPosition - JpegEOI.Length;
                    ExpandBuffer();
                    continue;
                }

                if (endOfImage < startOfImage.Value)
                {
                    // Oops, wut?! Uhm. yeah. let's pretend this never happened, ok?
                    ResetBuffer(startOfImage.Value + JpegSOI.Length);
                    continue;
                }

                endOfImage += JpegEOI.Length;

                using (var image = new Bitmap(new MemoryStream(buffer, startOfImage.Value, endOfImage - startOfImage.Value)))
                {
                    await observer.OnFrame(image);
                }

                ResetBuffer(endOfImage);
                frameTimeoutCancellationTokenSource.CancelAfter(frameTimeout);
            }
        }
    }


    internal static class BufferExtensions
    {
        public static int IndexOf(this byte[] buffer, byte[] pattern, int limit = int.MaxValue, int startAt = 0)
        {
            var patternIndex = 0;
            int bufferIndex;

            for (bufferIndex = startAt; bufferIndex < buffer.Length && patternIndex < pattern.Length && bufferIndex < limit; bufferIndex++)
            {
                if (buffer[bufferIndex] == pattern[patternIndex])
                {
                    patternIndex++;
                }
                else
                {
                    patternIndex = 0;
                }
            }

            if (patternIndex == pattern.Length)
                return bufferIndex - pattern.Length;

            return -1;
        }
    }
}
