using Edge.Core.Parser.BinaryParser.MessageEntity;
using Edge.Core.Parser.BinaryParser.Util;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Edge.Core.Processor.Communicator
{
    /// <summary>
    /// Each write will create a tcp client
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class TcpClientShortConnCommunicator<T> : ICommunicator<byte[], T> where T : MessageTemplateBase
    {
        protected NLog.Logger logger = NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("Communicator");
        public static int tcpReceiveBufferSize = 1500;

        private Edge.Core.Parser.BinaryParser.ParserBase parser;

        private string remoteServerTcpListeningIpAddress;
        private int remoteServerTcpListeningPortNumber;

        /// <summary>
        /// after a tcp msg round-trip(fcc send and then recieved a msg), most likely this 
        /// ShortConn communicator don't need to listening on that used tcpclient anymore, 
        /// but let's still keep this flag just in case.
        /// </summary>
        private bool keepTcpListeningAfterReceivedAtcpMessage = false;

        private object syncObject = new object();
        private object syncObject_Feed = new object();

        /// <summary>
        /// 0 for not started, 1 for started.
        /// </summary>
        private int isStarted = 0;
        private bool disposed = false;
        private IMessageCutter<byte[]> messageCutter;

        //private List<MessageTemplateBase> continueParseStack = new List<MessageTemplateBase>();

        public event EventHandler OnConnected;
        public event EventHandler OnDisconnected;

        public event EventHandler<CommunicatorErrorMessageReadEventArg> OnErrorMessageRead;
        public event EventHandler<CommunicatorEventArg<byte[], T>> OnRawDataWriting;
        public event EventHandler<CommunicatorEventArg<byte[], T>> OnDataReceived;
        public string Identity { get; set; }

        public TcpClientShortConnCommunicator(IMessageCutter<byte[]> binaryMsgCutter,
            Edge.Core.Parser.BinaryParser.ParserBase parser,
            string remoteServerTcpListeningIpAddress,
            int remoteServerTcpListeningPortNumber) : this(binaryMsgCutter, parser,
                remoteServerTcpListeningIpAddress, remoteServerTcpListeningPortNumber, "false")
        {
        }

        public TcpClientShortConnCommunicator(IMessageCutter<byte[]> binaryMsgCutter,
            Edge.Core.Parser.BinaryParser.ParserBase parser,
            string remoteServerTcpListeningIpAddress,
            int remoteServerTcpListeningPortNumber,
            string keepTcpListening) : this(binaryMsgCutter, parser, remoteServerTcpListeningIpAddress
                , remoteServerTcpListeningPortNumber, keepTcpListening, null)
        {
        }

        public TcpClientShortConnCommunicator(IMessageCutter<byte[]> binaryMsgCutter,
            Edge.Core.Parser.BinaryParser.ParserBase parser,
            string remoteServerTcpListeningIpAddress,
            int remoteServerTcpListeningPortNumber,
            string keepTcpListening, string customLoggerFileName)
        {
            this.Identity = remoteServerTcpListeningIpAddress + ":" + remoteServerTcpListeningPortNumber;
            this.messageCutter = binaryMsgCutter;

            this.parser = parser;
            if (string.IsNullOrEmpty(keepTcpListening)) this.keepTcpListeningAfterReceivedAtcpMessage = false;
            else
            {
                if (keepTcpListening.Trim().ToUpper() == "TRUE") this.keepTcpListeningAfterReceivedAtcpMessage = true;
                else this.keepTcpListeningAfterReceivedAtcpMessage = false;
            }

            if (!string.IsNullOrEmpty(customLoggerFileName))
                if (customLoggerFileName == "*")
                    this.logger = NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("DynamicPrivate_Comm_" + remoteServerTcpListeningIpAddress + "_" + remoteServerTcpListeningPortNumber);
                else
                    this.logger = NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("DynamicPrivate_Comm_" + customLoggerFileName);

            this.remoteServerTcpListeningIpAddress = remoteServerTcpListeningIpAddress;
            this.remoteServerTcpListeningPortNumber = remoteServerTcpListeningPortNumber;
            if (this.messageCutter != null)
            {
                this.messageCutter.OnInvalidMessageRead += (____, ______) => logger.Info("Message from " + this.remoteServerTcpListeningIpAddress + ":" + this.remoteServerTcpListeningPortNumber + " Read Invalid bytes");
                this.messageCutter.OnMessageCut += (s, _) =>
                {
                    var safe = this.OnDataReceived;
                    var eventArg = new CommunicatorEventArg<byte[], T>();
                    try
                    {
                        eventArg.Data = this.messageCutter.Message;
                        //eventArg.Message = this.parser.Deserialize(this.messageCutter.Message.ToArray()) as T;
                        if (logger.IsDebugEnabled)
                            logger.Debug("      Parsed: " + eventArg.Message.ToLogString());
                    }
                    catch (Exception ex)
                    {
                        logger.Error("Message from " + this.remoteServerTcpListeningIpAddress + ":" + this.remoteServerTcpListeningPortNumber
                            + " exceptioned in deserilaizing bytes:\r\n 0x" + this.messageCutter.Message.ToHexLogString() + "\r\n exception detail:\r\n" + ex);
                    }

                    try
                    {
                        safe?.Invoke(this, eventArg);
                    }
                    catch (Exception ex)
                    {
                        logger.Error(this.remoteServerTcpListeningIpAddress + ":" + this.remoteServerTcpListeningPortNumber
                            + " exceptioned in handle message:\r\n" + eventArg.Message.ToLogString() + "\r\n exceptioned detail: \r\n" + ex);
                    }
                };
            }
        }

        public void Dispose()
        {
            this.disposed = true;
            //this.lastActiveTcpClient?.Dispose();
        }

        public Task<bool> Start()
        {
            if (0 == Interlocked.CompareExchange(ref this.isStarted, 1, 0))
            {
                try
                {
                    var _ = this.OnConnected;
                    _?.Invoke(this, null);
                    return Task.FromResult(true);
                }
                catch (Exception exxx)
                {
                    return Task.FromResult(false);
                }
            }

            return Task.FromResult(false);
        }

        public bool Write(T message)
        {
            if (message == null) return false;
            TcpClient tcpClient = null;
            byte[] rawData;
            try
            {
                rawData = this.parser.Serialize(message);
                var safe = this.OnRawDataWriting;
                var arg = new CommunicatorEventArg<byte[], T>() { Data = rawData, Message = message, Continue = true };
                safe?.Invoke(this, arg);
                try
                {
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("connecting to remote Tcp server("
                            + this.remoteServerTcpListeningIpAddress + ":" + this.remoteServerTcpListeningPortNumber + ")");
                    }

                    tcpClient = new TcpClient(this.remoteServerTcpListeningIpAddress, this.remoteServerTcpListeningPortNumber);
                    if (logger.IsDebugEnabled)
                        logger.Debug("  connected to remote Tcp server("
                        + this.remoteServerTcpListeningIpAddress + ":" + this.remoteServerTcpListeningPortNumber
                        + "), local port: " + tcpClient.Client.LocalEndPoint.ToString());
                    byte[] buffer = new byte[tcpReceiveBufferSize];
                    tcpClient.GetStream().BeginRead(buffer, 0,
                        tcpReceiveBufferSize,
                        this.Tcp_DataReceived, new System.Tuple<TcpClient, byte[]>(tcpClient, buffer));
                }
                catch (Exception exxx)
                {
                    logger.Error("Setup tcp client which connect to remote TCP server("
                        + this.remoteServerTcpListeningIpAddress + ":" + this.remoteServerTcpListeningPortNumber
                        + ") exceptioned: " + exxx);
                    return false;
                }
            }
            catch (Exception exx)
            {
                var msgLogStr = "";
                try
                {
                    msgLogStr = message.ToLogString();
                }
                catch
                {
                    msgLogStr = "exceptioned for get ToLogString()";
                }

                logger.Error("Tcp Write failed in serialize or event raise for msg: " + message.GetType() + " -> " + msgLogStr + "\r\n detail: " + exx);
                return false;
            }

            lock (this.syncObject)
            {
                try
                {
                    if (logger.IsDebugEnabled)
                        logger.Debug("TCP to " + tcpClient.Client.RemoteEndPoint + " Outgoing--->: " + message.ToLogString() + "\r\n     0x" + rawData.ToHexLogString());
                    tcpClient.Client.Send(rawData);
                }
                catch (Exception exx)
                {
                    logger.Error("Send tcp msg to "
                        + this.remoteServerTcpListeningIpAddress + ":" + this.remoteServerTcpListeningPortNumber + " Write(...) exceptioned, will re-build connection, detail: " + exx);
                    var _ = this.OnDisconnected;
                    _?.Invoke(this, null);
                    return false;
                }
            }

            return true;
        }

        public bool Write(T message, object extraControlParameter)
        {
            throw new NotImplementedException();
        }

        private void Tcp_DataReceived(IAsyncResult ar)
        {
            System.Tuple<TcpClient, byte[]> parameter = null;
            byte[] dataRead = null;
            int bytesReadCount = 0;
            try
            {
                parameter = (System.Tuple<TcpClient, byte[]>)(ar.AsyncState);
                dataRead = parameter.Item2;
                bytesReadCount = parameter.Item1.Client.EndReceive(ar);
                if (bytesReadCount == 0)
                    throw new Exception("tcp client received 0 count data which indicates the connection is broken, trigger disconnection");
            }
            catch (Exception tcpExp)
            {
                logger.Info("Tcp client EndReceive from " + this.remoteServerTcpListeningIpAddress + " exceptioned: " + tcpExp + "\r\n will re-build connection...");
                var _ = this.OnDisconnected;
                _?.Invoke(this, null);
                return;
            }

            try
            {
                if (logger.IsDebugEnabled)
                    logger.Debug("TCP from " + parameter.Item1.Client.RemoteEndPoint.ToString() + " <---Incoming: 0x" + dataRead.Take(bytesReadCount).ToHexLogString());
                try
                {
                    lock (this.syncObject_Feed)
                    {
                        this.messageCutter.Feed(dataRead.Take(bytesReadCount).ToArray());
                    }
                }
                catch (Exception ex)
                {
                    logger.Error(" Exception in TCP Parsing bytes: 0x" + dataRead.Take(bytesReadCount).ToHexLogString() + ", detail: \r\n" + ex.ToString());
                }

                if (this.keepTcpListeningAfterReceivedAtcpMessage)
                {
                    try
                    {
                        byte[] buffer = new byte[tcpReceiveBufferSize];
                        parameter.Item1.GetStream().BeginRead(buffer, 0,
                            tcpReceiveBufferSize,
                            this.Tcp_DataReceived, new System.Tuple<TcpClient, byte[]>(parameter.Item1, buffer));
                    }
                    catch (Exception ex)
                    {
                        logger.Error(" Exception in keepTcpListeningAfterReceivedAtcpMessage's Tcp_DataReceived(...):\r\n" + ex);

                        try
                        {
                            parameter.Item1.Dispose();
                        }
                        catch (Exception exx)
                        {
                        }
                        finally
                        {
                            var _ = this.OnDisconnected;
                            _?.Invoke(this, null);
                        }
                    }
                }
                else
                {
                    try
                    {
                        parameter.Item1.Dispose();
                    }
                    catch (Exception ex)
                    {
                        logger.Error(" Exception in Dispose TcpClient after received a message:\r\n" + ex.ToString());
                    }
                    finally
                    {
                        var _ = this.OnDisconnected;
                        _?.Invoke(this, null);
                    }
                }
            }
            catch (Exception ex)
            {
                logger.Error(" Exception in Tcp_DataReceived(...):\r\n" + ex.ToString());
                var _ = this.OnDisconnected;
                _?.Invoke(this, null);
            }
        }
    }
}
