//                           UNCLASSIFIED
/*-------------------------------------------------------------------------
RAYTHEON PROPRIETARY: THIS DOCUMENT CONTAINS DATA OR INFORMATION
PROPRIETARY TO RAYTHEON COMPANY AND IS RESTRICTED TO USE ONLY BY PERSONS
AUTHORIZED BY RAYTHEON COMPANY IN WRITING TO USE IT. DISCLOSURE TO
UNAUTHORIZED PERSONS WOULD LIKELY CAUSE SUBSTANTIAL COMPETITIVE HARM TO
RAYTHEON COMPANY'S BUSINESS POSITION. NEITHER SAID DOCUMENT NOR ITS
CONTENTS SHALL BE FURNISHED OR DISCLOSED TO OR COPIED OR USED BY PERSONS
OUTSIDE RAYTHEON COMPANY WITHOUT THE EXPRESS WRITTEN APPROVAL OF RAYTHEON
COMPANY.

THIS PROPRIETARY NOTICE IS NOT APPLICABLE IF DELIVERED TO THE U.S.
GOVERNMENT.

UNPUBLISHED WORK - COPYRIGHT RAYTHEON COMPANY.
-------------------------------------------------------------------------*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using NLog;
using Raytheon.Common;
using Raytheon.Instruments.GeneralIO;

namespace Raytheon.Instruments
{
   /// <summary>
   /// A class that implements a Pickering DIO card
   /// </summary>
   public class DIOIcs8003 : IGeneralIO
   {
      #region PrivateClassMembers
      private string _name;
      private string _ipAddress;
      private int _ipPort;
      private SelfTestResult _selfTestResult;
      private State _state;
      private object _syncObj = new Object();
      private int _numChannelPerPort = 8;
      private int _channelStartIndex = 0;
      private int _numInputChannels;
      private int _numOutputChannels;
      private bool _shallWeInitializeOutput = false;
      private NetworkStream _dioStream;
      private PortDirections _portDirections;

      private const int _READ_BUFFER_SIZE = 128;
      private const int _READ_TIMEOUT = 5000;
      private const int _BUFFER_OFFSET = 0;
      private byte[] _dataBuffer;

      // Carriage return for string
      private const string _CARRIAGE_RETURN = "\n";

      // Reset Device
      private const String _RestDevice = "*RST";

      // Execute Self-Test
      private const String _CommandSelfTest = "*TST?";

      private Dictionary<string, IODatatypes.DIOChannelInfo> _signalNameToChannelInfoMap = new Dictionary<string, IODatatypes.DIOChannelInfo>();

      private readonly ILogger _logger;

      private readonly IConfigurationManager _configurationManager;
      private readonly IConfiguration _configuration;

      #endregion

      #region PublicClassFunctions

      /// <summary>
      /// DIOIcs8003 factory constructor
      /// </summary>
      /// <param name="deviceName"></param>
      /// <param name="configurationManager"></param>
      public DIOIcs8003(string deviceName, IConfigurationManager configurationManager)
      {
         Name = deviceName;

         _logger = LogManager.GetLogger($"{this.GetType().Name} - {deviceName}");

         _configurationManager = configurationManager;
         _configuration = _configurationManager.GetConfiguration(Name);

         _dataBuffer = new byte[_READ_BUFFER_SIZE];

         _portDirections = new PortDirections(BitDirection.Input, BitDirection.Input, BitDirection.Input, BitDirection.Input, BitDirection.Input);

         string dioModuleDefPath = _configuration.GetConfigurationValue(deviceName, ConfigXml.DIO_MODULE_DEF_FILEPATH.ToString());

         if (!Path.IsPathRooted(dioModuleDefPath))
            dioModuleDefPath = Path.GetFullPath(Path.Combine(_configurationManager.ConfigurationStoragePath, dioModuleDefPath));

         IConfigurationFile dioModuleConfig = new ConfigurationFile(dioModuleDefPath);

         _ipAddress = dioModuleConfig.ReadValue(Name, ConfigIni.DIO_ADDRESS.ToString());
         Int32.TryParse(dioModuleConfig.ReadValue(Name, ConfigIni.DIO_PORT.ToString()), out _ipPort);

         Boolean.TryParse(dioModuleConfig.ReadValue(Name, ConfigIni.SHALL_WE_DRIVE_OUTPUT_UPON_INITIALIZATION.ToString()), out _shallWeInitializeOutput);

         List<string> outputSignalNames = dioModuleConfig.ReadAllKeys($"{Name}.{ConfigIni.OUTPUT_SIGNALS}");
         List<string> intputSignalNames = dioModuleConfig.ReadAllKeys($"{Name}.{ConfigIni.INPUT_SIGNALS}");

         Int32.TryParse(dioModuleConfig.ReadValue(Name, ConfigIni.NUM_CHANNELS_PER_PORT.ToString()), out _numChannelPerPort);
         Int32.TryParse(dioModuleConfig.ReadValue(Name, ConfigIni.NUM_OUTPUT_CHANNELS.ToString()), out _numOutputChannels);
         Int32.TryParse(dioModuleConfig.ReadValue(Name, ConfigIni.NUM_INPUT_CHANNELS.ToString()), out _numInputChannels);
         Int32.TryParse(dioModuleConfig.ReadValue(Name, ConfigIni.CHANNEL_START_INDEX.ToString()), out _channelStartIndex);

         if (!(_channelStartIndex == 0 || _channelStartIndex == 1))
         {
            throw new Exception($"The value for key {ConfigIni.CHANNEL_START_INDEX.ToString()} in section {Name} must be 0 or 1 in {dioModuleDefPath}");
         }

         IODatatypes.DIOChannelInfo info;
         foreach (string signalName in outputSignalNames)
         {
            if (_signalNameToChannelInfoMap.ContainsKey(signalName))
               throw new Exception($"Key {signalName} in section {Name}.{ConfigIni.OUTPUT_SIGNALS} conflicts with the same key defined in another section.");

            string iniLine = dioModuleConfig.ReadValue($"{Name}.{ConfigIni.OUTPUT_SIGNALS}", signalName);
            string[] infoTokens = iniLine.Split('|');
            if (infoTokens.Length != 2)
            {
               throw new Exception($"Key {signalName} in section {Name}.{ConfigIni.OUTPUT_SIGNALS} does not contain 2 tokens");
            }

            info.channelNumber = Convert.ToUInt32(infoTokens[0]);
            info.ioType = IODatatypes.IOType.DigitalOutput;
            info.initialValue = Convert.ToInt32(infoTokens[1]);

            _signalNameToChannelInfoMap[signalName] = info;
         }

         foreach (string signalName in intputSignalNames)
         {
            if (_signalNameToChannelInfoMap.ContainsKey(signalName))
               throw new Exception($"Key {signalName} in section {Name}.{ConfigIni.INPUT_SIGNALS} conflicts with the same key defined in another section.");

            string iniLine = dioModuleConfig.ReadValue($"{Name}.{ConfigIni.INPUT_SIGNALS}", signalName);

            info.channelNumber = Convert.ToUInt32(iniLine);
            info.ioType = IODatatypes.IOType.DigitalInput;
            info.initialValue = -1;

            _signalNameToChannelInfoMap[signalName] = info;
         }

         _selfTestResult = SelfTestResult.Unknown;
         _state = State.Uninitialized;
      }

      ~DIOIcs8003()
      {

      }

      /// <summary>
      /// Initialize the card
      /// </summary>
      public void Initialize()
      {
         lock (_syncObj)
         {
            if (_state == State.Uninitialized)
            {
               //Create and open a socket to the dio card
               TcpClient dioSocket = new TcpClient(_ipAddress, _ipPort);
               _dioStream = dioSocket.GetStream();

               // Set the timeout (read)
               _dioStream.ReadTimeout = _READ_TIMEOUT;

               Thread.Sleep(1000);

               // Flush the buffer
               if (_dioStream.DataAvailable)
               {
                  Thread.Sleep(1000);
                  Read();
               }

               // Send command to reset device
               Write(_RestDevice);

               _state = State.Ready;
            }
            else
            {
               throw new Exception("Expected the state to be Uninitialized, state was: " + _state.ToString() + " on card " + _name);
            }
         }
      }

      /// <summary>
      /// Return map of all signals
      /// </summary>
      public Dictionary<string, IODatatypes.DIOChannelInfo> GetAllSignals()
      {
         return _signalNameToChannelInfoMap;
      }

      /// <summary>
      /// 
      /// </summary>
      /// <returns></returns>
      public bool ClearErrors()
      {
         throw new NotImplementedException();
      }

      /// <summary>
      /// 
      /// </summary>
      public string DetailedStatus
      {
         get
         {
            return "This is a ICS8003 DIO Card " + _name;
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public bool DisplayEnabled
      {
         get
         {
            throw new NotImplementedException();
         }

         set
         {
            throw new NotImplementedException();
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public bool FrontPanelEnabled
      {
         get
         {
            throw new NotImplementedException();
         }

         set
         {
            throw new NotImplementedException();
         }
      }

      /// <summary>
      /// 
      /// </summary>
      /// <param name="signalName"></param>
      /// <param name="state"></param>
      public void SetBit(string signalName, IODatatypes.BitState state)
      {
         lock (_syncObj)
         {
            if (!_signalNameToChannelInfoMap.ContainsKey(signalName))
               throw new Exception($"Signal name {signalName} doesn't exist for card: " + _name);

            if (_signalNameToChannelInfoMap[signalName].channelNumber >= _numOutputChannels || _signalNameToChannelInfoMap[signalName].channelNumber < _channelStartIndex)
            {
               throw new Exception($"The output channel number {_signalNameToChannelInfoMap[signalName].channelNumber} specified must be >= {_channelStartIndex} and < {_numOutputChannels + _channelStartIndex} on card " + _name);
            }

            uint bit = _signalNameToChannelInfoMap[signalName].channelNumber;
            String msg;
            if (state == IODatatypes.BitState.High)
            {
               msg = "Route:Close ";
            }
            else
            {
               msg = "Route:Open ";
            }

            uint conversion = ConvertChannelToBit(bit);

            // Byte 1
            if (bit <= 8)
            {
               if (_portDirections._PortOne == BitDirection.Input)
               {
                  InitializeOutputsLow(1);

                  _portDirections._PortOne = BitDirection.Output;
               }

               msg = msg + "1,";
            }
            // Byte 2
            else if (bit > 8 && bit <= 16)
            {
               if (_portDirections._PortTwo == BitDirection.Input)
               {
                  InitializeOutputsLow(2);

                  _portDirections._PortTwo = BitDirection.Output;
               }

               msg = msg + "2,";
            }
            // Byte 3
            else if (bit > 16 && bit <= 24)
            {
               if (_portDirections._PortThree == BitDirection.Input)
               {
                  InitializeOutputsLow(3);

                  _portDirections._PortThree = BitDirection.Output;
               }

               msg = msg + "3,";
            }
            // Byte 4
            else if (bit > 24 && bit <= 32)
            {
               if (_portDirections._PortFour == BitDirection.Input)
               {
                  InitializeOutputsLow(4);

                  _portDirections._PortFour = BitDirection.Output;
               }

               msg = msg + "4,";
            }
            // Byte 5
            else if (bit > 32 && bit <= 40)
            {
               if (_portDirections._PortFive == BitDirection.Input)
               {
                  InitializeOutputsLow(5);

                  _portDirections._PortFive = BitDirection.Output;
               }

               msg = msg + "5,";
            }

            // Set the bit
            msg = msg + conversion.ToString();

            // Send msg
            Write(msg);
         }
      }

      /// <summary>
      /// 
      /// </summary>
      /// <param name="signalName"></param>
      /// <returns></returns>
      public IODatatypes.BitState GetBitState(string signalName)
      {
         lock (_syncObj)
         {
            if (!_signalNameToChannelInfoMap.ContainsKey(signalName))
               throw new Exception($"Signal name {signalName} doesn't exist for card: " + _name);

            if (_signalNameToChannelInfoMap[signalName].channelNumber >= _numInputChannels || _signalNameToChannelInfoMap[signalName].channelNumber < _channelStartIndex)
            {
               throw new Exception($"The input channel number {_signalNameToChannelInfoMap[signalName].channelNumber} specified must be >= {_channelStartIndex} and < {_numInputChannels + _channelStartIndex} on card " + _name);
            }

            uint bit = _signalNameToChannelInfoMap[signalName].channelNumber;
            uint conversion = ConvertChannelToBit(bit);

            String msg = "Sense:Bit? ";
            // Byte 1
            if (bit <= 8)
            {
               msg = msg + "1,";
            }
            // Byte 2
            else if (bit > 8 && bit <= 16)
            {
               msg = msg + "2,";
            }
            // Byte 3
            else if (bit > 16 && bit <= 24)
            {
               msg = msg + "3,";
            }
            // Byte 4
            else if (bit > 24 && bit <= 32)
            {
               msg = msg + "4,";
            }
            // Byte 5
            else if (bit > 32 && bit <= 40)
            {
               msg = msg + "5,";
            }

            msg = msg + conversion.ToString();

            String result = Query(msg);
            if (result == "1")
            {
               return IODatatypes.BitState.High;
            }
            else
            {
               return IODatatypes.BitState.Low;
            }
         }
      }

      private uint ConvertChannelToBit(uint bit)
      {
         uint conversion = (bit % 8);
         if (conversion == 0)
         {
            conversion = 7;
         }
         else
         {
            conversion -= 1;
         }

         return conversion;
      }

      private String Query(String msg)
      {
         lock (_syncObj)
         {
            // Send message
            Write(msg);

            // Wait 
            Thread.Sleep(100);

            // Get response (and parse)
            String rsp = Parse(Read());

            return rsp;
         }
      }

      private String Read()
      {
         lock (_syncObj)
         {
            String rsp = "";

            // Flush
            FlushReadBuffer(ref _dataBuffer);

            // Get response
            _dioStream.Read(_dataBuffer, _BUFFER_OFFSET, _dataBuffer.Length);

            rsp = Encoding.ASCII.GetString(_dataBuffer);

            return rsp;
         }
      }


      private void Write(String msg)
      {
         lock (_syncObj)
         {
            // Check for connect

            string commandString = msg + _CARRIAGE_RETURN;

            byte[] commandBuffer = Encoding.ASCII.GetBytes(commandString);

            // Send message
            _dioStream.Write(commandBuffer, _BUFFER_OFFSET, commandBuffer.Length);
         }
      }

      private void FlushReadBuffer(ref byte[] buffer)
      {
         // Clear the buffer
         Array.Clear(buffer, _BUFFER_OFFSET, buffer.Length);
      }

      // Initialize a port to be an output in a known (low) state
      private void InitializeOutputsLow(int port)
      {
         // Initialize port as an output (initialize to low)
         String msg = "Source:Data:Port" + port.ToString() + " 0";

         Write(msg);
      }

      private String Parse(String msg)
      {
         String Result = "";

         // Parse the message by line endings (\n)
         String[] Temp = msg.Split('\n');

         // Result in first line
         Result = Temp[0];
         return Result;
      }

      /// <summary>
      /// 
      /// </summary>
      public InstrumentMetadata Info
      {
         get
         {
            throw new NotImplementedException();
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public string Name
      {
         get { return _name; }
         set { _name = value; }
      }

      /// <summary>
      /// 
      /// </summary>
      public uint NumberOfInputBits
      {
         get
         {
            return (uint)_numInputChannels;
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public uint NumberOfOutputBits
      {
         get
         {
            return (uint)_numOutputChannels;
         }
      }

      /// <summary>
      /// 
      /// </summary>
      /// <returns></returns>
      public SelfTestResult PerformSelfTest()
      {
         lock (_syncObj)
         {
            // Send command to perform self-test (might be empty string)
            String results = Query(_CommandSelfTest);
            if (results == "0")
            {
               _selfTestResult = SelfTestResult.Pass;
            }
            else
            {
               _selfTestResult = SelfTestResult.Fail;
            }

            return _selfTestResult;
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public void Reset()
      {
         lock (_syncObj)
         {
            Shutdown();
            Initialize();
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public SelfTestResult SelfTestResult
      {
         get
         {
            return _selfTestResult;
         }
      }

      /// <summary>
      /// 
      /// </summary>
      /// <param name="bit"></param>
      public void SetTristate(string signalName)
      {
         throw new NotImplementedException();
      }

      /// <summary>
      /// 
      /// </summary>
      public State Status
      {
         get
         {
            return _state;
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public void Shutdown()
      {
         lock (_syncObj)
         {
            if (_dioStream != null)
            {
               _dioStream.Close();
            }
            _state = State.Uninitialized;
         }
      }
      #endregion
   }
}
