// Created with MonoDevelop
//
//    Copyright (C) 2019 Hiperscan GmbH Dresden, Germany
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

using Hiperscan.SGS.Common;
using Hiperscan.Unix;


namespace Hiperscan.SGS.SgsNt.Protocol
{

    abstract partial class BaseProtocol
    {
        public const int HEADER_START_LENGTH = sizeof(byte);
        public const int HEADER_ADDR_LENGTH  = sizeof(byte);
        public const int HEADER_ERRNO_LENGTH = sizeof(Errno);
        public const int HEADER_LENGTH       = HEADER_START_LENGTH + HEADER_ADDR_LENGTH + HEADER_ERRNO_LENGTH;

        public const int PAYLOAD_CMD_LENGTH = 2 * sizeof(byte);
        public const int PAYLOAD_LEN_LENGTH = sizeof(UInt16);
        public const int PAYLOAD_MAX_LENGTH = 17 * 1024;

        public const int CHECKSUM_LENGTH = sizeof(UInt32);

        public const int HEADER_START_OFFSET = 0;
        public const int HEADER_ADDR_OFFSET  = HEADER_START_LENGTH;
        public const int HEADER_ERRNO_OFFSET = HEADER_ADDR_OFFSET + HEADER_ADDR_LENGTH;
        public const int PAYLOAD_CMD_OFFSET  = HEADER_LENGTH;
        public const int PAYLOAD_LEN_OFFSET  = PAYLOAD_CMD_OFFSET + PAYLOAD_CMD_LENGTH;
        public const int PAYLOAD_DATA_OFFSET = PAYLOAD_LEN_OFFSET + PAYLOAD_LEN_LENGTH;

        public const int NUMBER_OF_REPEATS = 5;

        public abstract class Frame
        {
            private static readonly Crc32 CRC32 = new Crc32();

            protected Frame(AddressType addr, char c1, char c2, Type payload_data_type, UInt16 payload_data_len)
            {
                if (payload_data_len > BaseProtocol.PAYLOAD_MAX_LENGTH)
                    throw new Exception(string.Format(Catalog.GetString("Payload length exceeds transfer limit: {0} > {1}"), payload_data_len, BaseProtocol.PAYLOAD_MAX_LENGTH));

                this.PayloadDataType  = payload_data_type;

                this.Buffer = new byte[HEADER_LENGTH + PAYLOAD_CMD_LENGTH + PAYLOAD_LEN_LENGTH + payload_data_len + CHECKSUM_LENGTH];

                this.HeaderStart   = HeaderStartType.FrameStart;
                this.HeaderAddress = addr;
                this.HeaderErrno   = Errno.Success;

                this.PayloadCmd1 = c1;
                this.PayloadCmd2 = c2;

                if (this.PayloadDataType != null && this.PayloadDataType.IsArray == false &&
                    this.PayloadDataType != typeof(string) &&  Marshal.SizeOf(this.PayloadDataType) != payload_data_len)
                    throw new InvalidDataException(string.Format(Catalog.GetString("Unexpected Payload data length for type {0}"), this.PayloadDataType));

                this.PayloadLength = payload_data_len;
            }

            protected Frame(Frame frame)
            {
                this.PayloadDataType = frame.PayloadDataType;

                this.Buffer = (byte[])frame.Buffer.Clone();
            }

            protected Frame(byte[] buf, Type payload_data_type)
            {
                this.PayloadDataType = payload_data_type;

                if (buf[0] != (byte)HeaderStartType.FrameStart)
                {
                    Console.WriteLine(Frame.GetString(buf));
                    throw new InvalidDataException(string.Format(Catalog.GetString("Unexpected header start: 0x{0:X2}"), buf[0]));
                }

                UInt16 payload_data_len = BitConverter.ToUInt16(buf, PAYLOAD_LEN_OFFSET);

                if (payload_data_len != buf.Length - HEADER_LENGTH - PAYLOAD_CMD_LENGTH - PAYLOAD_LEN_LENGTH - CHECKSUM_LENGTH)
                {
                    Console.WriteLine(Frame.GetString(buf));
                    throw new InvalidDataException(string.Format(Catalog.GetString("Unexpected data buffer length: {0}"), payload_data_len));
                }

                this.Buffer = (byte[])buf.Clone();
            }

            public UInt32 ComputeCrc32()
            {
                return Crc32.FromBigEndian(BaseProtocol.Frame.CRC32.ComputeHash(this.Buffer, 0, this.Buffer.Length - CHECKSUM_LENGTH));
            }

            public HeaderStartType HeaderStart
            {
                get         { return (HeaderStartType)this.Buffer[HEADER_START_OFFSET]; }
                protected set { this.Buffer[HEADER_START_OFFSET] = (byte)value; }
            }

            public AddressType HeaderAddress 
            {
                get         { return (AddressType)this.Buffer[HEADER_ADDR_OFFSET]; }
                protected set { this.Buffer[HEADER_ADDR_OFFSET] = (byte)value; }
            }

            public Errno HeaderErrno
            {
                get { return (Errno)BitConverter.ToUInt16(this.Buffer, HEADER_ERRNO_OFFSET); }
                set { Array.Copy(BitConverter.GetBytes((UInt16)value), 0, this.Buffer, HEADER_ERRNO_OFFSET, HEADER_ERRNO_LENGTH); }
            }

            public char PayloadCmd1
            {
                get         { return (char)this.Buffer[PAYLOAD_CMD_OFFSET+0];  }
                protected set { this.Buffer[PAYLOAD_CMD_OFFSET+0] = (byte)value; }
            }

            public char PayloadCmd2
            {
                get         { return (char)this.Buffer[PAYLOAD_CMD_OFFSET + 1]; }
                protected set { this.Buffer[PAYLOAD_CMD_OFFSET + 1] = (byte)value; }
            }

            public UInt16 PayloadLength
            {
                get         { return BitConverter.ToUInt16(this.Buffer, PAYLOAD_LEN_OFFSET); }
                protected set { Array.Copy(BitConverter.GetBytes(value), 0, this.Buffer, PAYLOAD_LEN_OFFSET, PAYLOAD_LEN_LENGTH); }
            }

            public byte[] PayloadData
            {
                get         { return this.Buffer.Skip(PAYLOAD_DATA_OFFSET).Take(this.PayloadLength).ToArray(); }
                protected set { Array.Copy(value, 0, this.Buffer, PAYLOAD_DATA_OFFSET, this.PayloadLength); }
            }

            public UInt32 Checksum
            {
                get { return BitConverter.ToUInt32(this.Buffer.Skip(PAYLOAD_DATA_OFFSET+this.PayloadLength).Take(CHECKSUM_LENGTH).ToArray(), 0); }
                protected set { Array.Copy(BitConverter.GetBytes(value), 0, this.Buffer, PAYLOAD_DATA_OFFSET + this.PayloadLength, CHECKSUM_LENGTH); }
            }

            public byte ByteValue
            {
                get
                {
                    if (this.PayloadDataType != typeof(byte))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'byte'."));

                    return this.PayloadData[0];
                }
                set
                {
                    if (this.PayloadDataType != typeof(byte))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'byte'."));

                    this.PayloadData = new byte[] { value };
                }
            }

            public Int16 Int16Value
            {
                get
                {
                    if (this.PayloadDataType != typeof(short))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int16'."));

                    return BitConverter.ToInt16(this.PayloadData, 0);
                }
                set
                {
                    if (this.PayloadDataType != typeof(short))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int16'."));

                    this.PayloadData = BitConverter.GetBytes(value);
                }
            }

            public UInt16 UInt16Value
            {
                get
                {
                    if (this.PayloadDataType != typeof(UInt16))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'UInt16'."));

                    return BitConverter.ToUInt16(this.PayloadData, 0);
                }
                set
                {
                    if (this.PayloadDataType != typeof(UInt16))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'UInt16'."));

                    this.PayloadData = BitConverter.GetBytes(value);
                }
            }

            public Int32 Int32Value
            {
                get
                {
                    if (this.PayloadDataType != typeof(Int32))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int32'."));

                    return BitConverter.ToInt32(this.PayloadData, 0);
                }
                set
                {
                    if (this.PayloadDataType != typeof(Int32))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int32'."));

                    this.PayloadData = BitConverter.GetBytes(value);
                }
            }

            public uint UInt32Value
            {
                get
                {
                    if (this.PayloadDataType != typeof(UInt32))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'UInt32'."));

                    return BitConverter.ToUInt32(this.PayloadData, 0);
                }
                set
                {
                    if (this.PayloadDataType != typeof(UInt32))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'UInt32'."));

                    this.PayloadData = BitConverter.GetBytes(value);
                }
            }

            public Int64 Int64Value
            {
                get
                {
                    if (this.PayloadDataType != typeof(Int64))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int64'."));

                    return BitConverter.ToInt64(this.PayloadData, 0);
                }
                set
                {
                    if (this.PayloadDataType != typeof(Int64))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int64'."));

                    this.PayloadData = BitConverter.GetBytes(value);
                }
            }

            public string StringValue
            {
                get
                {
                    if (this.PayloadDataType != typeof(string))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'string'."));

                    return Encoding.UTF8.GetString(this.PayloadData, 0, this.PayloadLength);
                }
                set
                {
                    if (this.PayloadDataType != typeof(string))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'string'."));

                    string extension = new string(' ', this.PayloadLength - value.Length);
                    value = (value ?? String.Empty) + (extension ?? String.Empty);

                    if (value.Length != this.PayloadLength)
                        throw new Exception(Catalog.GetString("Unexpected string length."));

                    this.PayloadData = Encoding.UTF8.GetBytes(value);
                }
            }

            public byte[] ByteArrayValue
            {
                get
                {
                    if (this.PayloadDataType != typeof(byte[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'byte[]'."));

                    return this.PayloadData;
                }
                set
                {
                    if (this.PayloadDataType != typeof(byte[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'byte[]'."));

                    if (value.Length != this.PayloadLength)
                        throw new Exception(Catalog.GetString("Parameter data array has unexpected length."));

                    this.PayloadData = value;
                }
            }

            public Int16[] Int16ArrayValue
            {
                get
                {
                    if (this.PayloadDataType != typeof(short[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int16[]'."));

                    if (this.PayloadLength % sizeof(Int16) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    Int16[] buf = new Int16[(this.PayloadLength) / sizeof(Int16)];
                    System.Buffer.BlockCopy(this.PayloadData, 0, buf, 0, this.PayloadLength);
                    return buf;
                }
                set
                {
                    if (this.PayloadDataType != typeof(short[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int16[]'."));

                    if (this.PayloadLength % sizeof(Int16) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    if (value.Length != this.PayloadLength / sizeof(Int16))
                        throw new Exception(Catalog.GetString("Parameter data array has unexpected length."));

                    byte[] buf = new byte[this.PayloadLength];
                    System.Buffer.BlockCopy(value, 0, buf, 0, this.PayloadLength);
                    this.PayloadData = buf;
                }
            }

            public UInt16[] UInt16ArrayValue
            {
                get
                {
                    if (this.PayloadDataType != typeof(UInt16[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'UInt16[]'."));

                    if (this.PayloadLength % sizeof(Int16) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    UInt16[] buf = new UInt16[(this.PayloadLength) / sizeof(UInt16)];
                    System.Buffer.BlockCopy(this.PayloadData, 0, buf, 0, this.PayloadLength);
                    return buf;
                }
                set
                {
                    if (this.PayloadDataType != typeof(UInt16[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'UInt16[]'."));

                    if (this.PayloadLength % sizeof(UInt16) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    if (value.Length != this.PayloadLength / sizeof(UInt16))
                        throw new Exception(Catalog.GetString("Parameter data array has unexpected length."));

                    byte[] buf = new byte[this.PayloadLength];
                    System.Buffer.BlockCopy(value, 0, buf, 0, this.PayloadLength);
                    this.PayloadData = buf;
                }
            }

            public Int32[] Int32ArrayValue
            {
                get
                {
                    if (this.PayloadDataType != typeof(Int32[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int32[]'."));

                    if (this.PayloadLength % sizeof(Int32) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    Int32[] buf = new Int32[(this.PayloadLength) / sizeof(Int32)];
                    System.Buffer.BlockCopy(this.PayloadData, 0, buf, 0, this.PayloadLength);
                    return buf;
                }
                set
                {
                    if (this.PayloadDataType != typeof(Int32[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'Int32[]'."));

                    if (this.PayloadLength % sizeof(Int32) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    if (value.Length != this.PayloadLength / sizeof(Int32))
                        throw new Exception(Catalog.GetString("Parameter data array has unexpected length."));

                    byte[] buf = new byte[this.PayloadLength];
                    System.Buffer.BlockCopy(value, 0, buf, 0, this.PayloadLength);
                    this.PayloadData = buf;
                }
            }

            public UInt32[] UInt32ArrayValue
            {
                get
                {
                    if (this.PayloadDataType != typeof(UInt32[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'UInt32[]'."));

                    if (this.PayloadLength % sizeof(UInt32) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    UInt32[] buf = new UInt32[(this.PayloadLength) / sizeof(UInt32)];
                    System.Buffer.BlockCopy(this.PayloadData, 0, buf, 0, this.PayloadLength);
                    return buf;
                }
                set
                {
                    if (this.PayloadDataType != typeof(UInt32[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'UInt32[]'."));

                    if (this.PayloadLength % sizeof(UInt32) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    if (value.Length != this.PayloadLength / sizeof(UInt32))
                        throw new Exception(Catalog.GetString("Parameter data array has unexpected length."));

                    byte[] buf = new byte[this.PayloadLength];
                    System.Buffer.BlockCopy(value, 0, buf, 0, this.PayloadLength);
                    this.PayloadData = buf;
                }
            }

            public float[] FloatArrayValue
            {
                get
                {
                    if (this.PayloadDataType != typeof(float[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'float[]'."));

                    if (this.PayloadLength % sizeof(float) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    float[] buf = new float[this.PayloadLength / sizeof(float)];
                    System.Buffer.BlockCopy(this.PayloadData, 0, buf, 0, this.PayloadLength);
                    return buf;
                }
                set
                {
                    if (this.PayloadDataType != typeof(float[]))
                        throw new Exception(Catalog.GetString("Parameter data type is not 'float[]'."));

                    if (this.PayloadLength % sizeof(float) != 0)
                        throw new Exception(Catalog.GetString("Payload data array has unexpected length."));

                    if (value.Length != this.PayloadLength / sizeof(float))
                        throw new Exception(Catalog.GetString("Parameter data array has unexpected length."));

                    byte[] buf = new byte[this.PayloadLength];
                    System.Buffer.BlockCopy(value, 0, buf, 0, this.PayloadLength);
                    this.PayloadData = buf;
                }
            }

            public byte[] TransferStructValue
            {
                get
                {
                    if (typeof(ITransferStruct).IsAssignableFrom(this.PayloadDataType) == false)
                        throw new Exception(Catalog.GetString("Parameter data type is not derived from 'ITransferStruct'."));

                    return this.PayloadData;
                }
                set
                {
                    if (typeof(ITransferStruct).IsAssignableFrom(this.PayloadDataType) == false)
                        throw new Exception(Catalog.GetString("Parameter data type is not derived from 'ITransferStruct'."));

                    if (value.Length != this.PayloadLength)
                        throw new Exception(Catalog.GetString("Parameter data array has unexpected length."));

                    this.PayloadData = value;
                }
            }

            public byte[] Buffer      { get; set; }
            public Type PayloadDataType { get; private set; }

            private const int MAX_BYTE_PRINT_COUNT = 20;

            public byte this[int ix]
            {
                get { return this.Buffer[ix]; }
            }

            public static bool operator ==(Frame lhs, Frame rhs)
            {
                if (lhs is null && (rhs is null) == false)
                    return false;

                if ((lhs is null) == false && rhs is null)
                    return false;

                if (lhs is null && rhs is null)
                    return true;

                return lhs.Equals(rhs);
            }

            public static bool operator !=(Frame lhs, Frame rhs)
            {
                if (lhs is null && (rhs is null) == false)
                    return true;

                if ((lhs is null) == false && rhs is null)
                    return true;

                if (lhs is null && rhs is null)
                    return false;

                return lhs.Equals(rhs) == false;
            }

            public override bool Equals(object obj)
            {
                if (obj == null)
                    return false;

                if (obj is Frame)
                    return this.Buffer.SequenceEqual((obj as Frame).Buffer);

                return false;
            }

            public override int GetHashCode()
            {
                return this.PayloadCmd1.GetHashCode() ^ this.PayloadCmd2.GetHashCode();
            }

            internal static string GetString(byte[] buf)
            {
                StringBuilder sb = new StringBuilder();

                for (int ix = 0; ix < buf.Length; ++ix)
                {
                    sb.AppendFormat(" 0x{0:X2}", buf[ix]);

                    if (ix == MAX_BYTE_PRINT_COUNT)
                    {
                        sb.Append(" ...");
                        break;
                    }
                }

                return sb.ToString();
            }

            public override string ToString()
            {
                StringBuilder sb = new StringBuilder();
  
                sb.AppendFormat(" '{0}' '{1}' =>", (char)this.Buffer[BaseProtocol.PAYLOAD_CMD_OFFSET],
                                (char)this.Buffer[BaseProtocol.PAYLOAD_CMD_OFFSET + 1]);

                sb.Append(Frame.GetString(this.Buffer));
              
                return sb.ToString();
            }
        }
    }
}