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.Rendering
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Controls

    ''' <summary>
    ''' Official Nexamas UI side-panel surface. It is the visual drawer body only:
    ''' Float lifecycle, docking, outside-dismiss, keyboard-dismiss, focus, and motion are
    ''' owned by MASDrawer through MASFloatRuntime.
    ''' </summary>
    Partial Public NotInheritable Class MASSidePanel
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract

#Region "Fields"

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

        Private ReadOnly _primitives As New MASVisualPrimitivesPainter()
        Private ReadOnly _relativeChildBounds As New Dictionary(Of MASControlBase, SKRect)()

        Private _title As String = String.Empty
        Private _showCloseButton As Boolean = True
        Private _hoverClose As Boolean
        Private _pressedClose As Boolean
        Private _disposedLocal As Boolean

#End Region

#Region "Events"

        Public Event CloseRequested As EventHandler

#End Region

#Region "Constructor / Factory"

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

        Public Sub New(title As String)
            Me.New()
            _title = MASSidePanelSurfacePolicy.NormalizeTitle(title)
        End Sub

        Public Shared Function Create(Optional title As String = Nothing) As MASSidePanel
            Return New MASSidePanel(title)
        End Function

        Friend Shared Function [Default]() As MASSidePanel
            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 = MASSidePanelSurfacePolicy.NormalizeTitle(value)
                If String.Equals(_title, normalized, StringComparison.Ordinal) Then Return

                _title = normalized
                RaiseTextChanged()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASSidePanel.Title")
                InvalidateVisual()
            End Set
        End Property

        Public Property ShowCloseButton As Boolean
            Get
                Return _showCloseButton
            End Get
            Set(value As Boolean)
                If _showCloseButton = value Then Return
                _showCloseButton = value
                If Not value Then
                    _hoverClose = False
                    _pressedClose = False
                End If
                InvalidateVisual()
            End Set
        End Property

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

        Public Function WithCloseButton(value As Boolean) As MASSidePanel
            ShowCloseButton = value
            Return Me
        End Function

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

        Public Function Add(control As MASControlBase) As MASSidePanel
            AddChild(control)
            Return Me
        End Function

        Public Function Add(control As MASControlBase,
                            relativeContentBounds As SKRect) As MASSidePanel
            AddChild(control, relativeContentBounds)
            Return Me
        End Function

        Public Function Remove(control As MASControlBase) As MASSidePanel
            RemoveChild(control)
            Return Me
        End Function

        Public Function Clear() As MASSidePanel
            ClearChildren()
            Return Me
        End Function

#End Region

#Region "Child content"

        Friend ReadOnly Property Controls As ControlsLayer
            Get
                Return ChildLayerInternal
            End Get
        End Property

        Friend Sub AddChild(child As MASControlBase)
            If child Is Nothing Then Throw New ArgumentNullException(NameOf(child))
            ChildLayerInternal.Add(child)
            ApplyChildPlacement(child)
            InvalidateVisual()
        End Sub

        Friend Sub AddChild(child As MASControlBase,
                            relativeContentBounds As SKRect)
            If child Is Nothing Then Throw New ArgumentNullException(NameOf(child))

            _relativeChildBounds(child) = NormalizeRelativeBounds(relativeContentBounds)
            ChildLayerInternal.Add(child)
            ApplyChildPlacement(child)
            InvalidateVisual()
        End Sub

        Friend Sub RemoveChild(child As MASControlBase)
            If child Is Nothing Then Return

            If HasChildLayerInternal() Then
                ChildLayerInternal.Remove(child)
            End If

            _relativeChildBounds.Remove(child)
            InvalidateVisual()
        End Sub

        Friend Sub ClearChildren()
            If HasChildLayerInternal() Then
                ChildLayerInternal.Clear()
            End If

            _relativeChildBounds.Clear()
            InvalidateVisual()
        End Sub

        Friend Function GetContentBoundsLogical() As SKRect
            Dim slotContent As SKRect = GetLayoutContentBoundsLogicalInternal()
            If slotContent.Width > 1.0F AndAlso slotContent.Height > 1.0F Then Return slotContent

            Dim b As SKRect = BoundsLogical
            If b.Width <= 1.0F OrElse b.Height <= 1.0F Then Return SKRect.Empty

            Return MASSidePanelSurfacePolicy.ResolveContentBounds(b)
        End Function

        Friend Sub SetChildBoundsInContent(child As MASControlBase,
                                           relativeContentBounds As SKRect)
            If child Is Nothing Then Throw New ArgumentNullException(NameOf(child))
            _relativeChildBounds(child) = NormalizeRelativeBounds(relativeContentBounds)
            ApplyChildPlacement(child)
        End Sub

        Protected Overrides Sub OnBoundsChanged()
            MyBase.OnBoundsChanged()
            ApplyAllChildPlacements()
        End Sub

#End Region

#Region "Layout / size"

        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 desired As New SKSize(DrawerTokens.DefaultWidthDip, DrawerTokens.DefaultHeightDip)
            Dim minimum As New SKSize(DrawerTokens.MinWidthDip, DrawerTokens.MinHeightDip)
            Dim contentInset As MASLayoutInset = MASSidePanelSurfacePolicy.ResolveContentInset()

            Return MASSizeResolver.FromMeasured(
                desiredSize:=desired,
                minSize:=minimum,
                maxSize:=New SKSize(DrawerTokens.MaxWidthDip, Single.PositiveInfinity),
                intent:=MASSize.Normalize(intent),
                context:=safeContext,
                canGrowWidth:=True,
                canGrowHeight:=True,
                contentInset:=contentInset,
                visualOverflowInset:=MASLayoutInset.Empty,
                hitOverflowInset:=MASLayoutInset.Empty,
                isFallback:=False)
        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.DrawPanelShellAcrylic_NoShadow(canvas, ctx, bounds, dpi, _contract)
            DrawHeader(canvas, ctx, bounds, dpi)
            DrawSidePanelRevealMotionOverlay(canvas, ctx, bounds, dpi)
        End Sub

        Private Sub DrawHeader(canvas As SKCanvas,
                               ctx As MASThemeContext,
                               bounds As SKRect,
                               dpi As Single)
            Dim header As New SKRect(bounds.Left,
                                     bounds.Top,
                                     bounds.Right,
                                     bounds.Top + DrawerTokens.HeaderHeightDip * dpi)

            Using paint As New SKPaint With {
                .IsAntialias = True,
                .Style = SKPaintStyle.Stroke,
                .StrokeWidth = Math.Max(1.0F, DrawerTokens.SeparatorStrokeDip * dpi),
                .Color = ResolveSeparatorColor(ctx)
            }
                canvas.DrawLine(header.Left + DrawerTokens.PaddingLeftDip * dpi,
                                header.Bottom,
                                header.Right - DrawerTokens.PaddingRightDip * dpi,
                                header.Bottom,
                                paint)
            End Using

            Dim titleRect As New SKRect(header.Left + DrawerTokens.PaddingLeftDip * dpi,
                                        header.Top,
                                        header.Right - ResolveCloseReservePx(dpi),
                                        header.Bottom)

            TypographyTextPrimitives.DrawSingleLineText(
                canvas:=canvas,
                ctx:=ctx,
                text:=ResolveTitleText(),
                bounds:=titleRect,
                dpi:=dpi,
                style:=MASTypography.MASTextStyle.Body,
                color:=ResolveTitleColor(ctx),
                align:=TypographyTextPrimitives.TextAlign.Left,
                drawShadow:=False)

            If _showCloseButton Then DrawCloseButton(canvas, ctx, header, dpi)
        End Sub

        Private Sub DrawCloseButton(canvas As SKCanvas,
                                    ctx As MASThemeContext,
                                    header As SKRect,
                                    dpi As Single)
            Dim buttonRect As SKRect = ResolveCloseButtonBoundsPx(header, dpi)
            If buttonRect.Width <= 1.0F OrElse buttonRect.Height <= 1.0F Then Return

            If _hoverClose OrElse _pressedClose Then
                Using fill As New SKPaint With {
                    .IsAntialias = True,
                    .Style = SKPaintStyle.Fill,
                    .Color = ResolveCloseFillColor(ctx, _pressedClose)
                }
                    Dim radius As Single = DrawerTokens.CloseButtonRadiusDip * dpi
                    canvas.DrawRoundRect(buttonRect, radius, radius, fill)
                End Using
            End If

            DrawSidePanelCloseButtonPulse(canvas, ctx, buttonRect, dpi)

            Using p As New SKPaint With {
                .IsAntialias = True,
                .Style = SKPaintStyle.Stroke,
                .StrokeWidth = Math.Max(1.0F, DrawerTokens.CloseGlyphStrokeDip * dpi),
                .StrokeCap = SKStrokeCap.Round,
                .Color = ResolveTitleColor(ctx).WithAlpha(DrawerTokens.CloseGlyphAlpha)
            }
                Dim inset As Single = DrawerTokens.CloseGlyphInsetDip * dpi
                canvas.DrawLine(buttonRect.Left + inset,
                                buttonRect.Top + inset,
                                buttonRect.Right - inset,
                                buttonRect.Bottom - inset,
                                p)
                canvas.DrawLine(buttonRect.Right - inset,
                                buttonRect.Top + inset,
                                buttonRect.Left + inset,
                                buttonRect.Bottom - inset,
                                p)
            End Using
        End Sub

#End Region

#Region "Input"

        Protected Overrides Sub OnMouseMove(ctx As MASThemeContext, ptPx As SKPoint)
            Dim hit As Boolean = IsCloseHit(ctx, ptPx)
            If hit <> _hoverClose Then
                _hoverClose = hit
                InvalidateVisual()
            End If
        End Sub

        Protected Overrides Function OnMouseDown(ctx As MASThemeContext,
                                                 ptPx As SKPoint,
                                                 button As Integer) As Boolean
            If Not Enabled OrElse Not _showCloseButton Then Return False
            If button <> CInt(MouseButtons.Left) Then Return False
            If Not IsCloseHit(ctx, ptPx) Then Return False

            RequestFocus()
            _pressedClose = True
            _hoverClose = True
            StartSidePanelCloseButtonMotion()
            InvalidateVisual()
            Return True
        End Function

        Protected Overrides Function OnMouseUp(ctx As MASThemeContext,
                                               ptPx As SKPoint,
                                               button As Integer) As Boolean
            If Not _pressedClose Then Return False

            Dim activate As Boolean = IsCloseHit(ctx, ptPx)
            _pressedClose = False
            _hoverClose = activate
            If activate Then RaiseCloseRequestedSafe()
            InvalidateVisual()
            Return True
        End Function

        Protected Overrides Sub OnMouseLeave(ctx As MASThemeContext)
            If _hoverClose OrElse _pressedClose Then
                _hoverClose = False
                _pressedClose = False
                InvalidateVisual()
            End If
        End Sub

        Protected Overrides Sub OnMouseCancel(ctx As MASThemeContext)
            _hoverClose = False
            _pressedClose = False
            InvalidateVisual()
        End Sub

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

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

#End Region

#Region "Helpers"

        Private Sub ApplyAllChildPlacements()
            Dim snapshot As MASControlBase() = GetChildrenSnapshotInternal()
            For Each child As MASControlBase In snapshot
                ApplyChildPlacement(child)
            Next
        End Sub

        Private Sub ApplyChildPlacement(child As MASControlBase)
            If child Is Nothing Then Return
            Dim content As SKRect = GetContentBoundsLogical()
            If content.Width <= 1.0F OrElse content.Height <= 1.0F Then
                child.SetLocalLayoutBoundsInternal(SKRect.Empty)
                Return
            End If

            Dim relative As SKRect = SKRect.Empty
            If Not _relativeChildBounds.TryGetValue(child, relative) OrElse relative.Width <= 0.0F OrElse relative.Height <= 0.0F Then
                child.SetLocalLayoutBoundsInternal(content)
                Return
            End If

            child.SetLocalLayoutBoundsInternal(New SKRect(
                content.Left + relative.Left,
                content.Top + relative.Top,
                content.Left + relative.Right,
                content.Top + relative.Bottom))
        End Sub

        Private Shared Function NormalizeRelativeBounds(value As SKRect) As SKRect
            Return MASSidePanelSurfacePolicy.NormalizeRelativeBounds(value)
        End Function

        Private Function IsCloseHit(ctx As MASThemeContext,
                                    ptPx As SKPoint) As Boolean
            If Not _showCloseButton Then Return False
            Dim bounds As SKRect = GetPixelBounds(ctx)
            If bounds.Width <= 1.0F OrElse bounds.Height <= 1.0F Then Return False
            Dim dpi As Single = PixelSnap.SafeDpi(If(ctx Is Nothing, 1.0F, ctx.Dpi))
            Dim header As New SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + DrawerTokens.HeaderHeightDip * dpi)
            Return ResolveCloseButtonBoundsPx(header, dpi).Contains(ptPx.X, ptPx.Y)
        End Function

        Private Shared Function ResolveCloseButtonBoundsPx(header As SKRect,
                                                           dpi As Single) As SKRect
            Return MASSidePanelSurfacePolicy.ResolveCloseButtonBoundsPx(header, dpi)
        End Function

        Private Function ResolveCloseReservePx(dpi As Single) As Single
            If Not _showCloseButton Then Return DrawerTokens.PaddingRightDip * dpi
            Return (DrawerTokens.CloseButtonSizeDip + DrawerTokens.CloseButtonRightInsetDip + DrawerTokens.CloseButtonGapDip) * dpi
        End Function

        Private Function ResolveTitleText() As String
            Return MASSidePanelSurfacePolicy.ResolveRenderTitle(_title)
        End Function

        Private Shared Function NormalizeText(value As String) As String
            Return MASSidePanelSurfacePolicy.NormalizeTitle(value)
        End Function

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

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

        Private Shared Function ResolveSeparatorColor(ctx As MASThemeContext) As SKColor
            If ctx IsNot Nothing AndAlso ctx.TextColorTheme IsNot Nothing Then
                Return ctx.TextColorTheme.SecondaryText.WithAlpha(DrawerTokens.SeparatorAlpha)
            End If

            Return Nexamas.UI.Values.Color.Text.Secondary.WithAlpha(DrawerTokens.SeparatorAlpha)
        End Function

        Private Shared Function ResolveCloseFillColor(ctx As MASThemeContext,
                                                      pressed As Boolean) As SKColor
            Dim baseColor As SKColor = ResolveTitleColor(ctx)
            Return baseColor.WithAlpha(If(pressed, DrawerTokens.ClosePressedFillAlpha, DrawerTokens.CloseHoverFillAlpha))
        End Function

        Private Sub RaiseCloseRequestedSafe()
            Try
                RaiseEvent CloseRequested(Me, EventArgs.Empty)
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException1, "MASSidePanel.CloseRequested")
            End Try
        End Sub

#End Region

        Friend Function CreateSidePanelReadinessManifest() As MASSidePanelReadinessManifest
            Return MASSidePanelReadinessManifest.CreateCurrent()
        End Function

#Region "Dispose"

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

            StopSidePanelRevealMotion(resetState:=True)
            StopSidePanelCloseButtonMotion(resetState:=True)

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

            MyBase.Dispose(disposing)
        End Sub

#End Region

    End Class

End Namespace
