﻿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 IndianHealthService.BMXNet.Net;
using IndianHealthService.BMXNet.Services;
using IndianHealthService.BMXNet.Model;
using System.Windows.Forms;
 
namespace IndianHealthService.BMXNet
{
    /// <summary>
    /// BMXNetSessionConnection implements low-level connectivity to RPMS databases.
    /// </summary>
    [DnsPermission(SecurityAction.Assert, Unrestricted = true)]
    internal abstract class BMXNetSessionConnection
    {
        public BMXNetSessionConnection(BMXNetBroker aBroker)
        {
            this.Broker = aBroker;
            m_sWKID = "BMX";
            m_sWINH = "";
            m_sPRCH = "";
            m_sWISH = "";
            m_cHDR = ADEBHDR(m_sWKID, m_sWINH, m_sPRCH, m_sWISH); 
        }

        protected BMXNetBroker _broker=null;

        public BMXNetBroker Broker
        {
            get { return _broker; }
            set { _broker=value;}
        }

        protected EncryptionProvider EncryptionProvider
        {
            get
            {
                return this.Broker.EncryptionProvider;
            }
        }


        #region RPX Fields

        private string m_sWKID;
        private string m_sWISH;
        private string m_sPRCH;
        private string m_sWINH;
        private string m_cHDR;
        protected string m_cAppContext;
        private string m_cDUZ;
 
        #endregion RPX Fields

        #region RPX Functions

        /// <summary>
        /// Given strInput = "13" builds "013" if nLength = 3.  Default for nLength is 3.
        /// </summary>
        /// <param name="strInput"></param>
        /// <returns></returns>
        protected string ADEBLDPadString(string strInput)
        {
            return ADEBLDPadString(strInput, 3);
        }

        /// <summary>
        /// Given strInput = "13" builds "013" if nLength = 3  Default for nLength is 3.
        /// </summary>
        /// <param name="strInput"></param>
        /// <param name="nLength">Default = 3</param>
        /// <returns></returns>
        protected string ADEBLDPadString(string strInput, int nLength /*=3*/)
        {
            return strInput.PadLeft(nLength, '0');
        }

        /// <summary>
        /// Concatenates zero-padded length of sInput to sInput
        /// Given "Hello" returns "004Hello"
        /// If nSize = 5, returns "00004Hello"
        /// Default for nSize is 3.
        /// </summary>
        /// <param name="sInput"></param>
        /// <returns></returns>
        protected string ADEBLDB(string sInput)
        {
            return ADEBLDB(sInput, 3);
        }

        /// <summary>
        /// Concatenates zero-padded length of sInput to sInput
        /// Given "Hello" returns "004Hello"
        /// If nSize = 5, returns "00004Hello"
        /// Default for nSize is 3.
        /// </summary>
        /// <param name="sInput"></param>
        /// <param name="nSize"></param>
        /// <returns></returns>
        protected string ADEBLDB(string sInput, int nSize /*=3*/)
        {
            int nLen = sInput.Length;
            string sLen = this.ADEBLDPadString(nLen.ToString(), nSize);
            return sLen + sInput;
        }

        /// <summary>
        /// Build protocol header
        /// </summary>
        /// <param name="sWKID"></param>
        /// <param name="sWINH"></param>
        /// <param name="sPRCH"></param>
        /// <param name="sWISH"></param>
        /// <returns></returns>
        protected string ADEBHDR(string sWKID, string sWINH, string sPRCH, string sWISH)
        {
            string strResult;
            strResult = sWKID + ";" + sWINH + ";" + sPRCH + ";" + sWISH + ";";
            strResult = ADEBLDB(strResult);
            return strResult;
        }
        protected string ADEBLDMsg(string cHDR, string cRPC, string cParam)
        {
            string sMult = "";
            return ADEBLDMsg(cHDR, cRPC, cParam, ref sMult);
        }

        protected string ADEBLDPadString(int anInteger)
        {
            return ADEBLDPadString(anInteger.ToString(), 3);
        }

        protected string ADEBLDPadString(int anInteger, int nLength /*=3*/)
        {
            return this.ADEBLDPadString(anInteger.ToString(), nLength);
        }


        protected string ADEBLDMsg(string cHDR, string cRPC, string cParam, ref string cMult)
        {
            //Builds RPC message
            //Automatically splits parameters longer than 200 into subscripted array
            String body;
            cMult = "";

            if (cParam == "")
            {
                body = "0" + cRPC;
            }
            else
            {
                StringBuilder parametersBuilder = new StringBuilder((int)(cParam.Length * 1.2));
                int parameterCount = M.PieceLength(cParam, "^");
                int maxChunkLength = 400;

                for (int j = 1; j <= parameterCount; j++)
                {
                    String piece = M.Piece(cParam, "^", j);

                    if (piece.Length > maxChunkLength)
                    {
                        if (j == parameterCount)
                        {
                            //Place holder for the long parameter
                            String prefix = ".x";
                            parametersBuilder.Append(ADEBLDPadString(prefix.Length + 1) + "2" + prefix);

                            StringBuilder encodedMultiPart = new StringBuilder((int)(piece.Length * 1.2));
                            int subscript = 1;
                            int startChunk = 0;
                            do
                            {
                                int chunkLength = Math.Min(piece.Length - startChunk, maxChunkLength);
                                String chunk = piece.Substring(startChunk, chunkLength);
                                String subscriptString = subscript.ToString();

                                encodedMultiPart.Append(ADEBLDPadString(subscriptString.Length));
                                encodedMultiPart.Append(subscriptString);
                                encodedMultiPart.Append(ADEBLDPadString(chunk.Length));
                                encodedMultiPart.Append(chunk);

                                subscript++;
                                startChunk += chunkLength;
                            } while (startChunk < piece.Length);
                            cMult = encodedMultiPart.ToString();
                        }
                        else
                        {
                            throw new BMXNetException("RPC parameter exceeds " + maxChunkLength.ToString() + ". Lone long parameter must be last.");
                        }
                    }
                    else
                    {
                        parametersBuilder.Append(ADEBLDPadString(piece.Length + 1) + "0" + piece);
                    }
                }

                String parameters = parametersBuilder.ToString();
                body = (cMult.Length > 0 ? "1" : "0") + cRPC + "^" + ADEBLDPadString(parameters.Length, 5) + parameters;
            }

            return cHDR + ADEBLDB(body, 5);
        }


        public static int FindChar(byte[] c, char y)
        {           
            int n = 0;
            int nRet = -1;
            for (n = 0; n < c.Length; n++)
            {
                if (y == (char)c[n])
                {
                    nRet = n;
                    break;
                }
            }

            return nRet;
        }

        public static int FindChar(string s, char y)
        {
            int n = 0;
            int nRet = -1;
            foreach (char c in s)
            {
                if (y == c)
                {
                    nRet = n;
                    break;
                }
                n++;
            }
            return nRet;
        }


        /// <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>
        public static int FindSubString(string sString, string sSubString)
        {
            int nFound = -1;
            int nLimit = sString.Length - sSubString.Length + 1;
            if (nLimit < 0)
                return -1;

            int nSubLength = sSubString.Length;
            for (int j = 0; j < nLimit; j++)
            {
                if (sString.Substring(j, nSubLength) == sSubString)
                {
                    nFound = j;
                    break;
                }
            }
            return nFound;
        }

    
        protected 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();
        }

   

        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
            {
                Variable = Variable.Replace("^", "~");
                string sParam = Variable + "^" + Increment + "^" + TimeOut;
                string sRet = TransmitRPC("BMX LOCK", sParam);
                return (sRet == "1");
            }
            catch (Exception ex)
            {
                
                string sMsg = ex.Message;
                return false;
            }
        }

        static ReaderWriterLock m_rwl = new ReaderWriterLock();
  

        /// <summary>
        /// Returns a reference to the internal ReaderWriterLock member.
        /// </summary>
        public ReaderWriterLock BMXRWL
        {
            get
            {
                return m_rwl;
            }
        }
   
        public abstract bool IsConnected { get; }

        public abstract void Close();

        protected string EncodeSendString(String cSendString, String cMulti)
        {
            String encoded = null;

            int nLen = cSendString.Length;
            string sLen = nLen.ToString();
            sLen = sLen.PadLeft(5, '0');
            encoded = sLen + cSendString;

            nLen += 15;
            sLen = nLen.ToString();
            sLen = sLen.PadLeft(5, '0');

            encoded = "{BMX}" + sLen + encoded;
            encoded = encoded + cMulti;

            return encoded;
        }

        protected string DecodeReceiveString(String aString)
        {
            return aString;
        }

        protected String SendReceiveString(String sendString)
        {
            return this.SendReceiveString(sendString, "");
        }

        private String _createContextRpc = "BMX CREATE CONTEXT";

        protected String CreateContextRpc
        {
            get { return _createContextRpc; }
            set { _createContextRpc = value; }
        }

        protected abstract String SendReceiveString(String sendString, String multi);

        public abstract int ReceiveTimeout { get; set; }
        public abstract int SendTimeout { get; set; }
        public abstract Encoding ConnectionEncoding { get; set; }


        /// <remarks>
        /// AppContext is managed an client and server with c/s calls to change
        /// appContext on the server.  There is no use for AppContext on the client
        /// so recommend integrated AppContext into BMX Protocol so always passing
        /// it and managing switching it on the server.
        /// </remarks>
        public string TransmitRPC(string anRpc, string rpcParameter)
        {
            this.AssertConnected();


            if (rpcParameter.Length > 32600)
            {
                throw new Exception("RPC parameter length exceeds maximum allowable size.");
            }

            try
            {
                string sMult = "";
                string sSend = ADEBLDMsg(m_cHDR, anRpc, rpcParameter, ref sMult);
                return this.SendReceiveString(sSend, sMult);
            }
            catch (ApplicationException exception)
            {
                // The writer lock request timed out.
                //TODO: Remark: to writer lock for Transmit RPC
                //SMH: Wrong error message. We do have an exception, but nothing to do with a lock.
                //Debug.Write("TransmitRPC writer lock request timed out.\n");
                Debug.Write("Exception: " + exception.Message);
                throw exception;
            }
            catch (Exception problem)
            {
                throw problem;
            }
        }

   
        public abstract string GetLoginFacility(String aDuz);


        #endregion RPX Functions

        protected void AssertConnected()
        {
            if (!this.IsConnected)
            {
                throw new BMXNetException("BMXNetBroker.TransmitRPC failed because BMXNetBroker is not connected to RPMS.");
            }
        }

        private String _job = "";

        public String Job
        {
            get { return _job; }
            set { _job = 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
            {
                if (m_cAppContext == value)
                    return;

                //Send the changed context to RPMS
                if ((value != null) && (value != ""))
                {
                    string encryptedContext = this.EncryptionProvider.Encrypt(value);                     
                    string sAuthentication = this.TransmitRPC("BMX CREATE CONTEXT", encryptedContext);

                    if (BMXNetBroker.FindSubString(sAuthentication, "does not have access to option") > -1)
                    {
                        throw new BMXNetException(sAuthentication);
                    }
                    if (BMXNetBroker.FindSubString(sAuthentication, "is not registered with port") > -1)
                    {
                        throw new BMXNetException(sAuthentication);
                    }
                    m_cAppContext = value;
                }
     
            }
        }

        public string DUZ
        {
            get
            {
                return m_cDUZ;
            }
            set { this.m_cDUZ = value; }
        }

        private EncryptionKeyProvider _keyProvider = null;

        public EncryptionKeyProvider KeyProvider
        {
            get { return _keyProvider; }
            set { _keyProvider = value; }
        }


        protected string RemoveADEEncryp(string sInput)
        {
            //Encrypt a string
            string strResult = null;
            string strPercent = null;
            string strAssoc = null;
            string strIdix = null;
            int nPercent = 0;
            int nAssocix = 0;
            int nIdix = 0;
            Debug.Assert(sInput != "");
            System.Random rRand = new Random(DateTime.Now.Second);

            nPercent = rRand.Next(0, 10000);
            nPercent += 72439;
            nAssocix = nPercent % 20;
            nAssocix++;
            Debug.Assert(nAssocix < 21);
            strPercent = nPercent.ToString();
            strPercent = strPercent.Substring(1, 2);
            nIdix = Convert.ToInt32(strPercent);
            nIdix = nIdix % 20;
            nIdix++;
            Debug.Assert(nIdix < 21);

           Debug.Assert(strAssoc.Length == 94);
           Debug.Assert(strIdix.Length == 94);
            string sEncrypted = "";

            foreach (char c in sInput)
            {
                string d = c.ToString();
                int nFindChar = strIdix.IndexOf(c);
                if (nFindChar > -1)
                {
                    d = strAssoc.Substring(nFindChar, 1);
                }
                sEncrypted += d;
            }

            strResult = (char)(nIdix + 31) + sEncrypted + (char)(nAssocix + 31);

            return strResult;
        }


        protected string RemoveLoadKey(int nID)
        {
            nID -= 102;
            Debug.Assert(nID < 20);
            return this.KeyProvider.Keys[nID];
        }


        internal string RemoveADEDecryp(string sInput)
        {
            //Encrypt a string
            string strAssoc = null;
            string strIdix = null;
            int nAssocix;
            int nIdix;
            Debug.Assert(sInput != "");

            //get associator string index
            char cAssocix = sInput[sInput.Length - 1];
            nAssocix = (int)cAssocix;
            nAssocix -= 31;
            Debug.Assert(nAssocix < 21);

            //get identifier string index
            char cIdix = sInput[0];
            nIdix = (int)cIdix;
            nIdix -= 31;
            Debug.Assert(nIdix < 21);

            //get associator string
            Debug.Assert(strAssoc.Length == 94);
            Debug.Assert(strIdix.Length == 94);

            //translated result
            string sDecrypted = "";
            sInput = sInput.Substring(1, sInput.Length - 2);
            foreach (char c in sInput)
            {
                string d = c.ToString();
                int nFindChar = strAssoc.IndexOf(c);
                if (nFindChar > -1)
                {
                    d = strIdix.Substring(nFindChar, 1);
                }
                sDecrypted += d;
            }

            return sDecrypted;
        }

        internal string RemoveBMXEncrypt(string sInput)
        {

            ASCIIEncoding textConverter = new ASCIIEncoding();
            RijndaelManaged myRijndael = new RijndaelManaged();
            byte[] encrypted;
            byte[] toEncrypt;
            byte[] key;
            byte[] IV;

            string sKey = "pouphfoz sfdbqjuvmbwft qizmphfoz";
            string sIV = "Gichin Funakoshi";
            key = textConverter.GetBytes(sKey);
            IV = textConverter.GetBytes(sIV);

            //Get an encryptor.
            ICryptoTransform encryptor = myRijndael.CreateEncryptor(key, IV);

            //Encrypt the data.
            MemoryStream msEncrypt = new MemoryStream();
            CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write);

            //Convert the input data to a byte array.
            toEncrypt = textConverter.GetBytes(sInput);

            //Write all data to the crypto stream and flush it.
            csEncrypt.Write(toEncrypt, 0, toEncrypt.Length);
            csEncrypt.FlushFinalBlock();

            //Get encrypted array of bytes.
            encrypted = msEncrypt.ToArray();

            //Convert to string to send to RPMS
            string sEncrypted = "";
            byte bTmp;
            string sTmp;
            for (int j = 0; j < encrypted.Length; j++)
            {
                bTmp = encrypted[j];
                sTmp = bTmp.ToString();
                sEncrypted += sTmp;
                if (j < (encrypted.Length - 1))
                    sEncrypted += "~";
            }
            return sEncrypted;
        }

        internal string BMXDecrypt(string sInput)
        {
            try
            {
                byte[] fromEncrypt;
                ASCIIEncoding textConverter = new ASCIIEncoding();
                RijndaelManaged myRijndael = new RijndaelManaged();
                string sRPMSEncrypted = sInput;
                string sBar = "~";
                char[] cBar = sBar.ToCharArray();
                string[] sArray;
                sArray = sRPMSEncrypted.Split(cBar);
                byte[] bRPMSEncrypted = new byte[sArray.GetLength(0)];
                byte[] key;
                byte[] IV;

                //Convert the RPMS-stored string to a byte array
                for (int j = 0; j < sArray.GetLength(0); j++)
                {
                    bRPMSEncrypted[j] = Byte.Parse(sArray[j]);
                }

                //Get a decryptor that uses the same key and IV as the encryptor.
                string sKey = "pouphfoz sfdbqjuvmbwft qizmphfoz";
                string sIV = "Gichin Funakoshi";
                key = textConverter.GetBytes(sKey);
                IV = textConverter.GetBytes(sIV);
                ICryptoTransform decryptor = myRijndael.CreateDecryptor(key, IV);

                MemoryStream msDecrypt = new MemoryStream(bRPMSEncrypted);
                CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);

                fromEncrypt = new byte[bRPMSEncrypted.Length - 2];

                //Read the data out of the crypto stream.
                csDecrypt.Read(fromEncrypt, 0, fromEncrypt.Length);

                int nZ = FindChar(fromEncrypt, (char)0);

                //Convert the byte array back into a string.
                string sResult;
                if (nZ < 0)
                {
                    sResult = textConverter.GetString(fromEncrypt);
                }
                else
                {
                    sResult = textConverter.GetString(fromEncrypt, 0, nZ);
                }
                return sResult;
            }
            catch (Exception ex)
            {
                Debug.Write(ex.Message);
                return "";
            }
        }
 

        private String _userName = null;

        public String UserName
        {
            get {
                if (_userName == null)
                {
                    this._userName = this.TransmitRPC("BMX USER", this.DUZ);
                }
                
                return _userName; }
            set { _userName = value; }
        }      
    }
}
