Option Strict On
Option Explicit On

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

Namespace Nexamas.UI.DataBinding

    ''' <summary>
    ''' Reflection-backed ViewModel source for simple property and command member binding.
    ''' </summary>
    ''' <remarks>
    ''' The source is intentionally small: property paths are read/written on demand, reflection lookups are cached per
    ''' session, and INotifyPropertyChanged is observed only to refresh already-registered endpoints. It does not walk UI trees or create controls.
    ''' </remarks>
    Friend NotInheritable Class MASViewModelBindingSource

        Private Shared ReadOnly MemberFlags As BindingFlags = BindingFlags.Instance Or BindingFlags.Public Or BindingFlags.NonPublic
        Private ReadOnly _source As Object
        Private ReadOnly _propertyCache As New Dictionary(Of String, PropertyInfo)(StringComparer.OrdinalIgnoreCase)
        Private ReadOnly _methodCache As New Dictionary(Of String, MethodInfo)(StringComparer.OrdinalIgnoreCase)
        Private _reflectionCacheHitCount As Integer
        Private _reflectionCacheMissCount As Integer

        Friend Sub New(source As Object, Optional sourceName As String = "")
            If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
            _source = source
            Me.SourceName = If(String.IsNullOrWhiteSpace(sourceName), source.GetType().Name, sourceName.Trim())
        End Sub

        Friend ReadOnly Property SourceName As String

        Friend ReadOnly Property Source As Object
            Get
                Return _source
            End Get
        End Property

        Friend ReadOnly Property Notifier As INotifyPropertyChanged
            Get
                Return TryCast(_source, INotifyPropertyChanged)
            End Get
        End Property

        Friend ReadOnly Property ReflectionCacheEntryCount As Integer
            Get
                Return _propertyCache.Count + _methodCache.Count
            End Get
        End Property

        Friend ReadOnly Property ReflectionCacheHitCount As Integer
            Get
                Return _reflectionCacheHitCount
            End Get
        End Property

        Friend ReadOnly Property ReflectionCacheMissCount As Integer
            Get
                Return _reflectionCacheMissCount
            End Get
        End Property

        Friend Function TryGetValue(sourcePath As String,
                                    ByRef value As Object,
                                    ByRef failureMessage As String) As Boolean
            Dim current As Object = Nothing
            Dim parts As String() = NormalizePath(sourcePath)
            If parts.Length = 0 Then
                value = Nothing
                failureMessage = "A source property path is required."
                Return False
            End If

            current = _source
            For Each part As String In parts
                If current Is Nothing Then
                    value = Nothing
                    failureMessage = "Source path resolved to Nothing before " & part & "."
                    Return False
                End If

                Dim prop As PropertyInfo = FindProperty(current.GetType(), part)
                If prop Is Nothing OrElse Not prop.CanRead Then
                    value = Nothing
                    failureMessage = "Source property was not found or is not readable: " & part
                    Return False
                End If

                Try
                    current = prop.GetValue(current, Nothing)
                Catch ex As Exception
                    value = Nothing
                    failureMessage = ex.Message
                    Return False
                End Try
            Next

            value = current
            failureMessage = String.Empty
            Return True
        End Function

        Friend Function TrySetValue(sourcePath As String,
                                    value As Object,
                                    ByRef failureMessage As String) As Boolean
            Dim owner As Object = Nothing
            Dim prop As PropertyInfo = Nothing
            If Not TryResolveOwnerAndProperty(sourcePath, owner, prop, failureMessage) Then Return False
            If prop Is Nothing OrElse Not prop.CanWrite Then
                failureMessage = "Source property is not writable: " & If(sourcePath, String.Empty)
                Return False
            End If

            Try
                prop.SetValue(owner, ConvertForProperty(value, prop.PropertyType), Nothing)
                failureMessage = String.Empty
                Return True
            Catch ex As Exception
                failureMessage = ex.Message
                Return False
            End Try
        End Function

        Friend Sub InvokeCommandAction(memberName As String,
                                       context As MASCommandExecutionContext)
            Dim failure As String = String.Empty
            If Not TryInvokeCommandAction(memberName, context, failure) Then
                Throw New InvalidOperationException(failure)
            End If
        End Sub

        Friend Function TryInvokeCommandAction(memberName As String,
                                               context As MASCommandExecutionContext,
                                               ByRef failureMessage As String) As Boolean
            Return TryInvokeCommandActionCore(memberName, context, failureMessage)
        End Function

        Friend Function EvaluateCanExecute(memberName As String,
                                           context As MASCommandExecutionContext) As Boolean
            If String.IsNullOrWhiteSpace(memberName) Then Return True

            Dim failure As String = String.Empty
            Dim value As Object = Nothing
            If TryGetValue(memberName, value, failure) Then
                Return ToBooleanValue(value)
            End If

            If TryInvokeCanExecuteMethod(memberName, context, value, failure) Then
                Return ToBooleanValue(value)
            End If

            Return False
        End Function

        Friend Shared Function SourcePathMatches(changedPropertyName As String,
                                                 sourcePath As String) As Boolean
            If String.IsNullOrWhiteSpace(changedPropertyName) Then Return True
            Dim parts As String() = NormalizePath(sourcePath)
            If parts.Length = 0 Then Return False
            Return String.Equals(parts(0), changedPropertyName.Trim(), StringComparison.OrdinalIgnoreCase)
        End Function

        Private Function TryInvokeCommandActionCore(memberName As String,
                                                    context As MASCommandExecutionContext,
                                                    ByRef failureMessage As String) As Boolean
            If String.IsNullOrWhiteSpace(memberName) Then
                failureMessage = "An execute member name is required."
                Return False
            End If

            Dim trimmed As String = memberName.Trim()
            Dim method As MethodInfo = FindMethod(trimmed)
            If method IsNot Nothing Then
                Try
                    Dim parameters As ParameterInfo() = method.GetParameters()
                    If parameters.Length = 0 Then
                        method.Invoke(_source, New Object() {})
                    ElseIf parameters.Length = 1 Then
                        method.Invoke(_source, New Object() {ConvertForParameter(context, parameters(0).ParameterType)})
                    Else
                        failureMessage = "Command execute method has too many parameters: " & trimmed
                        Return False
                    End If

                    failureMessage = String.Empty
                    Return True
                Catch ex As Exception
                    failureMessage = If(ex.InnerException IsNot Nothing, ex.InnerException.Message, ex.Message)
                    Return False
                End Try
            End If

            Dim delegateValue As Object = Nothing
            Dim getFailure As String = String.Empty
            If TryGetValue(trimmed, delegateValue, getFailure) Then
                Try
                    If TypeOf delegateValue Is Action Then
                        DirectCast(delegateValue, Action).Invoke()
                    ElseIf TypeOf delegateValue Is Action(Of MASCommandExecutionContext) Then
                        DirectCast(delegateValue, Action(Of MASCommandExecutionContext)).Invoke(context)
                    ElseIf TypeOf delegateValue Is Action(Of Object) Then
                        DirectCast(delegateValue, Action(Of Object)).Invoke(context)
                    Else
                        failureMessage = "Command execute member is not an Action delegate: " & trimmed
                        Return False
                    End If

                    failureMessage = String.Empty
                    Return True
                Catch ex As Exception
                    failureMessage = ex.Message
                    Return False
                End Try
            End If

            failureMessage = "Command execute member was not found: " & trimmed
            Return False
        End Function

        Private Function TryInvokeCanExecuteMethod(memberName As String,
                                                   context As MASCommandExecutionContext,
                                                   ByRef value As Object,
                                                   ByRef failureMessage As String) As Boolean
            Dim method As MethodInfo = FindMethod(memberName)
            If method Is Nothing Then
                failureMessage = "CanExecute member was not found: " & memberName
                value = Nothing
                Return False
            End If

            Try
                Dim parameters As ParameterInfo() = method.GetParameters()
                If parameters.Length = 0 Then
                    value = method.Invoke(_source, New Object() {})
                ElseIf parameters.Length = 1 Then
                    value = method.Invoke(_source, New Object() {ConvertForParameter(context, parameters(0).ParameterType)})
                Else
                    value = Nothing
                    failureMessage = "CanExecute method has too many parameters: " & memberName
                    Return False
                End If

                failureMessage = String.Empty
                Return True
            Catch ex As Exception
                value = Nothing
                failureMessage = If(ex.InnerException IsNot Nothing, ex.InnerException.Message, ex.Message)
                Return False
            End Try
        End Function

        Private Function TryResolveOwnerAndProperty(sourcePath As String,
                                                    ByRef owner As Object,
                                                    ByRef prop As PropertyInfo,
                                                    ByRef failureMessage As String) As Boolean
            Dim parts As String() = NormalizePath(sourcePath)
            If parts.Length = 0 Then
                owner = Nothing
                prop = Nothing
                failureMessage = "A source property path is required."
                Return False
            End If

            owner = _source
            For index As Integer = 0 To parts.Length - 2
                If owner Is Nothing Then
                    prop = Nothing
                    failureMessage = "Source path resolved to Nothing before " & parts(index) & "."
                    Return False
                End If

                Dim currentProperty As PropertyInfo = FindProperty(owner.GetType(), parts(index))
                If currentProperty Is Nothing OrElse Not currentProperty.CanRead Then
                    prop = Nothing
                    failureMessage = "Source property was not found or is not readable: " & parts(index)
                    Return False
                End If

                owner = currentProperty.GetValue(owner, Nothing)
            Next

            If owner Is Nothing Then
                prop = Nothing
                failureMessage = "Source owner resolved to Nothing."
                Return False
            End If

            prop = FindProperty(owner.GetType(), parts(parts.Length - 1))
            If prop Is Nothing Then
                failureMessage = "Source property was not found: " & parts(parts.Length - 1)
                Return False
            End If

            failureMessage = String.Empty
            Return True
        End Function

        Private Function FindMethod(memberName As String) As MethodInfo
            If String.IsNullOrWhiteSpace(memberName) Then Return Nothing

            Dim key As String = _source.GetType().FullName & "|M|" & memberName.Trim()
            Dim cached As MethodInfo = Nothing
            If _methodCache.TryGetValue(key, cached) Then
                _reflectionCacheHitCount += 1
                Return cached
            End If

            _reflectionCacheMissCount += 1
            cached = _source.GetType().GetMethods(MemberFlags).
                FirstOrDefault(Function(method) String.Equals(method.Name, memberName.Trim(), StringComparison.OrdinalIgnoreCase))
            If cached IsNot Nothing Then _methodCache(key) = cached
            Return cached
        End Function

        Private Function FindProperty(type As Type, propertyName As String) As PropertyInfo
            If type Is Nothing OrElse String.IsNullOrWhiteSpace(propertyName) Then Return Nothing

            Dim key As String = type.FullName & "|P|" & propertyName.Trim()
            Dim cached As PropertyInfo = Nothing
            If _propertyCache.TryGetValue(key, cached) Then
                _reflectionCacheHitCount += 1
                Return cached
            End If

            _reflectionCacheMissCount += 1
            cached = type.GetProperties(MemberFlags).
                FirstOrDefault(Function(prop) String.Equals(prop.Name, propertyName.Trim(), StringComparison.OrdinalIgnoreCase))
            If cached IsNot Nothing Then _propertyCache(key) = cached
            Return cached
        End Function

        Private Shared Function NormalizePath(sourcePath As String) As String()
            Return If(sourcePath, String.Empty).
                Split(New Char() {"."c}, StringSplitOptions.RemoveEmptyEntries).
                Select(Function(part) part.Trim()).
                Where(Function(part) part.Length > 0).
                ToArray()
        End Function

        Private Shared Function ConvertForParameter(value As Object, targetType As Type) As Object
            If targetType Is Nothing OrElse targetType Is GetType(Object) Then Return value
            If value IsNot Nothing AndAlso targetType.IsInstanceOfType(value) Then Return value
            Return ConvertForProperty(value, targetType)
        End Function

        Private Shared Function ConvertForProperty(value As Object, targetType As Type) As Object
            If targetType Is Nothing OrElse targetType Is GetType(Object) Then Return value

            Dim actualType As Type = If(Nullable.GetUnderlyingType(targetType), targetType)
            If value Is Nothing Then
                If actualType.IsValueType Then Return Activator.CreateInstance(actualType)
                Return Nothing
            End If

            If actualType.IsInstanceOfType(value) Then Return value
            If actualType.IsEnum Then Return [Enum].Parse(actualType, Convert.ToString(value, CultureInfo.InvariantCulture), True)
            Return Convert.ChangeType(value, actualType, CultureInfo.InvariantCulture)
        End Function

        Private Shared Function ToBooleanValue(value As Object) As Boolean
            If value Is Nothing Then Return False
            If TypeOf value Is Boolean Then Return DirectCast(value, Boolean)
            Return Convert.ToBoolean(value, CultureInfo.InvariantCulture)
        End Function

    End Class

End Namespace
