using Edge.Core.Processor;using Edge.Core.IndustryStandardInterface.Pump;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Edge.Core.Parser.BinaryParser.Util;
using Edge.Core.Processor.Communicator;

namespace FuRen_Sinopec_IcCardReader
{
    public class StateMachineMessageCutter : IMessageCutter<byte[]>
    {
        public byte[] Message { get; private set; }

        public event EventHandler OnMessageCut;
        public event EventHandler<MessageCutterInvalidMessageReadEventArg> OnInvalidMessageRead;

        static NLog.Logger innerLogger = NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("StateMachineMessageCutter");

        private string loggerAppendix = "FuRen_Sinopec_IcCardReader msgCutter ";
        private readonly SizableWindow<byte> window;
        private State nextState = State.Uninitialized;

        /// <summary>
        /// 
        /// </summary>
        public StateMachineMessageCutter()
        {
            this.window = new SizableWindow<byte>(1);
            //each 0xFA pair in message body part need extend buffer 1 byte.
            int extendBufferTimes = 0;
            this.window.OnWindowFull += (data) =>
            {
                switch (nextState)
                {
                    case State.Uninitialized:
                        if (data[0] == 0xFA)
                        {
                            extendBufferTimes = 0;
                            this.window.NewSize = 2;
                            this.nextState = State.HeaderReady;
                            if (innerLogger.IsDebugEnabled)
                                innerLogger.Debug(this.loggerAppendix + " state is State.Uninitialized and next is 0xFA, switch to HeaderReady");
                        }
                        else
                        {
                            this.window.Clear();
                            return;
                        }

                        break;
                    case State.HeaderReady:
                        if (this.window.Count(h => h == 0xFA) == 1)
                        {
                            if (innerLogger.IsDebugEnabled)
                                innerLogger.Debug(this.loggerAppendix + " Only 1 time of 0xFA in header window, switch to LengthReady");
                            this.nextState = State.LengthReady;
                            this.window.NewSize += 4;
                        }
                        else
                        {
                            if (innerLogger.IsDebugEnabled)
                                innerLogger.Debug(this.loggerAppendix + " 2 times of 0xFA in header window, drop all and wait...");

                            // double 0xFA, not a starter, will drop this 2 0xFA.
                            this.nextState = State.Uninitialized;
                            this.window.Clear();
                            this.window.NewSize = 1;
                        }

                        break;
                    case State.LengthReady:
                        var bodyLen = this.window[4] * 10 + this.window[5];
                        if (bodyLen > 9999 || bodyLen < 2)
                        {
                            var safe8 = this.OnInvalidMessageRead;
                            safe8?.Invoke(this, new MessageCutterInvalidMessageReadEventArg()
                            {
                                Message = "MessageBody Length is a 2 bytes BCD encoded and expected value here is >=2 and <=9999 while now is " + bodyLen
                            });
                            this.nextState = State.Uninitialized;
                            this.window.Clear();
                            this.window.NewSize = 1;
                            return;
                        }

                        this.window.NewSize += bodyLen;
                        this.nextState = State.BodyReady;
                        if (innerLogger.IsDebugEnabled)
                            innerLogger.Debug(this.loggerAppendix + " MsgBodyLen caculated with: " + this.window.NewSize + ", and set default windowSize to this value.");
                        break;
                    case State.BodyReady:
                        /* window size not match with MsgBodyLen, indicates there's one or more 0xFA in body.*/
                        var s = this.Get0xFAPairCountInWindow(this.window.Skip(6));
                        if (extendBufferTimes < s)
                        {
                            this.window.NewSize += s - extendBufferTimes;
                            extendBufferTimes = s;
                            return;
                        }

                        extendBufferTimes = 0;

                        this.window.NewSize += 2;
                        // shrink the NewSize.
                        this.window.NewSize -= this.Reduce0xFAPair(this.window, 6);
                        this.nextState = State.CrcReady;
                        break;
                    case State.CrcReady:
                        bodyLen = this.window[4] * 10 + this.window[5];
                        var p = this.Get0xFAPairCountInWindow(this.window.Skip(6 + bodyLen));
                        if (extendBufferTimes < p)
                        {
                            this.window.NewSize += p - extendBufferTimes;
                            extendBufferTimes = p;
                            return;
                        }

                        this.Reduce0xFAPair(this.window, 6 + bodyLen);
                        if (this.window.Count != (6 + bodyLen + 2))
                        {
                            var safe8 = this.OnInvalidMessageRead;
                            safe8?.Invoke(this, new MessageCutterInvalidMessageReadEventArg()
                            {
                                Message = "CRC part is invalid"
                            });
                            this.nextState = State.Uninitialized;
                            this.window.Clear();
                            this.window.NewSize = 1;
                            return;
                        }

                        this.Message = this.window.ToArray();
                        var safe11 = this.OnMessageCut;
                        safe11?.Invoke(this, null);
                        this.nextState = State.Uninitialized;
                        this.window.Clear();
                        this.window.NewSize = 1;
                        return;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            };
        }

        /// <summary>
        /// if the 0xFA count is odd, will use round up for count/2.
        /// e.g.: 0xFA appeared 2 times, return 1.
        /// 0xFA appeared 3 times, return 2, this is considered as window is not big enought to include further 0xFA since they're always even.
        /// </summary>
        /// <returns></returns>
        private int Get0xFAPairCountInWindow(IEnumerable<byte> data)
        {
            return (int)Math.Round(((double)(data.Count(w => w == 0xFA)) / 2), MidpointRounding.AwayFromZero);
        }

        /// <summary>
        /// Iterates the target by searching all pair(one besides one) of 0xFA, and reduce each pair into a single 0xFA.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="startIndex"></param>
        /// <returns>how many timed of reduced happened. each reduce will remove one byte</returns>
        public int Reduce0xFAPair(IList<byte> target, int startIndex)
        {
            int reducedCount = 0;
            var faAppearedPositions = new List<int>();
            for (int i = startIndex; i < target.Count; i++)
            {
                if (target[i] == 0xFA)
                {
                    if (i <= (target.Count - 2))
                    {
                        if (target[i + 1] == 0xFA)
                        {
                            faAppearedPositions.Add(i);
                            i++;
                        }
                    }
                }
            }

            for (int i = 0; i < faAppearedPositions.Count; i++)
            {
                target.RemoveAt(faAppearedPositions[i] - i);
                reducedCount++;
            }

            return reducedCount;
        }

        private enum State
        {
            Uninitialized,
            //HeaderSeeking,
            HeaderReady,
            LengthReady,
            BodyReady,
            CrcReady,
        }

        public void Feed(byte[] next)
        {
            //innerLogger.Debug(this.loggerAppendix + " " + next.ToHexLogString() + " is feed in Window in state: " + nextState);
            for (int i = 0; i < next.Length; i++)
                this.window.Add(next[i]);
        }
    }
}