﻿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;
using System.Linq;

namespace IndianHealthService.BMXNet.Net
{
	/// <summary>
	/// BMXNetBroker implements low-level socket connectivity to RPMS databases.
	/// The VA RPC Broker must be running on the RPMS server in order for 
	/// BMXNetBroker to connect.
	/// </summary>
	[DnsPermission(SecurityAction.Assert, Unrestricted = true)]
    internal class BMXNetSessionSocketConnection : BMXNetSessionConnection
	{
        public static int DefaultSendTimeout = 20000;
        public static int DefaultReceiveTimeout = 40000;
        
        
        private int _sendTimeout = 0;

        public override int SendTimeout
        {
            get
            {
                if (_sendTimeout != 0)
                {
                    return _sendTimeout;
                }
                return this.ConnectionSpec.SendTimeout == 0 ? DefaultSendTimeout : this.ConnectionSpec.SendTimeout;
            }
            set { _sendTimeout = value; }
        }
    

        private int _receiveTimeout = 0;

        /// <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 override int ReceiveTimeout
        {
            get
            {
                if (_receiveTimeout != 0)
                {
                    return _receiveTimeout;
                }
                return this.ConnectionSpec.ReceiveTimeout == 0 ? DefaultReceiveTimeout : this.ConnectionSpec.ReceiveTimeout;
            }
            set
            {
                _receiveTimeout = value;
            }
        }

        public override Encoding ConnectionEncoding // Default Encoding determined by Windows; typically a Windows Code Page
        {
            get;
            set;
        }
            

		public BMXNetSessionSocketConnection(BMXNetBroker aBroker):base(aBroker)
		{
			m_sWKID = "BMX";
			m_sWINH = "";
			m_sPRCH = "";
			m_sWISH = "";
			m_cHDR = ADEBHDR(m_sWKID,m_sWINH,m_sPRCH,m_sWISH);

            ConnectionEncoding = Encoding.Default;

		}



		#region RPX Fields

		private string		m_sWKID;
		private string		m_sWISH;
		private string		m_sPRCH;
		private string		m_sWINH;
		private string		m_cHDR;
		private bool		m_bConnected;
		private TcpClient	m_pCommSocket;
		private	string		m_sNameSpace = "";
        
		#endregion RPX Fields


        public TcpClient Socket
        {
            get { return this.m_pCommSocket; }
        }

		/// <summary>
		/// Returns index of first instance of sSubString in sString.
		/// If sSubString not found, returns -1.
		/// </summary>
		/// <returns></returns>

        [SocketPermissionAttribute(SecurityAction.Assert,
     Access = "Connect",
     Host = "All",
     Port = "All",
     Transport = "All")]
        protected virtual void OpenConnectionCommon()
		{
			try
			{
                TcpClient connector = null; 

				try 
				{
					connector = new TcpClient();
                    connector.SendTimeout = this.SendTimeout;
                    connector.ReceiveTimeout = this.ReceiveTimeout;
					connector.Connect(this.ServerAddress, this.ServerPort);	
				}
				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


        private BMXNetSocketConnectionSpec _connectionSpec = null;

        internal BMXNetSocketConnectionSpec ConnectionSpec
        {
            get { return _connectionSpec; }
            set { _connectionSpec = value; }
        }


        [SocketPermissionAttribute(SecurityAction.Assert,
             Access = "Connect",
             Host = "All",
             Port = "All",
             Transport = "All")]
        public virtual bool OpenConnection(BMXNetSocketConnectionSpec aSpec)
        {
            this.ConnectionSpec = aSpec;

            try
            {
                m_bConnected = false;
                this.OpenConnectionCommon();
                if (this.CheckConnection())
                {
                    bool authenicated = false;

                    if (aSpec.UseWindowsAuthentication)
                    {
                        authenicated = this.SendSecurityRequest(aSpec.WindowsIdentity);
                    }
                    else
                    {
                        authenicated = SendSecurityRequest(aSpec.EncryptedAccessVerifyCode);
                    }

                    this.m_bConnected = authenicated;



                    int integerDuz = 0;
                    if (!int.TryParse(this.DUZ, out integerDuz))
                    {
                        throw new BMXNetException("Invalid DUZ, Unable to authenticate user.");
                    }

                    this.UserName = this.GetUserName();
                }
                return this.IsConnected;
            }          
            catch (BMXNetException bmxEx)
            {
                throw bmxEx;
            }
            catch (Exception ex)
            {
                string s = ex.Message + ex.StackTrace;
                throw new BMXNetException(s);
            }
            finally
            {
                if (!this.IsConnected)
                {
                    this.PrimitiveCloseConnection();
                }
            }
        }

        bool m_bLogging = false;

        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();
        }

        private bool CheckConnection()
        {
            try
            {
                String cMSG = ADEBLDMsg(m_cHDR, "BMX CONNECT STATUS", "");
#if DEBUG
                _watch = new Stopwatch();
                _watch.Start();
#endif
                this.SendString(this.Socket, cMSG);
                String strReceive = ReceiveString(this.Socket);
#if DEBUG
                _watch.Stop();

                Debug.WriteLine("Time: " + _watch.ElapsedMilliseconds + " ms");
                Debug.WriteLine("---");
                _watch = null;
#endif

                String port = BMXNetBroker.Piece(strReceive, "|", 1);
                String message = BMXNetBroker.Piece(strReceive, "|", 2);

                if (!message.Contains("OK"))
                {
                    throw new BMXNetException(message.Length==0 ? strReceive : message);
                }

                this.Job = BMXNetBroker.Piece(strReceive, "|", 3);

                return true;
            }
            catch (Exception potentialProblem)
            {   //If there's an issue, assume old BMX 2.x/3.x so this feature is not supports BMX CONNECT STATUS
                if (potentialProblem.Message.Contains("BMX CONNECT STATUS"))
                {
                    return true;
                }
                else
                {
                    throw potentialProblem;
                }
            }
        }

        public override bool IsConnected
        {
            get
            {
                return this.m_bConnected;
            }
        }

        Stopwatch _watch = new Stopwatch();

        protected override String SendReceiveString(string cSendString, string cMult)
        {
#if DEBUG
            _watch = new Stopwatch();
            _watch.Start();
#endif
            this.SendString(this.m_pCommSocket, cSendString, cMult);
            Debug.WriteLine("Time After Send: " + _watch.ElapsedMilliseconds + " ms");
            string _received = this.ReceiveString(this.m_pCommSocket);
#if DEBUG
            _watch.Stop();

            Debug.WriteLine("Time: " + _watch.ElapsedMilliseconds + " ms");
            Debug.WriteLine("---");
            _watch = null;
#endif
            return _received;
        }


		protected virtual void SendString(TcpClient tcpClient, string cSendString)
		{
			string sMult = "";
			SendString(tcpClient, cSendString, sMult);
		}

        protected virtual void SendString(TcpClient tcpClient, string cSendString, string cMult)
		{
#if DEBUG
            Debug.WriteLine("Sending (T:" + Thread.CurrentThread.ManagedThreadId + "): " + cSendString);
#endif
            String encodedString = this.EncodeSendString(cSendString, cMult);
		
			NetworkStream ns = tcpClient.GetStream();
            ns.WriteTimeout = this.SendTimeout;
			byte[] sendBytes = ConnectionEncoding.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)
		{
#if DEBUG
            Debug.WriteLine("Time At Start of Receive: " + _watch.ElapsedMilliseconds + " ms");
#endif

            /******************INIT*****************/
            NetworkStream ns = tcpClient.GetStream();
            ns.ReadTimeout = this.ReceiveTimeout; //Timeout; throw exception automatically if we time out.

            /*************SECURITY S-PACKET READ***********************/
            int secPackLen = ns.ReadByte();  //Read S-Pack Length Byte
            byte[] secPackContents = new byte[secPackLen];;
            if (secPackLen > 0)
            {
                ns.Read(secPackContents, 0, secPackLen);
            }

            /****************ERROR S-PACKET READ******************/
            int errPackLen = ns.ReadByte(); //Read S-Pack Length Byte
            byte[] errPackConents = new byte[errPackLen];;
            if (errPackLen > 0)
            {
                ns.Read(errPackConents, 0, errPackLen);
            }

            //If either one of these exists, then we have an error on the Mumps Database
            if (secPackLen > 0 || errPackLen > 0)
            {
                //We still have to read the Data to empty the tcp OS buffers even if we have security/errors in the DB
                while (ns.DataAvailable) ns.ReadByte() ;  //while loop to empty the buffer -- this is theoretically slow, but that's not important here

                //Now decode them.
                string secString = ConnectionEncoding.GetString(secPackContents);
                string errString = ConnectionEncoding.GetString(errPackConents);
                throw new BMXNetException("Mumps Database Security/Error: " + secString + " | " + errString);
            }

            /*****************DATA STREAM READ*******************/
            //Data stream from VISTA ends when we find the EOT (ASCII 4) character
            MemoryStream mStream = new MemoryStream(1500); // Auto expanding memory storage for what we receive
            int numberOfBytesRead = 0;
            bool bFinished = false;  // Have we found the End of Transmission (ASCII 4) yet?

            // Main Read Loop
            while (!bFinished) //while we are not done
            {
                byte[] bReadBuffer = new byte[1500];        //buffer size = MTU. Message MUST be smaller.
                numberOfBytesRead = ns.Read(bReadBuffer, 0, bReadBuffer.Length);    //read data
                bFinished = FindChar(bReadBuffer, (char)4) > -1;                    //look for the EOT (ASCII 4) character
                if (!bFinished) mStream.Write(bReadBuffer, 0, numberOfBytesRead);  //if we are not done, put the whole stream in
                else mStream.Write(bReadBuffer, 0, numberOfBytesRead - 1);           //otherwise, number of bytes minus the $C(4) at the end
            }

            //At this point, we are done reading from the Network Stream. Now we have to decode the data.
            
            //Decode the entire message.
            string sReadBuffer = ConnectionEncoding.GetString(mStream.ToArray()); //decode

            String decodedReceiveString = this.DecodeReceiveString(sReadBuffer);

            if (this.m_bLogging)
            {
                Log("Received: " + decodedReceiveString, this.m_LogWriter);
            }
#if DEBUG
            Debug.WriteLine("Time At End of Receive: " + _watch.ElapsedMilliseconds + " ms");
            Debug.WriteLine("Received(T:" + Thread.CurrentThread.ManagedThreadId + "): " + decodedReceiveString.Replace((char)30, (char)10));
#endif
            return decodedReceiveString;
            

            /* OLD CODE
            NetworkStream ns = tcpClient.GetStream();
            ns.ReadTimeout = this.ReceiveTimeout;

            
            //TAE: This following is suspect. NetworkSTream Read and ReadTimeout provide
            //the same behavior.  Look at removing in the futuer.  For now, this works.
            int cyclePause = 25;
			int cycles = 0;
            DateTime start = DateTime.Now;
			while (ns.DataAvailable == false)
			{
                if (cycles++ > 999)
					break;
                if ((DateTime.Now-start).TotalMilliseconds > this.ReceiveTimeout)
                    break;
				Thread.Sleep(cyclePause);
			}

			Debug.Assert(ns.DataAvailable);
			if (!ns.DataAvailable)
			{
                this.Close();
				throw new Exception("BMXNetBroker.ReceiveString timeout.  Connection Closed.");
			}

			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;
			bool bStarted = false;
			int lpBuf = 0;
			string sError = "";
			string sAppError = "";
			do
			{

				numberOfBytesRead = ns.Read(bReadBuffer, 0, bReadBuffer.Length); 
				if ((numberOfBytesRead == 1)&&(bStarted == false))
				{
                    //TAE: This following is suspect. If Read is blocking then this sleep is extra.
                    //This is rarely called
                    Thread.Sleep(15);
					numberOfBytesRead += ns.Read(bReadBuffer,1, bReadBuffer.Length-1); 
				}
				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 != "")
					{
                        sAppError = Encoding.ASCII.GetString(bReadBuffer, lpBuf + 1 + nErrLen + 1, nAppLen);
                        throw new BMXNetException(sError);
					}
					sAppError = Encoding.ASCII.GetString(bReadBuffer, lpBuf+1+nErrLen+1, nAppLen);
					lpBuf += (nErrLen + nAppLen + 2);
					numberOfBytesRead -= (nErrLen + nAppLen + 2);
					bStarted = true;
				}

                bFinished = FindChar(bReadBuffer, (char)4) > -1;
            	Debug.Assert(numberOfBytesRead > -1);
				sReadBuffer = Encoding.ASCII.GetString(bReadBuffer, lpBuf, numberOfBytesRead);
				lpBuf = 0;
                if (bFinished)
				{
					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);
            }
#if DEBUG
            Debug.WriteLine("Time At End of Receive: " + _watch.ElapsedMilliseconds + " ms");
#endif
            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);
#if DEBUG
            _watch = new Stopwatch();
            _watch.Start();
#endif
            SendString(this.Socket, cMSG);
            strReceive = ReceiveString(this.Socket);
#if DEBUG
            _watch.Stop();

            Debug.WriteLine("Time: " + _watch.ElapsedMilliseconds + " ms");
            Debug.WriteLine("---");
            _watch = null;
#endif

			sTest = strReceive.Substring(0,3);

			char[] cDelim = {(char) 13,(char) 10,(char) 0};
			string sDelim = new string(cDelim);
			int nPiece = 1;
			this.DUZ = M.Piece(strReceive, sDelim , nPiece);
            if ((this.DUZ.Length==0 ) || ("0".Equals(this.DUZ)))
			{
				nPiece = 7;
				string sReason = M.Piece(strReceive, sDelim, nPiece);
				throw new Exception(sReason);
			}

			return true;		
		}

        public String EncryptAccessVerifyCode(string anAccessCode, string aVerifyCode)
        {
            string accessVerifyPair = anAccessCode.ToUpper() + ";" + aVerifyCode.ToUpper();
            return this.EncryptionProvider.Encrypt(accessVerifyPair);
        }

		private bool SendSecurityRequest(string encryptedAccessVerifyCode)
		{
			string		strReceive = "";
			string		cMSG;
		
			cMSG = ADEBLDMsg(m_cHDR, "XUS AV CODE", encryptedAccessVerifyCode);
#if DEBUG
            _watch = new Stopwatch();
            _watch.Start();
#endif      
            SendString(this.Socket, cMSG);
            strReceive = ReceiveString(this.Socket);
#if DEBUG
            _watch.Stop();

            Debug.WriteLine("Time: " + _watch.ElapsedMilliseconds + " ms");
            Debug.WriteLine("---");
            _watch = null;
#endif

            if (strReceive.StartsWith("M ERROR="))
            {
                this.DUZ = "0";
                throw new BMXNetException("Server error has occured: " + strReceive);
            }


			char[] cDelim = {(char) 13,(char) 10,(char) 0};
			string U = new string(cDelim);
			
            this.DUZ = M.Piece(strReceive,U, 1);
            String resultCode = M.Piece(strReceive, U, 2);
            String resultMessageIndex = M.Piece(strReceive, U, 3);
//R(0)=DUZ if sign-on was OK, zero if not OK.
 //R(1)=(0=OK, 1,2...=Can't sign-on for some reason).
 //R(2)=verify needs changing.
 //R(3)=Message.
 //R(4)=0
 //R(5)=count of the number of lines of text, zero if none.

            //extracall but same code-path as winidentity login
          
            if ((this.DUZ == "0") || (this.DUZ == ""))
			{
				string sReason = M.Piece(strReceive, U, 7);
				throw new BMXNetException(sReason);
			}
			
			return true;		
		}


		public override void Close()
		{
			if (m_bConnected) 
			{
				this.PrimitiveCloseConnection();
			}
        }

        protected void PrimitiveCloseConnection() {

            if (this.Socket != null)
            {
                SendString(this.Socket, "#BYE#");
                this.Socket.Close();
                m_pCommSocket = null;
            }
            this.ConnectionSpec = null;
			this.DUZ = "";
            m_bConnected=false;
		}
	

		public override string GetLoginFacility(String aDuz)
		{
			try
			{
				if (!this.IsConnected) 
				{
					throw new BMXNetException("BMXNetBroker is not connected to RPMS");
				}
#if DEBUG
                _watch = new Stopwatch();
                _watch.Start();
#endif      
                this.SendString(this.Socket, ADEBLDMsg(m_cHDR, "BMXGetFac", aDuz));
                return this.ReceiveString(this.Socket);
			}
			catch (BMXNetException bmxEx)
			{
				throw new BMXNetException(bmxEx.Message + " || "+bmxEx.StackTrace);
			}
			catch (Exception ex)
			{
				throw ex;
			}
		}


        protected String GetUserName()
        {
            return this.TransmitRPC("BMX USER", this.DUZ);
        }

        /// <summary>
        /// Ping the server, will reset read-timeout
        /// </summary>
        /// <returns>Answer the #milliseconds to run or -1 if theres an issue</returns>
        public double ImHereServer()
        {
            try
            {
                if (this.IsConnected)
                {
                    DateTime past = DateTime.Now;
                    this.TransmitRPC("BMX IM HERE","");
                    return (DateTime.Now - past).TotalMilliseconds;
                }
                else
                {
                    return -1;
                }
            }
            catch
            {
                return -1;
            }
        }

		#region RPX Properties

 

		public bool Connected
		{
			get
			{
				return m_bConnected;
			}
		}
	

		public string ServerAddress
		{
			get
			{
				return this.ConnectionSpec.Server;
			}            
		}

		public string ServerNamespace
		{
			get
			{
                return this.ConnectionSpec.NameSpace;
			}
		}

		public int ServerPort
		{
			get
			{
				return this.ConnectionSpec.Port;
			}		
		}


		#endregion RPX Properties



  
    }

    /*
1 ;;1;Signons not currently allowed on this processor.
2 ;;1;Maximum number of users already signed on to this processor.
3 ;;1;This device has not been defined to the system -- contact system manager.
4 ;;0;Not a valid Windows Identity map value.
5 ;;0;No Access Allowed for this User.
6 ;;0;Invalid device password.
7 ;;0;Device locked due to too many invalid sign-on attempts.
8 ;;1;This device is out of service.
9 ;;0;*** MULTIPLE SIGN-ONS NOT ALLOWED ***
10 ;;1;You don't have access to this device!
11 ;;0;Your access code has been terminated. Please see your site manager!
12 ;;0;VERIFY CODE MUST be changed before continued use.
13 ;;1;This device may only be used outside of this time frame |
14 ;;0;'|' is not a valid UCI!
15 ;;0;'|' is not a valid program name!
16 ;;0;No PRIMARY MENU assigned to user or User is missing KEY to menu!
17 ;;0;Your access to the system is prohibited from |.
18 ;;0;Windows Integrated Security Not Allowed on this port.*/
}
