Option Strict On

Imports System.IO
Imports System.Xml.Serialization
Imports System.Collections.Specialized

Imports Mbark.BusinessObjects.EntityClasses

Namespace Mbark.SystemCoordination

    <Serializable()> Public Class SystemCoordinatorSettings
        Public DeploymentId As Guid
        Public IdentificationBackground As String
        <XmlArrayItem(GetType(RegistrarTabletHostnamePair))> Public TabletDesignations As ArrayList
    End Class

    <Serializable()> Public Structure RegistrarTabletHostnamePair
        Public RegistrarHostname As String
        Public TabletHostname As String
    End Structure

    Public Class SystemCoordinator
        Inherits MarshalByRefObject
        Implements ISelectedDeploymentProvider
        Implements ISelectedDeploymentChanger
        Implements ISystemCoordinator


        Private Shared smIsInitialized As Boolean

        Private Shared smSelectedDeployment As DeploymentEntity
        Private Shared smSelectedDeploymentChangedBroadcaster As New RemotingEventBroadcaster

        Private Shared smTabletDesignations As ArrayList

        Private Shared smKeepAlives As New Hashtable

        Public Const KeepAliveWatchdogFrequency As Double = 10000.0
        Public Const ClientKeepAliveFrequency As Double = 4000.0

        Private Shared WithEvents smKeepAliveWatchDog As Timers.Timer

        Private Shared smSerializer As New XmlSerializer(GetType(SystemCoordinatorSettings))

        Public Sub New()
            If Not smIsInitialized Then Initialize()
        End Sub

        Private Shared Sub Initialize()

            Try

                Dim sr As New StreamReader("current_settings.xml")
                Dim settings As SystemCoordinatorSettings = DirectCast(smSerializer.Deserialize(sr), SystemCoordinatorSettings)
                BindFromSettingsToSelf(settings)
                sr.Close()

                smKeepAliveWatchDog = New Timers.Timer(KeepAliveWatchdogFrequency)
                smKeepAliveWatchDog.Start()

                smIsInitialized = True

            Catch ex As FileNotFoundException
                Console.WriteLine("No settings file (current_settings.xml) found.")
            End Try

        End Sub

        Public Shared Sub SaveCurrentSettings()
            Dim sw As StreamWriter
            Try
                Dim settings As New SystemCoordinatorSettings
                BindFromSelfToSettings(settings)
                sw = New StreamWriter("current_settings.xml")
                smSerializer.Serialize(sw, settings)
            Catch ex As Exception
                Mbark.UI.PrettyPrintException(ex)
            Finally
                sw.Close()
            End Try

        End Sub

        Private Shared Sub BindFromSettingsToSelf(ByVal settings As SystemCoordinatorSettings)
            smSelectedDeployment = New DeploymentEntity(settings.DeploymentId)

            ' FIXME: Make sure  the deployment is actually in the db

            If settings.TabletDesignations.Count = 0 Then
                Dim pair As New RegistrarTabletHostnamePair
                pair.TabletHostname = Net.Dns.GetHostByName("localhost").HostName
                pair.RegistrarHostname = Net.Dns.GetHostByName("localhost").HostName
                settings.TabletDesignations.Add(pair)
            End If
            smTabletDesignations = settings.TabletDesignations
            Console.WriteLine("Selected deployment = " & smSelectedDeployment.ToString)
        End Sub

        Private Shared Sub BindFromSelfToSettings(ByVal settings As SystemCoordinatorSettings)
            settings.DeploymentId = smSelectedDeployment.DeploymentId
            settings.TabletDesignations = smTabletDesignations
            'For i As Integer = 0 To smTabletDesignations.Count - 1
            '    Dim pair As RegistrarTabletHostnamePair = DirectCast(smTabletDesignations(i), RegistrarTabletHostnamePair)
            '    DesignateTabletToRegistrarImplementation(pair.RegistrarHostname, pair.TabletHostname)
            'Next
        End Sub

        Public ReadOnly Property SelectedDeployment() As DeploymentEntity _
        Implements ISelectedDeploymentProvider.SelectedDeployment
            Get
                Return smSelectedDeployment
            End Get
        End Property

        Public Sub SelectDeployment(ByVal newDeployment As DeploymentEntity) _
        Implements ISelectedDeploymentChanger.SelectDeployment
            Dim oldDeployment As DeploymentEntity = smSelectedDeployment
            smSelectedDeployment = newDeployment
            SaveCurrentSettings()
            Console.WriteLine("Deployment selected")
            smSelectedDeploymentChangedBroadcaster.BroadcastRemotingEvent(Hostname, New SelectedDeploymentChangedEventArgs(oldDeployment, newDeployment))
        End Sub

        Public Shared ReadOnly Property RemoteSelectedDeploymentProvider() As ISelectedDeploymentProvider
            Get
                Return DirectCast(GetObject(GetType(ISelectedDeploymentProvider)), ISelectedDeploymentProvider)
            End Get
        End Property

        Public ReadOnly Property SelectedDeploymentChanged() As IBroadcaster _
        Implements ISelectedDeploymentProvider.SelectedDeploymentChanged
            Get
                Return smSelectedDeploymentChangedBroadcaster
            End Get
        End Property


        Private Shared smManagerArrivalOrDepartureBroadcaster As New RemotingEventBroadcaster
        Public ReadOnly Property ManagerArrivalOrDeparture() As Support.IBroadcaster Implements ISystemCoordinator.ManagerArrivalOrDeparture
            Get
                Return smManagerArrivalOrDepartureBroadcaster
            End Get
        End Property

        Private Class KeepAliveData
            Public Hostname As String
            Public KeepAlive As DateTime
            Public Category As ManagerCategory
            Public LatestState As Integer

            Public Sub New(ByVal hostname As String, ByVal category As ManagerCategory, ByVal latestState As Integer)
                Me.KeepAlive = DateTime.UtcNow
                Me.Hostname = hostname
                Me.Category = category
                Me.LatestState = latestState
            End Sub
        End Class

        Private Shared Function KeepAliveKey(ByVal hostname As String, ByVal category As ManagerCategory) As String
            Return hostname & "+" & category.ToString
        End Function

        Public Sub KeepAlive(ByVal hostname As String, ByVal category As ManagerCategory, ByVal latestState As Integer) _
        Implements ISystemCoordinator.KeepAlive

            Dim key As String = KeepAliveKey(hostname, category)
            If smKeepAlives(key) Is Nothing Then
                Console.WriteLine(key & " has no keepalive.")
                Arrive(hostname, category)
            End If

            With DirectCast(smKeepAlives(key), KeepAliveData)
                .KeepAlive = UtcNow
                .LatestState = latestState
            End With

            If HostnameExpired(hostname, category) Then Depart(hostname, category)

        End Sub

        Private Sub SignalArrivalOrDeparture(ByVal hostname As String, ByVal category As ManagerCategory, ByVal arrival As Boolean)
            Dim e As New ManagerArrivalOrDepartureEventArgs(hostname, category, arrival)
            smManagerArrivalOrDepartureBroadcaster.BroadcastRemotingEvent(hostname, e)
        End Sub


        Private Shared ReadOnly Property UtcNow() As DateTime
            Get
                Return DateTime.UtcNow
            End Get
        End Property

        Private Shared Function HostnameExpired(ByVal hostname As String, ByVal category As ManagerCategory) As Boolean
            Dim key As String = KeepAliveKey(hostname, category)

            ' The host has timedout if there isn't even an any in the keepalive table
            If smKeepAlives(key) Is Nothing Then Return True

            Dim data As KeepAliveData = DirectCast(smKeepAlives(key), KeepAliveData)
            Dim lastKeepAlive As DateTime = data.KeepAlive
            Dim now As DateTime = UtcNow()
            If now.Ticks - lastKeepAlive.Ticks >= KeepAliveWatchdogFrequency * TimeSpan.TicksPerMillisecond Then Return True
        End Function

        Private Shared Function Hostname() As String
            Return Net.Dns.GetHostName
        End Function

        Private Shared Sub smKeepAliveWatchDog_Elapsed(ByVal sender As Object, ByVal e As Timers.ElapsedEventArgs) _
        Handles smKeepAliveWatchDog.Elapsed
            DropExpiredClients()
        End Sub

        Private Shared Sub DropExpiredClients()

            Dim toDrop As New StringCollection

            Dim en As IEnumerator = smKeepAlives.Keys.GetEnumerator

            ' Get a list of clients to drop
            While en.MoveNext
                Dim key As String = DirectCast(en.Current, String)
                Dim data As KeepAliveData = DirectCast(smKeepAlives(key), KeepAliveData)
                If HostnameExpired(data.Hostname, data.Category) Then
                    smManagerArrivalOrDepartureBroadcaster.BroadcastRemotingEvent(data.Hostname, New ManagerArrivalOrDepartureEventArgs(data.Hostname, data.Category, False))
                    toDrop.Add(key)
                End If
            End While

            ' Drop those clients
            For i As Integer = 0 To toDrop.Count - 1
                Console.WriteLine(DateTime.Now & ": Dropping " & toDrop(i))
                smKeepAlives.Remove(toDrop(i))
            Next


        End Sub


        Public Sub Arrive(ByVal hostname As String, ByVal category As ManagerCategory) Implements ISystemCoordinator.Arrive

            Dim key As String = KeepAliveKey(hostname, category)
            If Not smKeepAlives.ContainsKey(key) Then
                SyncLock Me
                    smKeepAlives(key) = New KeepAliveData(hostname, category, -1)
                End SyncLock
                Console.WriteLine(key & " has arrived.")
                SignalArrivalOrDeparture(hostname, category, True)
            Else
                Console.WriteLine(key & " is already connected.")
                Throw New AlreadyConnectedException
            End If


        End Sub


        Public Sub Depart(ByVal hostname As String, ByVal category As ManagerCategory) Implements ISystemCoordinator.Depart

            Dim key As String = KeepAliveKey(hostname, category)
            If smKeepAlives.Contains(key) Then
                SyncLock Me
                    smKeepAlives.Remove(key)
                End SyncLock
                Console.WriteLine(key & " has departed.")
                SignalArrivalOrDeparture(hostname, category, False)
            End If

        End Sub

        Public ReadOnly Property LatestState(ByVal hostname As String, ByVal category As ManagerCategory) As Integer _
        Implements ISystemCoordinator.LatestState
            Get
                Dim key As String = KeepAliveKey(hostname, category)
                If smKeepAlives.ContainsKey(key) Then
                    Return DirectCast(smKeepAlives(key), KeepAliveData).LatestState
                Else
                    Return -1
                End If
            End Get
        End Property

        Public ReadOnly Property DesignatedRegistrar(ByVal tabletHostname As String) As String _
        Implements ISystemCoordinator.DesignatedRegistrar
            Get
                For i As Integer = 0 To smTabletDesignations.Count - 1
                    Dim pair As RegistrarTabletHostnamePair = DirectCast(smTabletDesignations(i), RegistrarTabletHostnamePair)
                    If pair.TabletHostname = tabletHostname Then Return pair.RegistrarHostname
                Next
                Return String.Empty
            End Get
        End Property

        Public ReadOnly Property DesignatedTablet(ByVal registrarHostname As String) As String _
        Implements ISystemCoordinator.DesignatedTablet
            Get
                For i As Integer = 0 To smTabletDesignations.Count - 1
                    Dim pair As RegistrarTabletHostnamePair = DirectCast(smTabletDesignations(i), RegistrarTabletHostnamePair)
                    If pair.RegistrarHostname = registrarHostname Then Return pair.TabletHostname
                Next
                Return String.Empty
            End Get
        End Property

        Public Sub DesignateTabletToRegistrar(ByVal registrarHostname As String, ByVal tabletHostname As String) _
        Implements ISystemCoordinator.DesignateTabletToRegistrar
            DesignateTabletToRegistrarImplementation(registrarHostname, tabletHostname)
        End Sub


        Private Shared Sub DesignateTabletToRegistrarImplementation(ByVal registrarHostname As String, ByVal tabletHostname As String)
            ' We separate the implementation since interface implementations cannot be shared
            Dim pair As New RegistrarTabletHostnamePair
            pair.RegistrarHostname = registrarHostname
            pair.TabletHostname = tabletHostname
            smTabletDesignations.Add(pair)
        End Sub

        Private Shared smImportantStateChangeBroadcaster As New RemotingEventBroadcaster
        Public ReadOnly Property ImportantStateChange() As Support.IBroadcaster _
        Implements ISystemCoordinator.ImportantStateChange
            Get
                Return smImportantStateChangeBroadcaster
            End Get
        End Property

        Public ReadOnly Property IsAlive(ByVal hostname As String, ByVal category As ManagerCategory) As Boolean _
        Implements ISystemCoordinator.IsAlive
            Get
                Return Not HostnameExpired(hostname, category)
            End Get
        End Property

        Private Shared smBiographicFormChangeBroadcaster As New RemotingEventBroadcaster
        Public ReadOnly Property BiographicFormChange() As Support.IBroadcaster Implements ISystemCoordinator.BiographicFormChange
            Get
                Return smBiographicFormChangeBroadcaster
            End Get
        End Property
    End Class

End Namespace