Option Strict On
Option Explicit On

Imports System
Imports System.ComponentModel
Imports System.Globalization
Imports System.Reflection

Namespace Nexamas.UI.PropertyGrid

    ''' <summary>
    ''' Internal normalized property row. It may be manually supplied or bound to a public CLR property.
    ''' </summary>
    Friend NotInheritable Class MASPropertyGridPropertyState

        Private ReadOnly _target As Object
        Private ReadOnly _propertyInfo As PropertyInfo
        Private _value As Object
        Private _valueText As String

        Private Sub New(category As String,
                        name As String,
                        displayName As String,
                        value As Object,
                        valueType As Type,
                        kind As MASPropertyGridValueKind,
                        isEditable As Boolean,
                        target As Object,
                        propertyInfo As PropertyInfo)
            Me.Category = MASPropertyGridText.Normalize(category, "General")
            Me.Name = MASPropertyGridText.Normalize(name, "Property")
            Me.DisplayName = MASPropertyGridText.Normalize(displayName, Me.Name)
            _value = value
            Me.ValueType = If(valueType, If(value IsNot Nothing, value.GetType(), GetType(String)))
            Me.Kind = kind
            Me.IsEditable = isEditable
            _target = target
            _propertyInfo = propertyInfo
            _valueText = MASPropertyGridText.FormatValue(value)
        End Sub

        Friend ReadOnly Property Category As String
        Friend ReadOnly Property Name As String
        Friend ReadOnly Property DisplayName As String
        Friend ReadOnly Property ValueType As Type
        Friend ReadOnly Property Kind As MASPropertyGridValueKind
        Friend ReadOnly Property IsEditable As Boolean

        Friend ReadOnly Property Value As Object
            Get
                RefreshFromTarget()
                Return _value
            End Get
        End Property

        Friend ReadOnly Property ValueText As String
            Get
                RefreshFromTarget()
                Return _valueText
            End Get
        End Property

        Friend ReadOnly Property KindName As String
            Get
                Return Kind.ToString()
            End Get
        End Property

        Friend ReadOnly Property IsObjectBound As Boolean
            Get
                Return _target IsNot Nothing AndAlso _propertyInfo IsNot Nothing
            End Get
        End Property

        Friend Function TrySetValue(value As Object) As MASPropertyGridEditResult
            If Not IsEditable Then Return MASPropertyGridEditResult.Failed(Name, "Property is read-only.")

            Dim converted As Object = Nothing
            Dim message As String = String.Empty
            If Not TryConvertValue(value, converted, message) Then
                Return MASPropertyGridEditResult.Failed(Name, message)
            End If

            Return CommitValue(converted)
        End Function

        Friend Function TrySetValueText(text As String) As MASPropertyGridEditResult
            If Not IsEditable Then Return MASPropertyGridEditResult.Failed(Name, "Property is read-only.")

            Dim converted As Object = Nothing
            Dim message As String = String.Empty
            If Not TryConvertText(text, converted, message) Then
                Return MASPropertyGridEditResult.Failed(Name, message)
            End If

            Return CommitValue(converted)
        End Function

        Friend Shared Function CreateManual(category As String,
                                            name As String,
                                            value As Object,
                                            isEditable As Boolean) As MASPropertyGridPropertyState
            Dim valueType As Type = If(value IsNot Nothing, value.GetType(), GetType(String))
            Return New MASPropertyGridPropertyState(
                category,
                name,
                name,
                value,
                valueType,
                MASPropertyGridText.InferKind(valueType, value),
                isEditable,
                Nothing,
                Nothing)
        End Function

        Friend Shared Function CreateBound(target As Object,
                                           propertyInfo As PropertyInfo,
                                           fallbackCategory As String) As MASPropertyGridPropertyState
            If target Is Nothing Then Throw New ArgumentNullException(NameOf(target))
            If propertyInfo Is Nothing Then Throw New ArgumentNullException(NameOf(propertyInfo))

            Dim category As String = fallbackCategory
            Dim categoryAttribute As CategoryAttribute = TryCast(Attribute.GetCustomAttribute(propertyInfo, GetType(CategoryAttribute)), CategoryAttribute)
            If categoryAttribute IsNot Nothing Then category = categoryAttribute.Category

            Dim displayName As String = propertyInfo.Name
            Dim displayNameAttribute As DisplayNameAttribute = TryCast(Attribute.GetCustomAttribute(propertyInfo, GetType(DisplayNameAttribute)), DisplayNameAttribute)
            If displayNameAttribute IsNot Nothing Then displayName = displayNameAttribute.DisplayName

            Dim readOnlyAttribute As ReadOnlyAttribute = TryCast(Attribute.GetCustomAttribute(propertyInfo, GetType(ReadOnlyAttribute)), ReadOnlyAttribute)
            Dim isEditable As Boolean = propertyInfo.CanWrite AndAlso (readOnlyAttribute Is Nothing OrElse Not readOnlyAttribute.IsReadOnly)
            Dim value As Object = Nothing
            Try
                value = propertyInfo.GetValue(target, Nothing)
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(masCaughtException1)
            End Try

            Return New MASPropertyGridPropertyState(
                category,
                propertyInfo.Name,
                displayName,
                value,
                propertyInfo.PropertyType,
                MASPropertyGridText.InferKind(propertyInfo.PropertyType, value),
                isEditable,
                target,
                propertyInfo)
        End Function

        Private Sub RefreshFromTarget()
            If _target Is Nothing OrElse _propertyInfo Is Nothing OrElse Not _propertyInfo.CanRead Then Return

            Try
                _value = _propertyInfo.GetValue(_target, Nothing)
                _valueText = MASPropertyGridText.FormatValue(_value)
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(masCaughtException1)
            End Try
        End Sub

        Private Function CommitValue(converted As Object) As MASPropertyGridEditResult
            RefreshFromTarget()
            Dim oldValue As Object = _value

            If Object.Equals(oldValue, converted) Then
                _valueText = MASPropertyGridText.FormatValue(converted)
                Return MASPropertyGridEditResult.Unchanged(Name, converted)
            End If

            If _target IsNot Nothing AndAlso _propertyInfo IsNot Nothing Then
                Try
                    _propertyInfo.SetValue(_target, converted, Nothing)
                Catch ex As Exception
                    Return MASPropertyGridEditResult.Failed(Name, ex.Message)
                End Try
            End If

            _value = converted
            _valueText = MASPropertyGridText.FormatValue(converted)
            Return MASPropertyGridEditResult.Changed(Name, oldValue, converted)
        End Function

        Private Function TryConvertValue(value As Object, ByRef converted As Object, ByRef message As String) As Boolean
            If value Is Nothing Then
                converted = Nothing
                Return True
            End If

            Dim targetType As Type = Nullable.GetUnderlyingType(ValueType)
            If targetType Is Nothing Then targetType = ValueType
            If targetType Is Nothing OrElse targetType.IsInstanceOfType(value) Then
                converted = value
                Return True
            End If

            Return TryConvertText(MASPropertyGridText.FormatValue(value), converted, message)
        End Function

        Private Function TryConvertText(text As String, ByRef converted As Object, ByRef message As String) As Boolean
            Dim raw As String = If(text, String.Empty).Trim()
            Dim targetType As Type = Nullable.GetUnderlyingType(ValueType)
            If targetType Is Nothing Then targetType = ValueType
            If targetType Is Nothing Then targetType = GetType(String)

            Try
                If targetType Is GetType(String) Then
                    converted = raw
                    Return True
                End If

                If targetType Is GetType(Boolean) Then
                    Dim boolValue As Boolean
                    If Boolean.TryParse(raw, boolValue) Then
                        converted = boolValue
                        Return True
                    End If

                    Select Case raw.ToLowerInvariant()
                        Case "1", "yes", "y", "on", "checked", "true"
                            converted = True
                            Return True
                        Case "0", "no", "n", "off", "unchecked", "false"
                            converted = False
                            Return True
                    End Select

                    message = "Boolean value expected."
                    Return False
                End If

                If targetType.IsEnum Then
                    converted = [Enum].Parse(targetType, raw, True)
                    Return True
                End If

                If targetType Is GetType(DateTime) Then
                    Dim dateValue As DateTime
                    If DateTime.TryParse(raw, CultureInfo.InvariantCulture, DateTimeStyles.None, dateValue) Then
                        converted = dateValue
                        Return True
                    End If

                    message = "Date/time value expected."
                    Return False
                End If

                If MASPropertyGridText.IsNumericType(targetType) Then
                    converted = Convert.ChangeType(raw, targetType, CultureInfo.InvariantCulture)
                    Return True
                End If

                Dim converter As TypeConverter = TypeDescriptor.GetConverter(targetType)
                If converter IsNot Nothing AndAlso converter.CanConvertFrom(GetType(String)) Then
                    converted = converter.ConvertFromInvariantString(raw)
                    Return True
                End If

                message = "Property type is not editable from text."
                Return False
            Catch ex As Exception
                message = ex.Message
                Return False
            End Try
        End Function

    End Class

End Namespace
