Option Strict On
Option Explicit On

Imports System
Imports System.Windows.Forms
Imports Nexamas.UI.Controls
Imports Nexamas.UI.Composition
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Theming
Imports SkiaSharp

Namespace Nexamas.UI.Application

    ''' <summary>
    ''' Official MAS host for lightweight layered WinForms scenes such as startup/preflight screens.
    ''' It owns ThemeHost, ControlsLayer, input routing, and UpdateLayeredWindow presentation internally.
    ''' </summary>
    Friend NotInheritable Class MASLayeredSceneHost
        Implements IDisposable

        Friend Delegate Sub DrawSceneCallback(canvas As SKCanvas,
                                              width As Integer,
                                              height As Integer,
                                              ctx As MASThemeContext)

        Private ReadOnly _owner As Form
        Private ReadOnly _themeHost As ThemeHost
        Private ReadOnly _controlsLayer As ControlsLayer
        Private ReadOnly _presenter As MASLayeredWindowPresenter
        Private ReadOnly _drawScene As DrawSceneCallback
        Private ReadOnly _compositionApplier As MASLayoutApplier
        Private ReadOnly _compositionScrollKeyPrefix As String
        Private ReadOnly _compositionScrollState As MASCompositionScrollState

        Private _compositionLayout As IMASLayoutNode
        Private _compositionViewportLogical As SKRect
        Private _compositionHasAppliedState As Boolean
        Private _compositionAppliedThemeStamp As Integer
        Private _compositionAppliedDpi As Single
        Private _compositionAppliedSizeProfileKind As MASSizeProfileKind
        Private _compositionApplying As Boolean
        Private _disposed As Boolean
        Private _renderQueued As Boolean

        Friend Sub New(owner As Form,
                       drawScene As DrawSceneCallback,
                       Optional renderPadding As Integer = 2,
                       Optional supersampleScale As Integer = 2)

            If owner Is Nothing Then Throw New ArgumentNullException(NameOf(owner))
            If drawScene Is Nothing Then Throw New ArgumentNullException(NameOf(drawScene))

            _owner = owner
            _drawScene = drawScene
            _themeHost = New ThemeHost()
            _controlsLayer = New ControlsLayer(AddressOf SafeInvalidate)
            _controlsLayer.SetHost(_themeHost)
            _presenter = New MASLayeredWindowPresenter(
                owner:=owner,
                renderPadding:=renderPadding,
                supersampleScale:=supersampleScale)

            _compositionApplier = New MASLayoutApplier()
            _compositionScrollState = New MASCompositionScrollState(AddressOf RearrangeCompositionLayoutForScroll)
            _compositionScrollKeyPrefix =
                "LayeredScene." &
                Guid.NewGuid().ToString("N") &
                "."
        End Sub

        Friend ReadOnly Property Dpi As Single
            Get
                Dim ctx As MASThemeContext = EnsureContext()

                If ctx Is Nothing Then Return 1.0F

                Return Math.Max(1.0F, ctx.Dpi)
            End Get
        End Property

        Friend Sub Add(control As MASControlBase)
            ThrowIfDisposed()

            If control Is Nothing Then Throw New ArgumentNullException(NameOf(control))

            _controlsLayer.Add(control)
        End Sub

        Friend Sub FocusControl(control As MASControlBase)
            ThrowIfDisposed()

            If control Is Nothing Then Return

            _controlsLayer.FocusControl(control)
        End Sub

        Friend Function BuildCompositionScrollKey(suffix As String) As String
            ThrowIfDisposed()

            Dim safeSuffix As String = If(suffix, String.Empty).Trim()
            If safeSuffix.Length = 0 Then safeSuffix = "Scroll"

            Return _compositionScrollKeyPrefix & safeSuffix
        End Function

        Friend Function ApplyCompositionLayout(layout As IMASLayoutNode,
                                               viewportLogical As SKRect) As Boolean
            ThrowIfDisposed()

            If layout Is Nothing Then
                Return ClearCompositionLayout()
            End If

            If viewportLogical.IsEmpty OrElse
               viewportLogical.Width <= 1.0F OrElse
               viewportLogical.Height <= 1.0F Then
                Return False
            End If

            Dim ctx As MASThemeContext = EnsureContext()
            If ctx Is Nothing Then Return False

            If CanReuseCompositionLayout(layout, viewportLogical, ctx) Then Return True

            Dim diagnostics As New MASLayoutDiagnosticBag()

            _compositionApplying = True

            Try
                Dim integrationContext As MASSizeLayoutIntegrationContext = MASSizeLayoutIntegrationGateway.FromTheme(ctx)

                Dim measureContext As New MASLayoutMeasureContext(
                    availableSize:=New SKSize(viewportLogical.Width, viewportLogical.Height),
                    integrationContext:=integrationContext,
                    diagnostics:=diagnostics)

                layout.Measure(measureContext)

                Dim arrangeContext As New MASLayoutArrangeContext(
                    boundsLogical:=viewportLogical,
                    integrationContext:=integrationContext,
                    diagnostics:=diagnostics,
                    scrollOffsetResolver:=AddressOf _compositionScrollState.GetOffset)

                Dim arrangeResult As MASLayoutArrangeResult = layout.Arrange(arrangeContext)

                Dim scrollOffsetChanged As Boolean =
                    _compositionScrollState.ApplyRequests(
                        requests:=If(arrangeResult Is Nothing, Nothing, arrangeResult.ScrollRequests),
                        ownerKeyPrefix:=_compositionScrollKeyPrefix,
                        ctx:=ctx)

                If scrollOffsetChanged Then
                    arrangeResult = layout.Arrange(arrangeContext)
                    _compositionScrollState.ApplyRequests(
                        requests:=If(arrangeResult Is Nothing, Nothing, arrangeResult.ScrollRequests),
                        ownerKeyPrefix:=_compositionScrollKeyPrefix,
                        ctx:=ctx)
                End If

                Dim target As New MASLayoutApplyTarget(
                    controlsLayer:=_controlsLayer,
                    viewportLogical:=viewportLogical,
                    themeContext:=ctx,
                    requestInvalidate:=AddressOf SafeInvalidate)

                Dim success As Boolean = _compositionApplier.Apply(target, arrangeResult, diagnostics)

                If success AndAlso Not diagnostics.HasErrors Then
                    _compositionLayout = layout
                    _compositionViewportLogical = viewportLogical
                    RememberCompositionApplyState(layout, viewportLogical, ctx)
                    If scrollOffsetChanged Then SafeInvalidate()
                End If

                Return success AndAlso Not diagnostics.HasErrors

            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException1)
                Return False
            Finally
                _compositionApplying = False
            End Try
        End Function

        Friend Function RouteCompositionPointerWheel(pointLogical As SKPoint,
                                                     delta As Integer) As Boolean
            If _disposed Then Return False

            If Not _compositionScrollState.TryWheelAt(_compositionScrollKeyPrefix, pointLogical, delta) Then
                Return False
            End If

            RearrangeCompositionLayoutForScroll()
            Return True
        End Function

        Friend Function RouteCompositionPointerDown(pointLogical As SKPoint) As Boolean
            If _disposed Then Return False

            If Not _compositionScrollState.TryPointerDownAt(_compositionScrollKeyPrefix, pointLogical) Then
                Return False
            End If

            RearrangeCompositionLayoutForScroll()
            Return True
        End Function

        Friend Function RouteCompositionPointerMove(pointLogical As SKPoint) As Boolean
            If _disposed Then Return False

            If Not _compositionScrollState.TryPointerMoveAt(_compositionScrollKeyPrefix, pointLogical) Then
                Return False
            End If

            RearrangeCompositionLayoutForScroll()
            Return True
        End Function

        Friend Function RouteCompositionPointerUp(pointLogical As SKPoint) As Boolean
            If _disposed Then Return False

            If Not _compositionScrollState.TryPointerUpAt(_compositionScrollKeyPrefix, pointLogical) Then
                Return False
            End If

            SafeInvalidate()
            Return True
        End Function

        Friend Function RouteCompositionPointerLeave() As Boolean
            If _disposed Then Return False

            If Not _compositionScrollState.PointerLeave(_compositionScrollKeyPrefix) Then
                Return False
            End If

            SafeInvalidate()
            Return True
        End Function

        Friend Sub InvalidateCompositionLayout()
            ThrowIfDisposed()

            ResetCompositionApplyState()
            SafeInvalidate()
        End Sub

        Private Function ClearCompositionLayout() As Boolean
            _compositionLayout = Nothing
            _compositionViewportLogical = SKRect.Empty
            ResetCompositionApplyState()
            _compositionScrollState.ClearOwner(_compositionScrollKeyPrefix)

            Dim diagnostics As New MASLayoutDiagnosticBag()
            Dim target As New MASLayoutApplyTarget(
                controlsLayer:=_controlsLayer,
                viewportLogical:=SKRect.Empty,
                themeContext:=EnsureContext(),
                requestInvalidate:=AddressOf SafeInvalidate)

            Return _compositionApplier.Apply(target, New MASLayoutArrangeResult(), diagnostics)
        End Function


        Private Sub RearrangeCompositionLayoutForScroll()
            If _compositionLayout Is Nothing Then Return
            If _compositionViewportLogical.IsEmpty Then Return

            Dim ctx As MASThemeContext = EnsureContext()
            If ctx Is Nothing Then Return

            Dim diagnostics As New MASLayoutDiagnosticBag()

            Try
                Dim integrationContext As MASSizeLayoutIntegrationContext = MASSizeLayoutIntegrationGateway.FromTheme(ctx)
                Dim arrangeContext As New MASLayoutArrangeContext(
                    boundsLogical:=_compositionViewportLogical,
                    integrationContext:=integrationContext,
                    diagnostics:=diagnostics,
                    scrollOffsetResolver:=AddressOf _compositionScrollState.GetOffset)

                Dim arrangeResult As MASLayoutArrangeResult = _compositionLayout.Arrange(arrangeContext)

                Dim scrollOffsetChanged As Boolean =
                    _compositionScrollState.ApplyRequests(
                        requests:=If(arrangeResult Is Nothing, Nothing, arrangeResult.ScrollRequests),
                        ownerKeyPrefix:=_compositionScrollKeyPrefix,
                        ctx:=ctx)

                If scrollOffsetChanged Then
                    arrangeResult = _compositionLayout.Arrange(arrangeContext)
                    _compositionScrollState.ApplyRequests(
                        requests:=If(arrangeResult Is Nothing, Nothing, arrangeResult.ScrollRequests),
                        ownerKeyPrefix:=_compositionScrollKeyPrefix,
                        ctx:=ctx)
                End If

                Dim target As New MASLayoutApplyTarget(
                    controlsLayer:=_controlsLayer,
                    viewportLogical:=_compositionViewportLogical,
                    themeContext:=ctx,
                    requestInvalidate:=AddressOf SafeInvalidate)

                Dim success As Boolean = _compositionApplier.Apply(target, arrangeResult, diagnostics)
                If success AndAlso Not diagnostics.HasErrors Then
                    RememberCompositionApplyState(_compositionLayout, _compositionViewportLogical, ctx)
                    SafeInvalidate()
                Else
                    ResetCompositionApplyState()
                End If

            Catch masCaughtExceptionRearrange As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtExceptionRearrange)
            End Try
        End Sub


        Private Function CanReuseCompositionLayout(layout As IMASLayoutNode,
                                                  viewportLogical As SKRect,
                                                  ctx As MASThemeContext) As Boolean
            If _compositionApplying Then Return False
            If Not _compositionHasAppliedState Then Return False
            If _compositionLayout Is Nothing Then Return False
            If Not Object.ReferenceEquals(_compositionLayout, layout) Then Return False
            If Not IsSameRect(_compositionViewportLogical, viewportLogical) Then Return False
            If ctx Is Nothing Then Return False
            If _compositionAppliedThemeStamp <> ctx.ThemeStamp Then Return False
            If Math.Abs(_compositionAppliedDpi - ctx.Dpi) > 0.001F Then Return False
            If _compositionAppliedSizeProfileKind <> MASRuntimeSizeProfileService.CurrentKind Then Return False

            Return True
        End Function

        Private Sub RememberCompositionApplyState(layout As IMASLayoutNode,
                                                  viewportLogical As SKRect,
                                                  ctx As MASThemeContext)
            _compositionLayout = layout
            _compositionViewportLogical = viewportLogical
            _compositionAppliedThemeStamp = If(ctx Is Nothing, 0, ctx.ThemeStamp)
            _compositionAppliedDpi = If(ctx Is Nothing, 1.0F, ctx.Dpi)
            _compositionAppliedSizeProfileKind = MASRuntimeSizeProfileService.CurrentKind
            _compositionHasAppliedState = True
        End Sub

        Private Sub ResetCompositionApplyState()
            _compositionHasAppliedState = False
            _compositionAppliedThemeStamp = 0
            _compositionAppliedDpi = 0.0F
            _compositionAppliedSizeProfileKind = MASSizeProfileKind.[Default]
        End Sub

        Private Shared Function IsSameRect(left As SKRect,
                                           right As SKRect) As Boolean
            Const Epsilon As Single = 0.01F

            Return Math.Abs(left.Left - right.Left) <= Epsilon AndAlso
                   Math.Abs(left.Top - right.Top) <= Epsilon AndAlso
                   Math.Abs(left.Right - right.Right) <= Epsilon AndAlso
                   Math.Abs(left.Bottom - right.Bottom) <= Epsilon
        End Function

        Private Sub ReapplyCompositionLayout()
            If _compositionLayout Is Nothing Then Return
            If _compositionViewportLogical.IsEmpty Then Return

            Try
                ApplyCompositionLayout(_compositionLayout, _compositionViewportLogical)
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException2)
            End Try
        End Sub

        Friend Sub MouseMove(e As MouseEventArgs)
            If e Is Nothing Then Return

            Dim ctx As MASThemeContext = EnsureContext()
            If ctx Is Nothing Then Return

            _controlsLayer.MouseMove(ctx, New SKPoint(e.X, e.Y))
        End Sub

        Friend Sub MouseLeave()
            Dim ctx As MASThemeContext = EnsureContext()
            If ctx Is Nothing Then Return

            _controlsLayer.MouseLeave(ctx)
            SafeInvalidate()
        End Sub

        Friend Sub MouseDown(e As MouseEventArgs)
            If e Is Nothing Then Return

            Dim ctx As MASThemeContext = EnsureContext()
            If ctx Is Nothing Then Return

            _controlsLayer.MouseDown(ctx, New SKPoint(e.X, e.Y), CInt(e.Button))
        End Sub

        Friend Sub MouseUp(e As MouseEventArgs)
            If e Is Nothing Then Return

            Dim ctx As MASThemeContext = EnsureContext()
            If ctx Is Nothing Then Return

            _controlsLayer.MouseUp(ctx, New SKPoint(e.X, e.Y), CInt(e.Button))
        End Sub

        Friend Sub MouseWheel(e As MouseEventArgs)
            If e Is Nothing Then Return

            Dim ctx As MASThemeContext = EnsureContext()
            If ctx Is Nothing Then Return

            _controlsLayer.MouseWheel(ctx, New SKPoint(e.X, e.Y), e.Delta)
        End Sub

        Friend Function KeyDown(e As KeyEventArgs) As Boolean
            If e Is Nothing Then Return False

            Dim ctx As MASThemeContext = EnsureContext()
            If ctx Is Nothing Then Return False

            Dim handled As Boolean = _controlsLayer.KeyDown(ctx, e.KeyCode)

            If handled Then SafeInvalidate()

            Return handled
        End Function

        Friend Function KeyUp(e As KeyEventArgs) As Boolean
            If e Is Nothing Then Return False

            Dim ctx As MASThemeContext = EnsureContext()
            If ctx Is Nothing Then Return False

            Dim handled As Boolean = _controlsLayer.KeyUp(ctx, e.KeyCode)

            If handled Then SafeInvalidate()

            Return handled
        End Function

        Friend Sub Render()
            If _disposed Then Return
            If _owner.IsDisposed Then Return
            If Not _owner.IsHandleCreated Then Return
            If _owner.ClientSize.Width <= 0 OrElse _owner.ClientSize.Height <= 0 Then Return

            _presenter.Render(AddressOf DrawLayeredScene)
        End Sub

        Private Function EnsureContext() As MASThemeContext
            If _disposed Then Return Nothing

            Try
                _themeHost.Ensure(1.0F)
                Return _themeHost.TryGetContext()
            Catch masCaughtException3 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException3)
                Return Nothing
            End Try
        End Function

        Private Sub DrawLayeredScene(canvas As SKCanvas,
                                     width As Integer,
                                     height As Integer)

            If canvas Is Nothing Then Return
            If width <= 0 OrElse height <= 0 Then Return

            _themeHost.EnsureFromSurface(
                controlW:=width,
                controlH:=height,
                surfaceW:=width,
                surfaceH:=height)

            Dim ctx As MASThemeContext = _themeHost.TryGetContext()

            If ctx Is Nothing Then
                Throw New InvalidOperationException("ThemeContext is Nothing.")
            End If

            canvas.Clear(SKColors.Transparent)

            _drawScene.Invoke(canvas, width, height, ctx)
            _controlsLayer.Render(canvas, ctx)
            _compositionScrollState.RenderIndicators(_compositionScrollKeyPrefix, canvas, ctx)
        End Sub

        Private Sub SafeInvalidate()
            If _disposed Then Return
            If _owner.IsDisposed Then Return
            If Not _owner.IsHandleCreated Then Return
            If _renderQueued Then Return

            _renderQueued = True

            Try
                _owner.BeginInvoke(New MethodInvoker(AddressOf RenderQueued))
            Catch beginInvokeEx As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(beginInvokeEx)
                _renderQueued = False

                Try
                    Render()
                Catch renderFallbackEx As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(renderFallbackEx)
                End Try
            End Try
        End Sub

        Private Sub RenderQueued()
            If _disposed Then Return
            If _owner.IsDisposed Then Return

            _renderQueued = False

            Try
                Render()
            Catch masCaughtException4 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException4)
            End Try
        End Sub

        Private Sub ThrowIfDisposed()
            If _disposed Then Throw New ObjectDisposedException(NameOf(MASLayeredSceneHost))
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            If _disposed Then Return

            Try
                _compositionScrollState.ClearOwner(_compositionScrollKeyPrefix)
            Catch masCaughtException5 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException5)
            End Try

            Try
                If _compositionApplier IsNot Nothing Then
                    Dim diagnostics As New MASLayoutDiagnosticBag()
                    Dim target As New MASLayoutApplyTarget(
                        controlsLayer:=_controlsLayer,
                        viewportLogical:=SKRect.Empty,
                        themeContext:=EnsureContext(),
                        requestInvalidate:=Nothing)

                    If Not _compositionApplier.DisposeWithTarget(target, diagnostics) Then
                        _compositionApplier.ReleaseOwnershipAfterRootShutdownDetachFault(diagnostics)
                        Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(
                            New InvalidOperationException("Layered scene Composition detach failed during dispose."),
                            "MASLayeredSceneHost.CompositionDisposeWithTarget")
                    End If
                End If
            Catch masCaughtException6 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException6)
            End Try

            _disposed = True

            Try
                _controlsLayer.Dispose()
            Catch masCaughtException7 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException7)
            End Try

            Try
                _themeHost.Dispose()
            Catch masCaughtException8 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException8)
            End Try
        End Sub

    End Class

End Namespace
