Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.Windows.Forms
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Components.ListPopup
Imports Nexamas.UI.Components.ListRendering
Imports Nexamas.UI.Composition
Imports Nexamas.UI.Controls
Imports Nexamas.UI.FloatRuntime
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Motion
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Components

    Partial Public NotInheritable Class MASComboBox

#Region "Helpers"

        Private Function NormalizeSelectionIndex(value As Integer) As Integer
            If value < -1 OrElse value >= _items.Count Then Return -1
            Return value
        End Function

        Private Function GetKeyboardPageStep() As Integer
            Dim stepRows As Integer = FensterTokens.MaxVisibleRows

            If stepRows <= 0 Then
                stepRows = 5
            End If

            Return Math.Max(1, stepRows - 1)
        End Function

        Private Sub RaiseSelectionChangedSafe()
            Try
                RaiseEvent SelectionChanged(Me, EventArgs.Empty)
            Catch masCaughtException16 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException16, "MASComboBox.SelectionChanged")
            End Try
        End Sub

        Private Sub RaiseSelectionChangedIfEffectiveValueChanged(previousIndex As Integer,
                                                                 previousText As String)

            If previousIndex = _selectedIndex AndAlso
               String.Equals(If(previousText, String.Empty), SelectedText, StringComparison.Ordinal) Then

                Return
            End If

            RaiseSelectionChangedSafe()
        End Sub

        Private Sub SyncOpenDropdownSelection()
            If Not _isDropdownOpen Then Return
            If _ddService Is Nothing Then Return

            Dim content As MASDropDownListFloatContent =
                TryCast(_ddService.CurrentContent, MASDropDownListFloatContent)

            If content Is Nothing Then Return

            Try
                content.SyncSelectedIndexFromOwner(_selectedIndex)
            Catch masCaughtException17 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException17, "MASComboBox.SyncDropdownSelection")
            End Try
        End Sub

        Private Function IsCurrentDropdownHandleOpen() As Boolean
            Dim runtime As MASFloatRuntime = ResolveFloatRuntime()

            If runtime Is Nothing Then Return False
            If _ddService Is Nothing Then Return False
            If _ddService.CurrentHandle Is Nothing Then Return False

            Try
                Return runtime.IsOpen(_ddService.CurrentHandle)
            Catch masCaughtException18 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException18, "MASComboBox.VerifyDropdownHandle")
                Return False
            End Try
        End Function

        Private Function ResolveFloatRuntime() As MASFloatRuntime
            If _floatRuntime IsNot Nothing Then Return _floatRuntime

            If _comboRuntimeServices IsNot Nothing Then
                Return _comboRuntimeServices.TryGetFloatRuntime()
            End If

            Return Nothing
        End Function

        Private Function ResolveSurfaceSizeProvider() As Func(Of SKSizeI)
            Return Function() ResolveSurfaceSizePx()
        End Function

        Private Function ResolveSurfaceSizePx() As SKSizeI
            If SurfaceSizeProvider IsNot Nothing Then
                Try
                    Dim provided As SKSizeI = SurfaceSizeProvider.Invoke()

                    If provided.Width > 1 AndAlso provided.Height > 1 Then
                        Return provided
                    End If
                Catch masCaughtException19 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException19, "MASComboBox.SurfaceSizeProvider")
                End Try
            End If

            If _comboRuntimeServices IsNot Nothing Then
                Return _comboRuntimeServices.TryGetSurfaceSizePx()
            End If

            Return SKSizeI.Empty
        End Function

        Private Function CalculateDropdownSizePx(ctx As MASThemeContext,
                                                 dpi As Single,
                                                 anchorBoundsPx As SKRect) As SKSize

            dpi = PixelSnap.SafeDpi(dpi)

            Dim anchorWidthPx As Single = If(anchorBoundsPx.Width > 1.0F, anchorBoundsPx.Width, _lastPixelBounds.Width)
            Dim layoutPlan As MASComboBoxLayoutPlan = MASComboBoxLayoutPlan.Create(ctx, anchorBoundsPx, SizeIntent)
            Dim maxHeightPx As Single = If(layoutPlan Is Nothing, ComboBoxTokens.DropMaxHeightDip * dpi, layoutPlan.PopupMaxHeightPx)
            Dim metricDpi As Single = If(layoutPlan Is Nothing, dpi, layoutPlan.MetricDpi)
            Dim widthPx As Single = MASIntrinsicControlSizeMetrics.ResolveComboBoxDropdownPreferredWidthPx(
                anchorWidthPx:=anchorWidthPx,
                measuredItemTextWidthPx:=EstimatePreferredDropdownItemTextWidthPx(ctx),
                itemCount:=_items.Count,
                dpi:=dpi)

            Dim heightPx As Single =
                ListPopupLayout.EstimatePreferredHeightPx(
                    dpi:=dpi,
                    totalItems:=_items.Count,
                    maxHeightPx:=maxHeightPx,
                    minHeightPx:=If(layoutPlan Is Nothing, ComboBoxTokens.DropMinHeightDip * dpi, layoutPlan.PopupMinHeightPx),
                    metricDpi:=metricDpi)

            Return New SKSize(
                Math.Max(1.0F, widthPx),
                Math.Max(1.0F, heightPx))
        End Function

        Private Function EstimatePreferredDropdownItemTextWidthPx(ctx As MASThemeContext) As Single

            If ctx Is Nothing OrElse ctx.Typography Is Nothing Then
                Return 0.0F
            End If

            Using paint As SKPaint = ctx.Typography.CreatePaint(MASTypography.MASTextStyle.Body, SKColors.Black)
                If paint Is Nothing Then Return 0.0F

                PrepareMeasurementPaint(paint)

                Dim measuredTextWidth As Single = 0.0F
                Dim measured As Integer = 0

                For Each item As String In _items
                    measuredTextWidth =
                        Math.Max(
                            measuredTextWidth,
                            MASShapedText.MeasureWidth(If(item, String.Empty), paint))

                    measured += 1

                    If measured >= ComboBoxTokens.DropdownMaxMeasuredItems Then
                        Exit For
                    End If
                Next

                Return measuredTextWidth
            End Using
        End Function

        Private Shared Sub PrepareMeasurementPaint(paint As SKPaint)
            If paint Is Nothing Then Return

            paint.IsAntialias = True
            paint.IsDither = True
        End Sub

        Private Shared Function ResolveComboBoxMASInteractionState(isEnabled As Boolean,
                                                                   isPressed As Boolean,
                                                                   isOpen As Boolean,
                                                                   isHover As Boolean,
                                                                   hasFocus As Boolean) As MASInteractionState

            If Not isEnabled Then Return MASInteractionState.Disabled
            If isPressed Then Return MASInteractionState.Pressed

            ' ComboBox is registered as InteractionRole.Input.
            ' Opened is not legal for Input, so open state is visualized as Focused.
            If isOpen Then Return MASInteractionState.Focused
            If hasFocus Then Return MASInteractionState.Focused
            If isHover Then Return MASInteractionState.Hovered

            Return MASInteractionState.Enabled
        End Function

        Private Sub PulsePress()
            _pressed = True

            StopPressPulse(clearPressed:=False)
            _pressed = True

            Dim plan As MASMotionTransitionPlan = MASMotionSystem.CreateTransitionPlan(MASMotionTokenId.PressDown, 0.0F, 1.0F)

            _pressPulseRunner = MASMotionSystem.CreateTimelineRunner(
                owner:=Me,
                consumerName:="MASComboBox.PressPulse",
                plan:=plan,
                requestFrame:=AddressOf InvalidateVisual,
                applyFrame:=AddressOf ApplyPressPulseMotionFrame,
                completed:=AddressOf CompletePressPulseMotionFrame)
            _pressPulseRunner.Start()

            InvalidateVisual()
        End Sub

        Private Sub ApplyPressPulseMotionFrame(frame As MASMotionFrame)
            If frame Is Nothing Then Return
            InvalidateVisual()
        End Sub

        Private Sub CompletePressPulseMotionFrame(frame As MASMotionFrame)
            CompletePressPulse()
        End Sub

        Private Sub CompletePressPulse()
            StopPressPulse(clearPressed:=False)
            _pressed = False
            InvalidateVisual()
        End Sub

        Private Sub StopPressPulse(clearPressed As Boolean)
            If _pressPulseRunner IsNot Nothing Then
                _pressPulseRunner.Dispose()
                _pressPulseRunner = Nothing
            End If

            If clearPressed Then
                _pressed = False
            End If
        End Sub

        Private Function ApplyKeyboardTextSearch(text As String) As Boolean
            Dim typed As String = NormalizeKeyboardSearchInput(text)
            If typed.Length = 0 Then Return False
            If _items.Count <= 0 Then Return True

            Dim nowUtc As DateTime = Nexamas.UI.Timing.MASInteractionClock.UtcNow()
            If (nowUtc - _keyboardSearchLastUtc).TotalMilliseconds > ComboBoxTokens.KeyboardSearchResetMilliseconds Then
                _keyboardSearchPrefix = String.Empty
            End If

            _keyboardSearchLastUtc = nowUtc
            _keyboardSearchPrefix &= typed

            If SelectFirstMatchingText(_keyboardSearchPrefix) Then
                Return True
            End If

            If typed.Length < _keyboardSearchPrefix.Length AndAlso SelectFirstMatchingText(typed) Then
                _keyboardSearchPrefix = typed
                Return True
            End If

            Return True
        End Function

        Private Shared Function NormalizeKeyboardSearchInput(text As String) As String
            If String.IsNullOrEmpty(text) Then Return String.Empty

            Dim buffer As New System.Text.StringBuilder(text.Length)
            For Each ch As Char In text
                If Not Char.IsControl(ch) Then
                    buffer.Append(ch)
                End If
            Next

            Return buffer.ToString()
        End Function

        Private Function SelectFirstMatchingText(prefix As String) As Boolean
            If String.IsNullOrEmpty(prefix) Then Return False
            If _items.Count <= 0 Then Return False

            Dim startIndex As Integer = If(_selectedIndex >= 0, (_selectedIndex + 1) Mod _items.Count, 0)

            For offset As Integer = 0 To _items.Count - 1
                Dim index As Integer = (startIndex + offset) Mod _items.Count
                Dim candidate As String = If(_items(index), String.Empty)

                If candidate.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase) Then
                    SelectedIndex = index
                    Return True
                End If
            Next

            Return False
        End Function

        Private Sub ResetKeyboardSearch()
            _keyboardSearchPrefix = String.Empty
            _keyboardSearchLastUtc = DateTime.MinValue
        End Sub

        Private Sub CloseDropdownForItemMutation()
            If _isDropdownOpen Then
                CloseDropdownSafe()
            ElseIf _ddService IsNot Nothing AndAlso
                   (_ddService.CurrentHandle IsNot Nothing OrElse _ddService.CurrentContent IsNot Nothing) Then
                CloseDropdownSafe()
            End If
        End Sub

        Private Sub ResetInputAll()
            _hover = False
            ResetKeyboardSearch()
            StopPressPulse(clearPressed:=True)
        End Sub

#End Region

    End Class

End Namespace
