Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Composition
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.TextInput
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Controls

    ''' <summary>
    ''' Official Nexamas UI callout surface.  It owns only the visual content body;
    ''' floating lifecycle and placement are owned by MASPopover through FloatRuntime.
    ''' </summary>
    Partial Public NotInheritable Class MASCallout
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract

#Region "Fields"

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

        Private ReadOnly _primitives As New MASVisualPrimitivesPainter()

        Private _title As String = String.Empty
        Private _text As String = String.Empty
        Private _placement As MASCalloutPlacement = MASCalloutPlacement.Auto
        Private _disposedLocal As Boolean

#End Region

#Region "Constructor / Factory"

        Public Sub New()
            MyBase.New()
        End Sub

        Public Shared Function Create(Optional title As String = Nothing,
                                      Optional text As String = Nothing) As MASCallout
            Dim callout As New MASCallout()
            callout._title = MASCalloutVisualPolicy.NormalizeTitle(title)
            callout._text = MASCalloutVisualPolicy.NormalizeBody(text)
            Return callout
        End Function

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

#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
                RaiseTextChanged()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASCallout.Title")
                InvalidateVisual()
            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
                RaiseTextChanged()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASCallout.Text")
                InvalidateVisual()
            End Set
        End Property

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

                _placement = normalized
                InvalidateVisual()
            End Set
        End Property

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

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

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

        Public Shadows Function WithSize(sizeIntent As MASSize) As MASCallout
            MyBase.SetSize(sizeIntent)
            Return Me
        End Function

#End Region

#Region "Layout"

        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 maxTextWidth As Single = CalloutTokens.MaxWidthDip - CalloutTokens.PaddingLeftDip - CalloutTokens.PaddingRightDip
            Dim titleSize As SKSize = MeasureText(safeContext, ResolveTitleText(), MASTypography.MASTextStyle.Small, maxTextWidth, True)
            Dim messageSize As SKSize = MeasureText(safeContext, ResolveMessageText(), MASTypography.MASTextStyle.Body, maxTextWidth, True)
            Dim desiredWidth As Single = Math.Max(CalloutTokens.MinWidthDip,
                                                  Math.Min(CalloutTokens.MaxWidthDip,
                                                           Math.Max(titleSize.Width, messageSize.Width) + CalloutTokens.PaddingLeftDip + CalloutTokens.PaddingRightDip))
            Dim desiredHeight As Single = Math.Max(CalloutTokens.MinHeightDip,
                                                   CalloutTokens.PaddingTopDip +
                                                   Math.Min(titleSize.Height, 42.0F) +
                                                   CalloutTokens.TitleMessageGapDip +
                                                   Math.Min(messageSize.Height, 96.0F) +
                                                   CalloutTokens.PaddingBottomDip)

            desiredHeight = Math.Min(CalloutTokens.MaxHeightDip, desiredHeight)

            Return MASSizeResolver.FromMeasured(
                desiredSize:=New SKSize(desiredWidth, desiredHeight),
                minSize:=New SKSize(CalloutTokens.MinWidthDip, CalloutTokens.MinHeightDip),
                maxSize:=New SKSize(CalloutTokens.MaxWidthDip, CalloutTokens.MaxHeightDip),
                intent:=MASSize.Normalize(intent),
                context:=safeContext,
                canGrowWidth:=True,
                canGrowHeight:=False,
                contentInset:=New MASLayoutInset(CalloutTokens.PaddingLeftDip, CalloutTokens.PaddingTopDip, CalloutTokens.PaddingRightDip, CalloutTokens.PaddingBottomDip),
                visualOverflowInset:=MASLayoutInset.Empty,
                hitOverflowInset:=MASLayoutInset.Empty,
                isFallback:=False)
        End Function

        Private Function MeasureText(context As MASSizeContext,
                                     text As String,
                                     style As MASTypography.MASTextStyle,
                                     maxWidth As Single,
                                     allowWrap As Boolean) As SKSize
            Dim result As MASTextMeasureResult =
                MASLayoutTextMeasureGateway.Measure(
                    If(context Is Nothing, Nothing, context.IntegrationContext),
                    If(String.IsNullOrEmpty(text), " ", text),
                    style,
                    maxWidth,
                    allowWrap:=allowWrap)

            Return result.Size
        End Function

        Private Shared Function CreateSizeContext(context As MASLayoutMeasureContext) As MASSizeContext
            If context Is Nothing Then Return MASIntrinsicControlSizeMetrics.EnsureContext(Nothing)
            Return context.ToSizeContext()
        End Function

#End Region

#Region "Rendering"

        Protected Overrides Sub Render(canvas As SKCanvas,
                                       ctx As MASThemeContext,
                                       pixelBounds As SKRect)
            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If pixelBounds.Width <= 1.0F OrElse pixelBounds.Height <= 1.0F Then Return

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)
            Dim bounds As SKRect = PixelSnap.SnapRect(pixelBounds, dpi)
            If bounds.Width <= 1.0F OrElse bounds.Height <= 1.0F Then Return

            _primitives.DrawCardSurface(canvas, ctx, bounds, dpi, _contract, MASCardVisualStyle.Secondary)
            DrawArrow(canvas, ctx, bounds, dpi)
            DrawContent(canvas, ctx, bounds, dpi)
            DrawCalloutRevealMotionOverlay(canvas, ctx, bounds, dpi)
        End Sub

        Private Sub DrawContent(canvas As SKCanvas,
                                ctx As MASThemeContext,
                                bounds As SKRect,
                                dpi As Single)
            Dim content As New SKRect(
                bounds.Left + CalloutTokens.PaddingLeftDip * dpi,
                bounds.Top + CalloutTokens.PaddingTopDip * dpi,
                bounds.Right - CalloutTokens.PaddingRightDip * dpi,
                bounds.Bottom - CalloutTokens.PaddingBottomDip * dpi)

            If content.Width <= 1.0F OrElse content.Height <= 1.0F Then Return

            Dim titleColor As SKColor = ResolveTitleColor(ctx)
            Dim textColor As SKColor = ResolveMessageColor(ctx)

            Dim titleText As String = ResolveTitleText()
            Dim bodyText As String = ResolveMessageText()
            Dim titleRect As New SKRect(content.Left, content.Top, content.Right, content.Top + 44.0F * dpi)
            Dim bodyRect As New SKRect(content.Left, titleRect.Bottom + CalloutTokens.TitleMessageGapDip * dpi, content.Right, content.Bottom)
            Dim isRtl As Boolean = MASCalloutVisualPolicy.ResolveTextIsRtl(titleText, bodyText)

            Dim titleBottom As Single = DrawWrappedText(canvas, ctx, titleText, titleRect, dpi, MASTypography.MASTextStyle.Small, titleColor, MASCalloutVisualPolicy.MaxTitleLines, isRtl)
            bodyRect.Top = Math.Min(bodyRect.Bottom, titleBottom + CalloutTokens.TitleMessageGapDip * dpi)
            DrawWrappedText(canvas, ctx, bodyText, bodyRect, dpi, MASTypography.MASTextStyle.Body, textColor, MASCalloutVisualPolicy.MaxBodyLines, isRtl)
        End Sub

        Private Function DrawWrappedText(canvas As SKCanvas,
                                             ctx As MASThemeContext,
                                             text As String,
                                             bounds As SKRect,
                                             dpi As Single,
                                             style As MASTypography.MASTextStyle,
                                             color As SKColor,
                                             maxLines As Integer,
                                             isRtl As Boolean) As Single
            If canvas Is Nothing OrElse ctx Is Nothing OrElse ctx.Typography Is Nothing Then Return bounds.Top
            If bounds.Width <= 1.0F OrElse bounds.Height <= 1.0F Then Return bounds.Top

            Dim paint As SKPaint = ctx.Typography.GetPaint(style, color)
            If paint Is Nothing Then Return bounds.Top

            Dim lines As List(Of String) = MASCalloutVisualPolicy.BuildWrappedLines(text, paint, bounds.Width, maxLines)
            If lines Is Nothing OrElse lines.Count = 0 Then Return bounds.Top

            Dim lineHeight As Single = paint.TextSize * MASTypography.WrapLineHeightMul
            Dim baseline As Single = bounds.Top - paint.FontMetrics.Ascent
            Dim lastBaseline As Single = baseline

            For i As Integer = 0 To lines.Count - 1
                If baseline > bounds.Bottom + lineHeight Then Exit For

                Dim x As Single = bounds.Left
                If isRtl Then x = bounds.Right - MASShapedText.MeasureWidth(lines(i), paint)
                MASShapedText.Draw(canvas, lines(i), MASTextCrispRendering.SnapTextX(x), MASTextCrispRendering.SnapBaseline(baseline), paint)
                lastBaseline = baseline
                baseline += lineHeight
            Next

            Return Math.Min(bounds.Bottom, lastBaseline + paint.FontMetrics.Descent)
        End Function

        Private Sub DrawArrow(canvas As SKCanvas,
                              ctx As MASThemeContext,
                              bounds As SKRect,
                              dpi As Single)
            Dim arrowPlacement As MASCalloutPlacement = MASCalloutVisualPolicy.ResolveArrowPlacement(_placement)
            If arrowPlacement = MASCalloutPlacement.Auto Then Return

            Dim fill As SKColor = ResolveArrowFill(ctx)
            Dim arrow As Single = CalloutTokens.ArrowSizeDip * dpi
            If arrow <= 1.0F Then Return

            Using path As New SKPath()
                Select Case arrowPlacement
                    Case MASCalloutPlacement.Top
                        path.MoveTo(bounds.MidX - arrow, bounds.Bottom - 1.0F)
                        path.LineTo(bounds.MidX + arrow, bounds.Bottom - 1.0F)
                        path.LineTo(bounds.MidX, bounds.Bottom + arrow)

                    Case MASCalloutPlacement.Bottom
                        path.MoveTo(bounds.MidX - arrow, bounds.Top + 1.0F)
                        path.LineTo(bounds.MidX + arrow, bounds.Top + 1.0F)
                        path.LineTo(bounds.MidX, bounds.Top - arrow)

                    Case MASCalloutPlacement.Left
                        path.MoveTo(bounds.Right - 1.0F, bounds.MidY - arrow)
                        path.LineTo(bounds.Right - 1.0F, bounds.MidY + arrow)
                        path.LineTo(bounds.Right + arrow, bounds.MidY)

                    Case MASCalloutPlacement.Right
                        path.MoveTo(bounds.Left + 1.0F, bounds.MidY - arrow)
                        path.LineTo(bounds.Left + 1.0F, bounds.MidY + arrow)
                        path.LineTo(bounds.Left - arrow, bounds.MidY)
                End Select

                path.Close()
                Using paint As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = fill}
                    canvas.DrawPath(path, paint)
                End Using
            End Using
        End Sub

        Private Shared Function ResolveTitleColor(ctx As MASThemeContext) As SKColor
            If ctx IsNot Nothing AndAlso ctx.TextColorTheme IsNot Nothing Then
                Return ctx.TextColorTheme.PrimaryText.WithAlpha(CalloutTokens.TitleTextAlpha)
            End If

            Return Nexamas.UI.Values.Color.Text.Primary.WithAlpha(CalloutTokens.TitleTextAlpha)
        End Function

        Private Shared Function ResolveMessageColor(ctx As MASThemeContext) As SKColor
            If ctx IsNot Nothing AndAlso ctx.TextColorTheme IsNot Nothing Then
                Return ctx.TextColorTheme.SecondaryText.WithAlpha(CalloutTokens.MessageTextAlpha)
            End If

            Return Nexamas.UI.Values.Color.Text.Secondary.WithAlpha(CalloutTokens.MessageTextAlpha)
        End Function

        Private Shared Function ResolveArrowFill(ctx As MASThemeContext) As SKColor
            If ctx IsNot Nothing AndAlso ctx.SurfaceTheme IsNot Nothing Then
                Return ctx.SurfaceTheme.SurfaceTop.WithAlpha(CalloutTokens.ArrowAlpha)
            End If

            Return Nexamas.UI.Values.Color.Surfaces.Type.Default.SurfaceTop.WithAlpha(CalloutTokens.ArrowAlpha)
        End Function


#End Region

#Region "Helpers"

        Private Function ResolveTitleText() As String
            Return MASCalloutVisualPolicy.ResolveTitleText(_title)
        End Function

        Private Function ResolveMessageText() As String
            Return MASCalloutVisualPolicy.ResolveBodyText(_text)
        End Function

        Friend Function CreateCalloutReadinessManifest() As MASCalloutReadinessManifest
            Return MASCalloutReadinessManifest.CreateCurrent()
        End Function

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

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


#End Region

#Region "Dispose"

        Protected Overrides Sub Dispose(disposing As Boolean)
            If _disposedLocal Then Return
            _disposedLocal = True

            StopCalloutRevealMotion(resetState:=True)

            If disposing Then
                Try
                    _primitives.Dispose()
                Catch masCaughtException1 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException1)
                End Try
            End If

            MyBase.Dispose(disposing)
        End Sub

#End Region

    End Class

End Namespace
