using System;
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
using ProtoBuf;
using PulseCat.Proxy.Objects;

namespace PulseCat.Proxy
{
    public class ProtoObjectReceiver : IProtoObjectReceiver
    {
        private readonly ISocket socket;
        private readonly ILogger logger;
        public EventHandler<EventArgs> NoDataReceived { get; set; }
        public EventHandler<ProtoObjectReceivedEventArgs> ProtoObjectReceived { get; set; }
        private bool shutdownReceived;
        private bool stopCalled = false;

        public ProtoObjectReceiver(ISocket socket, ILogger logger)
        {
            if (socket == null)
            {
                throw new NullReferenceException(nameof(socket));
            }
            this.socket = socket;
            this.logger = logger;
        }

        //stops the receive loop
        public void Stop()
        {
            stopCalled = true;
        }

        /// <summary>
        /// this will loop until <see cref="SocketShutdown"/> is recevied
        /// </summary>
        /// <returns></returns>
        public async Task ReceiveLoopAsync()
        {
            bool seekingObject = false;
            int lengthPrefix = 0;
            var bufferSize = 8128;
            var buffer = new byte[bufferSize];

            try
            {
                using (var memoryBuffer = new MemoryStream())
                {
                    while (socket.Connected && !shutdownReceived)
                    {
                        var bytesReceived =
                            await socket.ReceiveAsync(buffer, 0, bufferSize, SocketFlags.None).ConfigureAwait(false);
                        if (bytesReceived > 0)
                        {

                            //we are currently not looking for an object so we expect the next object length prefix to be read
                            if (!seekingObject && Serializer.TryReadLengthPrefix(buffer, 0, bytesReceived, PrefixStyle.Base128,
                                out lengthPrefix))
                            {
                                seekingObject = true;
                            }
                            await memoryBuffer.WriteAsync(buffer, 0, bytesReceived);

                            //now after writing the data into the memory stream check if we have received a whole object
                            if (memoryBuffer.Length == lengthPrefix + 1)
                            {
                                memoryBuffer.Position = 0;
                                var receivedObject = Serializer.DeserializeWithLengthPrefix<ProtoBaseObject>(memoryBuffer, PrefixStyle.Base128);
                                seekingObject = false;
                                memoryBuffer.SetLength(0);

                                if (ProtoObjectResolver.Instance.ResolveType(receivedObject.ObjectTypeId) ==
                                    typeof(ShutdownData))
                                {
                                    shutdownReceived = true;
                                }
                                await ProtoObjectReceived.InvokeAsync(this, new ProtoObjectReceivedEventArgs(receivedObject))
                                    .ConfigureAwait(false);
                            }
                        }
                        else
                        {
                            if (!stopCalled)
                            {
                                await NoDataReceived.InvokeAsync(this, null).ConfigureAwait(false);
                            }
                        }

                    }


                }
            }
            catch (Exception ex)
            {
                
                if (!stopCalled)
                {
                    logger.WriteLine(ex.Message, MessageLevel.Fatal);
                    throw;
                }
            }

        }
    }
}