using System;
using System.Diagnostics;
using System.Text;
using System.IO;
using System.Net.Sockets;
using System.Net;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Security.Principal;
using System.Threading;
using System.Timers;

namespace IndianHealthService.BMXNet
{
	/// <summary>
	/// BMXNetLib implements low-level socket connectivity to RPMS databases.
	/// The VA RPC Broker must be running on the RPMS server in order for 
	/// BMXNetLib to connect.
	/// </summary>
	[DnsPermission(SecurityAction.Assert, Unrestricted = true)]
	public class BMXNetNativeLib:BMXNetLib
	{
		public BMXNetNativeLib()
		{
			m_sWKID = "BMX";
			m_sWINH = "";
			m_sPRCH = "";
			m_sWISH = "";
			m_cHDR = ADEBHDR(m_sWKID,m_sWINH,m_sPRCH,m_sWISH);

		}


		#region Piece Functions

		/// <summary>
		/// Corresponds to M's $L(STRING,DELIMITER)
		/// </summary>
		/// <param name="sInput"></param>
		/// <param name="sDelim"></param>
		/// <returns></returns>
		public static int PieceLength(string sInput, string sDelim)
		{
			char[] cDelim = sDelim.ToCharArray();
			string [] cSplit = sInput.Split(cDelim);
			return cSplit.GetLength(0);
		}

		/// <summary>
		/// Corresponds to M's $$Piece function
		/// </summary>
		/// <param name="sInput"></param>
		/// <param name="sDelim"></param>
		/// <param name="nNumber"></param>
		/// <returns></returns>
		public static string Piece(string sInput, string sDelim, int nNumber)
		{
			try
			{
				char[] cDelim = sDelim.ToCharArray();
				string [] cSplit = sInput.Split(cDelim);
				int nLength = cSplit.GetLength(0);
				if ((nLength < nNumber)||(nNumber < 1))
					return "";
				return cSplit[nNumber-1];
			}
			catch (Exception bmxEx)
			{
				string sMessage =  bmxEx.Message + bmxEx.StackTrace;
				throw new BMXNetException(sMessage);
			}

		}

		public static string Piece(string sInput, string sDelim, int nNumber, int nEnd)
		{
			try 
			{
				if (nNumber < 0)
					nNumber = 1;

				if (nEnd < nNumber)
					return "";

				if (nEnd == nNumber)
					return Piece(sInput, sDelim, nNumber);

				char[] cDelim = sDelim.ToCharArray();
				string [] cSplit = sInput.Split(cDelim);
				int nLength = cSplit.GetLength(0);
				if ((nLength < nNumber)||(nNumber < 1))
					return "";

				//nNumber = 1-based index of the starting element to return
				//nLength = count of elements
				//nEnd = 1-based index of last element to return
				//nCount = number of elements past nNumber to return

				//convert nNumber to 0-based index:
				nNumber--;

				//convert nEnd to 0-based index;
				nEnd--;

				//Calculate nCount;
				int nCount = nEnd - nNumber + 1;

				//Adjust nCount for number of elements
				if (nCount + nNumber >= nLength)
				{
					nCount = nLength - nNumber;
				}

				string sRet =  string.Join(sDelim, cSplit, nNumber, nCount );
				return sRet;
			}
			catch (Exception bmxEx)
			{
				string sMessage =  bmxEx.Message + bmxEx.StackTrace;
				throw new BMXNetException(sMessage);
			}
		}

		#endregion Piece Functions

		#region RPX Fields

		private string		m_sWKID;
		private string		m_sWISH;
		private string		m_sPRCH;
		private string		m_sWINH;
		private string		m_cHDR;
		private string		m_cAppContext;
		private bool		m_bConnected;
		private int			m_nMServerPort;
		private string		m_cServerAddress;
		private string		m_cDUZ;
		private string		m_cLoginFacility;
		private TcpClient	m_pCommSocket;
		private	string		m_sNameSpace = "";
        private int         m_nReceiveTimeout = 30000;

		#endregion RPX Fields



		/// <summary>
		/// Returns index of first instance of sSubString in sString.
		/// If sSubString not found, returns -1.
		/// </summary>
		/// <param name="sString"></param>
		/// <param name="sSubString"></param>
		/// <returns></returns>


		protected virtual void OpenConnectionCommon(string sServerAddress)
		{
			try
			{
				m_cServerAddress = sServerAddress;

				//Connect with the server
				TcpClient connector = new TcpClient();

				try 
				{
					connector = new TcpClient();
					connector.Connect(m_cServerAddress, m_nMServerPort);		
				}
				catch (SocketException exSocket)
				{
					string s = exSocket.Message + exSocket.StackTrace;
					throw new BMXNetException(s);
				}				
			
				//Prepare & send the connect message
				string cSend = "TCPconnect^" + m_sNameSpace + "^^";
				int nLen = cSend.Length;
				string sLen = nLen.ToString();
				sLen = sLen.PadLeft(5, '0');
				cSend = "{BMX}" + sLen + cSend;

				NetworkStream ns = connector.GetStream();
				byte[] sendBytes = Encoding.ASCII.GetBytes(cSend);
				ns.Write(sendBytes,0,sendBytes.Length);

				m_pCommSocket = connector;
				return;

			}
			catch (BMXNetException bmxEx)
			{
				throw bmxEx;
			}
			catch (Exception ex)
			{
				string s = ex.Message + ex.StackTrace;
				throw new BMXNetException(s);
			}		
		}//End OpenConnectionCommon

		[SocketPermissionAttribute(SecurityAction.Assert, 
			 Access="Connect",
			 Host="All",
			 Port="All",
			 Transport="All")]
		public virtual bool OpenConnection(string sServerAddress, WindowsIdentity winIdentity)
		{
			try
			{
				OpenConnectionCommon(sServerAddress);
				bool bSecurity;
				try
				{
					bSecurity = SendSecurityRequest(winIdentity);
					
				}
				catch (Exception ex)
				{
					//Close the connection
					SendString(m_pCommSocket, "#BYE#");
					m_pCommSocket.Close();
					m_bConnected = false;
					m_cServerAddress = "";
					throw ex;
				}
				
				m_bConnected = bSecurity;
				return m_bConnected;
			}
			catch (BMXNetException bmxEx)
			{
				throw bmxEx;
			}
			catch (Exception ex)
			{
				string s = ex.Message + ex.StackTrace;
				throw new BMXNetException(s);
			}
		}

        StreamWriter m_LogWriter;
        bool m_bLogging = false;

        public void StartLog()
        {
            try
            {
                if (m_bLogging)
                {
                    throw new Exception("Already logging.");
                }
                string sFile = "BMXLog " + DateTime.Now.DayOfYear.ToString() + " " +
                     DateTime.Now.Hour.ToString() + " " + DateTime.Now.Minute.ToString()
                     + " " + DateTime.Now.Second.ToString() + ".log";
                StartLog(sFile);
                return;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public void StartLog(string LogFileName)
        {
            try
            {
                if (m_bLogging)
                {
                    throw new Exception("Already logging.");
                }
                m_LogWriter = File.AppendText(LogFileName);
                m_bLogging = true;
                return;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public void StopLog()
        {
            try
            {
                //Close the writer and underlying file.
                if (m_bLogging == false)
                {
                    return;
                } 
                m_LogWriter.Close();
                m_bLogging = false;
                return;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        private static void Log(String logMessage, TextWriter w)
        {
            w.WriteLine("{0} {1}", DateTime.Now.ToLongTimeString(),
                DateTime.Now.ToLongDateString());
            w.WriteLine("  :");
            w.WriteLine("  :{0}", logMessage);
            w.WriteLine("-------------------------------");
            // Update the underlying file.
            w.Flush();
        }

		[SocketPermissionAttribute(SecurityAction.Assert, 
			 Access="Connect",
			 Host="All",
			 Port="All",
			 Transport="All")]
		public bool OpenConnection(string sServerAddress, string sAccess, string sVerify)
		{
			try
			{
				this.OpenConnectionCommon(sServerAddress);

				try
				{
					bool bSecurity = SendSecurityRequest(sAccess, sVerify);
				}
				catch (Exception ex)
				{
					//Close the connection
					SendString(m_pCommSocket, "#BYE#");
					m_pCommSocket.Close();
					m_bConnected = false;
					m_cServerAddress = "";
                    throw ex;
				}
				
				m_bConnected = true;
				return m_bConnected;
			}
			catch (BMXNetException bmxEx)
			{
				throw bmxEx;
			}
			catch (Exception ex)
			{
				string s = ex.Message + ex.StackTrace;
				throw new BMXNetException(s);
			}
		}

        public override bool IsConnected
        {
            get
            {
                return this.m_bConnected;
            }
        }

     
        protected override String SendReceiveString(string cSendString, string cMult)
        {
            this.SendString(this.m_pCommSocket, cSendString, cMult);
            return this.ReceiveString(this.m_pCommSocket);
        }


		protected virtual void SendString(TcpClient tcpClient, string cSendString)
		{
			string sMult = "";
			SendString(tcpClient, cSendString, sMult);
		}

        protected virtual void SendString(TcpClient tcpClient, string cSendString, string cMult)
		{
            String encodedString = this.EncodeSendString(cSendString, cMult);
		
			NetworkStream ns = tcpClient.GetStream();
			byte[] sendBytes = Encoding.ASCII.GetBytes(encodedString);
			ns.Write(sendBytes,0,sendBytes.Length);
            if (this.m_bLogging == true)
            {
                Log("Sent: " + cSendString, this.m_LogWriter);
            }
			return;
		}

 
		private string ReceiveString(TcpClient tcpClient)
		{
			NetworkStream ns = tcpClient.GetStream();

            int nTimeOut = this.m_nReceiveTimeout;
			int nCnt = 0;
            int nTimeElapsed = 0;
			while (ns.DataAvailable == false)
			{
				if (nCnt > 9999)
					break;
                if (nTimeElapsed > nTimeOut)
                    break;
				nCnt++;
                nTimeElapsed += 50;
				Thread.Sleep(50);
			}

			Debug.Assert(ns.DataAvailable == true);
			if (ns.DataAvailable == false)
			{
                this.CloseConnection();
				throw new Exception("BMXNetLib.ReceiveString timeout.  Connection Closed.");
				//return "";
			}

			byte[] bReadBuffer = new byte[1024];
			string sReadBuffer = "";
			StringBuilder sbAll = new StringBuilder("", 1024);
			int numberOfBytesRead = 0;

			// Incoming message may be larger than the buffer size.

			bool bFinished = false;
			int nFind = -1;
			bool bStarted = false;
			int lpBuf = 0;
			string sError = "";
			string sAppError = "";
			do
			{

				numberOfBytesRead = ns.Read(bReadBuffer, 0, bReadBuffer.Length); 
				if ((numberOfBytesRead == 1)&&(bStarted == false))
				{
					Thread.Sleep(15);
					numberOfBytesRead += ns.Read(bReadBuffer,1, bReadBuffer.Length-1); 
					//Debug.Write("ReceiveString waiting for data...\n");
				}
				if (bStarted == false)
				{
					//Process error info at beginning of returned string
					int nErrLen = bReadBuffer[0];
					int nAppLen = bReadBuffer[bReadBuffer[0]+1];
					if ((bReadBuffer[2] == 0)&&(bReadBuffer[3] == 0)) 
					{ //special case: M error trap invoked in SND^XWBTCPC
						lpBuf += 2;
					}
					sError = Encoding.ASCII.GetString(bReadBuffer, lpBuf + 1, nErrLen);
					if (sError != "")
					{
						throw new BMXNetException(sError);
					}
					sAppError = Encoding.ASCII.GetString(bReadBuffer, lpBuf+1+nErrLen+1, nAppLen);
					lpBuf += (nErrLen + nAppLen + 2);
					numberOfBytesRead -= (nErrLen + nAppLen + 2);
					bStarted = true;
				}

				nFind = FindChar(bReadBuffer, (char) 4);
				if (nFind > -1)
					bFinished = true;
				Debug.Assert(numberOfBytesRead > -1);
				sReadBuffer = Encoding.ASCII.GetString(bReadBuffer, lpBuf, numberOfBytesRead);
				lpBuf = 0;
				if (nFind > -1)
				{
					sbAll.Append(sReadBuffer, 0, numberOfBytesRead -1);
				}
				else 
				{
					sbAll.Append(sReadBuffer);
				}
			}
			while(!bFinished);

            String decodedReceiveString = this.DecodeReceiveString(sbAll.ToString());

            if (this.m_bLogging)
            {
                Log("Received: " + decodedReceiveString, this.m_LogWriter);
            }
            return decodedReceiveString;
			
		}
		private bool SendSecurityRequest(WindowsIdentity winIdentity)
		{
			string		strReceive = "";
			string cMSG;
			string sTest;
		
			//Build AV Call
			cMSG = ADEBLDMsg(m_cHDR, "BMX AV CODE", winIdentity.Name);
			SendString(m_pCommSocket, cMSG);

			strReceive = ReceiveString(m_pCommSocket);
			sTest = strReceive.Substring(0,3);


			char[] cDelim = {(char) 13,(char) 10,(char) 0};
			string sDelim = new string(cDelim);
			int nPiece = 1;
			m_cDUZ = Piece(strReceive, sDelim , nPiece);
			if ((m_cDUZ == "0")||(m_cDUZ == ""))
			{
				nPiece = 7;
				string sReason = Piece(strReceive, sDelim, nPiece);
				throw new Exception(sReason);
			}

			return true;		
		}

		private bool SendSecurityRequest(string sAccess, string sVerify)
		{
			string		strReceive = "";
			string		cMSG;
			sAccess = sAccess.ToUpper();
			sVerify = sVerify.ToUpper();
		
			//Build AV Call
			string strAV = sAccess + ";" + sVerify;
			strAV = ADEEncryp(strAV);
			cMSG = ADEBLDMsg(m_cHDR, "XUS AV CODE", strAV);
			SendString(m_pCommSocket, cMSG);

			strReceive = ReceiveString(m_pCommSocket);

			char[] cDelim = {(char) 13,(char) 10,(char) 0};
			string sDelim = new string(cDelim);
			int nPiece = 1;
			this.DUZ = Piece(strReceive, sDelim , nPiece);

            this.UserName = sAccess;

            if ((this.DUZ == "0") || (this.DUZ == ""))
			{
				nPiece = 7;
				string sReason = Piece(strReceive, sDelim, nPiece);
				throw new Exception(sReason);
			}
			
			return true;		
		}

		public override void CloseConnection()
		{
			if (!m_bConnected) 
			{
				return;
			}
			SendString(m_pCommSocket, "#BYE#");
			m_pCommSocket.Close();
			m_bConnected = false;
			m_cServerAddress = "";
			//			m_cDUZ2 = "";
            this.DUZ = "";
		}

		public bool Lock(string Variable)
		{
			return Lock(Variable, "", "");
		}

		public bool Lock(string Variable, string Increment)
		{
			return Lock(Variable, Increment, "");
		}

		/// <summary>
		/// Lock a local or global M variable
		/// Returns true if lock is obtained during TimeOut seconds
		/// Use + to increment, - to decrement lock.
		/// </summary>
		/// <param name="Variable"></param>
		/// <param name="Increment"></param>
		/// <param name="TimeOut"></param>
		/// <returns></returns>
		public bool Lock(string Variable, string Increment, string TimeOut)
		{
			try
			{
				string sContext = this.AppContext;
				this.AppContext = "BMXRPC";
				Variable = Variable.Replace("^","~");
				string sRet = "0";
				bool bRet = false;
				string sParam = Variable + "^" + Increment + "^" + TimeOut;
				sRet = TransmitRPC("BMX LOCK", sParam);
				bRet = (sRet == "1")?true:false;
				this.AppContext = sContext;
				return bRet;
			}
			catch (Exception ex)
			{
				string sMsg = ex.Message;
				return false;
			}
		}
		
		static ReaderWriterLock			m_rwl = new ReaderWriterLock();
		private int m_nRWLTimeout = 30000; //30-second default timeout

		/// <summary>
		/// Returns a reference to the internal ReaderWriterLock member.
		/// </summary>
		public ReaderWriterLock BMXRWL
		{
			get
			{
				return m_rwl;
			}
		}

		/// <summary>
		/// Sets and returns the timeout in milliseconds for locking the transmit port.
		/// If the transmit port is unavailable an ApplicationException will be thrown.
		/// </summary>
		public int RWLTimeout
		{
			get
			{
				return m_nRWLTimeout;
			}
			set
			{
				m_nRWLTimeout = value;
			}
		}

   

		public virtual string TransmitRPC(string sRPC, string sParam, int nLockTimeOut)
		{
			try
			{
				try
				{
					if (m_bConnected == false) 
					{
						throw new BMXNetException("BMXNetLib.TransmitRPC failed because BMXNetLib is not connected to RPMS.");
					}
					Debug.Assert(m_cDUZ != "");
					Debug.Assert(m_pCommSocket != null);

					string sOldAppContext = "";
					if (sRPC.StartsWith("BMX")&&(this.m_cAppContext != "BMXRPC"))
					{
						sOldAppContext  = this.m_cAppContext;
						this.AppContext = "BMXRPC";
					}
					string sMult = "";
					string sSend = ADEBLDMsg(m_cHDR, sRPC, sParam, ref sMult);
                    string strResult = SendReceiveString(sSend,sMult);			
					//Debug.Write("TransmitRPC Received: " + strResult + "\n");

					if (sOldAppContext != "")
					{
						this.AppContext = sOldAppContext;
					}
					return strResult;				
				}
				catch (Exception ex)
				{
					if (ex.Message == "Unable to write data to the transport connection.")
					{
						m_bConnected = false;
					}
					throw ex;
				}
				finally
				{
				}			
			}
			catch (ApplicationException aex)
			{
				// The writer lock request timed out.
				Debug.Write("TransmitRPC writer lock request timed out.\n");
				throw aex;
			}
			catch (Exception OuterEx)
			{
				throw OuterEx;
			}
		}

		public string TransmitRPC(string sRPC, string sParam)
		{
			try
			{
				return TransmitRPC(sRPC, sParam, m_nRWLTimeout);
			}
			catch (ApplicationException aex)
			{
				throw aex;
			}
			catch (Exception ex)
			{
				throw ex;
			}
		}

		public override string GetLoginFacility()
		{
			try
			{
				if (m_bConnected == false) 
				{
					throw new BMXNetException("BMXNetLib is not connected to RPMS");
				}

				if (m_cLoginFacility != "") 
				{
					return m_cLoginFacility;
				}

				Debug.Assert(m_pCommSocket != null);
				Debug.Assert(m_cDUZ != "");
				SendString(m_pCommSocket, ADEBLDMsg(m_cHDR, "BMXGetFac", m_cDUZ));
				string sFac = ReceiveString(m_pCommSocket);
				m_cLoginFacility = sFac;
				return sFac;
			}
			catch (BMXNetException bmxEx)
			{
				string sMessage =  bmxEx.Message + bmxEx.StackTrace;
				throw new BMXNetException(sMessage);
			}
			catch (Exception ex)
			{
				throw ex;
			}
		}



		#region RPX Properties

        /// <summary>
        /// Set and retrieve the timeout, in milliseconds, to receive a response from the RPMS server.
        /// If the retrieve time exceeds the timeout, an exception will be thrown and the connection will be closed.
        /// The default is 30 seconds.
        /// </summary>
        public int ReceiveTimeout
        {
            get { return m_nReceiveTimeout; }
            set { m_nReceiveTimeout = value; }
        }

		public string WKID
		{
			get
			{
				return m_sWKID;
			}
			set
			{
				m_sWKID = value;
			}
		}

		public string PRCH
		{
			get
			{
				return m_sPRCH;
			}
			set
			{
				m_sPRCH = value;
			}
		}

		public string WINH
		{
			get
			{
				return m_sWINH;
			}
			set
			{
				m_sWINH = value;
			}
		}

		public string WISH
		{
			get
			{
				return m_sWISH;
			}
			set
			{
				m_sWISH = value;
			}
		}

		/// <summary>
		/// Gets/sets the Kernel Application context
		/// Throws an exception if unable to set the context. 
		/// </summary>
		public string AppContext
		{
			get
			{
				return m_cAppContext;
			}
			set
			{
				//Send the changed context to RPMS
				if ((m_bConnected == true) && (value != ""))
				{
					try
					{
						string sRPC = ADEEncryp(value);
						string sAuthentication = TransmitRPC("XWB CREATE CONTEXT", sRPC);
						
						if (BMXNetLib.FindSubString(sAuthentication, "does not have access to option") > -1)
						{
							throw new BMXNetException(sAuthentication);
						}

					}
					catch (Exception ex)
					{
						Debug.Write(ex.Message);
						throw ex;
					}
				}
				m_cAppContext = value;
			}
		}

		public bool Connected
		{
			get
			{
				return m_bConnected;
			}
		}
	

		public string MServerAddress
		{
			get
			{
				return m_cServerAddress;
			}
		}

		public string MServerNamespace
		{
			get
			{
                return m_sNameSpace;
			}
            set
            {
                m_sNameSpace = value;
            }
		}

		public int MServerPort
		{
			get
			{
				return m_nMServerPort;
			}
			set
			{
				m_nMServerPort = value;
			}
		}

		public string NameSpace
		{
			get
			{
				return m_sNameSpace;
			}
			set
			{
				m_sNameSpace = value;
			}
		}

		#endregion RPX Properties

	}
}
