Imports System.Text
Namespace TCP
    ''' <summary>
    ''' Moniteur de communication pour un maitre sur une communication sérielle
    ''' </summary>
    ''' <remarks></remarks>
    Public NotInheritable Class MonitorClient
        Inherits ClsBaseAnimCom

        ''' <summary>
        ''' Classe de mémorisation des données des threads de communication
        ''' </summary>
        ''' <remarks></remarks>
        Private Class ClsMemThData
            ''' <summary>
            ''' Client TCP
            ''' </summary>
            ''' <remarks></remarks>
            Public TcpClient As Net.Sockets.TcpClient = Nothing
            ''' <summary>
            ''' ID de la trame en attente d'envoi
            ''' </summary>
            ''' <remarks></remarks>
            Public ID As UShort
            ''' <summary>
            ''' Trame en attente
            ''' </summary>
            ''' <remarks></remarks>
            Public Trame As SendingFrame
            ''' <summary>
            ''' Connexion active
            ''' </summary>
            ''' <remarks></remarks>
            Public Connexion As MonitorClient

            ''' <summary>
            ''' Ouvre la connexion TCP
            ''' </summary>
            ''' <param name="pIP"></param>
            ''' <param name="pPort"></param>
            ''' <remarks></remarks>
            Public Sub OpenConnect(ByVal pIP As Net.IPAddress, ByVal pPort As Integer, ByVal pThread As ClsDelegateThread)
                If TcpClient IsNot Nothing Then
                    Try
                        TcpClient.Close()
                    Catch ex As Exception
                        'Ne rien faire
                    End Try
                    TcpClient = Nothing
                End If
                Do While pThread.CanRun AndAlso Not My.Computer.Network.Ping(pIP.ToString, 1000)
                    Connexion.SetState(IAnimCom.EState.AutoConnect Or IAnimCom.EState.NotConnectable)
                    Sleep(1000)
                Loop
                Connexion.SetState(IAnimCom.EState.AutoConnect Or IAnimCom.EState.NotConnectedConnectable)
                If Not (pThread.CanRun) Then Exit Sub
                TcpClient = New Net.Sockets.TcpClient
                TcpClient.Connect(pIP, pPort)
                Connexion.SetState(IAnimCom.EState.AutoConnect Or IAnimCom.EState.Connected)
            End Sub
            ''' <summary>
            ''' Fermer la connexion TCP
            ''' </summary>
            ''' <remarks></remarks>
            Public Sub Close()
                Try
                    If TcpClient IsNot Nothing Then
                        TcpClient.Close()
                        TcpClient = Nothing
                    End If
                Catch ex As Exception

                End Try
            End Sub
            ''' <summary>
            ''' Récupère la trame suivante
            ''' </summary>
            ''' <remarks></remarks>
            Public Sub AddFrame()
                'Recup de la trame à envoyer
                SyncLock Connexion.mTramePrio
                    If Connexion.mTramePrio.Count > 0 Then
                        Trame = Connexion.mTramePrio.Dequeue
                    Else
                        Trame = Connexion.GetNextFrame()
                    End If
                End SyncLock
                If Trame Is Nothing Then
                    Sleep(100)
                    Return
                End If

                ID = CUShort(ID + 1) Mod UShort.MaxValue
            End Sub
        End Class

#Region "Membres partagés publique"
        ''' <summary>
        ''' Ferme toutes les connexion (Série et TCP)
        ''' </summary>
        ''' <param name="pWait">Spécifie si le programme doit attendre</param>
        ''' <remarks></remarks>
        Public Shared Sub CloseAllConnexions(ByVal pWait As Boolean)
            ClsBaseThread.StopAllThreads(True, pWait)
        End Sub
#End Region

#Region "Attributs privés"
#Region "Config Modbus"
        ''' <summary>
        ''' Délai de timeout
        ''' </summary>
        ''' <remarks></remarks>
        Private mTimeOut As Integer
#End Region

#Region "Configuration du port"
        ''' <summary>
        ''' Mémorise l'adresse IP distante si port COM déporté
        ''' </summary>
        ''' <remarks></remarks>
        Private mIP As Net.IPAddress
        ''' <summary>
        ''' Mémorise le Port TCP à utiliser
        ''' </summary>
        ''' <remarks></remarks>
        Private mPort As Integer
        ''' <summary>
        ''' Nombre max de connexion simultanées
        ''' </summary>
        ''' <remarks></remarks>
        Private mMaxNbConnexion As Integer
#End Region

#Region "Liste des envois"
        ''' <summary>
        ''' Liste des trames dans le buffer d'envoi
        ''' </summary>
        ''' <remarks></remarks>
        Private mTramePrio As New Queue(Of SendingFrame)
        ''' <summary>
        ''' Liste des trames en attente de génération de gestion par le code client
        ''' </summary>
        ''' <remarks></remarks>
        Private mTrameWait As New Queue(Of SendingFrame)
#End Region

#Region "Gestion interne"
        ''' <summary>
        ''' Mémorise le port série
        ''' </summary>
        ''' <remarks></remarks>
        Private mSocket As Net.Sockets.TcpClient = Nothing
        ''' <summary>
        ''' Threads de communication
        ''' </summary>
        ''' <remarks></remarks>
        Private mClients As New List(Of ClsDelegateThread)
        ''' <summary>
        ''' Mémorise les sockets ouverts
        ''' </summary>
        ''' <remarks></remarks>
        Private mSocketClients As New Dictionary(Of ClsDelegateThread, ClsMemThData)
        ''' <summary>
        ''' Thread de gestion des evenements
        ''' </summary>
        ''' <remarks></remarks>
        Private mThreadEvent As ClsDelegateThread
        ''' <summary>
        ''' Dernier ID de message utilisé
        ''' </summary>
        ''' <remarks></remarks>
        Private mLastID As UShort
#End Region
#End Region

#Region "Constructeurs"
        ''' <summary>
        ''' Constructeur de la classe
        ''' </summary>
        ''' <param name="pIP">IP du serveur</param>
        ''' <param name="pMaxNbConnexion">Nombre maximal de connexion à utiliser</param>
        ''' <param name="pTimeOut">Timeout</param>
        ''' <remarks></remarks>
        Public Sub New(ByVal pIP As Net.IPAddress, ByVal pMaxNbConnexion As Integer, ByVal pTimeOut As Integer)
            MyClass.New(pIP, 502, pMaxNbConnexion, pTimeOut)
        End Sub
        ''' <summary>
        ''' Constructeur de la classe
        ''' </summary>
        ''' <param name="pIP">IP du serveur</param>
        ''' <param name="pMaxNbConnexion">Nombre maximal de connexion à utiliser</param>
        ''' <param name="pTimeOut">Timeout</param>
        ''' <param name="pPortTCP">Port TCP à utiliser</param>
        ''' <remarks></remarks>
        Public Sub New(ByVal pIP As Net.IPAddress, ByVal pPortTCP As Integer, ByVal pMaxNbConnexion As Integer, ByVal pTimeOut As Integer)
            mPort = pPortTCP
            mIP = pIP
            mMaxNbConnexion = pMaxNbConnexion
            mTimeOut = pTimeOut
        End Sub
        ''' <summary>
        ''' Constructeur de la classe
        ''' </summary>
        ''' <param name="pParams">Paramètres de la connexion</param>
        ''' <remarks></remarks>
        Public Sub New(ByVal pParams As String)
            Dim ltStr As String() = pParams.Split(","c)
            If ltStr.Length <> 4 Then
                Throw New ArgumentException("Paramètres incorrectes")
            End If

            mIP = Net.IPAddress.Parse(ltStr(0))
            mPort = CInt(ltStr(1))
            mMaxNbConnexion = CInt(ltStr(2))
            mTimeOut = CInt(ltStr(3))
        End Sub
#End Region

#Region "Membres publiques"

        ''' <summary>
        ''' Ouvre les connexions
        ''' </summary>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Overrides Function Open() As Boolean
            mTramePrio.Clear()
            mTrameWait.Clear()

            Dim lRes As Boolean = MyBase.Open()

            If lRes Then
                'Génération du thread de gestion des réceptions
                mThreadEvent = New ClsDelegateThread("Evt TCPClient(" + mIP.ToString + ":" + CStr(mPort) + ")", New ClsDelegateThread.SubThread(AddressOf ProSubThreadEvt), New ClsDelegateThread.ErrThread(AddressOf ProErrThread))

                'Purge des threads précédent
                For Each lClient As KeyValuePair(Of ClsDelegateThread, ClsMemThData) In mSocketClients
                    lClient.Key.StopThread()
                    lClient.Value.Close()
                Next
                mSocketClients.Clear()
                mClients.Clear()

                'Création des threads
                Do While mClients.Count < mMaxNbConnexion
                    mClients.Add(New ClsDelegateThread("Com TCPClient(" + mIP.ToString + ":" + CStr(mPort) + ")", New ClsDelegateThread.SubThread(AddressOf ProSubThreadCom), New ClsDelegateThread.ErrThread(AddressOf ProErrThread)))
                Loop
            End If

            Return lRes
        End Function

#Region "Fermeture"
        ''' <summary>
        ''' Ferme la connexion
        ''' </summary>
        ''' <remarks></remarks>
        Public Overrides Sub Close()
            Close(False)
        End Sub
        ''' <summary>
        ''' Femre la connexion
        ''' </summary>
        ''' <param name="pWait">Indique si la connexion doit être fermée avant de treprendre la main</param>
        ''' <remarks></remarks>
        Public Overloads Sub Close(ByVal pWait As Boolean)
            Do While mClients.Count > 0
                mClients(0).StopThread(pWait)
                mClients.RemoveAt(0)
            Loop
            If mThreadEvent IsNot Nothing Then
                mThreadEvent.StopThread(pWait)
                mThreadEvent = Nothing
            End If
            MyBase.Close()
        End Sub
#End Region

#Region "Config Modbus"
        ''' <summary>
        ''' Retourne ou définit le délai de timeout
        ''' </summary>
        ''' <remarks></remarks>
        Public Property TimeOut() As Integer
            Get
                Return mTimeOut
            End Get
            Set(ByVal value As Integer)
                If value > 50 Then
                    mTimeOut = value
                End If
            End Set
        End Property
#End Region

#Region "Configuration du port"
        ''' <summary>
        ''' Retourne ou définit le Port TCP
        ''' </summary>
        ''' <remarks></remarks>
        Public Property PortTCP() As Integer
            Get
                Return mPort
            End Get
            Set(ByVal value As Integer)
                If State <> IAnimCom.EState.NotConnectedConnectable Then
                    Throw New InvalidOperationException("Opération interdite quand la connexion est établie")
                End If
                If value > 0 And value < 65536 Then
                    mPort = value
                End If
            End Set
        End Property
        ''' <summary>
        ''' Retourne ou définit l'adresse IP du serveur
        ''' </summary>
        ''' <remarks></remarks>
        Public Property IP() As Net.IPAddress
            Get
                Return mIP
            End Get
            Set(ByVal pValue As Net.IPAddress)
                If State <> IAnimCom.EState.NotConnectedConnectable Then
                    Throw New InvalidOperationException("Opération interdite quand la connexion est établie")
                End If
                mIP = pValue
            End Set
        End Property
        ''' <summary>
        ''' Retourne ou définit le nombre maximal de connexion simultanées autorisées
        ''' </summary>
        ''' <remarks></remarks>
        Public Property MaxNbConnexion() As Integer
            Get
                Return mMaxNbConnexion
            End Get
            Set(ByVal pValue As Integer)
                If State <> IAnimCom.EState.NotConnectedConnectable Then
                    Throw New InvalidOperationException("Opération interdite quand la connexion est établie")
                End If
                If pValue > 0 Then
                    mMaxNbConnexion = pValue
                End If
            End Set
        End Property
#End Region

#End Region

#Region "Fonction du thread"

        Private Sub ProSubThreadCom(ByVal pThread As ClsDelegateThread)
            SetState(IAnimCom.EState.AutoConnect Or IAnimCom.EState.NotConnectable)

            'Ouverture du port
            Dim lClient As ClsMemThData = Nothing
            SyncLock mSocketClients
                If Not mSocketClients.TryGetValue(pThread, lClient) Then
                    lClient = New ClsMemThData
                    lClient.Connexion = Me
                    mSocketClients.Add(pThread, lClient)
                End If
            End SyncLock

            'Attente de démarrage
            'TODO : Controle Adresse IP
            lClient.OpenConnect(mIP, mPort, pThread)
            If lClient.TcpClient Is Nothing Then Exit Sub


            Dim ltRecv As New List(Of Byte)
            Dim lLength As Integer

            'Init de la variable de sunchro
            Dim lStart As Integer = Environment.TickCount

            'Tant que l'on peut fonctionner
            Do While pThread.CanRun
                'Tests
                If lClient.Trame Is Nothing Then
                    lClient.AddFrame()
                Else
                    'Envoi

                    If lClient.TcpClient.Available > 0 Then
                        lClient.OpenConnect(mIP, mPort, pThread)
                        If lClient.TcpClient Is Nothing Then Exit Sub
                    End If

                    'Envoi de la requête
                    Dim ltFrame As Byte()
                    ltFrame = (New Frame(lClient.Trame.FrameSend, lClient.ID)).GetFrame
                    lClient.TcpClient.GetStream.Write(ltFrame, 0, ltFrame.Length)

                    'Init du timer
                    lStart = Environment.TickCount

                    'Purge des données reçues
                    ltRecv.Clear()

                    lLength = -1

                    'Attente de la réponse (+gestion du Timeout)
                    Do While Environment.TickCount < lStart + mTimeOut And Environment.TickCount >= lStart
                        If lLength = -1 And lClient.TcpClient.Available >= 6 Then
                            Dim lltData(5) As Byte
                            lClient.TcpClient.GetStream.Read(lltData, 0, 6)
                            ltRecv.AddRange(lltData)
                            lLength = ltRecv(4)
                            lLength = lLength << 8
                            lLength += ltRecv(5)
                            'RéInit du timer
                            lStart = Environment.TickCount
                        ElseIf lLength <= lClient.TcpClient.Available And lLength >= 0 Then
                            Dim lltData() As Byte
                            ReDim lltData(lLength - 1)
                            lClient.TcpClient.GetStream.Read(lltData, 0, lLength)
                            ltRecv.AddRange(lltData)
                            lClient.Trame = lClient.Trame.Clone(Frame.GetFrame(ltRecv.ToArray, False).Trame)
                            Exit Do
                        End If
                        Sleep(10)
                    Loop
                    lStart = Environment.TickCount

                    If ltRecv.Count = 0 Then
                        lClient.Trame.DecrementResend()
                        If lClient.Trame.LeftResend <= -1 Then
                            SyncLock mTrameWait
                                mTrameWait.Enqueue(lClient.Trame)
                                lClient.Trame = Nothing
                            End SyncLock
                        Else
                            SyncLock mTramePrio
                                mTramePrio.Enqueue(lClient.Trame)
                                lClient.Trame = Nothing
                            End SyncLock
                        End If
                        lClient.OpenConnect(mIP, mPort, pThread)
                        If lClient.TcpClient Is Nothing Then Exit Sub
                    Else
                        SyncLock mTrameWait
                            mTrameWait.Enqueue(lClient.Trame)
                            lClient.Trame = Nothing
                        End SyncLock
                    End If
                End If
            Loop

            'On sort du thread, on ferme donc le port COM
            If lClient.TcpClient IsNot Nothing AndAlso lClient.TcpClient.Connected Then
                Try
                    lClient.TcpClient.Close()
                Catch ex As Exception
#If DEBUG Then
                    Stop
#End If
                    'Ne rien faire
                End Try
                lClient.TcpClient = Nothing
            End If
            mSocketClients.Remove(pThread)
        End Sub

        Private Sub ProSubThreadEvt(ByVal pThread As ClsDelegateThread)

            Dim lTrame As SendingFrame

            'Tant que l'on peut fonctionner
            Do While pThread.CanRun
                'Tests
                If mTrameWait.Count > 0 Then
                    'Trame en attente de traitement par le code client

                    SyncLock mTrameWait
                        lTrame = mTrameWait.Dequeue
                    End SyncLock

                    lTrame = lTrame.RaiseEvt(Me)

                    If lTrame IsNot Nothing Then
                        SyncLock mTramePrio
                            mTramePrio.Enqueue(lTrame)
                        End SyncLock
                    End If
                Else
                    Sleep(100)
                End If
            Loop
        End Sub

        Private Function ProErrThread(ByVal pThread As ClsDelegateThread, ByVal pEx As System.Exception) As Boolean
            'Erreur dans le thread
#If DEBUG Then
            Debug.Print(pEx.ToString)
            Stop
#End If

            If pThread Is mThreadEvent Then
#If DEBUG Then
                Stop
#End If
                Return True
            ElseIf mClients.Contains(pThread) Then
#If DEBUG Then
                Stop
#End If
                Return True
            Else
#If DEBUG Then
                Stop
#End If
                Return False
            End If
        End Function
#End Region

        ''' <summary>
        ''' Retourne le type de la connexion (TCP)
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Overrides ReadOnly Property ConnexionType() As IAnimCom.EConnexType
            Get
                Return IAnimCom.EConnexType.TCP
            End Get
        End Property
        ''' <summary>
        ''' Retourne les paramètres
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Overrides ReadOnly Property StringParam() As String
            Get
                Return String.Format("{0},{1},{2},{3}", mIP, mPort, mMaxNbConnexion, mTimeOut)
            End Get
        End Property
    End Class
End Namespace