Option Strict On
Option Explicit On

Imports System
Imports Nexamas.UI.Host
Imports SkiaSharp
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Layout

Namespace Nexamas.UI.Composition

    ''' <summary>
    ''' Per-root-host Composition runtime.
    ''' It is the official host-level bridge from layout descriptions to Measure/Arrange/Apply.
    ''' It owns only the MASLayoutApplier for one MASSkiaRootHost and does not render, route input,
    ''' own Theme state, open overlays, or replace ControlsLayer.
    ''' </summary>
    Friend NotInheritable Class MASCompositionHostRuntime
        Implements IDisposable

        Private ReadOnly _rootHost As MASSkiaRootHost
        Private ReadOnly _applier As MASLayoutApplier
        Private ReadOnly _layout As MASHostLayoutFacade
        Private ReadOnly _page As MASHostPageFacade
        Private ReadOnly _screen As MASHostScreenFacade
        Private ReadOnly _scrollKeyPrefix As String
        Private ReadOnly _scrollState As MASCompositionScrollState

        Private _layoutFactory As Func(Of IMASLayoutNode)
        Private _lastLayout As IMASLayoutNode
        Private _lastDiagnostics As MASLayoutDiagnostic() = New MASLayoutDiagnostic() {}
        Private _lastApplySucceeded As Boolean = True
        Private _disposed As Boolean

        Friend Event Applied As EventHandler(Of MASCompositionAppliedEventArgs)

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

            _rootHost = rootHost
            _scrollKeyPrefix = "Host." & Me.GetHashCode().ToString(System.Globalization.CultureInfo.InvariantCulture) & "."
            _scrollState = New MASCompositionScrollState(Sub() RearrangeLastLayoutForScroll())
            _applier = New MASLayoutApplier()
            _layout = New MASHostLayoutFacade(Me)
            _page = New MASHostPageFacade(Me)
            _screen = New MASHostScreenFacade(Me)
        End Sub

        ''' <summary>
        ''' Low-level layout facade scoped to this host.
        ''' </summary>
        Friend ReadOnly Property Layout As MASHostLayoutFacade
            Get
                Return _layout
            End Get
        End Property

        ''' <summary>
        ''' Page recipe facade scoped to this host.
        ''' </summary>
        Friend ReadOnly Property Page As MASHostPageFacade
            Get
                Return _page
            End Get
        End Property

        ''' <summary>
        ''' Screen recipe facade scoped to this host.
        ''' </summary>
        Friend ReadOnly Property Screen As MASHostScreenFacade
            Get
                Return _screen
            End Get
        End Property

        Friend ReadOnly Property LastDiagnostics As MASLayoutDiagnostic()
            Get
                Return CType(_lastDiagnostics.Clone(), MASLayoutDiagnostic())
            End Get
        End Property

        Friend ReadOnly Property LastApplySucceeded As Boolean
            Get
                Return _lastApplySucceeded
            End Get
        End Property

        Friend ReadOnly Property ScrollKeyPrefix As String
            Get
                Return _scrollKeyPrefix
            End Get
        End Property

        Friend Function BuildScrollKey(scrollKey As String) As String
            Dim suffix As String = If(scrollKey, String.Empty).Trim()
            If suffix.Length = 0 Then suffix = "Page.Form"
            Return _scrollKeyPrefix & suffix
        End Function

        Friend ReadOnly Property HasLayoutFactory As Boolean
            Get
                Return _layoutFactory IsNot Nothing
            End Get
        End Property

        ''' <summary>
        ''' Stores and immediately applies a stable layout node. Use SetLayoutFactory when the node must be rebuilt on resize.
        ''' </summary>
        Friend Function SetLayout(layout As IMASLayoutNode) As Boolean
            If layout Is Nothing Then
                _layoutFactory = Nothing
                Return Clear()
            End If

            _layoutFactory = Function() layout
            Return RebuildCurrent()
        End Function

        ''' <summary>
        ''' Stores a layout factory and optionally applies it immediately.
        ''' The root host calls RebuildCurrent from its normal rebuild path, so resize/theme invalidation can rebuild the layout safely.
        ''' </summary>
        Friend Function SetLayoutFactory(factory As Func(Of IMASLayoutNode),
                                         Optional applyImmediately As Boolean = True) As Boolean
            _layoutFactory = factory

            If factory Is Nothing Then
                Return Clear()
            End If

            If Not applyImmediately Then
                Return True
            End If

            Return RebuildCurrent()
        End Function

        ''' <summary>
        ''' Rebuilds the currently stored layout factory and applies the result.
        ''' No-op when no factory is registered.
        ''' </summary>
        Friend Function RebuildCurrent() As Boolean
            If _disposed Then Return False
            If _layoutFactory Is Nothing Then Return True

            Dim layout As IMASLayoutNode = Nothing
            Dim diagnostics As New MASLayoutDiagnosticBag()

            Try
                _page.BeginLayoutFactoryBuild()

                Try
                    layout = _layoutFactory.Invoke()
                Finally
                    _page.EndLayoutFactoryBuild()
                End Try
            Catch masCaughtException1 As Exception
                diagnostics.AddError(
                    code:="MASComposition.LayoutFactoryException",
                    message:="The Composition layout factory threw an exception.",
                    detail:=masCaughtException1.GetType().Name & ": " & masCaughtException1.Message
                )

                SetLastResult(False, diagnostics)
                Return False
            End Try

            _lastLayout = layout
            Return Apply(layout)
        End Function

        ''' <summary>
        ''' Applies one layout node once without storing it as the current layout factory.
        ''' </summary>
        Friend Function Apply(layout As IMASLayoutNode) As Boolean
            _lastLayout = layout
            Dim diagnostics As New MASLayoutDiagnosticBag()
            Return ApplyInternal(layout, diagnostics)
        End Function

        ''' <summary>
        ''' Removes controls owned by this composition runtime from the host ControlsLayer.
        ''' </summary>
        Friend Function Clear() As Boolean
            If _disposed Then Return False

            _layoutFactory = Nothing
            _lastLayout = Nothing

            Dim diagnostics As New MASLayoutDiagnosticBag()

            Try
                _rootHost.EnsureThemeContextForComposition()

                Dim target As MASLayoutApplyTarget = MASLayoutApplyTarget.FromRootHost(_rootHost)
                Dim emptyResult As New MASLayoutArrangeResult()
                _scrollState.ClearOwner(_scrollKeyPrefix)
                Dim success As Boolean = _applier.Apply(target, emptyResult, diagnostics)
                _page.ClearTextRecipeCache()
                SetLastResult(success, diagnostics)
                Return success
            Catch masCaughtException2 As Exception
                diagnostics.AddError(
                    code:="MASComposition.ClearException",
                    message:="Composition clear failed.",
                    detail:=masCaughtException2.GetType().Name & ": " & masCaughtException2.Message
                )

                SetLastResult(False, diagnostics)
                Return False
            End Try
        End Function

        Private Function ApplyInternal(layout As IMASLayoutNode,
                                       diagnostics As MASLayoutDiagnosticBag) As Boolean
            If _disposed Then
                diagnostics.AddError("MASComposition.ApplyAfterDispose", "Composition apply was called after dispose.")
                SetLastResult(False, diagnostics)
                Return False
            End If

            If layout Is Nothing Then
                diagnostics.AddError("MASComposition.NullLayout", "Composition apply failed because the layout node is Nothing.")
                SetLastResult(False, diagnostics)
                Return False
            End If

            Try
                _rootHost.EnsureThemeContextForComposition()

                Dim target As MASLayoutApplyTarget = MASLayoutApplyTarget.FromRootHost(_rootHost)

                Dim integrationContext As MASSizeLayoutIntegrationContext = MASSizeLayoutIntegrationGateway.FromTheme(target.ThemeContext)

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

                layout.Measure(measureContext)

                If diagnostics.HasErrors Then
                    SetLastResult(False, diagnostics)
                    Return False
                End If

                Dim arrangeContext As New MASLayoutArrangeContext(
                    boundsLogical:=target.ViewportLogical,
                    integrationContext:=integrationContext,
                    diagnostics:=diagnostics,
                    scrollOffsetResolver:=AddressOf _scrollState.GetOffset
                )

                Dim arrangeResult As MASLayoutArrangeResult = layout.Arrange(arrangeContext)

                If ShouldBlockCompositionApply(arrangeResult, diagnostics, "MASComposition.ArrangeResultMissing") Then
                    SetLastResult(False, diagnostics)
                    Return False
                End If

                Dim scrollOffsetChanged As Boolean = _scrollState.ApplyRequests(arrangeResult.ScrollRequests, _scrollKeyPrefix, target.ThemeContext)

                If scrollOffsetChanged Then
                    arrangeResult = layout.Arrange(arrangeContext)

                    If ShouldBlockCompositionApply(arrangeResult, diagnostics, "MASComposition.ArrangeResultMissingAfterScroll") Then
                        SetLastResult(False, diagnostics)
                        Return False
                    End If

                    _scrollState.ApplyRequests(arrangeResult.ScrollRequests, _scrollKeyPrefix, target.ThemeContext)
                End If

                If diagnostics.HasErrors Then
                    SetLastResult(False, diagnostics)
                    Return False
                End If

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

                SetLastResult(success AndAlso Not diagnostics.HasErrors, diagnostics)
                If _lastApplySucceeded AndAlso scrollOffsetChanged Then _rootHost.RequestInvalidate()
                Return _lastApplySucceeded

            Catch masCaughtException3 As Exception
                diagnostics.AddError(
                    code:="MASComposition.ApplyException",
                    message:="Composition apply failed with an exception.",
                    detail:=masCaughtException3.GetType().Name & ": " & masCaughtException3.Message
                )

                SetLastResult(False, diagnostics)
                Return False
            End Try
        End Function


        ''' <summary>
        ''' Re-arranges the last successfully built layout without invoking the page factory or re-running Measure.
        ''' This path is used only for Composition scroll interactions where geometry is already measured and only
        ''' the runtime scroll offset changed. Resize, theme changes, and explicit page updates still use RebuildCurrent.
        ''' </summary>
        Private Function RearrangeLastLayoutForScroll() As Boolean
            If _disposed Then Return False
            If _lastLayout Is Nothing Then Return RebuildCurrent()

            Dim diagnostics As New MASLayoutDiagnosticBag()

            Try
                _rootHost.EnsureThemeContextForComposition()

                Dim target As MASLayoutApplyTarget = MASLayoutApplyTarget.FromRootHost(_rootHost)
                Dim integrationContext As MASSizeLayoutIntegrationContext = MASSizeLayoutIntegrationGateway.FromTheme(target.ThemeContext)
                Dim arrangeContext As New MASLayoutArrangeContext(
                    boundsLogical:=target.ViewportLogical,
                    integrationContext:=integrationContext,
                    diagnostics:=diagnostics,
                    scrollOffsetResolver:=AddressOf _scrollState.GetOffset
                )

                Dim arrangeResult As MASLayoutArrangeResult = _lastLayout.Arrange(arrangeContext)

                If ShouldBlockCompositionApply(arrangeResult, diagnostics, "MASComposition.ScrollArrangeResultMissing") Then
                    SetLastResult(False, diagnostics)
                    Return False
                End If

                Dim scrollOffsetChanged As Boolean = _scrollState.ApplyRequests(arrangeResult.ScrollRequests, _scrollKeyPrefix, target.ThemeContext)

                If scrollOffsetChanged Then
                    arrangeResult = _lastLayout.Arrange(arrangeContext)

                    If ShouldBlockCompositionApply(arrangeResult, diagnostics, "MASComposition.ScrollArrangeResultMissingAfterScroll") Then
                        SetLastResult(False, diagnostics)
                        Return False
                    End If

                    _scrollState.ApplyRequests(arrangeResult.ScrollRequests, _scrollKeyPrefix, target.ThemeContext)
                End If

                If diagnostics.HasErrors Then
                    SetLastResult(False, diagnostics)
                    Return False
                End If

                Dim success As Boolean = _applier.Apply(target, arrangeResult, diagnostics)
                SetLastResult(success AndAlso Not diagnostics.HasErrors, diagnostics)
                If _lastApplySucceeded Then _rootHost.RequestInvalidate()
                Return _lastApplySucceeded

            Catch masCaughtExceptionScrollRearrange As Exception
                diagnostics.AddError(
                    code:="MASComposition.ScrollRearrangeException",
                    message:="Composition scroll rearrange failed and will require a full rebuild on the next host rebuild.",
                    detail:=masCaughtExceptionScrollRearrange.GetType().Name & ": " & masCaughtExceptionScrollRearrange.Message
                )

                SetLastResult(False, diagnostics)
                Return False
            End Try
        End Function


        ''' <summary>
        ''' Blocks Composition apply before ControlsLayer mutation whenever Measure/Arrange produced an error or no
        ''' arrange result. This gives Composition the same fail-before-mutate ownership guarantee as Application
        ''' retained layout.
        ''' </summary>
        Private Shared Function ShouldBlockCompositionApply(arrangeResult As MASLayoutArrangeResult,
                                                            diagnostics As MASLayoutDiagnosticBag,
                                                            code As String) As Boolean
            If diagnostics IsNot Nothing AndAlso diagnostics.HasErrors Then Return True
            If arrangeResult IsNot Nothing Then Return False

            If diagnostics IsNot Nothing Then
                diagnostics.AddError(
                    If(String.IsNullOrWhiteSpace(code), "MASComposition.ArrangeResultMissing", code),
                    "Composition layout did not produce an arrange result.",
                    "Composition must fail before ControlsLayer mutation when Measure/Arrange cannot produce verified placements.")
            End If

            Return True
        End Function

        Private Sub SetLastResult(succeeded As Boolean, diagnostics As MASLayoutDiagnosticBag)
            _lastApplySucceeded = succeeded
            _lastDiagnostics = If(diagnostics, New MASLayoutDiagnosticBag()).ToArray()
            RaiseEvent Applied(Me, New MASCompositionAppliedEventArgs(_lastApplySucceeded, LastDiagnostics))
        End Sub


        ''' <summary>
        ''' Routes root-host wheel input to active Composition scroll regions.
        ''' Returns True only when a Composition scroll offset changed and the current layout was re-arranged.
        ''' </summary>
        Friend Function RoutePointerWheel(pointLogical As SKPoint, delta As Integer) As Boolean
            If _disposed Then Return False

            If Not _scrollState.TryWheelAt(_scrollKeyPrefix, pointLogical, delta) Then
                Return False
            End If

            RearrangeLastLayoutForScroll()
            Return True
        End Function

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

            If Not _scrollState.TryPointerDownAt(_scrollKeyPrefix, pointLogical) Then
                Return False
            End If

            RearrangeLastLayoutForScroll()
            Return True
        End Function

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

            If Not _scrollState.TryPointerMoveAt(_scrollKeyPrefix, pointLogical) Then
                Return False
            End If

            RearrangeLastLayoutForScroll()
            Return True
        End Function

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

            If Not _scrollState.TryPointerUpAt(_scrollKeyPrefix, pointLogical) Then
                Return False
            End If

            _rootHost.RequestInvalidate()
            Return True
        End Function

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

            If Not _scrollState.PointerLeave(_scrollKeyPrefix) Then
                Return False
            End If

            _rootHost.RequestInvalidate()
            Return True
        End Function

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

            If Not _scrollState.CancelPointerCapture(_scrollKeyPrefix) Then
                Return False
            End If

            _rootHost.RequestInvalidate()
            Return True
        End Function

        Friend Sub RenderScrollIndicators(canvas As SKCanvas, ctx As MASThemeContext)
            If _disposed Then Return
            _scrollState.RenderIndicators(_scrollKeyPrefix, canvas, ctx)
        End Sub

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

            Dim diagnostics As New MASLayoutDiagnosticBag()
            Dim applierDisposed As Boolean = False

            Try
                _rootHost.EnsureThemeContextForComposition()
                _scrollState.ClearOwner(_scrollKeyPrefix)
                _page.ClearTextRecipeCache()

                Dim target As MASLayoutApplyTarget = MASLayoutApplyTarget.FromRootHost(_rootHost)
                applierDisposed = _applier.DisposeWithTarget(target, diagnostics)
                If Not applierDisposed Then
                    _applier.ReleaseOwnershipAfterRootShutdownDetachFault(diagnostics)
                End If
            Catch masCaughtException4 As Exception
                diagnostics.AddError(
                    code:="MASComposition.DisposeCleanupException",
                    message:="Composition dispose cleanup failed before owned controls were fully detached.",
                    detail:=masCaughtException4.GetType().Name & ": " & masCaughtException4.Message)
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException4)
            Finally
                ReleaseRetainedLayoutReferencesForDispose()
            End Try

            If Not applierDisposed OrElse diagnostics.HasErrors Then
                SetLastResult(False, diagnostics)
            End If

            _disposed = True
        End Sub

        Private Sub ReleaseRetainedLayoutReferencesForDispose()
            ' APP-R04: after root-host shutdown the runtime must not retain a layout factory
            ' closure or the last layout tree. Ownership diagnostics are preserved in _lastDiagnostics;
            ' retained layout references are not part of the post-dispose contract.
            _layoutFactory = Nothing
            _lastLayout = Nothing
        End Sub

    End Class

End Namespace
