using Edge.Core.Parser.BinaryParser.Attributes;
using Edge.Core.Parser.BinaryParser.MessageEntity;
using Edge.Core.Parser.BinaryParser.Util;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Edge.Core.Parser.BinaryParser
{
    public abstract class ParserBase : IMessageParser<byte[], MessageTemplateBase>
    {
        protected static NLog.Logger logger = NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("Communicator");

        private int debug_ConvertToObject_depth = 0;

        private IMessageTemplateLookup templateLookup;

        /// <summary>
        /// ongoing processing raw data array, make 2 of this array is for thread safe
        /// </summary>
        private byte[] processingRawDataUsedInSerialize = null;

        /// <summary>
        /// ongoing processing raw data array, make 2 of this array is for thread safe
        /// </summary>
        private byte[] processingRawDataUsedInDeserialize = null;

        private int positiveIndexAccu = 0;

        /// <summary>
        /// Fired on when bytes incoming and will start to convert to a object, but not started yet.
        /// </summary>
        public event EventHandler<ParsingEventArg<byte[], MessageTemplateBase>> Deserializing;

        /// <summary>
        /// Fired on when bytes incoming and started to convert to a object, and finished already.
        /// </summary>
        public event EventHandler<DeserializeEventArg<byte[], MessageTemplateBase>> Deserialized;

        public event EventHandler<ParsingEventArg<MessageTemplateBase, byte[]>> Serializing;

        public event EventHandler<ParsingEventArg<MessageTemplateBase, byte[]>> Serialized;

        /// <summary>
        /// Fired on a Format Attibute marked field will starting to convert to bytes, but not started yet.
        /// string in ParsingEventArg is the field name, byte[] should always null here since not started.
        /// </summary>
        public event EventHandler<ParsingEventArg<string, byte[]>> FieldSerializing;

        /// <summary>
        /// Fired on a Format Attibute marked field started to convert to bytes, and finished already.
        /// string in ParsingEventArg is the field name, byte[] should have values for this already serialized field.
        /// </summary>
        public event EventHandler<ParsingEventArg<string, byte[]>> FieldSerialized;

        protected ParserBase(IMessageTemplateLookup templateLookup)
        {
            this.templateLookup = templateLookup;
        }

        protected ParserBase() : this(null)
        {
        }

        /// <summary>
        /// There's a build-in check in Deserialize process, if all fields in a template had been filled, but still
        /// some extra bytes left, an exception will throw, this must be the error in raw bytes.
        /// But you can.
        /// </summary>
        public bool IgnoreExtraTailBytesInDeserialize
        {
            get; set;
        }

        /// <summary>
        /// Deserialize a byte[] into a Message entity, the `TemplateLookup` must specified firstly via constructor.
        /// </summary>
        /// <param name="rawData"></param>
        /// <returns></returns>
        public virtual MessageTemplateBase Deserialize(byte[] rawData)
        {
            if (this.templateLookup == null) throw new ArgumentNullException("Must specify a 'TemplateLookup' to start Deserialize");
            MessageTemplateBase messageTemplateBase = templateLookup.GetMessageTemplateByRawBytes(rawData);
            return messageTemplateBase.ToObject(rawData);
            //return this.Deserialize(rawData, templateLookup.GetMessageTemplateByRawBytes(rawData));
        }

        public virtual MessageTemplateBase Deserialize(byte[] rawData, MessageTemplateBase withTemplate)
        {
            this.debug_ConvertToObject_depth = 0;

            var debug = rawData.ToHexLogString();

            var safeEvent = this.Deserializing;
            safeEvent?.Invoke(this, new ParsingEventArg<byte[], MessageTemplateBase>() { From = rawData, To = default(MessageTemplateBase) });

            //MessageTemplateBase template = templateLookup.GetMessageTemplateByRawBytes(rawData);
            this.processingRawDataUsedInDeserialize = rawData;
            this.ConvertToObject(withTemplate, this.processingRawDataUsedInDeserialize);
            if (this.processingRawDataUsedInDeserialize.Length > 0)
            {
                if (!IgnoreExtraTailBytesInDeserialize)
                    throw new FormatException("Unexpected Element Deserialize error occured(additional tail existed)");
            }

            #region if message level custom validation defined? then execute it

            Exception customValidationException;
            if ((customValidationException = withTemplate.Validate()) != null)
            {
                throw customValidationException;
            }

            #endregion

            var safeDeserializedEvent = this.Deserialized;
            safeDeserializedEvent?.Invoke(this, new DeserializeEventArg<byte[], MessageTemplateBase>() { From = rawData, Extra = this.processingRawDataUsedInDeserialize, To = withTemplate });

            return withTemplate;
        }

        public virtual byte[] Serialize(MessageTemplateBase message)
        {

            #region if message level custom validation defined? then execute it

            Exception customValidationException;
            if ((customValidationException = message?.Validate()) != null)
            {
                throw customValidationException;
            }

            #endregion

            var safeEvent = this.Serializing;
            safeEvent?.Invoke(this, new ParsingEventArg<MessageTemplateBase, byte[]>() { From = message, To = null });

            this.processingRawDataUsedInSerialize = new byte[0];
            this.positiveIndexAccu = 0;
            var content = this.ConvertToBytes(message);

            safeEvent = this.Serialized;
            safeEvent?.Invoke(this, new ParsingEventArg<MessageTemplateBase, byte[]>() { From = message, To = content });

            return content;
        }

        private void ConvertToObject(object elementInstance, byte[] rawData)
        {
            //if (++this.debug_ConvertToObject_depth >= 6) logger.Info("debug_ConvertToObject_depth is " + this.debug_ConvertToObject_depth + ": 0x" + rawData.ToHexLogString());

            if (rawData.Length == 0 || elementInstance == null)
            {
                return;
            }
            else
            {
                // Only processing Format related Attribute marked properties, and order by its Index
                var targetPropertyList = elementInstance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                                            .Where(p => p.GetCustomAttributes(typeof(AttributeBase), true).Length > 0)
                                            .OrderBy(info => ((AttributeBase)(info.GetCustomAttributes(typeof(AttributeBase), true)[0])).Index).ToList();
                foreach (var propertyInfo in targetPropertyList)
                {
                    // normal plain Property
                    if (propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true).Length == 0)
                    {
                        int currentElementLength = -1;
                        var format =
                           (FormatAttribute)propertyInfo.GetCustomAttributes(typeof(FormatAttribute), true)[0];
                        if (!string.IsNullOrEmpty(format.LengthOrCountLink))
                        {
                            currentElementLength = int.Parse(
                                    elementInstance.GetType().GetProperty(format.LengthOrCountLink).GetValue(elementInstance, null).ToString());
                            if (!string.IsNullOrEmpty(format.LengthOrCountLinkExpression))
                            {
                                currentElementLength += ParseLengthOrCountLinkExpression(format.LengthOrCountLinkExpression);
                            }
                        }
                        else
                        {
                            currentElementLength = format.FixedLengthOrCount;
                        }

                        // check if the remaining raw data bytes are enough to fill the current property
                        if (this.processingRawDataUsedInDeserialize.Count() < currentElementLength)
                        {
                            throw new ArgumentException(
                                string.Format("ConvertToObject Error: The remaining raw data bytes are insufficient to fill the current property '{0}'",
                                propertyInfo.Name));
                        }

                        this.FillPropertyData(format.EncodingType, propertyInfo, elementInstance, this.processingRawDataUsedInDeserialize.Take(currentElementLength).ToArray());

                        #region validate the range for Range Attribute marked field.

                        // Validate the range, once failed, throw a exception
                        var rangeValidator = propertyInfo.GetCustomAttributes(typeof(RangeAttribute), true).Length > 0
                            ? propertyInfo.GetCustomAttributes(typeof(RangeAttribute), true)[0] : null;
                        if (rangeValidator != null
                            && !((RangeAttribute)rangeValidator).IsValid(propertyInfo.GetValue(elementInstance, null)))
                        {
                            throw new ArgumentException(((RangeAttribute)rangeValidator).ErrorMessage);
                        }

                        #endregion

                        this.processingRawDataUsedInDeserialize = this.processingRawDataUsedInDeserialize.Skip(currentElementLength).ToArray();
                    }
                    // the IList Property
                    else
                    {
                        bool isCascadedList = false;
                        var listFormat =
                            (EnumerableFormatAttribute)propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true)[0];
                        int? listCount = null;
                        if (!string.IsNullOrEmpty(listFormat.LengthOrCountLink) && listFormat.LengthOrCountLink.ToLower() == "%cascade")
                        {
                            // never know the end of the data, it's a flow, so set a big value.
                            listCount = this.processingRawDataUsedInDeserialize.Length;
                            listCount += ParseLengthOrCountLinkExpression(listFormat.LengthOrCountLinkExpression);
                            isCascadedList = true;
                        }
                        else if (!string.IsNullOrEmpty(listFormat.LengthOrCountLink))
                        {
                            var linkPropertyValue = elementInstance.GetType().GetProperty(listFormat.LengthOrCountLink).GetValue(elementInstance, null);
                            if (linkPropertyValue is Enum)
                            {
                                listCount = (int)linkPropertyValue;
                            }
                            else
                            {
                                listCount = int.Parse(linkPropertyValue.ToString());
                            }
                            if (!string.IsNullOrEmpty(listFormat.LengthOrCountLinkExpression))
                            {
                                listCount += ParseLengthOrCountLinkExpression(listFormat.LengthOrCountLinkExpression);
                            }
                        }
                        else if (listFormat.FixedLengthOrCount > 0)
                        {
                            listCount = listFormat.FixedLengthOrCount;
                        }
                        else
                        {
                            throw new FormatException("Can't resolve the count number for IList property: " + propertyInfo.Name);
                        }

                        IList list = null;
                        // since it's CascadedList, then probably there's no data anymore, it's a empty list, no need to do any parsing.
                        if (this.processingRawDataUsedInDeserialize.Count() == 0 && isCascadedList)
                        {

                        }
                        else
                        {
                            list = (IList)Activator.CreateInstance(propertyInfo.PropertyType);
                            var genericArg = list.GetType().GetGenericArguments()[0];

                            for (int i = 0; i < listCount; i++)
                            {
                                if (genericArg.IsPrimitive)
                                {
                                    var subPrimitiveInstance = Activator.CreateInstance(genericArg);
                                    // assume primitive always take 1 byte, since we only use int and byte.  need refine future.
                                    subPrimitiveInstance = this.processingRawDataUsedInDeserialize.Take(listFormat.ElementLength).ToArray().ToInt32();
                                    if (genericArg.IsAssignableFrom(typeof(Byte)))
                                    {
                                        list.Add(Convert.ToByte(subPrimitiveInstance));
                                    }
                                    else if (genericArg.IsAssignableFrom(typeof(int)))
                                    {
                                        list.Add(subPrimitiveInstance);
                                    }
                                    else
                                    {
                                        throw new ArgumentException("Now Only support primitive types of 'byte' and 'int'");
                                    }

                                    this.processingRawDataUsedInDeserialize = this.processingRawDataUsedInDeserialize.Skip(listFormat.ElementLength).ToArray();
                                }
                                else
                                {
                                    var subElementInstance = Activator.CreateInstance(genericArg);
                                    list.Add(subElementInstance);
                                    this.ConvertToObject(subElementInstance, this.processingRawDataUsedInDeserialize);
                                }

                                // no data need to be processing, break it since we might set a big listCount earlier for cascade structure.
                                // or you'll see a big List<T>
                                if (this.processingRawDataUsedInDeserialize.Length == 0)
                                {
                                    break;
                                }
                            }
                        }

                        propertyInfo.SetValue(elementInstance, list, null);
                    }
                }
            }
        }

        /// <summary>
        /// parse the expression and return the operator value.
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual int ParseLengthOrCountLinkExpression(string expression)
        {
            if (expression != null
                && (expression.Contains('+') || expression.Contains('-')))
            {
                var pureOperatorAndNumber = expression.Trim().Replace(" ", "");
                if (pureOperatorAndNumber.Contains('+'))
                {
                    return int.Parse(pureOperatorAndNumber.Substring(1));
                }
                else if (pureOperatorAndNumber.Contains('-'))
                {
                    return -int.Parse(pureOperatorAndNumber.Substring(1));
                }
            }

            return 0;
        }
        /// <summary>
        /// Set the value to a property in a specific object based on the encoding type associated with the property.
        /// </summary>
        /// <param name="encodingType"></param>
        /// <param name="propertyInfo"></param>
        /// <param name="elementInstance"></param>
        /// <param name="data"></param>
        private void FillPropertyData(EncodingType encodingType, PropertyInfo propertyInfo, object elementInstance, byte[] data)
        {
            switch (encodingType)
            {
                case EncodingType.ASCII:
                case EncodingType.ASCII_PadLeftWithZero:
                    propertyInfo.SetValue(elementInstance, Encoding.ASCII.GetString(data), null);
                    break;
                case EncodingType.ASCIIInt:
                    propertyInfo.SetValue(elementInstance, int.Parse(Encoding.ASCII.GetString(data)), null);
                    break;
                case EncodingType.ASCIILong:
                    propertyInfo.SetValue(elementInstance, long.Parse(Encoding.ASCII.GetString(data)), null);
                    break;
                case EncodingType.BCD:
                    var tempBytes = data;
                    var newValue = tempBytes.Length == 1 ? tempBytes[0].GetBCD() : tempBytes.GetBCD();
                    if (propertyInfo.PropertyType.FullName == "System.Byte")
                    {
                        propertyInfo.SetValue(elementInstance, (byte)newValue, null);
                    }
                    else
                    {
                        propertyInfo.SetValue(elementInstance, newValue, null);
                    }
                    break;
                case EncodingType.BcdString:
                    tempBytes = data;
                    //if (propertyInfo.PropertyType.BaseType.FullName == "System.Enum") {
                    //    if (!Enum.IsDefined(propertyInfo.PropertyType, tempBytes.GetBCD()))
                    //        throw new ArgumentException("The given value is not a valid " + propertyInfo.PropertyType + " element.");
                    //}
                    var fieldValue = tempBytes.Length == 1 ? tempBytes[0].GetBCD().ToString() : tempBytes.GetBCDString();
                    if (propertyInfo.PropertyType.IsAssignableFrom(typeof(long)))
                    {
                        propertyInfo.SetValue(elementInstance, long.Parse(fieldValue)
                                              , null);
                    }
                    else if (propertyInfo.PropertyType.IsAssignableFrom(typeof(DateTime)))
                    {
                        propertyInfo.SetValue(elementInstance, DateTime.ParseExact(fieldValue, "yyyyMMddHHmmss", System.Globalization.CultureInfo.InvariantCulture));
                    }
                    else if (propertyInfo.PropertyType.IsAssignableFrom(typeof(decimal)))
                    {
                        propertyInfo.SetValue(elementInstance, decimal.Parse(fieldValue)
                                              , null);
                    }
                    else
                    {
                        propertyInfo.SetValue(elementInstance, fieldValue
                                              , null);
                    }
                    break;
                case EncodingType.BIN:
                    if (propertyInfo.PropertyType.IsAssignableFrom(typeof(Enum)))
                    {
                        var o = Enum.ToObject(propertyInfo.PropertyType, data.ToInt32());
                        propertyInfo.SetValue(elementInstance, o, null);
                    }
                    else if (propertyInfo.PropertyType == typeof(Byte))
                    {
                        try
                        {
                            propertyInfo.SetValue(elementInstance, (byte)(data.ToInt32()), null);
                        }
                        catch (Exception e)
                        {
                        }

                    }
                    // in case the Property is a Nullable enum type.
                    else if (propertyInfo.PropertyType.IsGenericType && propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                    {
                        if (propertyInfo.PropertyType.GetGenericArguments()[0].IsEnum)
                        {
                            var o = Enum.ToObject(propertyInfo.PropertyType.GetGenericArguments()[0], data.ToInt32());
                            propertyInfo.SetValue(elementInstance, o, null);
                        }
                        else
                        {
                            var value = Convert.ChangeType(data.ToInt32(), propertyInfo.PropertyType);
                            propertyInfo.SetValue(elementInstance, value, null);
                        }
                    }
                    else if (propertyInfo.PropertyType == typeof(ushort))
                    {
                        try
                        {
                            propertyInfo.SetValue(elementInstance, Convert.ToUInt16(data.ToInt32()));
                        }
                        catch (Exception ex)
                        {

                            throw;
                        }
                    }
                    else
                    {
                        propertyInfo.SetValue(elementInstance, data.ToInt32(), null);
                    }
                    break;
                case EncodingType.HexString:
                    propertyInfo.SetValue(elementInstance, ByteArrayToString(data));
                    break;
                case EncodingType.ReverseHexString:
                    propertyInfo.SetValue(elementInstance, ByteArrayToString(data.Reverse().ToArray()));
                    break;
            }

        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="encodingType"></param>
        /// <param name="propertyValue"></param>
        /// <param name="propertyValueFixedLength">if length is avarible bytes, then leave null, the serialized bytes will auto fits the actual value.</param>
        /// <param name="elementInstance"></param>
        /// <returns></returns>
        private byte[] SerializePropertyData(EncodingType encodingType, object propertyValue, int? propertyValueFixedLength, object elementInstance)
        {
            string enumTypeConvertedValue;
            switch (encodingType)
            {
                case EncodingType.ASCII:
                    //if (propertyValue is Enum)
                    //{
                    //    var temp = ((int)propertyValue).ToString();
                    //    enumTypeConvertedValue = temp.ToString().PadLeft(propertyValueFixedLength, '0');
                    //}
                    //else
                    //{
                    enumTypeConvertedValue = propertyValue.ToString();
                    //}
                    if (propertyValueFixedLength.HasValue && enumTypeConvertedValue.Length > propertyValueFixedLength.Value)
                        throw new ArgumentOutOfRangeException("ASCII encoded property with value: " + enumTypeConvertedValue
                            + " exceed its declared Fixedlength(" + propertyValueFixedLength + "), try shorten the value");
                    if (propertyValueFixedLength.HasValue)
                        return Encoding.ASCII.GetBytes(enumTypeConvertedValue.PadRight(propertyValueFixedLength.Value, ' '));
                    else
                        return Encoding.ASCII.GetBytes(enumTypeConvertedValue);
                    break;
                case EncodingType.ASCII_PadLeftWithZero:
                    enumTypeConvertedValue = propertyValue.ToString();
                    if (propertyValueFixedLength.HasValue && enumTypeConvertedValue.Length > propertyValueFixedLength.Value)
                        throw new ArgumentOutOfRangeException("ASCII_PadLeftWithZero encoded property with value: " + enumTypeConvertedValue
                            + " exceed its declared Fixedlength(" + propertyValueFixedLength + "), try shorten the value");
                    if (propertyValueFixedLength.HasValue)
                        return Encoding.ASCII.GetBytes(enumTypeConvertedValue.PadLeft(propertyValueFixedLength.Value, '0'));
                    else
                        return Encoding.ASCII.GetBytes(enumTypeConvertedValue);
                    break;

                case EncodingType.ASCIIInt:
                    if (propertyValue is Enum)
                    {
                        var temp = ((int)propertyValue).ToString();
                        enumTypeConvertedValue = temp.ToString().PadLeft(propertyValueFixedLength.Value, '0');
                    }
                    else
                    {
                        enumTypeConvertedValue = propertyValue.ToString().PadLeft(propertyValueFixedLength.Value, '0'); ;
                    }

                    return Encoding.ASCII.GetBytes(enumTypeConvertedValue);
                    break;
                case EncodingType.ASCIILong:
                    if (propertyValue is Enum)
                    {
                        var temp = ((long)propertyValue).ToString();
                        enumTypeConvertedValue = temp.ToString().PadLeft(propertyValueFixedLength.Value, '0');
                    }
                    else
                    {
                        enumTypeConvertedValue = propertyValue.ToString().PadLeft(propertyValueFixedLength.Value, '0'); ;
                    }

                    return Encoding.ASCII.GetBytes(enumTypeConvertedValue);
                    break;
                case EncodingType.BCD:

                    if (propertyValue is Enum)
                    {
                        enumTypeConvertedValue = ((int)propertyValue).ToString();
                    }
                    else
                    {
                        enumTypeConvertedValue = propertyValue.ToString();
                    }

                    return long.Parse(enumTypeConvertedValue).GetBCDBytes(propertyValueFixedLength.Value);
                    break;
                case EncodingType.BcdString:

                    if (propertyValue is Enum)
                    {
                        enumTypeConvertedValue = ((int)propertyValue).ToString();
                    }
                    else
                    {
                        enumTypeConvertedValue = propertyValue.ToString();
                    }

                    return System.Numerics.BigInteger.Parse(enumTypeConvertedValue).GetBCDBytes(propertyValueFixedLength.Value);
                    break;
                case EncodingType.BIN:
                    if (propertyValue is Enum)
                    {
                        enumTypeConvertedValue = ((int)propertyValue).ToString();
                    }
                    else
                    {
                        enumTypeConvertedValue = propertyValue.ToString();
                    }

                    if (propertyValueFixedLength.HasValue)
                    {
                        if (long.Parse(enumTypeConvertedValue) > int.MaxValue)
                        {
                            return long.Parse(enumTypeConvertedValue).GetBinBytes(propertyValueFixedLength.Value);
                        }
                        else
                        {
                            return (int.Parse(enumTypeConvertedValue)).GetBinBytes(propertyValueFixedLength.Value);
                        }
                    }
                    else
                    {
                        logger.Info($"Parsing value: {enumTypeConvertedValue}");
                        return (int.Parse(enumTypeConvertedValue)).GetBinBytes(1);
                    }
                    break;
                case EncodingType.HexString:
                    return StringToByteArray(propertyValue.ToString());
                    break;
            }

            return null;
        }

        public static byte[] StringToByteArray(string hex)
        {
            int numberChars = hex.Length;
            byte[] bytes = new byte[numberChars / 2];
            for (int i = 0; i < numberChars; i += 2)
                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
            return bytes;
        }

        public static string ByteArrayToString(byte[] arr)
        {
            return BitConverter.ToString(arr).Replace("-", "");
        }

        static bool IsNullable<T>(T obj)
        {
            if (obj == null) return true; // obvious
            Type type = typeof(T);
            if (!type.IsValueType) return true; // ref-type
            if (Nullable.GetUnderlyingType(type) != null) return true; // Nullable<T>
            return false; // value-type
        }

        private byte[] ConvertToBytes(object elementInstance)
        {
            // Only processing MessageAttribute marked properties, and order by its Index in DESC, like 10, 9, 8, 7.... -1, -2...-10.
            var targetPropertyList = elementInstance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                                        .Where(p => p.GetCustomAttributes(typeof(AttributeBase), true).Length > 0)
                                        .OrderByDescending(info => ((AttributeBase)(info.GetCustomAttributes(typeof(AttributeBase), true)[0])).Index);
            foreach (var propertyInfo in targetPropertyList)
            {
                // normal Format Property
                if (propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true).Length == 0
                    && propertyInfo.GetCustomAttributes(typeof(FormatAttribute), true).Length > 0)
                {

                    #region validate the Range Attibute marked property, if existed and failed to pass, throw exception

                    // Validate the range, once failed, throw a exception
                    var propertyRangeAttributes = propertyInfo.GetCustomAttributes(typeof(RangeAttribute), true);
                    if (propertyRangeAttributes.Length > 0)
                    {
                        var rangeAttribute = (RangeAttribute)propertyRangeAttributes[0];
                        if (rangeAttribute != null)
                        {
                            if (!rangeAttribute.IsValid(propertyInfo.GetValue(elementInstance, null)))
                            {
                                throw new ArgumentException(rangeAttribute.ErrorMessage);
                            }
                        }
                    }

                    #endregion

                    var format =
                        (FormatAttribute)propertyInfo.GetCustomAttributes(typeof(FormatAttribute), true)[0];
                    if (format.LengthOrCountLinkExpression != null
                        && format.LengthOrCountLinkExpression.Contains("%OnSerializingBytesCount")
                        )
                    {
                        var lengthValue = this.processingRawDataUsedInSerialize.Length;
                        // if this property's value is default with null (most likely the nullable int), then will not set any value on it, the serializing
                        // result will omit this property.
                        if (lengthValue == 0 && propertyInfo.GetValue(elementInstance, null) == null)
                        {

                        }
                        else
                            propertyInfo.SetValue(elementInstance, this.processingRawDataUsedInSerialize.Length, null);
                    }
                    else if (!string.IsNullOrEmpty(format.LengthOrCountLink))
                    {
                        var lengthLinkProperty = targetPropertyList.FirstOrDefault(t => t.Name == format.LengthOrCountLink);
                        if (lengthLinkProperty != null)
                        {
                            propertyInfo.SetValue(elementInstance, propertyInfo.GetValue(elementInstance, null).ToString().Length, null);
                        }
                    }

                    var propertyValue = propertyInfo.GetValue(elementInstance, null);
                    // if this property value is null, then indicate it's a optional field, then skip to parsing it.
                    if (propertyValue == null)
                    {
                        var safeEvent = this.FieldSerializing;
                        safeEvent?.Invoke(this, new ParsingEventArg<string, byte[]>() { From = propertyInfo.Name, To = null });
                        continue;
                    }

                    var safe = this.FieldSerializing;
                    safe?.Invoke(this, new ParsingEventArg<string, byte[]>() { From = propertyInfo.Name, To = null });

                    byte[] currentPropertyBytes = this.SerializePropertyData(format.EncodingType,
                        propertyValue, format.FixedLengthOrCount > 0 ? format.FixedLengthOrCount : default(int?),
                                                                          elementInstance);
                    safe = this.FieldSerialized;
                    safe?.Invoke(this, new ParsingEventArg<string, byte[]>() { From = propertyInfo.Name, To = currentPropertyBytes });

                    this.processingRawDataUsedInSerialize = this.processingRawDataUsedInSerialize.AppendToHeader(currentPropertyBytes);
                    if (format.Index >= 0) this.positiveIndexAccu += currentPropertyBytes.Length;

                    // try fill the LengthLink field.
                    //if (!string.IsNullOrEmpty(format.LengthOrCountLink))
                    //{
                    //    var lengthLinkProperty = targetPropertyList.FirstOrDefault(t => t.Name == format.LengthOrCountLink);
                    //    if (lengthLinkProperty != null
                    //        && string.IsNullOrEmpty(format.LengthOrCountLinkExpression)
                    //        // if it's not a LengthOrCountLinkExpression field, then the linking field is directly reflected the length, so it must euqal 0
                    //        // if not, means the users code set the length explicitly, accept this behavior to NOT overwirte it.
                    //        && int.Parse(lengthLinkProperty.GetValue(elementInstance, null).ToString()) == 0)
                    //    {
                    //        lengthLinkProperty.SetValue(elementInstance, currentPropertyBytes.Length, null);
                    //    }

                    //    else if (false && !string.IsNullOrEmpty(format.LengthOrCountLinkExpression))
                    //    {
                    //        var pair = format.LengthOrCountLinkExpression.Split(';');
                    //        bool found = false;
                    //        foreach (string p in pair)
                    //        {
                    //            var mappings = p.Split(':');
                    //            if (currentPropertyBytes.Length == int.Parse(mappings[1]))
                    //            {
                    //                //lengthLinkProperty.SetValue(elementInstance, int.Parse(mappings[0], null));
                    //                lengthLinkProperty.SetValue(elementInstance, int.Parse(mappings[0], null), null);
                    //                found = true;
                    //                break;
                    //            }
                    //        }

                    //        if (!found)
                    //        { throw new ArgumentException("Serializing LengthOrCountLinkExpression: " + format.LengthOrCountLinkExpression + " didn't found match values in link field: " + format.LengthOrCountLink); }
                    //    }
                    //}
                }
                // the Enumerable Property
                else if (propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true).Length > 0)
                {
                    var enumerableFormat =
                        (EnumerableFormatAttribute)propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true)[0];
                    // for now, we only support List<T>
                    if (propertyInfo.PropertyType.IsAssignableFrom(typeof(IList)))
                    {
                        throw new ArgumentException("For now, we only support Enumerable of List<T>");
                    }

                    var list = (IList)propertyInfo.GetValue(elementInstance, null);
                    if (list != null)
                    {
                        if (enumerableFormat.FixedLengthOrCount > 0)
                        {
                            if (enumerableFormat.FixedLengthOrCount != list.Count)
                            {
                                throw new ArgumentException("The current count(" + list.Count + ") of EnumerableFormat property: "
                                    + propertyInfo.Name + " must match its declared FixedLengthOrCount(" + enumerableFormat.FixedLengthOrCount + ")");
                            }
                        }

                        if (list.GetType().GetGenericArguments()[0].IsPrimitive)
                        {
                            var safe = this.FieldSerializing;
                            if (safe != null) { safe(this, new ParsingEventArg<string, byte[]>() { From = propertyInfo.Name, To = null }); }

                            byte[] primitiveSerilaizedBytes = new byte[0];
                            for (int i = list.Count - 1; i >= 0; i--)
                            {
                                primitiveSerilaizedBytes = primitiveSerilaizedBytes.AppendToHeader(
                                      this.SerializePropertyData(
                                          enumerableFormat.EncodingType,
                                          list[i],
                                          1,
                                          elementInstance)
                                      );
                            }

                            this.processingRawDataUsedInSerialize = this.processingRawDataUsedInSerialize.AppendToHeader(primitiveSerilaizedBytes);
                            if (enumerableFormat.Index >= 0) this.positiveIndexAccu += primitiveSerilaizedBytes.Length;
                            safe = this.FieldSerialized;
                            if (safe != null) { safe(this, new ParsingEventArg<string, byte[]>() { From = propertyInfo.Name, To = primitiveSerilaizedBytes }); }

                        }
                        else
                        {
                            // reverse
                            for (int i = list.Count - 1; i >= 0; i--)
                            {
                                this.processingRawDataUsedInSerialize.AppendToHeader(this.ConvertToBytes(list[i]));
                            }
                        }


                        if (!string.IsNullOrEmpty(enumerableFormat.LengthOrCountLink) && enumerableFormat.LengthOrCountLink.ToLower() != "%cascade")
                        {
                            var countLinkProperty = targetPropertyList.FirstOrDefault(t => t.Name == enumerableFormat.LengthOrCountLink);
                            if (countLinkProperty != null)
                            {
                                try
                                {
                                    countLinkProperty.SetValue(elementInstance, list.Count, null);
                                }
                                catch (Exception ex)
                                {
                                }

                            }
                        }
                        else
                        {
                            if (!string.IsNullOrEmpty(enumerableFormat.LengthOrCountLink) && enumerableFormat.LengthOrCountLink.ToLower() != "%cascade")
                            {
                                targetPropertyList.First(t => t.Name == enumerableFormat.LengthOrCountLink).
                                    SetValue(elementInstance, 0, null);
                            }
                        }
                    }
                }
            }

            return this.processingRawDataUsedInSerialize;
        }

        // make sure the Indexes property of one element including the sub elements 
        // of its list structures are sequential and unique
        protected void CheckAttributeIndexes(object elementInstance)
        {
            if (elementInstance == null)
            {
                return;
            }

            // Only processing WayneAttribute marked properties, and order by its Index in DESC
            var targetPropertyList = elementInstance.GetType()
                .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                .Where(p => p.GetCustomAttributes(typeof(AttributeBase), true).Length > 0)
                .OrderByDescending(
                    info =>
                        ((AttributeBase)(info.GetCustomAttributes(typeof(AttributeBase), true)[0]))
                            .Index);

            // make sure no duplicated Index defined.
            targetPropertyList.GroupBy(
                p => ((AttributeBase)(p.GetCustomAttributes(typeof(AttributeBase), true)[0])).Index)
                .ToList()
                .ForEach(g =>
                {
                    if (g.Count() > 1)
                    {
                        throw new ArgumentException(
                            string.Format("Duplicated Index: {0} defined, make sure the Index value unique", g.Key));
                    }
                });

            // make sure the Indexes are contiguously sequential and count from 0
            if (targetPropertyList.Max(
                p => ((AttributeBase)(p.GetCustomAttributes(typeof(AttributeBase), true)[0])).Index) !=
                (targetPropertyList.Count() - 1))
            {
                throw new ArgumentException(string.Format("Index must be in sequential and count from 0"));
            }

            // recursively check the sub element type of ListPropertyFormatAttribute
            foreach (var propertyInfo in targetPropertyList)
            {
                // the IList Property
                if (propertyInfo.GetCustomAttributes(typeof(EnumerableFormatAttribute), true).Length != 0)
                {
                    var subElementInstance = Activator.CreateInstance(propertyInfo.PropertyType.GetGenericArguments()[0]);
                    CheckAttributeIndexes(subElementInstance);
                }
            }
        }
    }
}
