Option Strict On
Option Explicit On

Imports System
Imports System.Globalization
Imports System.Windows.Forms
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Components.ListPopup
Imports Nexamas.UI.Controls
Imports Nexamas.UI.FloatRuntime
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

    ''' <summary>
    ''' Public Nexamas UI date input control.  The visible control is an input-like
    ''' popup trigger; the calendar grid is MAS-owned internal float content so DatePicker
    ''' does not create a second public Calendar API surface.
    ''' </summary>
    Partial Public NotInheritable Class MASDatePicker
        Inherits MASControlBase
        Implements IMASControlRuntimeServicesConsumer
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract
        Implements IMASLayoutSemanticBoundsProvider

#Region "Fields"

        Private Shared ReadOnly _contract As MASResolvedComponentContract =
            MASArchitectureRuntime.ResolveContractOrThrow(GetType(MASDatePicker))

        Private ReadOnly _shadowPainter As New MASShadowPainter()
        Private ReadOnly _primitives As New MASVisualPrimitivesPainter()
        Private ReadOnly _popupService As DropDownService

        Private _selectedDate As Nullable(Of Date) = Nothing
        Private _minimumDate As Nullable(Of Date) = Nothing
        Private _maximumDate As Nullable(Of Date) = Nothing
        Private _placeholder As String = DatePickerTokens.Placeholder
        Private _displayMonth As Date = FirstOfMonth(DateTime.Today)

        Private _hover As Boolean
        Private _pressed As Boolean
        Private _isCalendarOpen As Boolean
        Private _lastPixelBounds As SKRect

        Private _floatRuntime As MASFloatRuntime
        Private _runtimeServices As MASControlRuntimeServices

#End Region

#Region "Events"

        ''' <summary>
        ''' Raised when the effective selected date changes through the official DatePicker gateway.
        ''' Handler exceptions are contained at the Nexamas UI boundary.
        ''' </summary>
        Public Event SelectedDateChanged As EventHandler

#End Region

#Region "Constructor"

        Public Sub New()
            MyBase.New()

            _popupService =
                New DropDownService(
                    hasOwnedFloat:=
                        Function(handle As MASFloatHandle) As Boolean
                            Dim runtime As MASFloatRuntime = ResolveFloatRuntime()
                            If runtime Is Nothing OrElse handle Is Nothing Then Return False

                            Try
                                Return runtime.IsOpen(handle)
                            Catch masCaughtException1 As Exception
                                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException1, "MASDatePicker.IsCalendarOpen")
                                Return False
                            End Try
                        End Function,
                    closeOwnedFloat:=
                        Function(handle As MASFloatHandle) As Boolean
                            Dim runtime As MASFloatRuntime = ResolveFloatRuntime()
                            If runtime Is Nothing OrElse handle Is Nothing Then Return False

                            Try
                                Return runtime.Close(handle, MASFloatCloseReason.Programmatic)
                            Catch masCaughtException2 As Exception
                                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException2, "MASDatePicker.CloseCalendarFloat")
                                Return False
                            End Try
                        End Function,
                    showFloat:=
                        Function(ownerKey As String,
                                 content As IMASFloatContent,
                                 boundsPx As SKRect) As MASFloatHandle

                            Return ShowCalendarFloat(ownerKey, content, boundsPx)
                        End Function)
        End Sub

#End Region

#Region "Public API"

        Public Shared Function Create() As MASDatePicker
            Return New MASDatePicker()
        End Function

        Friend Shared Function [Default]() As MASDatePicker
            Return Create()
        End Function

        Friend Property FloatRuntime As MASFloatRuntime
            Get
                Return _floatRuntime
            End Get
            Set(value As MASFloatRuntime)
                If Object.ReferenceEquals(_floatRuntime, value) Then Return
                CloseCalendarSafe()
                _floatRuntime = value
                InvalidateVisual()
            End Set
        End Property

        Friend Property SurfaceSizeProvider As Func(Of SKSizeI) = Nothing

        Public Property SelectedDate As Nullable(Of Date)
            Get
                Return _selectedDate
            End Get
            Set(value As Nullable(Of Date))
                SetSelectedDateInternal(value, True)
            End Set
        End Property

        Public Property MinimumDate As Nullable(Of Date)
            Get
                Return _minimumDate
            End Get
            Set(value As Nullable(Of Date))
                Dim normalized As Nullable(Of Date) = NormalizeNullableDate(value)
                If NullableDatesEqual(_minimumDate, normalized) Then Return

                _minimumDate = normalized
                If _maximumDate.HasValue AndAlso _minimumDate.HasValue AndAlso _minimumDate.Value > _maximumDate.Value Then
                    _maximumDate = _minimumDate
                End If

                SetSelectedDateInternal(_selectedDate, False)
                CloseCalendarIfOpenForPolicyChange()
                InvalidateVisual()
            End Set
        End Property

        Public Property MaximumDate As Nullable(Of Date)
            Get
                Return _maximumDate
            End Get
            Set(value As Nullable(Of Date))
                Dim normalized As Nullable(Of Date) = NormalizeNullableDate(value)
                If NullableDatesEqual(_maximumDate, normalized) Then Return

                _maximumDate = normalized
                If _minimumDate.HasValue AndAlso _maximumDate.HasValue AndAlso _maximumDate.Value < _minimumDate.Value Then
                    _minimumDate = _maximumDate
                End If

                SetSelectedDateInternal(_selectedDate, False)
                CloseCalendarIfOpenForPolicyChange()
                InvalidateVisual()
            End Set
        End Property

        Public Property Placeholder As String
            Get
                Return _placeholder
            End Get
            Set(value As String)
                Dim normalized As String = If(value, String.Empty).Trim()
                If normalized.Length = 0 Then normalized = DatePickerTokens.Placeholder
                If String.Equals(_placeholder, normalized, StringComparison.Ordinal) Then Return

                _placeholder = normalized
                InvalidateLayoutSlotInternal()
                InvalidateVisual()
            End Set
        End Property

        Public ReadOnly Property DisplayText As String
            Get
                If Not _selectedDate.HasValue Then Return String.Empty
                Return _selectedDate.Value.ToString("d", CultureInfo.CurrentCulture)
            End Get
        End Property

        Public Function WithSelectedDate(value As Date) As MASDatePicker
            SelectedDate = value
            Return Me
        End Function

        Public Function ClearDate() As MASDatePicker
            SelectedDate = Nothing
            Return Me
        End Function

        Public Function WithDateRange(minimumDate As Nullable(Of Date),
                                      maximumDate As Nullable(Of Date)) As MASDatePicker
            _minimumDate = NormalizeNullableDate(minimumDate)
            _maximumDate = NormalizeNullableDate(maximumDate)

            If _minimumDate.HasValue AndAlso _maximumDate.HasValue AndAlso _minimumDate.Value > _maximumDate.Value Then
                Dim swap As Date = _minimumDate.Value
                _minimumDate = _maximumDate
                _maximumDate = swap
            End If

            SetSelectedDateInternal(_selectedDate, False)
            CloseCalendarIfOpenForPolicyChange()
            InvalidateVisual()
            Return Me
        End Function

        Public Function WithPlaceholder(value As String) As MASDatePicker
            Placeholder = value
            Return Me
        End Function

        Public Shadows Function WithSize(sizeIntent As Nexamas.UI.Layout.MASSize) As MASDatePicker
            MyBase.SetSize(sizeIntent)
            Return Me
        End Function

        Friend Function SetBounds(bounds As SKRect) As MASDatePicker
            SetLocalLayoutBoundsInternal(bounds)
            Return Me
        End Function

        Friend Function WithBounds(bounds As SKRect) As MASDatePicker
            Return SetBounds(bounds)
        End Function

        Friend Function WithBounds(x As Single,
                                   y As Single,
                                   width As Single,
                                   height As Single) As MASDatePicker
            SetLocalLayoutBoundsInternal(New SKRect(
                x,
                y,
                x + Math.Max(0.0F, width),
                y + Math.Max(0.0F, height)))
            Return Me
        End Function

        Friend Function WithFloatRuntime(runtime As MASFloatRuntime) As MASDatePicker
            FloatRuntime = runtime
            Return Me
        End Function

        Friend Function WithSurfaceSizeProvider(provider As Func(Of SKSizeI)) As MASDatePicker
            SurfaceSizeProvider = provider
            Return Me
        End Function

#End Region

#Region "Runtime Services"

        Friend Sub BindControlRuntimeServices(services As MASControlRuntimeServices) Implements IMASControlRuntimeServicesConsumer.BindControlRuntimeServices
            If Object.ReferenceEquals(_runtimeServices, services) Then Return

            CloseCalendarSafe()
            _runtimeServices = services
            InvalidateVisual()
        End Sub

#End Region

    End Class

End Namespace
