Option Strict On
Option Explicit On

Imports System
Imports System.Globalization
Imports System.Windows.Forms
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Controls
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Composition
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Components

    Friend Partial NotInheritable Class MASTimePickerClockView

#Region "Helpers"

        Private Function HitTestTarget(ptPx As SKPoint) As HitTarget
            If _lastDisplayRect.Contains(ptPx.X, ptPx.Y) Then Return New HitTarget() With {.Part = HitPart.DisplayEditor}
            If _lastHourDownRect.Contains(ptPx.X, ptPx.Y) Then Return New HitTarget() With {.Part = HitPart.HourDown}
            If _lastHourUpRect.Contains(ptPx.X, ptPx.Y) Then Return New HitTarget() With {.Part = HitPart.HourUp}
            If _lastMinuteDownRect.Contains(ptPx.X, ptPx.Y) Then Return New HitTarget() With {.Part = HitPart.MinuteDown}
            If _lastMinuteUpRect.Contains(ptPx.X, ptPx.Y) Then Return New HitTarget() With {.Part = HitPart.MinuteUp}
            If _lastNowRect.Contains(ptPx.X, ptPx.Y) Then Return New HitTarget() With {.Part = HitPart.NowAction}
            If _lastClearRect.Contains(ptPx.X, ptPx.Y) Then Return New HitTarget() With {.Part = HitPart.ClearAction}
            If _lastConfirmRect.Contains(ptPx.X, ptPx.Y) Then Return New HitTarget() With {.Part = HitPart.ConfirmAction}
            Return New HitTarget() With {.Part = HitPart.None}
        End Function

        Private Sub Activate(part As HitPart)
            Select Case part
                Case HitPart.DisplayEditor
                    BeginManualEdit()

                Case HitPart.HourDown
                    If _isEditingTime Then CancelManualEdit()
                    AdjustHours(-1)

                Case HitPart.HourUp
                    If _isEditingTime Then CancelManualEdit()
                    AdjustHours(1)

                Case HitPart.MinuteDown
                    If _isEditingTime Then CancelManualEdit()
                    AdjustMinutes(-_minuteStep)

                Case HitPart.MinuteUp
                    If _isEditingTime Then CancelManualEdit()
                    AdjustMinutes(_minuteStep)

                Case HitPart.NowAction
                    If _isEditingTime Then CancelManualEdit()
                    Dim previousTime As TimeSpan = _workingTime
                    _workingTime = ClampToRange(MASTimePicker.ResolveCurrentTimeOfDay())
                    StartTimeValueMotion(previousTime, _workingTime)
                    InvalidateVisual()

                Case HitPart.ClearAction
                    If _isEditingTime Then CancelManualEdit()
                    _onTimeSelected?.Invoke(Nothing)

                Case HitPart.ConfirmAction
                    If _isEditingTime AndAlso Not CommitManualEdit() Then Return
                    _onTimeSelected?.Invoke(_workingTime)
            End Select
        End Sub

        Private Sub BeginManualEdit()
            _isEditingTime = True
            _editBuffer = MASTimePickerClockFormatPolicy.FormatManualEditDigits(_workingTime, _clockFormat)
            _editSelectAll = True
            _hoverTarget = New HitTarget() With {.Part = HitPart.DisplayEditor}
            InvalidateVisual()
        End Sub

        Private Sub CancelManualEdit()
            _isEditingTime = False
            _editBuffer = String.Empty
            _editSelectAll = False
            InvalidateVisual()
        End Sub

        Private Function CommitManualEdit() As Boolean
            If Not _isEditingTime Then Return False

            Dim parsed As TimeSpan
            If TryBuildManualEditTime(_editBuffer, parsed) AndAlso IsTimeWithinRange(parsed) Then
                Dim previousTime As TimeSpan = _workingTime
                _workingTime = MASTimePicker.NormalizeTime(parsed, 1)
                StartTimeValueMotion(previousTime, _workingTime)
                _isEditingTime = False
                _editBuffer = String.Empty
                _editSelectAll = False
                InvalidateVisual()
                Return True
            End If

            InvalidateVisual()
            Return False
        End Function

        Private Function HandleManualEditKey(keyCode As Keys) As Boolean
            Select Case keyCode
                Case Keys.Enter
                    If CommitManualEdit() Then _onTimeSelected?.Invoke(_workingTime)
                    Return True

                Case Keys.Tab
                    CommitManualEdit()
                    Return True

                Case Keys.Escape
                    CancelManualEdit()
                    Return True

                Case Keys.Delete, Keys.Back
                    If _editSelectAll Then
                        _editBuffer = String.Empty
                        _editSelectAll = False
                    ElseIf _editBuffer.Length > 0 Then
                        _editBuffer = _editBuffer.Substring(0, _editBuffer.Length - 1)
                    End If
                    InvalidateVisual()
                    Return True
            End Select

            Return False
        End Function

        Private Function ApplyManualEditText(text As String) As Boolean
            Dim sawInput As Boolean = False
            Dim appendedDigit As Boolean = False

            For Each ch As Char In text
                sawInput = True
                If ch >= "0"c AndAlso ch <= "9"c Then
                    appendedDigit = TryAppendManualEditDigit(DigitValue(ch)) OrElse appendedDigit
                End If
            Next

            Return appendedDigit OrElse sawInput
        End Function

        Private Function TryAppendManualEditDigit(digit As Integer) As Boolean
            If digit < 0 OrElse digit > 9 Then Return False

            If _editSelectAll Then
                _editBuffer = String.Empty
                _editSelectAll = False
            End If

            If _editBuffer.Length >= 4 Then Return False

            Dim candidate As String = _editBuffer & digit.ToString(CultureInfo.InvariantCulture)
            If Not IsManualEditDigitsCandidateAllowed(candidate) Then Return False

            _editBuffer = candidate

            Dim parsed As TimeSpan
            If _editBuffer.Length = 4 AndAlso TryBuildManualEditTime(_editBuffer, parsed) AndAlso IsTimeWithinRange(parsed) Then
                Dim previousTime As TimeSpan = _workingTime
                _workingTime = MASTimePicker.NormalizeTime(parsed, 1)
                StartTimeValueMotion(previousTime, _workingTime)
                _isEditingTime = False
                _editBuffer = String.Empty
                _editSelectAll = False
                InvalidateVisual()
                _onTimeSelected?.Invoke(_workingTime)
                Return True
            End If

            InvalidateVisual()
            Return True
        End Function

        Private Shared Function TryResolveDigitFromKey(keyCode As Keys,
                                                       ByRef digit As Integer) As Boolean
            digit = -1
            Dim keyOnly As Keys = keyCode And Keys.KeyCode
            Dim keyValue As Integer = CInt(keyOnly)

            If keyValue >= CInt(Keys.D0) AndAlso keyValue <= CInt(Keys.D9) Then
                digit = keyValue - CInt(Keys.D0)
                Return True
            End If

            If keyValue >= CInt(Keys.NumPad0) AndAlso keyValue <= CInt(Keys.NumPad9) Then
                digit = keyValue - CInt(Keys.NumPad0)
                Return True
            End If

            Return False
        End Function

        Private Function ResolveManualEditDisplayText() As String
            If _editBuffer.Length = 0 Then Return MASTimePickerClockFormatPolicy.FormatManualEditPlaceholder(_clockFormat)
            If _editBuffer.Length <= 2 Then Return _editBuffer & If(_editBuffer.Length = 2, ":", String.Empty)
            Return _editBuffer.Substring(0, 2) & ":" & _editBuffer.Substring(2)
        End Function

        Private Function TryBuildManualEditTime(digits As String,
                                                ByRef value As TimeSpan) As Boolean
            Return MASTimePickerClockFormatPolicy.TryBuildManualEditTime(digits, _clockFormat, _workingTime, value)
        End Function

        Private Function IsManualEditDigitsCandidateAllowed(digits As String) As Boolean
            Return MASTimePickerClockFormatPolicy.IsManualEditDigitsCandidateAllowed(digits, _clockFormat)
        End Function

        Private Shared Function DigitValue(ch As Char) As Integer
            Return AscW(ch) - AscW("0"c)
        End Function

        Private Sub AdjustHours(deltaHours As Integer)
            AdjustMinutes(deltaHours * 60)
        End Sub

        Private Sub AdjustMinutes(deltaMinutes As Integer)
            Dim previousTime As TimeSpan = _workingTime
            _workingTime = ClampToRange(MASTimePicker.NormalizeTime(_workingTime.Add(TimeSpan.FromMinutes(deltaMinutes)), _minuteStep))
            StartTimeValueMotion(previousTime, _workingTime)
            InvalidateVisual()
        End Sub

        Private Function IsTimeWithinRange(value As TimeSpan) As Boolean
            Dim timeValue As TimeSpan = MASTimePicker.NormalizeTime(value, 1)
            If _minimumTime.HasValue AndAlso timeValue < _minimumTime.Value Then Return False
            If _maximumTime.HasValue AndAlso timeValue > _maximumTime.Value Then Return False
            Return True
        End Function

        Private Function ClampToRange(value As TimeSpan) As TimeSpan
            Dim timeValue As TimeSpan = MASTimePicker.NormalizeTime(value, _minuteStep)
            If _minimumTime.HasValue AndAlso timeValue < _minimumTime.Value Then timeValue = _minimumTime.Value
            If _maximumTime.HasValue AndAlso timeValue > _maximumTime.Value Then timeValue = _maximumTime.Value
            Return timeValue
        End Function

        Private Shared Function Inset(rect As SKRect, amount As Single) As SKRect
            Return PixelSnap.SnapRectHalf(New SKRect(rect.Left + amount, rect.Top + amount, rect.Right - amount, rect.Bottom - amount))
        End Function

        Private Shared Function SameHit(a As HitTarget, b As HitTarget) As Boolean
            Return a.Part = b.Part
        End Function

        Private Shared Function IsSameHit(target As HitTarget, part As HitPart) As Boolean
            Return target.Part = part
        End Function

#End Region

    End Class

End Namespace
