Public Class ModbusTCPComm
    Inherits System.ComponentModel.Component
    Implements ICommComponent

    Private Shared DLL(10) As MfgControl.AdvancedHMI.Drivers.ModbusTCP
    Private MyDLLInstance As Integer

    Private tmrPollList As New List(Of Windows.Forms.Timer)

    Private Structure PolledAddressInfo
        Dim Address As DecodedAddress
        ' Dim StartAddress As UInt16
        Dim dlgCallBack As ICommComponent.ReturnValues
        Dim PollRate As Integer
        Dim ID As Integer
    End Structure

    Public Structure DecodedAddress
        Dim Address As String
        Dim ReadFunctionCode As UInt16
        Dim WriteFunctionCode As UInt16
        Dim StartAddress As UInt16
    End Structure



    Private PolledAddressList As New List(Of PolledAddressInfo)


#Region "Properties"
    Private m_TargetIPAddress As String = "0.0.0.0"   '* this is a default value
    Public Property TargetIPAddress() As String
        Get
            Return m_TargetIPAddress.ToString
        End Get
        Set(ByVal value As String)
            m_TargetIPAddress = value

            '* If a new instance needs to be created, such as a different AMS Address
            CreateDLLInstance()


            If DLL(MyDLLInstance) IsNot Nothing Then
                DLL(MyDLLInstance).TargetIPAddress = value
            End If
        End Set
    End Property

    Private m_TargetTCPIPPort As UInt16 = 502
    Public Property TargetTCPIPPort() As UInt16
        Get
            Return m_TargetTCPIPPort
        End Get
        Set(ByVal value As UInt16)
            m_TargetTCPIPPort = value

            '* If a new instance needs to be created, such as a different AMS Address
            CreateDLLInstance()


            If DLL(MyDLLInstance) IsNot Nothing Then
                DLL(MyDLLInstance).TargetTCPIPPort = value
            End If
        End Set
    End Property

    '*****************************************************************

    '**************************************************
    '* Its purpose is to fetch
    '* the main form in order to synchronize the
    '* notification thread/event
    '**************************************************
    Private m_SynchronizingObject As Windows.Forms.Form
    '* do not let this property show up in the property window
    ' <System.ComponentModel.Browsable(False)> _
    Public Property SynchronizingObject() As Windows.Forms.Form
        Get
            'If Me.Site.DesignMode Then

            Dim host1 As System.ComponentModel.Design.IDesignerHost
            Dim obj1 As Object
            If (m_SynchronizingObject Is Nothing) AndAlso MyBase.DesignMode Then
                host1 = CType(Me.GetService(GetType(System.ComponentModel.Design.IDesignerHost)), System.ComponentModel.Design.IDesignerHost)
                If host1 IsNot Nothing Then
                    obj1 = host1.RootComponent
                    m_SynchronizingObject = CType(obj1, System.ComponentModel.ISynchronizeInvoke)
                End If
            End If
            'End If
            Return m_SynchronizingObject
        End Get

        Set(ByVal Value As Windows.Forms.Form)
            If Not Value Is Nothing Then
                m_SynchronizingObject = Value
            End If
        End Set
    End Property

    Private m_DisableSubscriptions As Boolean
    Public Property DisableSubScriptions() As Boolean Implements ICommComponent.DisableSubscriptions
        Get
            Return m_DisableSubscriptions
        End Get
        Set(ByVal value As Boolean)
            m_DisableSubscriptions = value

            If value Then
                '* Stop the poll timers
                For i As Int16 = 0 To tmrPollList.Count - 1
                    tmrPollList(i).Enabled = False
                Next
            Else
                '* Start the poll timers
                For i As Int16 = 0 To tmrPollList.Count - 1
                    tmrPollList(i).Enabled = True
                Next
            End If
        End Set
    End Property
#End Region

#Region "ConstructorDestructor"
    Private components As System.ComponentModel.IContainer

    Public Sub New(ByVal container As System.ComponentModel.IContainer)
        MyClass.New()

        'Required for Windows.Forms Class Composition Designer support
        container.Add(Me)
    End Sub

    Public Sub New()
        MyBase.New()

        'CreateDLLInstance()
    End Sub

    'Component overrides dispose to clean up the component list.
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        '* The handle linked to the DataLink Layer has to be removed, otherwise it causes a problem when a form is closed
        If DLL(MyDLLInstance) IsNot Nothing Then RemoveHandler DLL(MyDLLInstance).DataReceived, Dr

        If disposing AndAlso components IsNot Nothing Then
            components.Dispose()
        End If
        MyBase.Dispose(disposing)
    End Sub
#End Region
    '***************************************************************
    '* Create the Data Link Layer Instances
    '* if the AMS NET Address is the same, then resuse a common instance
    '***************************************************************
    Private Sub CreateDLLInstance()
        '* Still default, so ignore
        If m_TargetIPAddress = "0.0.0.0" Then Exit Sub

        If DLL(0) IsNot Nothing Then
            '* At least one DLL instance already exists,
            '* so check to see if it has the same IP address
            '* if so, reuse the instance, otherwise create a new one
            Dim i As Integer
            While DLL(i) IsNot Nothing AndAlso DLL(i).TargetIPAddress <> m_TargetIPAddress AndAlso i < 11
                i += 1
            End While
            MyDLLInstance = i
        End If

        If DLL(MyDLLInstance) Is Nothing Then
            DLL(MyDLLInstance) = New MfgControl.AdvancedHMI.Drivers.ModbusTCP
        End If
        DLL(MyDLLInstance).TargetTCPIPPort = m_TargetTCPIPPort
        'If DLL(MyDLLInstance).TargetIPAddress <> m_TargetIPAddress Then DLL(MyDLLInstance).TargetIPAddress = m_TargetIPAddress
        DLL(MyDLLInstance).TargetIPAddress = m_TargetIPAddress


        AddHandler DLL(MyDLLInstance).DataReceived, Dr
    End Sub



    '*******************************************************************
    '*******************************************************************
    Private CurrentID As Integer
    'Public Function Subscribe(ByVal PLCAddress As String, ByVal cycleTime As Integer, ByVal callBack As ICommComponent.ReturnValues) As Integer Implements ICommComponent.Subscribe
    Public Function Subscribe(ByVal PLCAddress As String, ByVal cycleTime As Integer, ByVal callBack As ICommComponent.ReturnValues, ByVal FullDataTypeBP As String, ByVal DataLengthBP As Integer) As Integer Implements ICommComponent.Subscribe
        '*********************************
        '* Get the address information
        '*********************************
        ParseAddress(PLCAddress)

        '***********************************************************
        '* Check if there was already a subscription made for this
        '***********************************************************
        'Dim Sym As ADSforTwinCAT.SymbolInfo = DLL(MyDLLInstance).UsedSymbols(i)
        Dim SubExists As Boolean
        Dim k As Integer

        For k = 0 To PolledAddressList.Count - 1
            If PolledAddressList(k).Address.Address = PLCAddress And PolledAddressList(k).dlgCallBack = callBack Then
                SubExists = True
                Exit For
            End If
        Next


        If (Not SubExists) Then
            '* The ID is used as a reference for removing polled addresses
            CurrentID += 1

            Dim tmpPA As New PolledAddressInfo

            tmpPA.PollRate = cycleTime
            tmpPA.dlgCallBack = callBack
            tmpPA.ID = CurrentID
            tmpPA.Address.Address = PLCAddress

            PolledAddressList.Add(tmpPA)

            PolledAddressList.Sort(AddressOf SortPolledAddresses)



            '********************************************************************
            '* Check to see if there already exists a timer for this poll rate
            '********************************************************************
            Dim j As Integer = 0
            While j < tmrPollList.Count AndAlso tmrPollList(j) IsNot Nothing AndAlso tmrPollList(j).Interval <> cycleTime
                j += 1
            End While

            If j >= tmrPollList.Count Then
                '* Add new timer
                Dim tmrTemp As New Windows.Forms.Timer
                If cycleTime > 0 Then
                    tmrTemp.Interval = cycleTime
                Else
                    tmrTemp.Interval = 250
                End If

                tmrPollList.Add(tmrTemp)
                AddHandler tmrPollList(j).Tick, AddressOf PollUpdate

                tmrTemp.Enabled = True
            End If

            Return tmpPA.ID
        Else
            '* Return the subscription that already exists
            Return PolledAddressList(k).ID
        End If
        'Return -1
    End Function

    '***************************************************************
    '* Used to sort polled addresses by File Number and element
    '* This helps in optimizing reading
    '**************************************************************
    Private Function SortPolledAddresses(ByVal A1 As PolledAddressInfo, ByVal A2 As PolledAddressInfo) As Integer
        If A1.Address.Address = A2.Address.Address Then
            If A1.Address.Address > A2.Address.Address Then
                Return 1
            ElseIf A1.Address.Address = A2.Address.Address Then
                Return 0
            Else
                Return -1
            End If
        End If
    End Function

    '**************************************************************
    '* Perform the reads for the variables added for notification
    '* Attempt to optimize by grouping reads
    '**************************************************************
    Private InternalRequest As Boolean '* This is used to dinstinquish when to send data back to notification request
    Private Sub PollUpdate(ByVal sender As System.Object, ByVal e As System.EventArgs)
        Dim intTimerIndex As Integer = tmrPollList.IndexOf(sender)

        '* Stop the poll timer
        tmrPollList(intTimerIndex).Enabled = False
        Dim i As Integer
        While i < PolledAddressList.Count
            Dim value As String()
            Try
                value = ReadAny(PolledAddressList(i).Address.Address, 1)
                '* TODO : optimize in as few reads as possible - try group reading
                'value = DLL(MyDLLInstance).ReadAny(PolledAddressList(i).ReadFunctionCode, PolledAddressList(i).StartAddress)

                m_SynchronizingObject.Invoke(PolledAddressList(i).dlgCallBack, CObj(value))
            Catch ex As Exception
                Dim y = 0
            End Try

            i += 1
        End While

        '* Start the poll timer
        tmrPollList(intTimerIndex).Enabled = True
    End Sub

    Public Function UnSubscribe(ByVal ID As Integer) As Integer Implements ICommComponent.UnSubscribe
        Dim i As Integer = 0
        While i < PolledAddressList.Count AndAlso PolledAddressList(i).ID <> ID
            i += 1
        End While

        If i < PolledAddressList.Count Then
            PolledAddressList.RemoveAt(i)
            If PolledAddressList.Count = 0 Then
                '* No more items to be polled, so remove all polling timers 28-NOV-10
                For j As Integer = 0 To tmrPollList.Count - 1
                    'tmrPollList(j).Enabled = False
                    tmrPollList(j).Enabled = False
                    tmrPollList.Remove(tmrPollList(j))
                Next
            End If
        End If
    End Function

    Public Function ReadAny(ByVal startAddress As String) As String Implements ICommComponent.ReadAny
        Return ReadAny(startAddress)(0)
    End Function

    Public Function ReadAny(ByVal startAddress As String, ByVal numberOfElements As Integer) As String() Implements ICommComponent.ReadAny

        Dim da As DecodedAddress = ParseAddress(startAddress)

        Select Case da.ReadFunctionCode
            Case 1 : Return DLL(MyDLLInstance).ReadCoils(da.StartAddress, numberOfElements)
            Case 2 : Return DLL(MyDLLInstance).ReadInputs(da.StartAddress, numberOfElements)
            Case 3 : Return DLL(MyDLLInstance).ReadHoldingRegisters(da.StartAddress, numberOfElements)
            Case 4 : Return DLL(MyDLLInstance).ReadInputRegisters(da.StartAddress, numberOfElements)
        End Select


        Return DLL(MyDLLInstance).ReadAny(da.ReadFunctionCode, da.StartAddress, numberOfElements)
    End Function

    Public Function WriteData(ByVal startAddress As String, ByVal dataToWrite As String) As String Implements ICommComponent.WriteData
        Dim tmp() As String = {dataToWrite}
        Dim da As DecodedAddress = ParseAddress(startAddress)
        Return DLL(MyDLLInstance).WriteAny(da.WriteFunctionCode, da.StartAddress, tmp, 1)
    End Function

    Public Function WriteData(ByVal startAddress As String, ByVal dataToWrite() As String, ByVal numberOfElements As UInt16) As Integer
        Dim da As DecodedAddress = ParseAddress(startAddress)
        DLL(MyDLLInstance).WriteAny(da.WriteFunctionCode, da.StartAddress, dataToWrite, numberOfElements)
    End Function

    'Overloaded version of WriteData to alow for FullDataType
    Public Function WriteData(ByVal startAddress As String, ByVal dataToWrite As String, ByVal FullDataTypeBP As String, ByVal DataLengthBP As Integer) As String Implements ICommComponent.WriteData
        If dataToWrite Is Nothing Then
            Return 0
        End If
        Return 0
    End Function

    '************************************************************
    '* Parse the address into function code and starting address
    '************************************************************
    Public Shared Function ParseAddress(ByVal address As String) As DecodedAddress
        If address.Length <> 5 Then
            Throw New MfgControl.AdvancedHMI.Drivers.PLCDriverException(1, "Invalid Adress")
        End If

        Dim DA As New DecodedAddress
        Try
            DA.Address = address
            Select Case CInt(address.Substring(0, 1))
                Case 0 : DA.ReadFunctionCode = 1
                    DA.WriteFunctionCode = 5
                Case 1 : DA.ReadFunctionCode = 2
                    DA.WriteFunctionCode = 0 '* Invalid
                Case 3 : DA.ReadFunctionCode = 4
                    DA.WriteFunctionCode = 0
                Case 4 : DA.ReadFunctionCode = 3
                    DA.WriteFunctionCode = 6
                Case Else
                    DA.ReadFunctionCode = 0 '* Invalid
                    DA.WriteFunctionCode = 0
            End Select

            DA.StartAddress = CInt(address.Substring(1)) - 1
        Catch ex As Exception
            Throw New MfgControl.AdvancedHMI.Drivers.PLCDriverException(1, "Invalid Adress")
        End Try

        Return DA
    End Function

    Private Dr As EventHandler = AddressOf DataLinkLayer_DataReceived
    Private Sub DataLinkLayer_DataReceived()
        '* Should we only raise an event if we are in AsyncMode?
        '        If m_AsyncMode Then
        '**************************************************************************
        '* If the parent form property (Synchronizing Object) is set, then sync the event with its thread
        '**************************************************************************
        '* Get the low byte from the Sequence Number
        '* The sequence number was added onto the end of the CIP packet by this code
        'Dim TNSReturned As Integer = DLL(MyDLLInstance).DataPacket(DLL(MyDLLInstance).DataPacket.Count - 2)
        'DataPackets(TNSReturned) = DLL(MyDLLInstance).DataPacket
        'Responded(TNSReturned) = True
    End Sub
End Class
