#if DEBUG && !UNITY_WP_8_1 && !UNITY_WSA
using System;
using System.Reflection;
using System.Collections.Generic;
#if WINRT || NETCORE
using System.Linq;
#endif

namespace FlyingWormConsole3.LiteNetLib.Utils
{
    public interface INetSerializable
    {
        void Serialize(NetDataWriter writer);
        void Desereialize(NetDataReader reader);
    }

    public abstract class NetSerializerHasher
    {
        public abstract ulong GetHash(string type);
        public abstract void WriteHash(ulong hash, NetDataWriter writer);
        public abstract ulong ReadHash(NetDataReader reader);
    }

    public sealed class FNVHasher : NetSerializerHasher
    {
        private readonly Dictionary<string, ulong> _hashCache = new Dictionary<string, ulong>();
        private readonly char[] _hashBuffer = new char[1024];

        public override ulong GetHash(string type)
        {
            ulong hash;
            if (_hashCache.TryGetValue(type, out hash))
            {
                return hash;
            }
            hash = 14695981039346656037UL; //offset
            int len = type.Length;
            type.CopyTo(0, _hashBuffer, 0, len);
            for (var i = 0; i < len; i++)
            {
                hash = hash ^ _hashBuffer[i];
                hash *= 1099511628211UL; //prime
            }
            _hashCache.Add(type, hash);
            return hash;
        }

        public override ulong ReadHash(NetDataReader reader)
        {
            return reader.GetULong();
        }

        public override void WriteHash(ulong hash, NetDataWriter writer)
        {
            writer.Put(hash);
        }
    }

    public sealed class NetSerializer
    {
        private sealed class CustomType
        {
            public readonly CustomTypeWrite WriteDelegate;
            public readonly CustomTypeRead ReadDelegate;

            public CustomType(CustomTypeWrite writeDelegate, CustomTypeRead readDelegate)
            {
                WriteDelegate = writeDelegate;
                ReadDelegate = readDelegate;
            }
        }

        private delegate void CustomTypeWrite(NetDataWriter writer, object customObj);
        private delegate object CustomTypeRead(NetDataReader reader);

        private sealed class StructInfo
        {
            public readonly Action<NetDataWriter>[] WriteDelegate;
            public readonly Action<NetDataReader>[] ReadDelegate;
            public readonly Type[] FieldTypes;
            public object Reference;
            public Func<object> CreatorFunc;
            public Action<object, object> OnReceive;
            public readonly ulong Hash;
            public readonly int MembersCount;

            public StructInfo(ulong hash, int membersCount)
            {
                Hash = hash;
                MembersCount = membersCount;
                WriteDelegate = new Action<NetDataWriter>[membersCount];
                ReadDelegate = new Action<NetDataReader>[membersCount];
                FieldTypes = new Type[membersCount];
            }

            public void Write(NetDataWriter writer, object obj)
            {
                Reference = obj;
                for (int i = 0; i < MembersCount; i++)
                {
                    WriteDelegate[i](writer);
                }
            }

            public void Read(NetDataReader reader)
            {
                for (int i = 0; i < MembersCount; i++)
                {
                    ReadDelegate[i](reader);
                }
            }
        }

        private readonly Dictionary<ulong, StructInfo> _cache;
        private readonly Dictionary<Type, CustomType> _registeredCustomTypes;

        private static readonly HashSet<Type> BasicTypes = new HashSet<Type>
        {
            typeof(int),
            typeof(uint),
            typeof(byte),
            typeof(sbyte),
            typeof(short),
            typeof(ushort),
            typeof(long),
            typeof(ulong),
            typeof(string),
            typeof(float),
            typeof(double),
            typeof(bool)
        };

        private readonly NetDataWriter _writer;
        private readonly NetSerializerHasher _hasher;
        private const int MaxStringLenght = 1024;

        public NetSerializer() : this(new FNVHasher())
        {
        }

        public NetSerializer(NetSerializerHasher hasher)
        {
            _hasher = hasher;
            _cache = new Dictionary<ulong, StructInfo>();
            _registeredCustomTypes = new Dictionary<Type, CustomType>();
            _writer = new NetDataWriter();
        }

        private bool RegisterCustomTypeInternal<T>(Func<T> constructor) where T : INetSerializable
        {
            var t = typeof(T);
            if (_registeredCustomTypes.ContainsKey(t))
            {
                return false;
            }

            var rwDelegates = new CustomType(
                (writer, obj) =>
                {
                    ((T)obj).Serialize(writer);
                },
                reader =>
                {
                    var instance = constructor();
                    instance.Desereialize(reader);
                    return instance;
                });
            _registeredCustomTypes.Add(t, rwDelegates);
            return true;
        }

        /// <summary>
        /// Register custom property type
        /// </summary>
        /// <typeparam name="T">INetSerializable structure</typeparam>
        /// <returns>True - if register successful, false - if type already registered</returns>
        public bool RegisterCustomType<T>() where T : struct, INetSerializable
        {
            return RegisterCustomTypeInternal(() => new T());
        }

        /// <summary>
        /// Register custom property type
        /// </summary>
        /// <typeparam name="T">INetSerializable class</typeparam>
        /// <returns>True - if register successful, false - if type already registered</returns>
        public bool RegisterCustomType<T>(Func<T> constructor) where T : class, INetSerializable
        {
            return RegisterCustomTypeInternal(constructor);
        }

        /// <summary>
        /// Register custom property type
        /// </summary>
        /// <param name="writeDelegate"></param>
        /// <param name="readDelegate"></param>
        /// <returns>True - if register successful, false - if type already registered</returns>
        public bool RegisterCustomType<T>(Action<NetDataWriter, T> writeDelegate, Func<NetDataReader, T> readDelegate)
        {
            var t = typeof(T);
            if (BasicTypes.Contains(t) || _registeredCustomTypes.ContainsKey(t))
            {
                return false;
            }

            var rwDelegates = new CustomType(
                (writer, obj) => writeDelegate(writer, (T)obj),
                reader => readDelegate(reader));

            _registeredCustomTypes.Add(t, rwDelegates);
            return true;
        }

        private static Delegate CreateDelegate(Type type, MethodInfo info)
        {
#if WINRT || NETCORE
            return info.CreateDelegate(type);
#else
            return Delegate.CreateDelegate(type, info);
#endif
        }

        private static Func<TClass, TProperty> ExtractGetDelegate<TClass, TProperty>(MethodInfo info)
        {
            return (Func<TClass, TProperty>)CreateDelegate(typeof(Func<TClass, TProperty>), info);
        }

        private static Action<TClass, TProperty> ExtractSetDelegate<TClass, TProperty>(MethodInfo info)
        {
            return (Action<TClass, TProperty>)CreateDelegate(typeof(Action<TClass, TProperty>), info);
        }

        private StructInfo RegisterInternal<T>() where T : class
        {
            Type t = typeof(T);
            ulong nameHash = _hasher.GetHash(t.Name);

            StructInfo info;
            if (_cache.TryGetValue(nameHash, out info))
            {
                return info;
            }

#if WINRT || NETCORE
            var props = t.GetRuntimeProperties().ToArray();
            int propsCount = props.Count();
#else
            var props = t.GetProperties(
                BindingFlags.Instance |
                BindingFlags.Public |
                BindingFlags.GetProperty |
                BindingFlags.SetProperty);
            int propsCount = props.Length;
#endif
            if (props == null || propsCount < 0)
            {
                throw new ArgumentException("Type does not contain acceptable fields");
            }

            info = new StructInfo(nameHash, propsCount);
            for (int i = 0; i < props.Length; i++)
            {
                var property = props[i];
                var propertyType = property.PropertyType;

                //Set field type
                info.FieldTypes[i] = propertyType.IsArray ? propertyType.GetElementType() : propertyType;
#if WINRT || NETCORE
                bool isEnum = propertyType.GetTypeInfo().IsEnum;
                var getMethod = property.GetMethod;
                var setMethod = property.SetMethod;
#else
                bool isEnum = propertyType.IsEnum;
                var getMethod = property.GetGetMethod();
                var setMethod = property.GetSetMethod();
#endif
                if (isEnum)
                {
                    var underlyingType = Enum.GetUnderlyingType(propertyType);
                    if (underlyingType == typeof(byte))
                    {
                        info.ReadDelegate[i] = reader =>
                        {
                            property.SetValue(info.Reference, Enum.ToObject(propertyType, reader.GetByte()), null);
                        };
                        info.WriteDelegate[i] = writer =>
                        {
                            writer.Put((byte)property.GetValue(info.Reference, null));
                        };
                    }
                    else if (underlyingType == typeof(int))
                    {
                        info.ReadDelegate[i] = reader =>
                        {
                            property.SetValue(info.Reference, Enum.ToObject(propertyType, reader.GetInt()), null);
                        };
                        info.WriteDelegate[i] = writer =>
                        {
                            writer.Put((int)property.GetValue(info.Reference, null));
                        };
                    }
                    else
                    {
                        throw new Exception("Not supported enum underlying type: " + underlyingType.Name);
                    }
                }
                else if (propertyType == typeof(string))
                {
                    var setDelegate = ExtractSetDelegate<T, string>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, string>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetString(MaxStringLenght));
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference), MaxStringLenght);
                }
                else if (propertyType == typeof(bool))
                {
                    var setDelegate = ExtractSetDelegate<T, bool>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, bool>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetBool());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(byte))
                {
                    var setDelegate = ExtractSetDelegate<T, byte>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, byte>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetByte());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(sbyte))
                {
                    var setDelegate = ExtractSetDelegate<T, sbyte>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, sbyte>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetSByte());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(short))
                {
                    var setDelegate = ExtractSetDelegate<T, short>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, short>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetShort());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(ushort))
                {
                    var setDelegate = ExtractSetDelegate<T, ushort>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, ushort>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetUShort());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(int))
                {
                    var setDelegate = ExtractSetDelegate<T, int>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, int>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetInt());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(uint))
                {
                    var setDelegate = ExtractSetDelegate<T, uint>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, uint>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetUInt());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(long))
                {
                    var setDelegate = ExtractSetDelegate<T, long>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, long>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetLong());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(ulong))
                {
                    var setDelegate = ExtractSetDelegate<T, ulong>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, ulong>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetULong());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(float))
                {
                    var setDelegate = ExtractSetDelegate<T, float>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, float>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetFloat());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(double))
                {
                    var setDelegate = ExtractSetDelegate<T, double>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, double>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetDouble());
                    info.WriteDelegate[i] = writer => writer.Put(getDelegate((T)info.Reference));
                }
                // Array types
                else if (propertyType == typeof(string[]))
                {
                    var setDelegate = ExtractSetDelegate<T, string[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, string[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetStringArray(MaxStringLenght));
                    info.WriteDelegate[i] = writer => writer.PutArray(getDelegate((T)info.Reference), MaxStringLenght);
                }
                else if (propertyType == typeof(byte[]))
                {
                    var setDelegate = ExtractSetDelegate<T, byte[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, byte[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetBytesWithLength());
                    info.WriteDelegate[i] = writer => writer.PutBytesWithLength(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(short[]))
                {
                    var setDelegate = ExtractSetDelegate<T, short[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, short[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetShortArray());
                    info.WriteDelegate[i] = writer => writer.PutArray(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(ushort[]))
                {
                    var setDelegate = ExtractSetDelegate<T, ushort[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, ushort[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetUShortArray());
                    info.WriteDelegate[i] = writer => writer.PutArray(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(int[]))
                {
                    var setDelegate = ExtractSetDelegate<T, int[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, int[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetIntArray());
                    info.WriteDelegate[i] = writer => writer.PutArray(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(uint[]))
                {
                    var setDelegate = ExtractSetDelegate<T, uint[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, uint[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetUIntArray());
                    info.WriteDelegate[i] = writer => writer.PutArray(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(long[]))
                {
                    var setDelegate = ExtractSetDelegate<T, long[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, long[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetLongArray());
                    info.WriteDelegate[i] = writer => writer.PutArray(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(ulong[]))
                {
                    var setDelegate = ExtractSetDelegate<T, ulong[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, ulong[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetULongArray());
                    info.WriteDelegate[i] = writer => writer.PutArray(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(float[]))
                {
                    var setDelegate = ExtractSetDelegate<T, float[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, float[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetFloatArray());
                    info.WriteDelegate[i] = writer => writer.PutArray(getDelegate((T)info.Reference));
                }
                else if (propertyType == typeof(double[]))
                {
                    var setDelegate = ExtractSetDelegate<T, double[]>(setMethod);
                    var getDelegate = ExtractGetDelegate<T, double[]>(getMethod);
                    info.ReadDelegate[i] = reader => setDelegate((T)info.Reference, reader.GetDoubleArray());
                    info.WriteDelegate[i] = writer => writer.PutArray(getDelegate((T)info.Reference));
                }
                else
                {
                    CustomType registeredCustomType;
                    bool array = false;

                    if (propertyType.IsArray)
                    {
                        array = true;
                        propertyType = propertyType.GetElementType();
                    }

                    if (_registeredCustomTypes.TryGetValue(propertyType, out registeredCustomType))
                    {
                        if (array) //Array type serialize/deserialize
                        {
                            info.ReadDelegate[i] = reader =>
                            {
                                ushort arrLength = reader.GetUShort();
                                Array arr = Array.CreateInstance(propertyType, arrLength);
                                for (int k = 0; k < arrLength; k++)
                                {
                                    arr.SetValue(registeredCustomType.ReadDelegate(reader), k);
                                }

                                property.SetValue(info.Reference, arr, null);
                            };

                            info.WriteDelegate[i] = writer =>
                            {
                                Array arr = (Array)property.GetValue(info.Reference, null);
                                writer.Put((ushort)arr.Length);
                                for (int k = 0; k < arr.Length; k++)
                                {
                                    registeredCustomType.WriteDelegate(writer, arr.GetValue(k));
                                }
                            };
                        }
                        else //Simple
                        {
                            info.ReadDelegate[i] = reader =>
                            {
                                property.SetValue(info.Reference, registeredCustomType.ReadDelegate(reader), null);
                            };

                            info.WriteDelegate[i] = writer =>
                            {
                                registeredCustomType.WriteDelegate(writer, property.GetValue(info.Reference, null));
                            };
                        }
                    }
                    else
                    {
                        throw new Exception("Unknown property type: " + propertyType.Name);
                    }
                }
            }
            _cache.Add(nameHash, info);

            return info;
        }

        /// <summary>
        /// Reads all available data from NetDataReader and calls OnReceive delegates
        /// </summary>
        /// <param name="reader">NetDataReader with packets data</param>
        public void ReadAllPackets(NetDataReader reader)
        {
            while (reader.AvailableBytes > 0)
            {
                ReadPacket(reader);
            }
        }

        /// <summary>
        /// Reads all available data from NetDataReader and calls OnReceive delegates
        /// </summary>
        /// <param name="reader">NetDataReader with packets data</param>
        /// <param name="userData">Argument that passed to OnReceivedEvent</param>
        public void ReadAllPackets<T>(NetDataReader reader, T userData)
        {
            while (reader.AvailableBytes > 0)
            {
                ReadPacket(reader, userData);
            }
        }

        /// <summary>
        /// Reads one packet from NetDataReader and calls OnReceive delegate
        /// </summary>
        /// <param name="reader">NetDataReader with packet</param>
        public void ReadPacket(NetDataReader reader)
        {
            ReadPacket(reader, null);
        }

        private StructInfo ReadInfo(NetDataReader reader)
        {
            ulong hash = _hasher.ReadHash(reader);
            StructInfo info;
            if (!_cache.TryGetValue(hash, out info))
            {
                throw new Exception("Undefined packet received");
            }
            return info;
        }

        /// <summary>
        /// Reads packet with known type
        /// </summary>
        /// <param name="reader">NetDataReader with packet</param>
        /// <returns>Returns packet if packet in reader is matched type</returns>
        public T ReadKnownPacket<T>(NetDataReader reader) where T : class, new()
        {
            var info = ReadInfo(reader);
            ulong typeHash = _hasher.GetHash(typeof(T).Name);
            if (typeHash != info.Hash)
            {
                return null;
            }
            info.Reference = info.CreatorFunc != null ? info.CreatorFunc() : Activator.CreateInstance<T>();
            info.Read(reader);
            return (T)info.Reference;
        }

        /// <summary>
        /// Reads packet with known type (non alloc variant)
        /// </summary>
        /// <param name="reader">NetDataReader with packet</param>
        /// <param name="target">Deserialization target</param>
        /// <returns>Returns true if packet in reader is matched type</returns>
        public bool ReadKnownPacket<T>(NetDataReader reader, T target) where T : class, new()
        {
            var info = ReadInfo(reader);
            ulong typeHash = _hasher.GetHash(typeof(T).Name);
            if (typeHash != info.Hash)
            {
                return false;
            }

            info.Reference = target;
            info.Read(reader);
            return true;
        }

        /// <summary>
        /// Reads one packet from NetDataReader and calls OnReceive delegate
        /// </summary>
        /// <param name="reader">NetDataReader with packet</param>
        /// <param name="userData">Argument that passed to OnReceivedEvent</param>
        public void ReadPacket(NetDataReader reader, object userData)
        {
            var info = ReadInfo(reader);
            if (info.CreatorFunc != null)
            {
                info.Reference = info.CreatorFunc();
            }
            info.Read(reader);
            info.OnReceive(info.Reference, userData);
        }

        /// <summary>
        /// Register and subscribe to packet receive event
        /// </summary>
        /// <param name="onReceive">event that will be called when packet deserialized with ReadPacket method</param>
        /// <param name="packetConstructor">Method that constructs packet intead of slow Activator.CreateInstance</param>
        public void Subscribe<T>(Action<T> onReceive, Func<T> packetConstructor) where T : class, new()
        {
            var info = RegisterInternal<T>();
            info.CreatorFunc = () => packetConstructor();
            info.OnReceive = (o, userData) => { onReceive((T)o); };
        }

        /// <summary>
        /// Register packet type for direct reading (ReadKnownPacket)
        /// </summary>
        /// <param name="packetConstructor">Method that constructs packet intead of slow Activator.CreateInstance</param>
        public void Register<T>(Func<T> packetConstructor = null) where T : class, new()
        {
            var info = RegisterInternal<T>();
            if (packetConstructor != null)
            {
                info.CreatorFunc = () => packetConstructor();
            }
            info.OnReceive = (o, userData) => { };
        }

        /// <summary>
        /// Register and subscribe to packet receive event (with userData)
        /// </summary>
        /// <param name="onReceive">event that will be called when packet deserialized with ReadPacket method</param>
        /// <param name="packetConstructor">Method that constructs packet intead of slow Activator.CreateInstance</param>
        public void Subscribe<T, TUserData>(Action<T, TUserData> onReceive, Func<T> packetConstructor) where T : class, new()
        {
            var info = RegisterInternal<T>();
            info.CreatorFunc = () => packetConstructor();
            info.OnReceive = (o, userData) => { onReceive((T)o, (TUserData)userData); };
        }

        /// <summary>
        /// Register and subscribe to packet receive event
        /// This metod will overwrite last received packet class on receive (less garbage)
        /// </summary>
        /// <param name="onReceive">event that will be called when packet deserialized with ReadPacket method</param>
        public void SubscribeReusable<T>(Action<T> onReceive) where T : class, new()
        {
            var info = RegisterInternal<T>();
            info.Reference = new T();
            info.OnReceive = (o, userData) => { onReceive((T)o); };
        }

        /// <summary>
        /// Register and subscribe to packet receive event
        /// This metod will overwrite last received packet class on receive (less garbage)
        /// </summary>
        /// <param name="onReceive">event that will be called when packet deserialized with ReadPacket method</param>
        public void SubscribeReusable<T, TUserData>(Action<T, TUserData> onReceive) where T : class, new()
        {
            var info = RegisterInternal<T>();
            info.Reference = new T();
            info.OnReceive = (o, userData) => { onReceive((T)o, (TUserData)userData); };
        }

        /// <summary>
        /// Serialize struct to NetDataWriter (fast)
        /// </summary>
        /// <param name="writer">Serialization target NetDataWriter</param>
        /// <param name="obj">Struct to serialize</param>
        public void Serialize<T>(NetDataWriter writer, T obj) where T : class, new()
        {
            var info = RegisterInternal<T>();
            _hasher.WriteHash(info.Hash, writer);
            info.Write(writer, obj);
        }

        /// <summary>
        /// Serialize struct to byte array
        /// </summary>
        /// <param name="obj">Struct to serialize</param>
        /// <returns>byte array with serialized data</returns>
        public byte[] Serialize<T>(T obj) where T : class, new()
        {
            _writer.Reset();
            Serialize(_writer, obj);
            return _writer.CopyData();
        }
    }
}
#endif