using System;
using MECF.Framework.Common.Communications.Tcp.Socket.Framing.Base;

namespace MECF.Framework.Common.Communications.Tcp.Socket.Framing
{
    public enum LengthField
    {
        OneByte = 1,
        TwoBytes = 2,
        FourBytes = 4,
        EigthBytes = 8,
    }

    public sealed class LengthFieldBasedFrameBuilder : FrameBuilder
    {
        public LengthFieldBasedFrameBuilder()
            : this(LengthField.FourBytes)
        {
        }

        public LengthFieldBasedFrameBuilder(LengthField lengthField)
            : this(new LengthFieldBasedFrameEncoder(lengthField), new LengthFieldBasedFrameDecoder(lengthField))
        {
        }

        public LengthFieldBasedFrameBuilder(LengthFieldBasedFrameEncoder encoder, LengthFieldBasedFrameDecoder decoder)
            : base(encoder, decoder)
        {
        }
    }

    public sealed class LengthFieldBasedFrameEncoder : IFrameEncoder
    {
        public LengthFieldBasedFrameEncoder(LengthField lengthField)
        {
            LengthField = lengthField;
        }

        public LengthField LengthField { get; private set; }

        public void EncodeFrame(byte[] payload, int offset, int count, out byte[] frameBuffer, out int frameBufferOffset, out int frameBufferLength)
        {
            byte[] buffer = null;

            switch (this.LengthField)
            {
                case LengthField.OneByte:
                    {
                        if (count > byte.MaxValue)
                        {
                            throw new ArgumentOutOfRangeException("count");
                        }

                        buffer = new byte[1 + count];
                        buffer[0] = (byte)count;
                        Array.Copy(payload, offset, buffer, 1, count);
                    }
                    break;
                case LengthField.TwoBytes:
                    {
                        if (count > short.MaxValue)
                        {
                            throw new ArgumentOutOfRangeException("count");
                        }

                        buffer = new byte[2 + count];
                        buffer[0] = (byte)((ushort)count >> 8);
                        buffer[1] = (byte)count;
                        Array.Copy(payload, offset, buffer, 2, count);
                    }
                    break;
                case LengthField.FourBytes:
                    {
                        buffer = new byte[4 + count];
                        uint unsignedValue = (uint)count;
                        buffer[0] = (byte)(unsignedValue >> 24);
                        buffer[1] = (byte)(unsignedValue >> 16);
                        buffer[2] = (byte)(unsignedValue >> 8);
                        buffer[3] = (byte)unsignedValue;
                        Array.Copy(payload, offset, buffer, 4, count);
                    }
                    break;
                case LengthField.EigthBytes:
                    {
                        buffer = new byte[8 + count];
                        ulong unsignedValue = (ulong)count;
                        buffer[0] = (byte)(unsignedValue >> 56);
                        buffer[1] = (byte)(unsignedValue >> 48);
                        buffer[2] = (byte)(unsignedValue >> 40);
                        buffer[3] = (byte)(unsignedValue >> 32);
                        buffer[4] = (byte)(unsignedValue >> 24);
                        buffer[5] = (byte)(unsignedValue >> 16);
                        buffer[6] = (byte)(unsignedValue >> 8);
                        buffer[7] = (byte)unsignedValue;
                        Array.Copy(payload, offset, buffer, 8, count);
                    }
                    break;
                default:
                    throw new NotSupportedException("Specified length field is not supported.");
            }

            frameBuffer = buffer;
            frameBufferOffset = 0;
            frameBufferLength = buffer.Length;
        }
    }

    public sealed class LengthFieldBasedFrameDecoder : IFrameDecoder
    {
        public LengthFieldBasedFrameDecoder(LengthField lengthField)
        {
            LengthField = lengthField;
        }

        public LengthField LengthField { get; private set; }

        public bool TryDecodeFrame(byte[] buffer, int offset, int count, out int frameLength, out byte[] payload, out int payloadOffset, out int payloadCount)
        {
            frameLength = 0;
            payload = null;
            payloadOffset = 0;
            payloadCount = 0;

            byte[] output = null;
            long length = 0;

            switch (this.LengthField)
            {
                case LengthField.OneByte:
                    {
                        if (count < 1)
                            return false;

                        length = buffer[offset];
                        if (count - 1 < length)
                            return false;

                        output = new byte[length];
                        Array.Copy(buffer, offset + 1, output, 0, length);
                    }
                    break;
                case LengthField.TwoBytes:
                    {
                        if (count < 2)
                            return false;

                        length = (short)(buffer[offset] << 8 | buffer[offset + 1]);
                        if (count - 2 < length)
                            return false;

                        output = new byte[length];
                        Array.Copy(buffer, offset + 2, output, 0, length);
                    }
                    break;
                case LengthField.FourBytes:
                    {
                        if (count < 4)
                            return false;

                        length = buffer[offset] << 24 |
                            buffer[offset + 1] << 16 |
                            buffer[offset + 2] << 8 |
                            buffer[offset + 3];
                        if (count - 4 < length)
                            return false;

                        output = new byte[length];
                        Array.Copy(buffer, offset + 4, output, 0, length);
                    }
                    break;
                case LengthField.EigthBytes:
                    {
                        if (count < 8)
                            return false;

                        int i1 = buffer[offset] << 24 |
                            buffer[offset + 1] << 16 |
                            buffer[offset + 2] << 8 |
                            buffer[offset + 3];
                        int i2 = buffer[offset + 4] << 24 |
                            buffer[offset + 5] << 16 |
                            buffer[offset + 6] << 8 |
                            buffer[offset + 7];

                        length = (uint)i2 | ((long)i1 << 32);
                        if (count - 8 < length)
                            return false;

                        output = new byte[length];
                        Array.Copy(buffer, offset + 8, output, 0, length);
                    }
                    break;
                default:
                    throw new NotSupportedException("Specified length field is not supported.");
            }

            payload = output;
            payloadOffset = 0;
            payloadCount = output.Length;

            frameLength = (int)this.LengthField + output.Length;

            return true;
        }
    }
}