Option Strict On
Option Explicit On

Imports System
Imports Nexamas.UI.FloatRuntime
Imports Nexamas.UI.General
Imports Nexamas.UI.Host
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Controls

    ''' <summary>
    ''' Official Nexamas UI popover gateway.  It owns only the callout request state and
    ''' delegates all floating lifecycle, conflict policy, outside dismissal, focus, and
    ''' animation to MASFloatRuntime.
    ''' </summary>
    Public NotInheritable Class MASPopover
        Implements IDisposable

#Region "Fields"

        Private ReadOnly _rootHost As MASSkiaRootHost
        Private _title As String = String.Empty
        Private _text As String = String.Empty
        Private _placement As MASCalloutPlacement = MASCalloutPlacement.Auto
        Private _closeOnOutsidePointer As Boolean = True
        Private _swallowOutsidePointer As Boolean = True
        Private _closeOnEscape As Boolean = True
        Private _ownerKey As String = CalloutTokens.DefaultOwnerKey
        Private _handle As MASFloatHandle
        Private _content As MASFloatControlsContent
        Private _callout As MASCallout
        Private _disposed As Boolean

#End Region

#Region "Events"

        Public Event Opened As EventHandler
        Public Event Closed As EventHandler

#End Region

#Region "Constructor"

        Friend Sub New(rootHost As MASSkiaRootHost)
            If rootHost Is Nothing Then Throw New ArgumentNullException(NameOf(rootHost))
            _rootHost = rootHost
        End Sub

#End Region

#Region "Public API"

        Public Property Title As String
            Get
                Return _title
            End Get
            Set(value As String)
                Dim normalized As String = MASCalloutVisualPolicy.NormalizeTitle(value)
                If String.Equals(_title, normalized, StringComparison.Ordinal) Then Return

                _title = normalized
                If _callout IsNot Nothing Then _callout.Title = _title
                RequestInvalidate()
            End Set
        End Property

        Public Property Text As String
            Get
                Return _text
            End Get
            Set(value As String)
                Dim normalized As String = MASCalloutVisualPolicy.NormalizeBody(value)
                If String.Equals(_text, normalized, StringComparison.Ordinal) Then Return

                _text = normalized
                If _callout IsNot Nothing Then _callout.Text = _text
                RequestInvalidate()
            End Set
        End Property

        Public Property Placement As MASCalloutPlacement
            Get
                Return _placement
            End Get
            Set(value As MASCalloutPlacement)
                Dim normalized As MASCalloutPlacement = MASPopoverPlacementPolicy.NormalizePlacement(value)
                If _placement = normalized Then Return

                _placement = normalized
                If _callout IsNot Nothing Then _callout.Placement = _placement
                RequestInvalidate()
            End Set
        End Property

        Public Property CloseOnOutsidePointer As Boolean
            Get
                Return _closeOnOutsidePointer
            End Get
            Set(value As Boolean)
                If _closeOnOutsidePointer = value Then Return
                _closeOnOutsidePointer = value
                If _content IsNot Nothing Then _content.CloseOnOutsidePointer = value
            End Set
        End Property

        Public Property SwallowOutsidePointer As Boolean
            Get
                Return _swallowOutsidePointer
            End Get
            Set(value As Boolean)
                If _swallowOutsidePointer = value Then Return
                _swallowOutsidePointer = value
                If _content IsNot Nothing Then _content.SwallowOutsidePointer = value
            End Set
        End Property

        Public Property CloseOnEscape As Boolean
            Get
                Return _closeOnEscape
            End Get
            Set(value As Boolean)
                If _closeOnEscape = value Then Return
                _closeOnEscape = value
                If _content IsNot Nothing Then _content.CloseOnEscape = value
            End Set
        End Property

        Public ReadOnly Property IsOpen As Boolean
            Get
                If _disposed OrElse _handle Is Nothing Then Return False

                Try
                    Return _rootHost.FloatRuntimeCore.IsOpen(_handle)
                Catch masCaughtException1 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(masCaughtException1, "MASPopover.IsOpen")
                    Return False
                End Try
            End Get
        End Property

        Public Function Open(anchorBoundsLogical As SKRect) As MASPopover
            If _disposed Then Throw New ObjectDisposedException(NameOf(MASPopover))

            Dim anchorPx As SKRect = _rootHost.LogicalToPx(anchorBoundsLogical)
            anchorPx = MASPopoverPlacementPolicy.NormalizeAnchorPx(anchorPx, New SKPoint(anchorPx.Left, anchorPx.Top))
            OpenCore(anchorPx)
            Return Me
        End Function

        Public Function Open(x As Single,
                             y As Single,
                             width As Single,
                             height As Single) As MASPopover
            Return Open(New SKRect(x, y, x + Math.Max(0.0F, width), y + Math.Max(0.0F, height)))
        End Function

        Public Function Close() As MASPopover
            CloseCore(MASFloatCloseReason.Programmatic)
            Return Me
        End Function

        Public Function Toggle(anchorBoundsLogical As SKRect) As MASPopover
            If IsOpen Then
                Close()
            Else
                Open(anchorBoundsLogical)
            End If

            Return Me
        End Function

        Public Function WithTitle(value As String) As MASPopover
            Title = value
            Return Me
        End Function

        Public Function WithText(value As String) As MASPopover
            Text = value
            Return Me
        End Function

        Public Function WithPlacement(value As MASCalloutPlacement) As MASPopover
            Placement = value
            Return Me
        End Function

        Public Function WithDismissPolicy(closeOnOutsidePointer As Boolean,
                                          Optional closeOnEscape As Boolean = True,
                                          Optional swallowOutsidePointer As Boolean = True) As MASPopover
            Me.CloseOnOutsidePointer = closeOnOutsidePointer
            Me.CloseOnEscape = closeOnEscape
            Me.SwallowOutsidePointer = swallowOutsidePointer
            Return Me
        End Function

#End Region

#Region "Float runtime"

        Private Sub OpenCore(anchorPx As SKRect)
            If _rootHost Is Nothing OrElse _rootHost.FloatRuntimeCore Is Nothing Then Return

            CloseCore(MASFloatCloseReason.Programmatic)
            EnsureContent()

            Dim availablePx As SKRect = _rootHost.GetSurfaceViewportPx()
            If availablePx.Width <= 1.0F OrElse availablePx.Height <= 1.0F Then Return

            Dim boundsPx As SKRect = ResolvePopoverBoundsPx(anchorPx, availablePx, _rootHost.GetDpi())
            If boundsPx.Width <= 1.0F OrElse boundsPx.Height <= 1.0F Then Return

            Dim request As New MASFloatRequest() With {
                .FloatKey = MASFloatKeys.Popover,
                .OwnerKey = _ownerKey,
                .Content = _content,
                .RequestedSlot = MASFloatSlot.Popup,
                .RequestedScope = MASFloatScope.Owner,
                .RequestedLayer = MASFloatLayerLevel.Interactive,
                .ShowPolicy = MASFloatShowPolicy.CloseSameOwnerThenShow,
                .AnchorRectPx = anchorPx,
                .AvailableBoundsPx = availablePx,
                .InitialBoundsPx = boundsPx,
                .RequestedPresentation = MASFloatPresentationProfiles.DropDownList(),
                .OnOpened = Sub(handle As MASFloatHandle)
                                If handle IsNot Nothing Then _handle = handle
                                If _callout IsNot Nothing Then _callout.StartCalloutRevealMotion()
                                RaiseOpenedSafe()
                            End Sub,
                .OnClosed = Sub(args As MASFloatClosedEventArgs)
                                _handle = Nothing
                                RaiseClosedSafe()
                            End Sub
            }

            Try
                _handle = _rootHost.FloatRuntimeCore.Show(request)
            Catch masCaughtException2 As Exception
                _handle = Nothing
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException2, "MASPopover.Open")
            End Try

            RequestInvalidate()
        End Sub

        Private Sub CloseCore(reason As MASFloatCloseReason)
            If _handle Is Nothing OrElse _rootHost Is Nothing Then Return

            Dim closing As MASFloatHandle = _handle
            _handle = Nothing

            Try
                _rootHost.FloatRuntimeCore.Close(closing, reason)
            Catch masCaughtException3 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException3, "MASPopover.Close")
            End Try

            RequestInvalidate()
        End Sub

        Private Sub EnsureContent()
            _callout = MASCallout.Create(MASCalloutVisualPolicy.ResolveTitleText(_title), MASCalloutVisualPolicy.ResolveBodyText(_text)).WithPlacement(_placement)
            _callout.WithSize(MASSize.[Default])

            _content = New MASFloatControlsContent(
                name:=CalloutTokens.FloatContentName,
                themeHost:=_rootHost.ThemeHostCore,
                invalidate:=AddressOf RequestInvalidate)

            _content.AcceptsPointerInput = True
            _content.AcceptsKeyboardInput = True
            _content.AcceptsTextInput = False
            _content.CloseOnOutsidePointer = _closeOnOutsidePointer
            _content.SwallowOutsidePointer = _swallowOutsidePointer
            _content.CloseOnEscape = _closeOnEscape
            _content.Add(_callout)
        End Sub

#End Region

#Region "Placement"

        Private Function ResolvePopoverBoundsPx(anchorPx As SKRect,
                                                availablePx As SKRect,
                                                dpi As Single) As SKRect
            dpi = PixelSnap.SafeDpi(dpi)
            Dim size As SKSize = MASPopoverPlacementPolicy.ResolveCalloutSizePx(_title, _text, dpi)
            Dim gap As Single = CalloutTokens.PlacementGapDip * dpi
            Dim normalizedAnchor As SKRect = MASPopoverPlacementPolicy.NormalizeAnchorPx(anchorPx, New SKPoint(availablePx.MidX, availablePx.MidY))
            Dim placement As MASCalloutPlacement = MASPopoverPlacementPolicy.ResolveEffectivePlacement(normalizedAnchor, availablePx, size, gap, _placement)

            If _callout IsNot Nothing Then _callout.Placement = placement

            Return MASPopoverPlacementPolicy.ResolveBoundsPx(normalizedAnchor, availablePx, size, gap, placement)
        End Function

#End Region

#Region "Helpers / disposal"

        Private Sub RequestInvalidate()
            If _rootHost Is Nothing Then Return
            Try
                _rootHost.RequestInvalidate()
            Catch masCaughtException4 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException4, "MASPopover.RequestInvalidate")
            End Try
        End Sub

        Private Sub RaiseOpenedSafe()
            Try
                RaiseEvent Opened(Me, EventArgs.Empty)
            Catch masCaughtException5 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException5, "MASPopover.Opened")
            End Try
        End Sub

        Private Sub RaiseClosedSafe()
            Try
                RaiseEvent Closed(Me, EventArgs.Empty)
            Catch masCaughtException6 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException6, "MASPopover.Closed")
            End Try
        End Sub

        Friend Function CreatePopoverReadinessManifest() As MASPopoverReadinessManifest
            Return MASPopoverReadinessManifest.CreateCurrent()
        End Function

        Public Sub Dispose() Implements IDisposable.Dispose
            If _disposed Then Return
            _disposed = True
            CloseCore(MASFloatCloseReason.Programmatic)

            If _content IsNot Nothing Then
                _content.Dispose()
                _content = Nothing
            End If

            _callout = Nothing
        End Sub

#End Region

    End Class

End Namespace
