using System;
using System.Threading;
using System.IO.Ports;
using System.Windows.Forms;
using LFP_Manager.DataStructure;
using LFP_Manager.Function;
using LFP_Manager.Utils;

namespace LFP_Manager.Threads
{
    public sealed class CsRs232Thread124050 : IDisposable
    {
        #region ENUMS

        private enum CommandType
        {
            NoCommand = 0,
            WriteParam = 1,
            WriteCoil = 2,
            WriteRegister = 3,
            ReadRegister = 4
        }

        private enum AddressChannel
        {
            Channel0 = 0,
            Channel1 = 1,
            Channel2 = 2,
            Channel3 = 3
        }

        #endregion

        #region CONSTANTS

        private static class Constants
        {
            public const int BUFFER_SIZE = 512;
            public const int DEFAULT_BAUD_RATE = 115200;
            public const int MAX_TIMEOUT_COUNT = 5;
            public const int DEFAULT_DATA_BITS = 8;
            public const int POLLING_INTERVAL = 100;
            public const int ERROR_RETRY_DELAY = 1000;
        }

        #endregion

        #region PRIVATE FIELDS

        private readonly object _lockObject = new object();
        private readonly CommConfig _config;
        private DeviceSystemData _systemData;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private volatile bool _isRunning;
        private volatile bool _isDisposed;

        private SerialPort _serialPort;
        private Thread _commThread;

        private readonly byte[] _receiveBuffer;
        private readonly byte[] _readBuffer;
        private int _bufferStart;
        private int _bufferEnd;
        private ushort _readPosition;
        private bool _bufferStarted;

        private readonly TUartTxBuff _uartTxBuff;

        private int _systemId;
        private bool _uartPolling;
        private bool _autoUartTx;
        private AddressChannel _currentChannel;

        private ushort _requestRegAddr;
        private ushort _requestRegLen;
        private bool _responseFlag;
        private int _responseType;
        private CommandType _currentCommand;
        private int _timeoutCount;

        private ushort _writeRegAddr;
        private short _writeCoilRegData;
        private byte[] _writeRegData;

        #endregion

        #region EVENTS

        public event UartDataUpdate OnUpdate;
        public event UartDataRecv OnDataRecv;
        public event UartDataPrint OnPrint;

        #endregion

        #region CONSTRUCTORS

        public CsRs232Thread124050(int systemId, CommConfig config, ref DeviceSystemData systemData)
        {
            if (config == null)
                throw new ArgumentNullException(nameof(config));

            _systemId = systemId;
            _config = config;
            _systemData = systemData;
            _cancellationTokenSource = new CancellationTokenSource();

            _receiveBuffer = new byte[Constants.BUFFER_SIZE];
            _readBuffer = new byte[Constants.BUFFER_SIZE];
            _uartTxBuff = new TUartTxBuff();
            _autoUartTx = true;

            InitializeSerialPort();
        }

        private void InitializeSerialPort()
        {
            try
            {
                _serialPort = new SerialPort();
                _serialPort.BaudRate = Constants.DEFAULT_BAUD_RATE;
                _serialPort.DataBits = Constants.DEFAULT_DATA_BITS;
                _serialPort.StopBits = StopBits.One;
                _serialPort.Parity = Parity.None;
                _serialPort.ReadTimeout = 500;
                _serialPort.WriteTimeout = 500;

                _serialPort.DataReceived += SerialPort_DataReceived;
                _serialPort.ErrorReceived += SerialPort_ErrorReceived;
                _serialPort.PinChanged += SerialPort_PinChanged;
            }
            catch (Exception ex)
            {
                if (OnPrint != null)
                {
                    OnPrint(this, string.Format("Failed to initialize serial port: {0}", ex.Message));
                }
                throw;
            }
        }

        #endregion

        #region PUBLIC METHODS

        public bool Start(ref CommConfig config, int systemId, bool polling)
        {
            ThrowIfDisposed();

            lock (_lockObject)
            {
                if (_isRunning)
                    return false;

                _systemId = systemId;
                _uartPolling = polling;

                try
                {
                    if (OpenPort(config.UartPort))
                    {
                        _isRunning = true;
                        _commThread = new Thread(CommThreadProcess);
                        _commThread.IsBackground = true;
                        _commThread.Name = "UART_Communication_Thread";
                        _commThread.Priority = ThreadPriority.AboveNormal;
                        _commThread.Start();
                        return true;
                    }
                }
                catch (Exception ex)
                {
                    if (OnPrint != null)
                    {
                        OnPrint(this, string.Format("Failed to start UART thread: {0}", ex.Message));
                    }
                }

                return false;
            }
        }

        public void Stop()
        {
            if (_isDisposed)
                return;

            lock (_lockObject)
            {
                _isRunning = false;
                if (_cancellationTokenSource != null)
                {
                    _cancellationTokenSource.Cancel();
                }
                ClosePort();
            }
        }

        public void Dispose()
        {
            if (_isDisposed)
                return;

            Stop();
            
            lock (_lockObject)
            {
                if (_cancellationTokenSource != null)
                {
                    _cancellationTokenSource.Dispose();
                }
                if (_serialPort != null)
                {
                    _serialPort.Dispose();
                }
                _isDisposed = true;
            }

            GC.SuppressFinalize(this);
        }

        public void SetPolling(bool flag, int systemId, ref DeviceSystemData systemData)
        {
            ThrowIfDisposed();

            lock (_lockObject)
            {
                _systemId = systemId;
                _uartPolling = flag;
                _systemData = systemData;
                _systemData.mNo = systemId;
            }
        }

        public void SetAutoTx(bool autoTx)
        {
            ThrowIfDisposed();

            lock (_lockObject)
            {
                _autoUartTx = autoTx;
            }
        }

        public void SetWriteCoilReg(ushort writeAddr, short writeData)
        {
            ThrowIfDisposed();

            lock (_lockObject)
            {
                _currentCommand = CommandType.WriteCoil;
                _writeRegAddr = writeAddr;
                _writeCoilRegData = writeData;
                Thread.Sleep(500);
            }
        }

        public void SetReadReg(ushort readAddr, ushort readLen, bool replyFlag)
        {
            ThrowIfDisposed();

            lock (_lockObject)
            {
                _currentCommand = CommandType.ReadRegister;
                _requestRegAddr = readAddr;
                _requestRegLen = readLen;
                _responseFlag = replyFlag;
            }
        }

        public void SetWriteReg(ushort writeAddr, short[] writeData)
        {
            ThrowIfDisposed();

            if (writeData == null)
                throw new ArgumentNullException(nameof(writeData));

            lock (_lockObject)
            {
                _currentCommand = CommandType.WriteParam;
                _writeRegAddr = writeAddr;

                TUartTRxData uartTRxData = new TUartTRxData();
                uartTRxData.type = (int)CommandType.WriteParam;
                uartTRxData.data = csSerialCommFunction.MakeWriteRegisterData((ushort)_systemId, writeAddr, writeData);
                uartTRxData.length = uartTRxData.data.Length;

                if (_uartTxBuff != null)
                {
                    _uartTxBuff.PutBuff(uartTRxData);
                }
            }
        }

        #endregion

        #region PRIVATE METHODS

        private void ThrowIfDisposed()
        {
            if (_isDisposed)
                throw new ObjectDisposedException(GetType().FullName);
        }

        private bool OpenPort(string portName)
        {
            try
            {
                if (string.IsNullOrEmpty(portName))
                    throw new ArgumentException("Port name cannot be null or empty", "portName");

                if (_serialPort.IsOpen)
                    _serialPort.Close();

                _serialPort.PortName = portName;
                _serialPort.Open();
                return true;
            }
            catch (Exception ex)
            {
                if (OnPrint != null)
                {
                    OnPrint(this, string.Format("Failed to open port {0}: {1}", portName, ex.Message));
                }
                return false;
            }
        }

        private void ClosePort()
        {
            try
            {
                if (_serialPort != null && _serialPort.IsOpen)
                    _serialPort.Close();
            }
            catch (Exception ex)
            {
                if (OnPrint != null)
                {
                    OnPrint(this, string.Format("Error closing port: {0}", ex.Message));
                }
            }
        }

        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                if (!_serialPort.IsOpen) return;

                int bytesToRead = _serialPort.BytesToRead;
                if (bytesToRead == 0) return;

                byte[] buffer = new byte[bytesToRead];
                int bytesRead = _serialPort.Read(buffer, 0, bytesToRead);

                lock (_lockObject)
                {
                    for (int i = 0; i < bytesRead; i++)
                    {
                        AddToBuffer(buffer[i]);
                    }
                }
            }
            catch (Exception ex)
            {
                if (OnPrint != null)
                {
                    OnPrint(this, string.Format("Error in data receive: {0}", ex.Message));
                }
            }
        }

        private void SerialPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
        {
            if (OnPrint != null)
            {
                OnPrint(this, string.Format("Serial port error: {0}", e.EventType));
            }
        }

        private void SerialPort_PinChanged(object sender, SerialPinChangedEventArgs e)
        {
            if (OnPrint != null)
            {
                OnPrint(this, string.Format("Serial pin changed: {0}", e.EventType));
            }
        }

        private void AddToBuffer(byte data)
        {
            _receiveBuffer[_bufferStart++] = data;
            _bufferStart %= Constants.BUFFER_SIZE;

            if (_bufferStart == _bufferEnd)
            {
                _bufferEnd = (_bufferEnd + 1) % Constants.BUFFER_SIZE;
            }
        }

        private int GetFromBuffer()
        {
            if (_bufferStart == _bufferEnd)
                return 0;

            int result = 0x0100 + _receiveBuffer[_bufferEnd++];
            _bufferEnd %= Constants.BUFFER_SIZE;
            return result;
        }

        private void FlushBuffer()
        {
            _bufferStart = _bufferEnd = 0;
        }

        private void ProcessReceivedData()
        {
            if (_readPosition >= Constants.BUFFER_SIZE)
            {
                ResetReceiveState();
                return;
            }

            if (!_bufferStarted)
            {
                int data = GetFromBuffer();
                if ((data & 0xFF) == _systemId || (data & 0xFF) == 0x7F)
                {
                    _readPosition = 0;
                    _readBuffer[_readPosition++] = (byte)(data & 0xFF);
                    _bufferStarted = true;
                }
                return;
            }

            int receivedByte = GetFromBuffer();
            if ((receivedByte & 0x0100) == 0)
                return;

            _readBuffer[_readPosition++] = (byte)(receivedByte & 0xFF);

            ProcessModbusResponse();
        }

        private void ProcessModbusResponse()
        {
            int result = csSerialCommFunction.ModbusPacketFromSlaveCheck(_readBuffer, _readPosition);

            switch (result)
            {
                case 0: // Need more data
                    return;

                case 1: // Valid packet
                    if (OnPrint != null)
                    {
                        OnPrint(this, csLog.trx_data_print(_readBuffer, _readPosition, 1));
                    }
                    _timeoutCount = 0;
                    ProcessValidResponse();
                    break;

                case 2: // Firmware update packet
                    if (OnPrint != null)
                    {
                        OnPrint(this, csLog.trx_data_print(_readBuffer, _readPosition, 1));
                    }
                    _timeoutCount = 0;
                    ProcessFirmwareResponse();
                    break;

                case -1: // Error packet
                    ResetReceiveState();
                    break;
            }
        }

        private void ProcessValidResponse()
        {
            try
            {
                int moduleId = _readBuffer[0];
                if (moduleId > 0) moduleId--;

                _systemData.CommFail = false;
                _systemData.ShelfCommFail = false;

                if (_responseType == 0)
                {
                    short[] resultCode = csSerialCommFunction124050.SerialRxProcess(_readBuffer, _requestRegAddr, _readPosition, ref _systemData);
                    if (OnUpdate != null)
                    {
                        OnUpdate(this, ref _systemData);
                    }
                }
                else
                {
                    _responseType = 0;
                    if (OnDataRecv != null)
                    {
                        byte[] responseData = new byte[_readPosition];
                        Array.Copy(_readBuffer, responseData, _readPosition);
                        OnDataRecv(responseData);
                    }
                }
            }
            catch (Exception ex)
            {
                if (OnPrint != null)
                {
                    OnPrint(this, string.Format("Error processing valid response: {0}", ex.Message));
                }
            }
            finally
            {
                ResetReceiveState();
            }
        }

        private void ProcessFirmwareResponse()
        {
            try
            {
                if (OnDataRecv != null)
                {
                    byte[] responseData = new byte[_readPosition];
                    Array.Copy(_readBuffer, responseData, _readPosition);
                    OnDataRecv(responseData);
                }
            }
            catch (Exception ex)
            {
                if (OnPrint != null)
                {
                    OnPrint(this, string.Format("Error processing firmware response: {0}", ex.Message));
                }
            }
            finally
            {
                ResetReceiveState();
            }
        }

        private void ResetReceiveState()
        {
            _bufferStarted = false;
            _readPosition = 0;
            _responseFlag = false;
        }

        private void CommThreadProcess()
        {
            while (_isRunning && !_cancellationTokenSource.Token.IsCancellationRequested)
            {
                try
                {
                    ProcessCommunication();
                    Thread.Sleep(Constants.POLLING_INTERVAL);
                }
                catch (Exception ex)
                {
                    if (OnPrint != null)
                    {
                        OnPrint(this, string.Format("Communication thread error: {0}", ex.Message));
                    }
                    Thread.Sleep(Constants.ERROR_RETRY_DELAY);
                }
            }
        }

        private void ProcessCommunication()
        {
            if (_serialPort == null || !_serialPort.IsOpen)
                return;

            bool writeMode = false;
            byte[] txData = PrepareTransmitData(ref writeMode);

            if (txData != null)
            {
                SendData(txData);
                if (_responseFlag)
                {
                    ProcessReceivedData();
                }
            }
        }

        private byte[] PrepareTransmitData(ref bool writeMode)
        {
            byte[] txData = null;
            _responseType = 0;

            if (_uartTxBuff != null && _uartTxBuff.CheckBuff())
            {
                TUartTRxData txBuffer = _uartTxBuff.GetBuff();
                if (txBuffer != null)
                {
                    txData = txBuffer.data;
                    writeMode = true;
                    _responseFlag = true;
                    _responseType = txBuffer.type;
                }
            }
            else if (_currentCommand == CommandType.WriteCoil)
            {
                txData = csSerialCommFunction.MakeWriteCoilData((ushort)_systemId, _writeRegAddr, _writeCoilRegData);
                _currentCommand = CommandType.NoCommand;
                _responseFlag = true;
            }
            else if (_currentCommand == CommandType.ReadRegister)
            {
                txData = csSerialCommFunction.MakeReadRegisterData((ushort)_systemId, csSerialCommFunction.READ_HOLDING_REG, _requestRegAddr, _requestRegLen);
                _currentCommand = CommandType.NoCommand;
                _responseFlag = true;
            }
            else if (_uartPolling && _autoUartTx)
            {
                txData = PreparePollingData();
            }

            return txData;
        }

        private byte[] PreparePollingData()
        {
            ushort command;

            switch (_currentChannel)
            {
                case AddressChannel.Channel0:
                    _currentChannel = AddressChannel.Channel1;
                    _requestRegAddr = 0x0000;
                    _requestRegLen = 0x0040;
                    command = csSerialCommFunction.READ_HOLDING_REG;
                    _responseFlag = true;
                    break;

                case AddressChannel.Channel1:
                    _currentChannel = AddressChannel.Channel2;
                    _requestRegAddr = 0x0040;
                    _requestRegLen = 0x0040;
                    command = csSerialCommFunction.READ_HOLDING_REG;
                    _responseFlag = true;
                    break;

                case AddressChannel.Channel2:
                    _currentChannel = AddressChannel.Channel3;
                    _requestRegAddr = 0x0080;
                    _requestRegLen = 0x0040;
                    command = csSerialCommFunction.READ_HOLDING_REG;
                    _responseFlag = true;
                    break;

                case AddressChannel.Channel3:
                    if (_config.ModuleQty > 1)
                    {
                        _systemId++;
                        if (_systemId > _config.ModuleQty)
                        {
                            _systemId = 1;
                        }
                    }
                    _currentChannel = AddressChannel.Channel0;
                    command = csSerialCommFunction.NO_CMD;
                    _responseFlag = false;
                    break;

                default:
                    _currentChannel = AddressChannel.Channel0;
                    _requestRegAddr = 0x0000;
                    _requestRegLen = 0x0040;
                    command = csSerialCommFunction.READ_HOLDING_REG;
                    break;
            }

            return command == csSerialCommFunction.NO_CMD ? null :
                csSerialCommFunction.MakeReadRegisterData((ushort)_systemId, command, _requestRegAddr, _requestRegLen);
        }

        private void SendData(byte[] data)
        {
            try
            {
                if (data == null)
                    throw new ArgumentNullException("data");

                FlushBuffer();
                _serialPort.Write(data, 0, data.Length);
                if (OnPrint != null)
                {
                    OnPrint(this, csLog.trx_data_print(data, data.Length, 0));
                }
            }
            catch (Exception ex)
            {
                if (OnPrint != null)
                {
                    OnPrint(this, string.Format("Send data error: {0}", ex.Message));
                }
                throw;
            }
        }

        #endregion
    }
}