Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Windows.Forms
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Composition
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.Values
Imports Nexamas.UI.Motion
Imports SkiaSharp

Namespace Nexamas.UI.Controls

    ''' <summary>
    ''' Internal passive visual used by the root-owned tooltip Float content.
    ''' Public SDK code registers tooltip targets through MASApplicationWindow.Services.Tooltips.
    ''' </summary>
    Friend NotInheritable Class MASTooltip
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract

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

        Friend Delegate Function TooltipTextProvider() As String

        Friend Enum TooltipPlacement
            Top = 0
            Bottom = 1
            Right = 2
            Left = 3
        End Enum

        Private ReadOnly _renderer As TooltipRenderer
        Private ReadOnly _state As New MASTooltipState()
        Private ReadOnly _text As New MASTooltipTextStore()
        Private ReadOnly _layout As New MASTooltipLayoutEngine()
        Private ReadOnly _timeline As New MASTooltipTimeline()
        Private ReadOnly _externalInvalidate As Action
        Private _surfaceMaterialKey As String = MASSurfaceMaterialIds.Auto

        Friend Sub New(Optional externalInvalidate As Action = Nothing)

            _externalInvalidate = externalInvalidate
            _renderer = New TooltipRenderer()

            Visible = True
            Enabled = True

             SetLocalLayoutBoundsInternal(New SKRect(0.0F, 0.0F, 1.0F, 1.0F))

        End Sub

        Friend Property SurfaceMaterial As String
            Get
                Return _surfaceMaterialKey
            End Get
            Set(value As String)
                Dim key As String = MASSurfaceTreatmentMaterialKeyNormalizer.NormalizeForStorage(value)
                If String.Equals(_surfaceMaterialKey, key, StringComparison.OrdinalIgnoreCase) Then Return
                _surfaceMaterialKey = key
                RequestTooltipInvalidate()
            End Set
        End Property

        Friend Function WithSurfaceMaterial(materialKey As String) As MASTooltip
            SurfaceMaterial = materialKey
            Return Me
        End Function

        Friend ReadOnly Property IsOpen As Boolean
            Get
                Return _state.IsOpen
            End Get
        End Property

        Friend ReadOnly Property HasVisibleTooltip As Boolean
            Get
                Return _state.Opacity > 0.01F AndAlso
                       Not String.IsNullOrWhiteSpace(_state.GetRenderedTooltipText())
            End Get
        End Property

        Friend Sub SetStaticText(text As String)
            _text.SetStaticText(text)
        End Sub

        Friend Sub AttachProvider(provider As TooltipTextProvider)
            _text.AttachProvider(provider)
        End Sub

        Friend Sub ClearProvider()
            _text.ClearProvider()
        End Sub

        Friend Sub SetViewportLogicalBounds(viewportLogical As SKRect)

            If viewportLogical.IsEmpty OrElse
               viewportLogical.Width <= 0.0F OrElse
               viewportLogical.Height <= 0.0F OrElse
               Not IsFinite(viewportLogical.Left) OrElse
               Not IsFinite(viewportLogical.Top) OrElse
               Not IsFinite(viewportLogical.Right) OrElse
               Not IsFinite(viewportLogical.Bottom) Then

                Return
            End If

            SetLocalLayoutBoundsInternal(viewportLogical)
            RequestTooltipInvalidate()

        End Sub

        Friend Sub SetViewportSizeLogical(width As Single,
                                          height As Single)

            If width <= 0.0F OrElse height <= 0.0F Then Return
            If Not IsFinite(width) OrElse Not IsFinite(height) Then Return

            SetViewportLogicalBounds(
                New SKRect(0.0F, 0.0F, width, height))

        End Sub

        Friend Sub ShowAt(clientX As Single,
                          clientY As Single,
                          Optional text As String = Nothing)

            ShowCore(
                anchorPt:=New SKPoint(clientX, clientY),
                hoverRectClientPx:=SKRect.Empty,
                hasHoverRect:=False,
                text:=text,
                showImmediately:=True)

        End Sub

        Friend Sub ShowForRect(hoverRectClientPx As SKRect,
                               clientX As Single,
                               clientY As Single,
                               Optional text As String = Nothing)

            ShowCore(
                anchorPt:=New SKPoint(clientX, clientY),
                hoverRectClientPx:=hoverRectClientPx,
                hasHoverRect:=True,
                text:=text,
                showImmediately:=False)

        End Sub

        Private Sub ShowCore(anchorPt As SKPoint,
                             hoverRectClientPx As SKRect,
                             hasHoverRect As Boolean,
                             text As String,
                             showImmediately As Boolean)

            If Not Visible OrElse Not Enabled Then Return

            If Not IsFinite(anchorPt.X) OrElse Not IsFinite(anchorPt.Y) Then
                HideTooltipImmediate()
                Return
            End If

            If hasHoverRect AndAlso Not IsUsableRect(hoverRectClientPx) Then
                HideTooltipImmediate()
                Return
            End If

            If text IsNot Nothing Then
                _text.SetStaticText(text)
            End If

            Dim resolvedText As String = _text.ResolveText()

            If String.IsNullOrWhiteSpace(resolvedText) Then
                HideTooltip()
                Return
            End If

            _state.WantsVisible = True
            _state.AnchorClientPx = anchorPt
            _state.LastPointerClientPx = anchorPt

            If Not _state.HasAnchor Then
                _state.SmoothAnchorClientPx = anchorPt
                _state.HasAnchor = True
            End If

            If hasHoverRect Then
                _state.HoverRectClientPx = hoverRectClientPx
                _state.HasHoverRect =
                    Not hoverRectClientPx.IsEmpty AndAlso
                    hoverRectClientPx.Width > 0.0F AndAlso
                    hoverRectClientPx.Height > 0.0F
            Else
                _state.ClearHoverRect()
            End If

            _state.PendingHide = False

            If _state.IsOpen OrElse
               _state.Opacity > 0.01F OrElse
               _state.TargetOpacity > 0.0F Then

                _state.PendingShow = False
                _state.IsOpen = True
                _state.TargetOpacity = 1.0F
                _state.VisibleText = resolvedText

                StartAnimation()
                RequestTooltipInvalidate()
                Return
            End If

            If _state.PendingShow Then

                Dim sameText As Boolean =
                    String.Equals(_state.PendingShowText,
                                  resolvedText,
                                  StringComparison.Ordinal)

                If sameText Then
                    _state.PendingShowText = resolvedText
                    _state.VisibleText = resolvedText

                    StartAnimation()
                    RequestTooltipInvalidate()
                    Return
                End If

            End If

            If showImmediately Then

                _state.PendingShow = False
                _state.PendingShowText = resolvedText
                _state.VisibleText = resolvedText
                _state.IsOpen = True
                _state.Opacity = 1.0F
                _state.TargetOpacity = 1.0F

                StartAnimation()
                RequestTooltipInvalidate()
                Return

            End If

            _state.PendingShow = True
            _state.PendingShowText = resolvedText
            _state.VisibleText = resolvedText
            _state.PendingShowStartedUtc = Nexamas.UI.Timing.MASVisualClock.UtcNow()
            _state.TargetOpacity = 0.0F

            StartAnimation()
            RequestTooltipInvalidate()

        End Sub

        Friend Sub UpdatePointer(clientX As Single,
                                 clientY As Single)

            If Not Visible OrElse Not Enabled Then Return

            If Not IsFinite(clientX) OrElse Not IsFinite(clientY) Then
                HideTooltipImmediate()
                Return
            End If

            Dim pt As New SKPoint(clientX, clientY)
            Dim moveDist As Single =
                MASTooltipTimeline.Distance(_state.LastPointerClientPx, pt)

            _state.LastPointerClientPx = pt
            _state.AnchorClientPx = pt

            If _state.HasHoverRect AndAlso
               Not MASTooltipTimeline.IsPointerInsideHoverRect(_state, pt) Then

                HideTooltip()
                Return
            End If

            If Not _state.HasAnchor Then
                _state.SmoothAnchorClientPx = pt
                _state.HasAnchor = True
            End If

            If _state.PendingHide AndAlso
               _state.WantsVisible AndAlso
               moveDist <= MASTooltipTimeline.StickyMoveToleranceClientPx * 1.25F Then

                _state.PendingHide = False
                _state.IsOpen = True
                _state.TargetOpacity = 1.0F
            End If

            If _state.PendingShow AndAlso
               moveDist > MASTooltipTimeline.StickyMoveToleranceClientPx Then

                _state.PendingShowStartedUtc = Nexamas.UI.Timing.MASVisualClock.UtcNow()
            End If

            If _state.IsOpen OrElse
               _state.TargetOpacity > 0.0F OrElse
               _state.PendingShow OrElse
               _state.PendingHide Then

                StartAnimation()
                RequestTooltipInvalidate()
            End If

        End Sub

        Friend Sub HideTooltip()

            _state.WantsVisible = False
            _state.PendingShow = False
            _state.ClearHoverRect()

            If _state.IsOpen OrElse
               _state.Opacity > 0.01F OrElse
               _state.TargetOpacity > 0.0F Then

                _state.PendingHide = True
                _state.PendingHideStartedUtc = Nexamas.UI.Timing.MASVisualClock.UtcNow()

                StartAnimation()
                RequestTooltipInvalidate()
                Return
            End If

            _state.CompleteHiddenState()
            RequestTooltipInvalidate()

        End Sub

        Friend Sub HideTooltipImmediate()
            ForceCloseImmediately()
        End Sub

        Friend Sub ClearTooltip()
            _text.Clear()
            ForceCloseImmediately()
        End Sub

        Friend Function GetTooltipText() As String
            Return _text.ResolveText()
        End Function

        Friend Function GetVisibleTooltipText() As String
            Return _state.GetRenderedTooltipText()
        End Function

        Friend Function GetVisibleOpacity() As Single
            Return MASMotionSystem.ClampProgress(_state.Opacity)
        End Function

        Friend Function GetVisiblePlacement() As TooltipPlacement
            Return _state.LastPlacement
        End Function

        Friend Function GetVisibleAnchorPx(ctx As MASThemeContext) As SKPoint
            Return _layout.GetAnchorPx(ctx, _state)
        End Function

        Friend Function GetArrowSizePx(ctx As MASThemeContext) As Single
            Return _layout.GetArrowSizePx(ctx)
        End Function

        Friend Function GetVisiblePanelRectPx(ctx As MASThemeContext) As SKRect
            If ctx Is Nothing Then Return SKRect.Empty
            Return _layout.GetPanelRectPx(ctx, _state, GetPixelBounds(ctx))
        End Function

        Friend Function GetVisibleTextRectPx(ctx As MASThemeContext) As SKRect
            If ctx Is Nothing Then Return SKRect.Empty
            Return _layout.GetTextRectPx(ctx, _state, GetPixelBounds(ctx))
        End Function

        Friend Function GetVisibleWrappedLines(ctx As MASThemeContext,
                                               dpi As Single) As List(Of String)

            Return _layout.BuildWrappedLines(
                ctx:=ctx,
                dpi:=dpi,
                text:=_state.GetRenderedTooltipText(),
                maxTextWidthPx:=_state.LastTextMaxWidthPx,
                maxLines:=Nexamas.UI.Values.TooltipTokens.MaxLines)

        End Function

        Friend Function GetVisibleLineHeightPx(ctx As MASThemeContext) As Single
            Return _layout.GetLineHeightPx(ctx)
        End Function

        Protected Overrides Function WantsKeyboardFocus() As Boolean
            Return False
        End Function


#Region "Layout Participant"

        Friend Function GetPreferredLayoutSize(context As MASLayoutMeasureContext) As SKSize Implements IMASLayoutParticipant.GetPreferredLayoutSize
            Return MeasureIntrinsicSize(CreateSizeContext(context), SizeIntent).DesiredSize
        End Function

        Friend Function GetMinLayoutSize(context As MASLayoutMeasureContext) As SKSize Implements IMASLayoutParticipant.GetMinLayoutSize
            Return MeasureIntrinsicSize(CreateSizeContext(context), SizeIntent).MinSize
        End Function

        Friend Function MeasureIntrinsicSize(context As MASSizeContext, intent As MASSize) As MASSizeResult Implements IMASIntrinsicSizeContract.MeasureIntrinsicSize
            Dim safeContext As MASSizeContext = MASIntrinsicControlSizeMetrics.EnsureContext(context)
            Dim textMeasure As MASTextMeasureResult = MASLayoutTextMeasureGateway.Measure(
                If(safeContext Is Nothing, Nothing, safeContext.IntegrationContext),
                GetTooltipText(),
                MASTypography.MASTextStyle.Small,
                Math.Max(TooltipTokens.MinTextWidth, TooltipTokens.MaxWidth - (TooltipTokens.PaddingX * 2.0F)),
                allowWrap:=True)

            Dim desired As SKSize = MASIntrinsicControlSizeMetrics.ResolveTooltipDesiredSize(textMeasure.Size, safeContext.AvailableSize)
            Dim minimum As SKSize = MASIntrinsicControlSizeMetrics.ResolveTooltipMinimumSize()

            Return MASSizeResolver.FromMeasured(
                desiredSize:=desired,
                minSize:=minimum,
                maxSize:=New SKSize(TooltipTokens.MaxWidth, Single.PositiveInfinity),
                intent:=intent,
                context:=safeContext,
                canGrowWidth:=False,
                canGrowHeight:=False,
                contentInset:=MASIntrinsicControlSizeMetrics.CreateTooltipContentInset(),
                visualOverflowInset:=MASIntrinsicControlSizeMetrics.DefaultVisualOverflowInset,
                hitOverflowInset:=MASLayoutInset.Empty,
                isFallback:=False)
        End Function

        Private Shared Function CreateSizeContext(context As MASLayoutMeasureContext) As MASSizeContext
            If context Is Nothing Then Return New MASSizeContext(New SKSize(Single.PositiveInfinity, Single.PositiveInfinity))
            Return context.ToSizeContext()
        End Function

#End Region

        Friend Sub RenderAsFloat(canvas As SKCanvas,
                                 ctx As MASThemeContext,
                                 boundsPx As SKRect)

            If canvas Is Nothing OrElse ctx Is Nothing Then Return

            If boundsPx.IsEmpty OrElse
               boundsPx.Width <= 0.5F OrElse
               boundsPx.Height <= 0.5F Then

                Return
            End If

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)

             SetLocalLayoutBoundsInternal(New SKRect(
                boundsPx.Left / dpi,
                boundsPx.Top / dpi,
                boundsPx.Right / dpi,
                boundsPx.Bottom / dpi))

            If Not HasVisibleTooltip Then Return

            _state.SetViewportBoundsPx(boundsPx)

            Try
                _renderer.Render(canvas, ctx, ctx.Dpi, Me)
            Finally
                _state.ClearViewportBoundsPx()
            End Try

        End Sub

        Private Sub RequestTooltipInvalidate()

            If IsDisposed Then Return

            Try
                InvalidateVisual()
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException1, "MASTooltip.RequestTooltipInvalidate.InvalidateVisual")
            End Try

            If _externalInvalidate IsNot Nothing Then
                Try
                    _externalInvalidate.Invoke()
                Catch masCaughtException2 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException2, "MASTooltip.RequestTooltipInvalidate.ExternalInvalidate")
                End Try
            End If

        End Sub

        Protected Overrides Sub Render(canvas As SKCanvas,
                                       ctx As MASThemeContext,
                                       pixelBounds As SKRect)

            If canvas Is Nothing OrElse ctx Is Nothing Then Return

            If Not HasVisibleTooltip Then Return

            _state.SetViewportBoundsPx(pixelBounds)

            Try
                _renderer.Render(canvas, ctx, ctx.Dpi, Me)
            Finally
                _state.ClearViewportBoundsPx()
            End Try

        End Sub

        Protected Overrides Function HitTest(ctx As MASThemeContext,
                                             ptPx As SKPoint) As Boolean

            Return False

        End Function

        Protected Overrides Sub OnVisibleChanged()
            MyBase.OnVisibleChanged()

            If Not Visible Then
                ForceCloseImmediately()
            End If
        End Sub

        Protected Overrides Sub OnEnabledChanged()
            MyBase.OnEnabledChanged()

            If Not Enabled Then
                ForceCloseImmediately()
            End If
        End Sub

        Protected Overrides Sub OnDetached()
            MyBase.OnDetached()
            ForceCloseImmediately()
        End Sub

        Protected Overrides Sub OnMouseCancel(ctx As MASThemeContext)
            MyBase.OnMouseCancel(ctx)
            HideTooltip()
        End Sub

        Protected Overrides Sub OnHostLostFocus(ctx As MASThemeContext)
            MyBase.OnHostLostFocus(ctx)
            ForceCloseImmediately()
        End Sub

        Private Function AdvanceTooltipMotionFrame(deltaMs As Integer) As Boolean

            Dim changed As Boolean = AdvanceTooltipTimeline(deltaMs)

            If changed Then
                RequestTooltipInvalidate()
            End If

            If Not WantsAnimationAlive() AndAlso
               Not _state.PendingShow AndAlso
               Not _state.PendingHide Then

                StopAnimation()
                Return False
            End If

            Return True

        End Function

        Private Function AdvanceTooltipTimeline(deltaMs As Integer) As Boolean

            Dim changed As Boolean = _timeline.Advance(_state, deltaMs)

            If _state.PendingShow OrElse
               _state.PendingHide OrElse
               _state.TargetOpacity <> _state.Opacity OrElse
               _state.Opacity > 0.001F Then

                StartAnimation()
            End If

            Return changed

        End Function

        Private Function WantsAnimationAlive() As Boolean

            If Not Visible OrElse Not Enabled Then Return False

            Return _state.PendingShow OrElse
                   _state.PendingHide OrElse
                   _state.IsOpen OrElse
                   _state.TargetOpacity > 0.0F OrElse
                   _state.Opacity > 0.001F

        End Function

        Private Sub StartAnimation()
            MASMotionSystem.RegisterFrameConsumer(
                owner:=Me,
                consumerName:="MASTooltip.Timeline",
                requestFrame:=AddressOf RequestTooltipInvalidate,
                advanceFrame:=AddressOf AdvanceTooltipMotionFrame)
        End Sub

        Private Sub StopAnimation()
            MASMotionSystem.UnregisterFrameConsumer(Me)
        End Sub

        Private Sub ForceCloseImmediately()

            _state.WantsVisible = False
            _state.PendingShow = False
            _state.PendingHide = False
            _state.ClearHoverRect()
            _state.CompleteHiddenState()

            StopAnimation()
            RequestTooltipInvalidate()

        End Sub

        Private Shared Function IsUsableRect(rect As SKRect) As Boolean
            If rect.IsEmpty Then Return False
            If rect.Width <= 0.0F OrElse rect.Height <= 0.0F Then Return False

            Return IsFinite(rect.Left) AndAlso
                   IsFinite(rect.Top) AndAlso
                   IsFinite(rect.Right) AndAlso
                   IsFinite(rect.Bottom)
        End Function

        Private Shared Function IsFinite(value As Single) As Boolean
            Return Not Single.IsNaN(value) AndAlso Not Single.IsInfinity(value)
        End Function

        Protected Overrides Sub Dispose(disposing As Boolean)

            If disposing Then

                Try
                    StopAnimation()
                Catch masCaughtException3 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException3, "MASTooltip.Dispose.StopAnimation")
                End Try

                Try
                    _renderer.Dispose()
                Catch masCaughtException6 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException6, "MASTooltip.Dispose.Renderer")
                End Try

            End If

            MyBase.Dispose(disposing)

        End Sub


#Region "Unified MASSize Fluent API"

        ''' <summary>
        ''' Strongly typed entry into the shared MASSize intent API.
        ''' This method keeps fluent chains on the concrete element while delegating all sizing decisions to MASControlBase/MASSize.
        ''' </summary>
        Friend Shadows Function WithSize(sizeIntent As Nexamas.UI.Layout.MASSize) As MASTooltip
            MyBase.SetSize(sizeIntent)
            Return Me
        End Function

        Friend Shadows Function WithDefaultSize() As MASTooltip
            MyBase.SetSize(Nexamas.UI.Layout.MASSize.[Default])
            Return Me
        End Function

        Friend Shadows Function WithCompactSize() As MASTooltip
            MyBase.SetSize(Nexamas.UI.Layout.MASSize.Compact)
            Return Me
        End Function

        Friend Shadows Function WithLargeSize() As MASTooltip
            MyBase.SetSize(Nexamas.UI.Layout.MASSize.Large)
            Return Me
        End Function

        Friend Shadows Function WithFillWidthSize() As MASTooltip
            MyBase.SetSize(Nexamas.UI.Layout.MASSize.FillWidth)
            Return Me
        End Function

        Friend Shadows Function WithHugContentSize() As MASTooltip
            MyBase.SetSize(Nexamas.UI.Layout.MASSize.HugContent)
            Return Me
        End Function

#End Region


        Friend Function CreateTooltipReadinessManifest() As MASTooltipReadinessManifest
            Return MASTooltipReadinessManifest.CreateCurrent()
        End Function
    End Class

End Namespace
