using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Xml.Linq;
using System.IO;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Data.SqlTypes;

using DevExpress.Utils.DPI;
using DevExpress.Utils.Helpers;
using DevExpress.Data;
using DevExpress.Data.Helpers;
using DevExpress.XtraBars;
using DevExpress.XtraBars.Navigation;

using SystemX.Net;
using SystemX.Common;
using SystemX.Net.BaseProtocol;
using SystemX.Net.Comm;
using SystemX.Net.Schedule;
using SystemX.Net.DB.LogProcess;
using SystemX.Common.Serialization;
using SystemX.Common.Archive;
using SystemX.Net.Platform.Common.Util;
using SystemX.Net.Platform.Common.ExtensionMethods;

//using SystemX.Common.Protocol.SIA;

using static SystemX.Net.Platform.Common.Util.LogMessage;
using static SystemX.Net.DB.LogProcess.XLogDBConnManager;

using SystemX.Net.Middleware.Log.Commons;
using SystemX.Net.Platform.SystemX.Common;
using System.Collections.Concurrent;
using SystemX.Net.Comm.IIS_FTP;
using SystemX.Net.Platform.SystemX.Net.XEFCore.DBContext.CPXV2LogJson;
using SystemX.Net.Platform.SystemX.Net.XEFCore;

namespace SystemX.Net.MiddlewareUI
{
    public partial class MainForm : DevExpress.XtraBars.FluentDesignSystem.FluentDesignForm
    {
        internal class ConnectInfoStore : ICloneable
        {
            /// <summary>
            /// Last Query Info
            /// </summary>
            public int nLastQueryTLStepVersion;
            public int nLastQueryTLFileNumber;
            public int nLastQueryTLVariantNumber;

            public bool bQueryVRFYLoaded;

            public DataSet getLatestStepVer;
            public DataSet geyLatestTestListInfo;

            public DataSet dsLatestVRFYRel;
            public DataSet dsLongTermLatestVRFYRel;

            public object Clone()
            {
                return this;
            }

            public void Initialize()
            {
                nLastQueryTLStepVersion = int.MinValue;
                nLastQueryTLFileNumber = int.MinValue;
                nLastQueryTLVariantNumber = int.MinValue;

                bQueryVRFYLoaded = false;

                getLatestStepVer = null;
                geyLatestTestListInfo = null;

                dsLatestVRFYRel = null;
                dsLongTermLatestVRFYRel = null;
            }

            public void InitVRFYStatus()
            {
                nLastQueryTLStepVersion = int.MinValue;
                nLastQueryTLFileNumber = int.MinValue;
                nLastQueryTLVariantNumber = int.MinValue;

                bQueryVRFYLoaded = false;

                getLatestStepVer = null;
                geyLatestTestListInfo = null;

                dsLatestVRFYRel = null;
                dsLongTermLatestVRFYRel = null;
            }

            public ConnectInfoStore()
            {
                nLastQueryTLStepVersion = int.MinValue;
                nLastQueryTLFileNumber = int.MinValue;
                nLastQueryTLVariantNumber = int.MinValue;

                bQueryVRFYLoaded = false;

                getLatestStepVer = null;
                geyLatestTestListInfo = null;

                dsLatestVRFYRel = null;
                dsLongTermLatestVRFYRel = null;
            }
        }

        private const int SC_CLOSE = 0xF060;
        private const int MF_ENABLED = 0x0;
        private const int MF_GRAYED = 0x1;
        private const int MF_DISABLED = 0x2;

        [DllImport("user32.dll")]
        private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

        [DllImport("user32.dll")]
        private static extern int EnableMenuItem(IntPtr hMenu, int wIDEnableItem, int wEnable);

        private const int MF_BYCOMMAND = 0x00000000;

        [DllImport("user32.dll")]
        public static extern int DeleteMenu(IntPtr hMenu, int nPosition, int wFlags);

        [DllImport("kernel32.dll", ExactSpelling = true)]
        private static extern IntPtr GetConsoleWindow();

        /// <summary>
        /// Base Variable
        /// </summary>
        private const int ALL_MANAGE_NUM = 110;

        private const int PORT_DISTRIBUTION_NUM = 10;

        private COMMON.LogProcessInfo LoadInfo;

        private bool MiddlewareLogStartState { set; get; }

        private bool bShowCpLogProcessTime { set; get; } = false;

        //RecvCommandQueue Task
        private Stopwatch stCheckReleaseTime;

        private Task taskRecvCommandProcess;
        private bool m_bTaskCommandBlock;

        //RecvStreamQueue Task
        private Task taskRecvStreamProcess;
        private bool m_bTaskStreamBlock;
        //
        CancellationTokenSource CTS;
        CancellationToken CT;
        //
        private static bool bTaskCommandWaitLock = false;
        private static bool bTaskStreamWaitLock = false;
        //
        //DataBase
        private XLogDBConnManager MngDBLogConn;

        private ConnectInfoStore[] thisConnInfo;

        public ConcurrentQueue<LogMappedPacket>[] LogDataQueue;

        public ConcurrentQueue<StringBuilder> LogDataRecvText;
        public ConcurrentQueue<StringBuilder> LogDataProcessText;

        private MapLogOption OptionMapLog;

        private CMapParameter ParamterMapLog;
        private CMapParameter ParamterMapInfoLog;

        Int64 nEnqueueCount;
        Int64 nDequeueCount;

        private static object objMappedLogDataInOutLock = new object();

        private static object objMappedLogTextInOutLock = new object();
        private static object objMappedProcessTextInOutLock = new object();

        private bool[] bBeforeLogReadyState;
        private bool[] bCompareLogReadyState;

        // TODO : for Bulk Insert Log
        private Stopwatch stCheckTermBulkWaitTime;

        private List<string> lstBulkTermLogInfo;
        private List<DataRow> lstBulkShortTermLog;
        private List<DataRow> lstBulkLongTermLog;

        private bool bTryBulkInsertBuffer { set; get; } = false;
        private int nBulkLogCount { set; get; } = 0;
        private int nBulkShortTermCount { set; get; } = 0;
        private int nBulkLongTermCount { set; get; } = 0;
        private long nBulkTimeValue { set; get; } = 0;
        private long nFTPTimeValue { set; get; } = 0;
        private bool bDatabaseConnResult { set; get; } = false;

        public object GetConnectInfo(int iPos)
        {
            return thisConnInfo[iPos];
        }

        public XLogDBConnManager GetDBConn()
        {
            return MngDBLogConn;
        }

        /// <summary>
        /// TimerLock
        /// </summary>
        private enum TIMER_LOCK
        {
            CHK_TIMER = 0,
            UI_TIMER = 1,
        }

        private bool[] m_bTimerLock;
        //
        private static object objMainCommandWait = new object();
        private static object objShortTermCommandWait = new object();
        private static object objLongTermCommandWait = new object();

        private static object objMainStreamWait = new object();
        private static object objShortTermStreamWait = new object();
        private static object objLongTermStreamWait = new object();

        private static object objExcuteMainCommandWait = new object();
        private static object objExcuteShortTermCommandWait = new object();
        private static object objExcuteLongTermCommandWait = new object();

        private static object objExcuteMainStreamWait = new object();
        private static object objExcuteShortTermStreamWait = new object();
        private static object objExcuteLongTermStreamWait = new object();
        //
        //private static object objSingleTransactionDataAccessWait = new object();

        private static object objClearServerInstanceWait = new object();

        // TODO : FTP ALIS
        public eFTPServiceStatus StateFTP { set; get; } = eFTPServiceStatus.None;

        private CtrlFTP ControlFTP;

        private Stopwatch stProcessCheckFTPTime;

        // TODO : SHM JSON
        //private XEFCore<CPXV2ShortTermLogJson> CpxShorTermLogJson;
        public MainForm()//(XEFCore<CPXV2ShortTermLogJson> cpxShorTermLogJson)      
        {
            //CpxShorTermLogJson = cpxShorTermLogJson;

            InitializeComponent();

            MessageOutput.ConsoleWrite(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss>>") +
                    @"Use CP-Server[X] Middleware [LOG] Version for [CPXV2][v240529] " +
                    "[SystemX.Net.Middleware.Log : MainForm]", ConsoleColor.Green, LogMessageLevel.FATAL);
            //
            EnableMenuItem(GetSystemMenu(this.Handle, false), SC_CLOSE, MF_GRAYED);

            IntPtr hMenu = GetSystemMenu(GetConsoleWindow(), false);
            //EnableMenuItem(hMenu, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
            DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);

            ConsoleUtil.ConsoleHide();

            MessageOutput.PrintLogLevel = LogMessageLevel.INFO;

            nEnqueueCount = 0;
            nDequeueCount = 0;

            bBeforeLogReadyState = new bool[SharedMemory.nMaxInfoFullAccessSize];
            bCompareLogReadyState = new bool[SharedMemory.nMaxInfoFullAccessSize];

            CTS = new CancellationTokenSource();
            CT = CTS.Token;
            //
            string strExcutePos = Environment.CurrentDirectory;
            string strServerInfoPos = strExcutePos + @"\Configure\LogInformation.xml";
            string strDBInfoPos = strExcutePos + @"\Configure\DBConnInfo.xml";
            string strMapLogOptionPos = strExcutePos + @"\Configure\MapLogOption.xml";

            LoadInfo = new COMMON.LogProcessInfo(strServerInfoPos);

            MiddlewareLogStartState = true;

            OptionMapLog = new MapLogOption(strMapLogOptionPos);

            if (OptionMapLog.Load() == false)
            {
                MiddlewareLogStartState = false;

                MessageOutput.ConsoleWrite(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss>>") + @"Failed to load map-log option information. [SystemX.Net.Middleware.UI.Log : MainForm.MainForm]", ConsoleColor.Yellow, LogMessageLevel.DEBUG);

                return;
            }
            else
            {
                ParamterMapLog = new CMapParameter();
                ParamterMapInfoLog = new CMapParameter();

                ParamterMapLog.strSetEnterMutexName = OptionMapLog.strMapLogEnterMutexName;
                ParamterMapLog.strSetAccessMutexName = OptionMapLog.strMapLogAccessMutexName;
                ParamterMapLog.strSetPath = OptionMapLog.strMapLogPath;
                ParamterMapLog.strSetFileName = OptionMapLog.strMapLogFileName;
                ParamterMapLog.strSetMapName = OptionMapLog.strMapLogName;

                ParamterMapInfoLog.strSetEnterMutexName = OptionMapLog.strMapInfoLogEnterMutexName;
                ParamterMapInfoLog.strSetAccessMutexName = OptionMapLog.strMapInfoLogAccessMutexName;
                ParamterMapInfoLog.strSetPath = OptionMapLog.strMapInfoLogPath;
                ParamterMapInfoLog.strSetFileName = OptionMapLog.strMapInfoLogFileName;
                ParamterMapInfoLog.strSetMapName = OptionMapLog.strMapInfoLogName;
            }

            if (LoadInfo.Load() == false)
            {
                MiddlewareLogStartState = false;

                MessageOutput.ConsoleWrite(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss>>") + @"Failed to load server information. [SystemX.Net.Middleware.UI.Log : MainForm.MainForm]", ConsoleColor.Yellow, LogMessageLevel.DEBUG);

                return;
            }
            else
            {
                string strGetTitle = this.Text;

                //this.Text = strGetTitle + " " + (LoadInfo.LOOP_BACK ? " [LocalHost]" : " [" + LoadInfo.SERVER_IP + "]");

                Title_label.Text = "CP ALIS" + ((LoadInfo.TITLE.Length > 0) ? " [" + LoadInfo.TITLE + "]" : "");
            }

            thisConnInfo = new ConnectInfoStore[ALL_MANAGE_NUM];
            LogDataQueue = new ConcurrentQueue<LogMappedPacket>[ALL_MANAGE_NUM];

            LogDataRecvText = new ConcurrentQueue<StringBuilder>();
            LogDataProcessText = new ConcurrentQueue<StringBuilder>();

            for (int i = 0; i < ALL_MANAGE_NUM; i++)
            {
                thisConnInfo[i] = new ConnectInfoStore();
                LogDataQueue[i] = new ConcurrentQueue<LogMappedPacket>();
            }

            MngDBLogConn = new XLogDBConnManager();
            MngDBLogConn.SetConfigFilePath(strDBInfoPos, true);

            string strGetFileName = Path.GetFileName(MngDBLogConn.GetConfigFilePath());
            string strGetFilePath = Path.GetDirectoryName(MngDBLogConn.GetConfigFilePath()).Split('\\').Last();

            labelDBPath.Text = @"\" + strGetFilePath + @"\" + strGetFileName;

            bDatabaseConnResult = MngDBLogConn.OpenConnection();

            if (bDatabaseConnResult)
            {
                labelDBMain.Text = "Information [" + MngDBLogConn.GetDBConnectInfo().ConnMain.SCHEMA + "]";
                labelDBShortTerm.Text = "ShortTerm [" + MngDBLogConn.GetDBConnectInfo().ConnShortTerm.SCHEMA + "]";
                labelShortTermSummaryTableName.Text = "ShortTerm [" + MngDBLogConn.GetDBConnectInfo().ConnShortTerm.SUMMARY_TABLE + "]";
                labelShortTermLogTableName.Text = "ShortTerm [" + MngDBLogConn.GetDBConnectInfo().ConnShortTerm.LOG_TABLE + "]";
                labelDBLongTerm.Text = "LongTerm [" + MngDBLogConn.GetDBConnectInfo().ConnLongTerm.SCHEMA + "]";

                panelDBMain2.BackColor = Color.LimeGreen;
                panelDBShortTerm2.BackColor = Color.LimeGreen;
                panelDBLongTerm2.BackColor = Color.LimeGreen;

                //옵션 확인 및 ON
                //DECLARE @ARITHABORT VARCHAR(3) = 'OFF'; IF ( (64 & @@OPTIONS) = 64 ) SET @ARITHABORT = 'ON'; SELECT @ARITHABORT AS ARITHABORT;
                string strGetQuery1 = "DECLARE @ARITHABORT VARCHAR(3) = 'OFF'; IF ( (64 & @@OPTIONS) = 64 ) SET @ARITHABORT = 'ON'; SELECT @ARITHABORT AS ARITHABORT;";
                string strGetQuery2 = "DECLARE @ARITHIGNORE VARCHAR(3) = 'OFF'; IF ( (128 & @@OPTIONS) = 128 ) SET @ARITHIGNORE = 'ON'; SELECT @ARITHIGNORE AS ARITHIGNORE;";
                /*
                SET ARITHIGNORE ON

                 - 쿼리 실행 중 오버플로 또는 0으로 나누기 오류에서 오류 메시지를 반환할지 여부를 제어

　                 오류 메시지가 반환될지 여부만 제어하며 이 설정에 상관 없이 오버플로 또는 0으로 나누기 오류를  
　                 발생시킨 계산에 NULL을 반환한다. 없는 값 처리를 해야하므고 오류를 발생시킨 것에서  null 반환을 받아야 한다!

                SET ARITHABORT ON이면 오버플로 또는 0으로 나누기 오류 발생 시 쿼리나 일괄 처리가 종료 
　                 트랜잭션에서 해당 오류가 발생하면 트랜잭션이 롤백시킨다.  그러나 이 설정이 INSERT, UPDATE, DELETE 문 실행 중에  
　                 발생한 오류에는 영향을 주지 않는다.  
                */
                DataSet ds1 = MngDBLogConn.CurrentConnection(eConnCategory.Main).QueryDataSet(strGetQuery1);
                DataSet ds2 = MngDBLogConn.CurrentConnection(eConnCategory.Main).QueryDataSet(strGetQuery2);

                if (XCommons.isHasRow(ds1))
                {
                    bool bOptionState = ds1.Tables[0].Rows[0]["ARITHABORT"].ToString() == "ON" ? true : false;

                    if (bOptionState == false)
                        MngDBLogConn.CurrentConnection(eConnCategory.Main).QueryDataSet("SET ARITHABORT ON");
                }
                if (XCommons.isHasRow(ds2))
                {
                    bool bOptionState = ds2.Tables[0].Rows[0]["ARITHIGNORE"].ToString() == "ON" ? true : false;

                    if (bOptionState == false)
                        MngDBLogConn.CurrentConnection(eConnCategory.Main).QueryDataSet("SET ARITHIGNORE ON");
                }
                //
                ds1 = MngDBLogConn.CurrentConnection(eConnCategory.ShortTerm).QueryDataSet(strGetQuery1);
                ds2 = MngDBLogConn.CurrentConnection(eConnCategory.ShortTerm).QueryDataSet(strGetQuery2);

                if (XCommons.isHasRow(ds1))
                {
                    bool bOptionState = ds1.Tables[0].Rows[0]["ARITHABORT"].ToString() == "ON" ? true : false;

                    if (bOptionState == false)
                        MngDBLogConn.CurrentConnection(eConnCategory.ShortTerm).QueryDataSet("SET ARITHABORT ON");
                }
                if (XCommons.isHasRow(ds2))
                {
                    bool bOptionState = ds2.Tables[0].Rows[0]["ARITHIGNORE"].ToString() == "ON" ? true : false;

                    if (bOptionState == false)
                        MngDBLogConn.CurrentConnection(eConnCategory.ShortTerm).QueryDataSet("SET ARITHIGNORE ON");
                }
                //
                ds1 = MngDBLogConn.CurrentConnection(eConnCategory.LongTerm).QueryDataSet(strGetQuery1);
                ds2 = MngDBLogConn.CurrentConnection(eConnCategory.LongTerm).QueryDataSet(strGetQuery2);

                if (XCommons.isHasRow(ds1))
                {
                    bool bOptionState = ds1.Tables[0].Rows[0]["ARITHABORT"].ToString() == "ON" ? true : false;

                    if (bOptionState == false)
                        MngDBLogConn.CurrentConnection(eConnCategory.LongTerm).QueryDataSet("SET ARITHABORT ON");
                }
                if (XCommons.isHasRow(ds2))
                {
                    bool bOptionState = ds2.Tables[0].Rows[0]["ARITHIGNORE"].ToString() == "ON" ? true : false;

                    if (bOptionState == false)
                        MngDBLogConn.CurrentConnection(eConnCategory.LongTerm).QueryDataSet("SET ARITHIGNORE ON");
                }

                if (MiddlewareLogStartState == false)
                    MessageOutput.ConsoleWrite(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss>>") + @"The table registered in the configuration file does not exist in the actual database. [SystemX.Net.MiddlewareUI : MainForm.MainForm]", ConsoleColor.Yellow, LogMessageLevel.DEBUG);
            }
            else
            {
                panelDBMain2.BackColor = Color.Red;
                panelDBShortTerm2.BackColor = Color.Red;
                panelDBLongTerm2.BackColor = Color.Red;

                MiddlewareLogStartState = false;

                MessageOutput.ConsoleWrite(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss>>") + @"Failed to connect to DB and to get table information. [SystemX.Net.MiddlewareUI : MainForm.MainForm]", ConsoleColor.Yellow, LogMessageLevel.DEBUG);

                return;
            }

            m_bTimerLock = new bool[10];
            Array.Clear(m_bTimerLock, 0, 10);

            if (LoadInfo.FTP_Use)
            {
                panelFTP.Visible = true;
                labelFTP.Text = "FTP " + IPAddress.Parse(LoadInfo.FTP_IPAddress).ToString() + "@" + LoadInfo.FTP_Port + "@" + LoadInfo.FTP_Account;
                labelFTPTime.Visible = true;

                StateFTP = StartFTPService();

                if (StateFTP == eFTPServiceStatus.Connected)
                {
                    stProcessCheckFTPTime = new Stopwatch();
                    stProcessCheckFTPTime.Start();

                    panelFTP.BackColor = Color.LimeGreen;
                }
                else
                {
                    if (MessageBox.Show("FTP server connection is not confirmed. Do you still want to proceed with running the middleware log handler?", "SystemX.Net.Middleware.UI.Log", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
                        MiddlewareLogStartState = false;

                    panelFTP.BackColor = Color.Red;
                }
            }
            else
                StateFTP = eFTPServiceStatus.Unused;

            if (MiddlewareLogStartState)
            {
                stCheckTermBulkWaitTime = new Stopwatch();

                lstBulkTermLogInfo = new List<string>();
                lstBulkShortTermLog = new List<DataRow>();
                lstBulkLongTermLog = new List<DataRow>();

                stCheckReleaseTime = new Stopwatch();
                stCheckReleaseTime.Start();

                taskRecvCommandProcess = null;
                m_bTaskCommandBlock = false;
                
                taskRecvCommandProcess = new Task(new Action(WatchRecvCommandQueue), CT);
                taskRecvCommandProcess.Start();
                //
                taskRecvStreamProcess = null;
                m_bTaskStreamBlock = false;
                
                taskRecvStreamProcess = new Task(new Action(WatchRecvStreamQueue), CT);
                taskRecvStreamProcess.Start();
                //
                UItimer.Enabled = true;

                MessageOutput.ConsoleWrite(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss>>") + @"MiddlewareUI start success. [SystemX.Net.MiddlewareUI : MainForm.MainForm]", ConsoleColor.White, LogMessageLevel.INFO);
            }

            Chktimer.Enabled = true;
        }

        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (MiddlewareLogStartState == true)
            {
                if (MessageBox.Show("You want to program exit?", "SystemX.Net.Middleware.UI.Log", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
                    e.Cancel = true;
                else
                    e.Cancel = false;
            }
            else
                e.Cancel = false;
        }

        private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            CTS.Cancel();

            if (taskRecvCommandProcess != null)
            {
                m_bTaskCommandBlock = true;

                taskRecvCommandProcess.Wait();
                taskRecvCommandProcess.ContinueWith(t => {
                    MessageOutput.ConsoleWrite(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss>>") +
                        @"taskRecvCommandProcess END. [SystemX.Net.MiddlewareUI : MainForm.MainForm_FormClosed]",
                        ConsoleColor.White,
                        LogMessageLevel.DEBUG);
                });
            }

            if (taskRecvStreamProcess != null)
            {
                m_bTaskStreamBlock = true;

                taskRecvStreamProcess.Wait();
                taskRecvStreamProcess.ContinueWith(t => {
                    MessageOutput.ConsoleWrite(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss>>") +
                        @"taskRecvStreamProcess END. [SystemX.Net.MiddlewareUI : MainForm.MainForm_FormClosed]",
                        ConsoleColor.White,
                        LogMessageLevel.DEBUG);
                });
            }
        }
  
        private void notifyIcon_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            if (this.Visible)
                this.Visible = false;
            else
                this.Visible = true;
        }

        private void label1_DoubleClick(object sender, EventArgs e)
        {
            ConsoleUtil.ConsoleVisibleControl();
        }

        private void contextMenuSubStrip_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
        {
            ToolStripItem tsi = e.ClickedItem;

            this.Close();
        }

        private void barCheckItemShowCpLogTime_CheckedChanged(object sender, ItemClickEventArgs e)
        {
            bShowCpLogProcessTime = barCheckItemShowCpLogTime.Checked;
        }

        private void buttonBulkInsertStart_Click(object sender, EventArgs e)
        {
            if (bTryBulkInsertBuffer == false)
                bTryBulkInsertBuffer = true;
        }

        private void buttonDbConnect_Click(object sender, EventArgs e)
        {
            bDatabaseConnResult = MngDBLogConn.CheckDatabaseConnection();
        }
    }
}
