Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Linq
Imports Nexamas.UI.CommandActions

Namespace Nexamas.UI.DataBinding

    ''' <summary>
    ''' Small internal ViewModel binding session for Text, Enabled, Selection, Lists, and Commands.
    ''' </summary>
    ''' <remarks>
    ''' The session owns binding records, ViewModel notification subscription, reentrant update guards, command leases,
    ''' and lifecycle evidence only. It does not create controls, render UI, scan forms, schedule frames, mutate layout policy, or replace CommandActionSystem.
    ''' </remarks>
    Friend NotInheritable Class MASViewModelBinder
        Implements IDisposable

        Friend Const OfficialSystemName As String = "Nexamas UI Data Binding / ViewModel Binding Foundation"

        Private ReadOnly _source As MASViewModelBindingSource
        Private ReadOnly _bindings As New List(Of BindingSlot)()
        Private ReadOnly _commandBindings As New List(Of MASViewModelCommandBindingRecord)()
        Private ReadOnly _commandLeases As New List(Of MASViewModelCommandBindingLease)()
        Private ReadOnly _lifecycleRecords As New List(Of MASViewModelBindingLifecycleRecord)()
        Private _sourceNotificationActive As Boolean
        Private _refreshDepth As Integer
        Private _pushDepth As Integer
        Private _blockedReentrantRefreshCount As Integer
        Private _blockedReentrantPushCount As Integer
        Private _disposed As Boolean

        Private Sub New(source As Object, sourceName As String)
            _source = New MASViewModelBindingSource(source, sourceName)
            _lifecycleRecords.Add(New MASViewModelBindingLifecycleRecord(MASViewModelBindingLifecycleStatus.Active, "Binding session is active."))
            If _source.Notifier IsNot Nothing Then
                AddHandler _source.Notifier.PropertyChanged, AddressOf OnSourcePropertyChanged
                _sourceNotificationActive = True
                _lifecycleRecords.Add(New MASViewModelBindingLifecycleRecord(MASViewModelBindingLifecycleStatus.SourceNotificationSubscribed, "ViewModel property notification is subscribed."))
            Else
                _lifecycleRecords.Add(New MASViewModelBindingLifecycleRecord(MASViewModelBindingLifecycleStatus.EvidenceOnly, "ViewModel does not expose INotifyPropertyChanged; no subscription was required."))
            End If
        End Sub

        Friend Shared Function Create(source As Object, Optional sourceName As String = "") As MASViewModelBinder
            Return New MASViewModelBinder(source, sourceName)
        End Function

        Friend ReadOnly Property BindingCount As Integer
            Get
                Return _bindings.Count
            End Get
        End Property

        Friend ReadOnly Property CommandBindingCount As Integer
            Get
                Return _commandBindings.Count
            End Get
        End Property

        Friend Function Bind(sourcePath As String,
                             target As MASViewModelBindingTarget,
                             Optional mode As MASViewModelBindingMode = MASViewModelBindingMode.OneWayToTarget) As MASViewModelBinder
            EnsureNotDisposed()
            If target Is Nothing Then
                _bindings.Add(New BindingSlot(sourcePath, Nothing, mode, MASViewModelBindingStatus.MissingTarget, "A binding target is required."))
                Return Me
            End If

            If target.Kind = MASViewModelBindingKind.Command Then
                _bindings.Add(New BindingSlot(sourcePath, target, mode, MASViewModelBindingStatus.Failed, "Use BindCommand for command bindings."))
                Return Me
            End If

            If String.IsNullOrWhiteSpace(sourcePath) Then
                _bindings.Add(New BindingSlot(sourcePath, target, mode, MASViewModelBindingStatus.MissingSourcePath, "A source property path is required."))
                Return Me
            End If

            If Not target.CanWrite Then
                _bindings.Add(New BindingSlot(sourcePath, target, mode, MASViewModelBindingStatus.MissingTargetWriter, "The binding target does not expose a writer."))
                Return Me
            End If

            _bindings.Add(New BindingSlot(sourcePath, target, mode, MASViewModelBindingStatus.Ready, String.Empty))
            RefreshBinding(_bindings(_bindings.Count - 1))
            Return Me
        End Function

        Friend Function BindText(sourcePath As String,
                                 target As MASViewModelBindingTarget,
                                 Optional mode As MASViewModelBindingMode = MASViewModelBindingMode.OneWayToTarget) As MASViewModelBinder
            Return Bind(sourcePath, target, mode)
        End Function

        Friend Function BindEnabled(sourcePath As String,
                                    target As MASViewModelBindingTarget,
                                    Optional mode As MASViewModelBindingMode = MASViewModelBindingMode.OneWayToTarget) As MASViewModelBinder
            Return Bind(sourcePath, target, mode)
        End Function

        Friend Function BindSelection(sourcePath As String,
                                      target As MASViewModelBindingTarget,
                                      Optional mode As MASViewModelBindingMode = MASViewModelBindingMode.OneWayToTarget) As MASViewModelBinder
            Return Bind(sourcePath, target, mode)
        End Function

        Friend Function BindList(sourcePath As String,
                                 target As MASViewModelBindingTarget,
                                 Optional mode As MASViewModelBindingMode = MASViewModelBindingMode.OneWayToTarget) As MASViewModelBinder
            Return Bind(sourcePath, target, mode)
        End Function

        Friend Function BindCommand(registry As MASCommandRegistry,
                                    commandId As String,
                                    text As String,
                                    executeMemberName As String,
                                    Optional canExecuteMemberName As String = "",
                                    Optional shortcut As MASCommandShortcutGesture = Nothing,
                                    Optional category As String = "",
                                    Optional description As String = "") As MASViewModelBinder
            EnsureNotDisposed()
            If registry Is Nothing Then
                _commandBindings.Add(New MASViewModelCommandBindingRecord(commandId, executeMemberName, canExecuteMemberName, MASViewModelBindingStatus.MissingCommandRegistry, "A CommandActionSystem registry is required."))
                Return Me
            End If

            If String.IsNullOrWhiteSpace(commandId) OrElse String.IsNullOrWhiteSpace(executeMemberName) Then
                _commandBindings.Add(New MASViewModelCommandBindingRecord(commandId, executeMemberName, canExecuteMemberName, MASViewModelBindingStatus.MissingSourcePath, "A command id and execute member are required."))
                Return Me
            End If

            Dim id As String = MASCommandRegistry.NormalizeCommandId(commandId)
            Dim existingLease As MASViewModelCommandBindingLease = _commandLeases.FirstOrDefault(Function(existingCommandLease) String.Equals(existingCommandLease.CommandId, id, StringComparison.OrdinalIgnoreCase) AndAlso Not existingCommandLease.IsReleased)
            If existingLease IsNot Nothing Then
                _commandBindings.Add(New MASViewModelCommandBindingRecord(id, executeMemberName, canExecuteMemberName, MASViewModelBindingStatus.Failed, "A command binding already exists for this session command id.", MASViewModelBindingLifecycleStatus.CommandRegistered, usesWeakSourceReference:=True, canRelease:=True))
                Return Me
            End If

            Dim lease As New MASViewModelCommandBindingLease(registry, _source, id, executeMemberName, canExecuteMemberName)

            registry.Register(
                id,
                text,
                Sub(context As MASCommandExecutionContext)
                    lease.Execute(context)
                End Sub,
                Function(context As MASCommandExecutionContext) lease.CanExecute(context),
                shortcut,
                category,
                description)

            _commandLeases.Add(lease)
            _commandBindings.Add(New MASViewModelCommandBindingRecord(id, executeMemberName, canExecuteMemberName, MASViewModelBindingStatus.Ready, String.Empty, MASViewModelBindingLifecycleStatus.CommandRegistered, usesWeakSourceReference:=True, canRelease:=True))
            Return Me
        End Function

        Friend Function RefreshAll() As Integer
            EnsureNotDisposed()
            If Not TryEnterRefresh() Then Return 0

            Try
                Dim refreshed As Integer = 0
                For Each slot As BindingSlot In _bindings
                    If RefreshBindingCore(slot) Then refreshed += 1
                Next

                Return refreshed
            Finally
                ExitRefresh()
            End Try
        End Function

        Friend Function RefreshPath(sourcePath As String) As Integer
            EnsureNotDisposed()
            If Not TryEnterRefresh() Then Return 0

            Try
                Dim refreshed As Integer = 0
                For Each slot As BindingSlot In _bindings
                    If MASViewModelBindingSource.SourcePathMatches(sourcePath, slot.SourcePath) AndAlso RefreshBindingCore(slot) Then refreshed += 1
                Next

                Return refreshed
            Finally
                ExitRefresh()
            End Try
        End Function

        Friend Function PushTargetValuesToSource() As Integer
            EnsureNotDisposed()
            If Not TryEnterPush() Then Return 0

            Try
                Dim updated As Integer = 0
                For Each slot As BindingSlot In _bindings
                    If slot.Mode <> MASViewModelBindingMode.TwoWay Then Continue For
                    If slot.Target Is Nothing Then
                        slot.UpdateStatus(MASViewModelBindingStatus.MissingTarget, "A binding target is required.")
                        Continue For
                    End If
                    If Not slot.Target.CanRead Then
                        slot.UpdateStatus(MASViewModelBindingStatus.MissingTargetReader, "The binding target does not expose a reader.")
                        Continue For
                    End If

                    Dim targetValue As Object = Nothing
                    Dim failure As String = String.Empty
                    If Not slot.Target.TryRead(targetValue, failure) Then
                        slot.UpdateStatus(MASViewModelBindingStatus.Failed, failure)
                        Continue For
                    End If

                    If _source.TrySetValue(slot.SourcePath, targetValue, failure) Then
                        slot.UpdateStatus(MASViewModelBindingStatus.Ready, String.Empty)
                        updated += 1
                    Else
                        slot.UpdateStatus(MASViewModelBindingStatus.Failed, failure)
                    End If
                Next

                Return updated
            Finally
                ExitPush()
            End Try
        End Function

        Friend Function CreateSnapshot() As MASViewModelBindingSnapshot
            Dim bindingRecords As MASViewModelBindingRecord() = _bindings.Select(Function(slot) slot.ToRecord()).ToArray()
            Return New MASViewModelBindingSnapshot(_source.SourceName, bindingRecords, _commandBindings.ToArray(), CreateLifecycleSnapshot())
        End Function

        Friend Function CreateLifecycleSnapshot() As MASViewModelBindingLifecycleSnapshot
            Dim records As New List(Of MASViewModelBindingLifecycleRecord)()
            records.AddRange(_lifecycleRecords)
            records.Add(New MASViewModelBindingLifecycleRecord(If(_disposed, MASViewModelBindingLifecycleStatus.Disposed, MASViewModelBindingLifecycleStatus.Active), If(_disposed, "Binding session has been disposed.", "Binding session is active.")))
            records.Add(New MASViewModelBindingLifecycleRecord(If(_sourceNotificationActive, MASViewModelBindingLifecycleStatus.SourceNotificationSubscribed, MASViewModelBindingLifecycleStatus.SourceNotificationUnsubscribed), If(_sourceNotificationActive, "ViewModel property notification is currently subscribed.", "ViewModel property notification is detached or not required.")))
            records.Add(New MASViewModelBindingLifecycleRecord(MASViewModelBindingLifecycleStatus.EvidenceOnly, "Refresh and push calls are protected by reentrant update guards.", isLoopGuardEvidence:=True))
            records.Add(New MASViewModelBindingLifecycleRecord(MASViewModelBindingLifecycleStatus.EvidenceOnly, "ViewModel reflection lookup cache is owned by the binding source session.", isReflectionCacheEvidence:=True))
            records.AddRange(_commandLeases.Select(Function(commandLease) commandLease.ToRecord()))

            Return New MASViewModelBindingLifecycleSnapshot(records.ToArray(), _bindings.Count, _commandBindings.Count, _disposed, _sourceNotificationActive, _blockedReentrantRefreshCount, _blockedReentrantPushCount, _source.ReflectionCacheEntryCount, _source.ReflectionCacheHitCount, _source.ReflectionCacheMissCount)
        End Function

        Private Function RefreshBinding(slot As BindingSlot) As Boolean
            If Not TryEnterRefresh() Then Return False

            Try
                Return RefreshBindingCore(slot)
            Finally
                ExitRefresh()
            End Try
        End Function

        Private Function RefreshBindingCore(slot As BindingSlot) As Boolean
            If slot Is Nothing Then Return False
            If slot.Target Is Nothing Then
                slot.UpdateStatus(MASViewModelBindingStatus.MissingTarget, "A binding target is required.")
                Return False
            End If
            If Not slot.Target.CanWrite Then
                slot.UpdateStatus(MASViewModelBindingStatus.MissingTargetWriter, "The binding target does not expose a writer.")
                Return False
            End If

            If slot.Mode = MASViewModelBindingMode.OneTime AndAlso slot.HasAppliedOnce AndAlso slot.Status = MASViewModelBindingStatus.Ready Then
                Return False
            End If

            Dim value As Object = Nothing
            Dim failure As String = String.Empty
            If Not _source.TryGetValue(slot.SourcePath, value, failure) Then
                slot.UpdateStatus(MASViewModelBindingStatus.MissingSourcePath, failure)
                Return False
            End If

            If slot.Target.TryWrite(value, failure) Then
                slot.HasAppliedOnce = True
                slot.UpdateStatus(MASViewModelBindingStatus.Ready, String.Empty)
                Return True
            End If

            slot.UpdateStatus(MASViewModelBindingStatus.Failed, failure)
            Return False
        End Function

        Private Sub OnSourcePropertyChanged(sender As Object, e As PropertyChangedEventArgs)
            If _disposed Then Return
            If _refreshDepth > 0 OrElse _pushDepth > 0 Then
                RememberBlockedRefresh()
                Return
            End If

            Dim propertyName As String = If(e Is Nothing, String.Empty, e.PropertyName)
            RefreshPath(propertyName)
        End Sub

        Private Function TryEnterRefresh() As Boolean
            If _refreshDepth > 0 OrElse _pushDepth > 0 Then
                RememberBlockedRefresh()
                Return False
            End If

            _refreshDepth += 1
            Return True
        End Function

        Private Sub ExitRefresh()
            If _refreshDepth > 0 Then _refreshDepth -= 1
        End Sub

        Private Function TryEnterPush() As Boolean
            If _pushDepth > 0 Then
                _blockedReentrantPushCount += 1
                _lifecycleRecords.Add(New MASViewModelBindingLifecycleRecord(MASViewModelBindingLifecycleStatus.ReentrantPushBlocked, "A nested target-to-source push was blocked.", isLoopGuardEvidence:=True))
                Return False
            End If

            _pushDepth += 1
            Return True
        End Function

        Private Sub ExitPush()
            If _pushDepth > 0 Then _pushDepth -= 1
        End Sub

        Private Sub RememberBlockedRefresh()
            _blockedReentrantRefreshCount += 1
            _lifecycleRecords.Add(New MASViewModelBindingLifecycleRecord(MASViewModelBindingLifecycleStatus.ReentrantRefreshBlocked, "A nested source-to-target refresh was blocked.", isLoopGuardEvidence:=True))
        End Sub

        Private Sub EnsureNotDisposed()
            If _disposed Then Throw New ObjectDisposedException(NameOf(MASViewModelBinder))
        End Sub

        Friend Sub Dispose()
            If _disposed Then Return

            If _source.Notifier IsNot Nothing AndAlso _sourceNotificationActive Then
                RemoveHandler _source.Notifier.PropertyChanged, AddressOf OnSourcePropertyChanged
                _sourceNotificationActive = False
                _lifecycleRecords.Add(New MASViewModelBindingLifecycleRecord(MASViewModelBindingLifecycleStatus.SourceNotificationUnsubscribed, "ViewModel property notification was unsubscribed."))
            End If

            For Each lease As MASViewModelCommandBindingLease In _commandLeases.ToArray()
                lease.Release()
            Next

            _disposed = True
            _lifecycleRecords.Add(New MASViewModelBindingLifecycleRecord(MASViewModelBindingLifecycleStatus.Disposed, "Binding session disposed; command leases released."))
        End Sub

        Private Sub DisposeAsIDisposable() Implements IDisposable.Dispose
            Dispose()
        End Sub

        Private NotInheritable Class BindingSlot
            Friend Sub New(sourcePath As String,
                           target As MASViewModelBindingTarget,
                           mode As MASViewModelBindingMode,
                           status As MASViewModelBindingStatus,
                           message As String)
                Me.SourcePath = If(sourcePath, String.Empty).Trim()
                Me.Target = target
                Me.Mode = mode
                Me.Status = status
                Me.Message = If(message, String.Empty)
            End Sub

            Friend ReadOnly Property SourcePath As String
            Friend ReadOnly Property Target As MASViewModelBindingTarget
            Friend ReadOnly Property Mode As MASViewModelBindingMode
            Friend Property Status As MASViewModelBindingStatus
            Friend Property Message As String
            Friend Property HasAppliedOnce As Boolean

            Friend Sub UpdateStatus(status As MASViewModelBindingStatus, message As String)
                Me.Status = status
                Me.Message = If(message, String.Empty)
            End Sub

            Friend Function ToRecord() As MASViewModelBindingRecord
                Return New MASViewModelBindingRecord(
                    If(Target Is Nothing, MASViewModelBindingKind.Text, Target.Kind),
                    SourcePath,
                    If(Target Is Nothing, String.Empty, Target.TargetName),
                    Mode,
                    Status,
                    Message)
            End Function
        End Class

    End Class

End Namespace
