Imports System.Reflection
Imports System.Text.RegularExpressions
Imports DigitalData.Modules.Logging

Public Class PropertyValues
    Private _logger As Logger
    Private _logConfig As LogConfig

    Private _indexPattern = "\((\d+)\)"
    Private _indexRegex As New Regex(_indexPattern)

    Public Sub New(LogConfig As LogConfig)
        _logConfig = LogConfig
        _logger = LogConfig.GetLogger()
    End Sub

    Public Class CheckPropertyValuesResult
        Public MissingProperties As New List(Of String)
        Public ValidProperties As New List(Of ValidProperty)
    End Class

    Public Class ValidProperty
        Public MessageId As String
        Public TableName As String
        Public TableColumn As String

        Public ISRequired As Boolean
        Public GroupCounter As Integer = -1

        Public Description As String
        Public Value As String
    End Class

    Public Function CheckPropertyValues(Document As Object, PropertyMap As Dictionary(Of String, XmlItemProperty), MessageId As String) As CheckPropertyValuesResult
        Dim oGlobalGroupCounter = 0
        Dim oMissingProperties As New List(Of String)
        Dim oResult As New CheckPropertyValuesResult()

        ' PropertyMap items with `IsGrouped = False` are handled normally
        Dim oDefaultProperties As Dictionary(Of String, XmlItemProperty) = PropertyMap.
            Where(Function(Item) Item.Value.IsGrouped = False).
            ToDictionary(Function(Item) Item.Key,
                         Function(Item) Item.Value)

        _logger.Debug("Found {0} default properties.", oDefaultProperties.Count)

        ' PropertyMap items with `IsGrouped = True` are grouped by group scope
        Dim oGroupedProperties = PropertyMap.
            Where(Function(Item) Item.Value.IsGrouped = True).
            ToLookup(Function(Item) Item.Value.GroupScope, ' Lookup key is group scope
                     Function(Item) Item)

        _logger.Debug($"Found [{PropertyMap.Count - oDefaultProperties.Count}] properties grouped in [{oGroupedProperties.Count}] group(s)")
        ' Iterate through groups to get group scope and group items
        For Each oGroup In oGroupedProperties
            Dim oGroupScope As String = oGroup.Key

            Dim oPropertyList As New Dictionary(Of XmlItemProperty, List(Of Object))
            Dim oRowCount = 0

            _logger.Debug($"Fetching Property values for group [{oGroupScope}].")

            ' get properties as a nested object, see `oPropertyList`
            For Each oProperty As KeyValuePair(Of String, XmlItemProperty) In oGroup
                Dim oPropertyValues As List(Of Object)
                _logger.Debug($"Fetching value for itemSpecification [{oProperty.Value.TableColumn}].")
                Try
                    oPropertyValues = GetPropValue(Document, oProperty.Key)
                Catch ex As Exception
                    _logger.Warn($"{MessageId} - Unknown error occurred while fetching property/TColumn [{0}] in group [{1}]:", oProperty.Value.TableColumn, oGroupScope)
                    _logger.Error(ex)
                    oPropertyValues = New List(Of Object)
                End Try

                ' Flatten result value
                oPropertyValues = GetFinalPropValue(oPropertyValues)

                ' Add to list
                oPropertyList.Add(oProperty.Value, oPropertyValues)

                ' check the first batch of values to determine the row count
                If oRowCount = 0 Then
                    oRowCount = oPropertyValues.Count
                End If
            Next

            ' Structure of oPropertyList
            ' [ # Propertyname       # Row 1              # Row 2
            '   PositionsMenge:      [BilledQuantity1,    BilledQuantity2, ...],
            '   PositionsSteuersatz: [ApplicablePercent1, ApplicablePercent2, ...],
            '   ...
            ' ]
            For oRowIndex = 0 To oRowCount - 1
                _logger.Debug("Processing row {0}", oRowIndex)

                For Each oColumn As KeyValuePair(Of XmlItemProperty, List(Of Object)) In oPropertyList
                    Dim oTableName As String = oColumn.Key.TableName
                    Dim oTableColumn As String = oColumn.Key.TableColumn
                    Dim oIsRequired As Boolean = oColumn.Key.IsRequired
                    Dim oPropertyDescription As String = oColumn.Key.Description
                    Dim oRowCounter = oRowIndex + oGlobalGroupCounter + 1
                    If IsNothing(oRowCounter) Then

                    End If
                    ' Returns nothing if oColumn.Value contains an empty list
                    Dim oPropertyValue = oColumn.Value.ElementAtOrDefault(oRowIndex)

                    _logger.Debug("Processing itemSpecification *TableColumn* [{0}].", oTableColumn)

                    If IsNothing(oPropertyValue) OrElse String.IsNullOrEmpty(oPropertyValue) Then
                        If oColumn.Key.IsRequired Then
                            _logger.Warn($"{MessageId} # oPropertyValue for specification [{oTableColumn}] is empty or not found but is required. Continuing with Empty String.")
                            oResult.MissingProperties.Add(oPropertyDescription)
                        Else
                            _logger.Debug($"{MessageId} # oPropertyValue for specification [{oTableColumn}] is empty or not found. Continuing with Empty String.")
                        End If

                        oPropertyValue = String.Empty
                    End If

                    _logger.Debug("ItemSpecification [{0}] has value '{1}'", oTableColumn, oPropertyValue)

                    oResult.ValidProperties.Add(New ValidProperty() With {
                        .MessageId = MessageId,
                        .Description = oPropertyDescription,
                        .Value = oPropertyValue,
                        .GroupCounter = oRowCounter,
                        .TableName = oTableName,
                        .TableColumn = oTableColumn,
                        .ISRequired = oIsRequired
                    })
                Next
            Next

            oGlobalGroupCounter += oRowCount
        Next

        ' Iterate through default properties
        For Each oItem As KeyValuePair(Of String, XmlItemProperty) In oDefaultProperties
            Dim oPropertyValueList As List(Of Object)
            Dim oTableColumn As String = oItem.Value.TableColumn
            Dim oPropertyValue As Object = Nothing
            Dim oTableName = oItem.Value.TableName
            Dim oIsRequired = oItem.Value.IsRequired
            Dim oDescription = oItem.Value.Description
            Try
                oPropertyValueList = GetPropValue(Document, oItem.Key)
            Catch ex As Exception
                _logger.Warn($"{MessageId} # Unknown error occurred while fetching specification {0} in group {1}:", oTableColumn, oItem.Value.GroupScope)
                _logger.Error(ex)
                oPropertyValueList = New List(Of Object)
            End Try

            Try
                If IsNothing(oPropertyValueList) Then
                    oPropertyValue = Nothing
                ElseIf TypeOf oPropertyValueList Is List(Of Object) Then
                    Select Case oPropertyValueList.Count
                        Case 0
                            oPropertyValue = Nothing
                        Case Else
                            Dim oList As List(Of Object) = DirectCast(oPropertyValueList, List(Of Object))
                            oPropertyValue = oList.Item(0)

                            ' This should hopefully show config errors
                            If TypeOf oPropertyValue Is List(Of Object) Then
                                _logger.Warn("Item with TableColumn [{0}] may be configured incorrectly", oTableColumn)
                                oPropertyValue = Nothing
                            End If
                    End Select
                End If
            Catch ex As Exception
                _logger.Warn("Unknown error occurred while processing specification [{0}]:", oTableColumn)
                _logger.Error(ex)
                oPropertyValue = Nothing
            End Try

            If IsNothing(oPropertyValue) OrElse String.IsNullOrEmpty(oPropertyValue) Then
                If oItem.Value.IsRequired Then
                    _logger.Warn($"{MessageId} # Specification [{oTableColumn}] is empty, but marked as required! Skipping.")
                    oResult.MissingProperties.Add(oTableColumn)
                    Continue For
                Else
                    _logger.Debug($"{MessageId} # oPropertyValue for specification [{oTableColumn}] is empty or not found. Skipping.")

                    Continue For
                End If
            End If

            oResult.ValidProperties.Add(New ValidProperty() With {
                .MessageId = MessageId,
                .Description = oDescription,
                .Value = oPropertyValue,
                .TableName = oTableName,
                .TableColumn = oTableColumn,
                .ISRequired = oIsRequired
             })
        Next

        Return oResult
    End Function

    Public Function GetPropValue(Obj As Object, PropertyName As String) As List(Of Object)
        Dim oNameParts As String() = PropertyName.Split("."c)

        If IsNothing(Obj) Then
            _logger.Debug("`Obj` is Nothing. Exiting.")
            Return New List(Of Object)
        End If


        If oNameParts.Length = 1 Then
            Dim oPropInfo As PropertyInfo = Obj.GetType().GetProperty(PropertyName)

            If IsNothing(oPropInfo) Then
                _logger.Debug("Property [{0}] does not exist(1).", PropertyName)
                Return New List(Of Object)
            Else
                Dim oPropValue = oPropInfo.GetValue(Obj, Nothing)
                Return New List(Of Object) From {oPropValue}
            End If
        End If

        For Each oPart As String In oNameParts
            Dim oType As Type = Obj.GetType()
            Dim oPartName = oPart
            Dim oIndex As Integer = Nothing
            Dim oHasIndex As Boolean = HasIndex(oPartName)

            If oHasIndex Then
                oPartName = StripIndex(oPart)
                oIndex = GetIndex(oPart)
            End If

            Dim oInfo As PropertyInfo = oType.GetProperty(oPartName)

            If IsNothing(oInfo) OrElse IsNothing(oInfo.GetValue(Obj, Nothing)) Then
                _logger.Debug("Property {0} does not exist(2).", oPartName)
                Return New List(Of Object)
            End If

            Obj = oInfo.GetValue(Obj, Nothing)

            If oHasIndex Then
                Obj = Obj(0)
            End If

            If IsArray(Obj) And Not oHasIndex Then
                Dim oCurrentPart As String = oPart
                Dim oSplitString As String() = New String() {oCurrentPart & "."}
                Dim oPathFragments = PropertyName.Split(oSplitString, StringSplitOptions.None)
                Dim oResults As New List(Of Object)

                ' if path has no more subitems, return an empty list
                If oPathFragments.Length = 1 Then
                    Return oResults
                End If

                For Each oArrayItem In Obj
                    Dim oResult As List(Of Object) = GetPropValue(oArrayItem, oPathFragments(1))

                    If Not IsNothing(oResult) Then
                        oResults.Add(oResult)
                    End If
                Next

                Return oResults
            End If
        Next

        Return New List(Of Object) From {Obj}
    End Function

    Public Function GetFinalPropValue(List As List(Of Object)) As List(Of Object)
        Dim oResult As New List(Of Object)

        For Each Item In List
            Dim oItemValue = DoGetFinalPropValue(Item)

            If Not IsNothing(oItemValue) Then
                oResult.Add(oItemValue)
            End If
        Next

        Return oResult
    End Function

    Private Function DoGetFinalPropValue(Value As Object) As String
        If TypeOf Value Is List(Of Object) Then
            Dim oList = DirectCast(Value, List(Of Object))
            Dim oCount = oList.Count

            Select Case oCount
                Case 0
                    Return Nothing
                Case Else
                    Return DoGetFinalPropValue(oList.First())
            End Select

            Return DoGetFinalPropValue(Value)
        Else
            Return Value.ToString
        End If
    End Function

    Private Function GetIndex(Prop As String) As Integer
        If Regex.IsMatch(Prop, _indexPattern) Then
            Dim oMatch = _indexRegex.Match(Prop)
            Dim oGroup = oMatch.Groups.Item(1)
            Dim oValue = oGroup.Value

            Return Integer.Parse(oValue)
        End If

        Return Nothing
    End Function

    Private Function StripIndex(Prop As String) As String
        Return Regex.Replace(Prop, _indexPattern, "")
    End Function

    Private Function HasIndex(Prop As String) As Boolean
        Return Regex.IsMatch(Prop, _indexPattern)
    End Function

End Class
