Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports Nexamas.UI.Components
Imports Nexamas.UI.Controls
Imports Nexamas.UI.Host
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Theming
Imports SkiaSharp

Namespace Nexamas.UI.Application

    Partial Public NotInheritable Class MASApplicationWindowControls

        ''' <summary>
        ''' Keeps layout-session replacement atomic: a failed new LayoutPage/LayoutRegions/LayoutApplicationSurface
        ''' recipe must not dispose the previously valid retained session or its resize/profile hooks.
        ''' </summary>
        Friend Shared Function ShouldRetainExistingLayoutSessionAfterFailedApply(applied As Boolean) As Boolean
            Return Not applied
        End Function


        ''' <summary>
        ''' Clears any retained Application layout session, regardless of whether it came from LayoutPage,
        ''' LayoutRegions, or LayoutApplicationSurface. The name is intentionally broad so future maintenance does
        ''' not treat workspace/region cleanup as a page-only path.
        ''' </summary>
        Friend Sub ClearRetainedLayoutSessionsInternal()
            If _layoutPageSession IsNot Nothing Then
                _layoutPageSession.Dispose()
                _layoutPageSession = Nothing
            End If

            If _layoutRegionsSession IsNot Nothing Then
                _layoutRegionsSession.Dispose()
                _layoutRegionsSession = Nothing
            End If

            If _layoutApplicationSurfaceSession IsNot Nothing Then
                _layoutApplicationSurfaceSession.Dispose()
                _layoutApplicationSurfaceSession = Nothing
            End If
        End Sub


        ''' <summary>
        ''' Centralizes the transition from retained Application layout ownership back to direct consumer mutation.
        ''' Remove/Clear/Place must pass through this gate so session hooks, application-surface background cleanup, and
        ''' layout-hosted control ownership are released consistently without duplicating tiny cleanup paths.
        ''' </summary>
        Friend Function ReleaseRetainedLayoutForDirectMutationInternal() As Boolean
            Dim hasRetainedState As Boolean = _layoutPageSession IsNot Nothing OrElse
                                             _layoutRegionsSession IsNot Nothing OrElse
                                             _layoutApplicationSurfaceSession IsNot Nothing OrElse
                                             _layoutHostedControls.Count > 0

            If Not ShouldReleaseRetainedLayoutForDirectMutation(hasRetainedState) Then Return False

            ClearRetainedLayoutSessionsInternal()
            Dim hostedControlsChanged As Boolean = ReleaseLayoutHostedControlsInternal()
            Return ShouldReportRetainedLayoutStateReleased(hasRetainedState, hostedControlsChanged)
        End Function


        Friend Shared Function ShouldReleaseRetainedLayoutForDirectMutation(hasRetainedLayoutState As Boolean) As Boolean
            Return hasRetainedLayoutState
        End Function


        Friend Shared Function ShouldReportRetainedLayoutStateReleased(hadRetainedLayoutState As Boolean,
                                                                       hostedControlsChanged As Boolean) As Boolean
            Return hadRetainedLayoutState OrElse hostedControlsChanged
        End Function




        ''' <summary>
        ''' Opens a retained Application layout build-admission scope. Controls added through the public
        ''' Add/AddControl helpers while LayoutPage, LayoutRegions, or LayoutApplicationSurface is building
        ''' its official recipe are admitted as layout-hosted controls, so the next successful recipe can
        ''' remove them when they are no longer desired. This keeps same-window page navigation from
        ''' stacking stale page controls while direct Add outside retained layout remains consumer-owned.
        ''' </summary>
        Friend Sub BeginRetainedLayoutBuildAdmissionInternal()
            _retainedLayoutBuildAdmissionDepth += 1
        End Sub


        Friend Sub EndRetainedLayoutBuildAdmissionInternal()
            If _retainedLayoutBuildAdmissionDepth <= 0 Then
                _retainedLayoutBuildAdmissionDepth = 0
                Return
            End If

            _retainedLayoutBuildAdmissionDepth -= 1
        End Sub


        Friend Function IsRetainedLayoutBuildAdmissionActiveInternal() As Boolean
            Return _retainedLayoutBuildAdmissionDepth > 0
        End Function


        ''' <summary>
        ''' Records a control admitted during a retained Application layout recipe. A control that was
        ''' already hosted before the recipe remains consumer-owned unless it was already layout-hosted;
        ''' newly admitted controls are marked so ReconcileLayoutHostedControlsInternal can dispose of
        ''' stale workspace/page content on the next successful layout pass.
        ''' </summary>
        Friend Function AdmitRetainedLayoutBuildControlInternal(control As MASControlBase,
                                                               wasAlreadyHosted As Boolean) As Boolean
            If control Is Nothing Then Return False
            If Not IsRetainedLayoutBuildAdmissionActiveInternal() Then Return False

            If wasAlreadyHosted AndAlso Not _layoutHostedControls.Contains(control) Then Return False
            Return _layoutHostedControls.Add(control)
        End Function


        ''' <summary>
        ''' Reconciles the controls that were hosted by the retained Application layout gateway itself.
        ''' Controls that were already in the layer before a layout recipe remain consumer-owned and are not
        ''' removed when a later recipe omits them; controls first admitted by LayoutPage/LayoutRegions/LayoutApplicationSurface
        ''' are removed when they are no longer desired by the successful next recipe.
        ''' </summary>
        Friend Function ReconcileLayoutHostedControlsInternal(controls As IEnumerable(Of MASControlBase)) As Boolean
            Dim desiredControls As New List(Of MASControlBase)()
            Dim desiredSet As New HashSet(Of MASControlBase)()

            If controls IsNot Nothing Then
                For Each control As MASControlBase In controls
                    If control Is Nothing Then Continue For
                    If desiredSet.Add(control) Then desiredControls.Add(control)
                Next
            End If

            Dim changed As Boolean = False
            Layer.BeginUpdateInternal()

            Try
                For Each oldControl As MASControlBase In _layoutHostedControls.ToArray()
                    If ShouldRemoveStaleLayoutHostedControl(True, desiredSet.Contains(oldControl)) Then
                        Dim wasHosted As Boolean = Contains(oldControl)
                        DisposeLayoutHostedControlInternal(oldControl)
                        If wasHosted Then changed = True

                        _layoutHostedControls.Remove(oldControl)
                    End If
                Next

                For Each control As MASControlBase In desiredControls
                    If Not Contains(control) Then
                        Layer.Add(control)
                        _layoutHostedControls.Add(control)
                        changed = True
                    End If
                Next

                ' Application layout recipes define their own semantic paint order.
                ' Re-applying it here keeps owned background surfaces behind their region content
                ' even when a surface is enabled after controls were already hosted.
                For Each control As MASControlBase In desiredControls
                    Layer.BringToFront(control)
                Next
            Finally
                Layer.EndUpdateInternal()
            End Try

            Return changed
        End Function


        ''' <summary>
        ''' Removes controls admitted by retained Application layout sessions when the user leaves the retained
        ''' layout path through Clear/Remove/Place. Manually hosted controls remain in the layer.
        ''' </summary>
        Friend Function ReleaseLayoutHostedControlsInternal() As Boolean
            Dim changed As Boolean = False
            Layer.BeginUpdateInternal()

            Try
                For Each control As MASControlBase In _layoutHostedControls.ToArray()
                    If control Is Nothing Then Continue For

                    Dim wasHosted As Boolean = Contains(control)
                    DisposeLayoutHostedControlInternal(control)
                    If wasHosted Then changed = True
                Next

                _layoutHostedControls.Clear()
            Finally
                Layer.EndUpdateInternal()
            End Try

            Return changed
        End Function


        Friend Shared Function ShouldRemoveStaleLayoutHostedControl(isLayoutHosted As Boolean,
                                                                    isDesiredByNextLayout As Boolean) As Boolean
            Return isLayoutHosted AndAlso Not isDesiredByNextLayout
        End Function


        Private Sub DisposeLayoutHostedControlInternal(control As MASControlBase)
            If control Is Nothing Then Return

            If Contains(control) Then
                Layer.RemoveAndDispose(control)
                Return
            End If

            Try
                control.Dispose()
            Catch masCaughtExceptionLayoutHostedDispose As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(
                    masCaughtExceptionLayoutHostedDispose,
                    "MASApplicationWindowControls.LayoutHostedControlDispose")
            End Try
        End Sub


        ''' <summary>
        ''' Direct Add/AddControl calls are consumer-owned admissions. If the same control had previously been
        ''' admitted by a retained Application layout recipe, this removes only the retained-layout ownership mark
        ''' while leaving the control itself and any active retained layout session untouched.
        ''' </summary>
        Friend Function AdoptConsumerOwnedControlInternal(control As MASControlBase) As Boolean
            If control Is Nothing Then Return False
            If Not ShouldReleaseLayoutOwnershipForDirectAdd(_layoutHostedControls.Contains(control)) Then Return False
            Return _layoutHostedControls.Remove(control)
        End Function


        Friend Shared Function ShouldReleaseLayoutOwnershipForDirectAdd(isLayoutHosted As Boolean) As Boolean
            Return isLayoutHosted
        End Function


        ''' <summary>
        ''' Applies a MASLayout plan through the owning controls gateway and reports whether geometry changed.
        ''' This core is deliberately private: retained Application layout sessions must enter through
        ''' TryApplySizeLayoutPlanInternal so diagnostics can block invalid recipes before geometry mutates.
        ''' Engines only produce facts; this facade remains the only direct-control Application owner allowed
        ''' to mutate layout slots.
        ''' </summary>
        Private Function ApplySizeLayoutPlanCore(plan As MASLayoutPlan,
                                                 Optional clipBounds As IDictionary(Of MASControlBase, SKRect) = Nothing) As Boolean
            If plan Is Nothing Then Return False

            Dim changed As Boolean = False
            Layer.BeginUpdateInternal()

            Try
                For Each pair As KeyValuePair(Of MASControlBase, MASLayoutSlot) In plan.Slots
                    If pair.Key Is Nothing OrElse pair.Value Is Nothing Then Continue For

                    Dim newClip As System.Nullable(Of SKRect) = Nothing
                    If clipBounds IsNot Nothing Then
                        Dim clipValue As SKRect = SKRect.Empty
                        If clipBounds.TryGetValue(pair.Key, clipValue) Then
                            newClip = New System.Nullable(Of SKRect)(clipValue)
                        End If
                    End If

                    If Not IsSamePlacement(pair.Key.LastLayoutSlotInternal, pair.Value, pair.Key.ClipBoundsLogicalInternal, newClip) Then
                        changed = True
                        pair.Key.SetLayoutPlacementInternal(pair.Value, newClip)
                    End If
                Next
            Finally
                Layer.EndUpdateInternal()
            End Try

            Return changed
        End Function


        ''' <summary>
        ''' Applies an Application-level MASLayout plan only after its diagnostic bag is known to be clean.
        ''' LayoutPage/LayoutRegions/LayoutApplicationSurface use this owner gate so duplicate controls or invalid
        ''' semantic-region plans fail without partially mutating previously valid control geometry.
        ''' </summary>
        Friend Function TryApplySizeLayoutPlanInternal(plan As MASLayoutPlan,
                                                      diagnostics As MASSizeLayoutDiagnosticBag,
                                                      Optional clipBounds As IDictionary(Of MASControlBase, SKRect) = Nothing) As Boolean
            If diagnostics IsNot Nothing AndAlso diagnostics.HasErrors Then Return False
            If ShouldBlockMissingApplicationLayoutPlan(plan, diagnostics) Then Return False
            If diagnostics IsNot Nothing AndAlso diagnostics.HasErrors Then Return False
            Return ApplySizeLayoutPlanCore(plan, clipBounds)
        End Function


        ''' <summary>
        ''' Blocks retained Application layout recipes when the engine fails to produce a plan.
        ''' A missing plan must be treated as a diagnostic failure so callers do not reconcile
        ''' hosted controls or replace a previously valid retained session after a no-output pass.
        ''' </summary>
        Friend Shared Function ShouldBlockMissingApplicationLayoutPlan(plan As MASLayoutPlan,
                                                                      diagnostics As MASSizeLayoutDiagnosticBag) As Boolean
            If plan IsNot Nothing Then Return False

            If diagnostics IsNot Nothing AndAlso Not diagnostics.HasErrors Then
                diagnostics.AddError(
                    "MASLayout.ApplicationPlanMissing",
                    "MASLayout did not produce an Application layout plan.",
                    "Application layout recipes must fail without hosting controls or mutating geometry when no plan is available.")
            End If

            Return True
        End Function




        ''' <summary>
        ''' Blocks explicit-region and application-surface recipes when an individual region fails to produce
        ''' a MASLayout plan. Region-level no-output passes must be diagnostic failures; otherwise controls
        ''' collected from that region could be hosted without a corresponding layout slot.
        ''' </summary>
        Friend Shared Function ShouldBlockMissingApplicationRegionLayoutPlan(plan As MASLayoutPlan,
                                                                           diagnostics As MASSizeLayoutDiagnosticBag,
                                                                           code As String,
                                                                           detail As String) As Boolean
            If plan IsNot Nothing Then Return False

            If diagnostics IsNot Nothing AndAlso Not diagnostics.HasErrors Then
                diagnostics.AddError(
                    If(String.IsNullOrWhiteSpace(code), "MASLayout.ApplicationRegionPlanMissing", code),
                    "MASLayout did not produce an Application region layout plan.",
                    If(detail, "Application region layout recipes must fail without hosting controls or replacing retained sessions when a region emits no plan."))
            End If

            Return True
        End Function


        Private Shared Function IsSamePlacement(currentSlot As MASLayoutSlot,
                                                newSlot As MASLayoutSlot,
                                                currentClip As System.Nullable(Of SKRect),
                                                newClip As System.Nullable(Of SKRect)) As Boolean
            If Not IsSameSlot(currentSlot, newSlot) Then Return False
            Return IsSameNullableRect(currentClip, newClip)
        End Function


        Private Shared Function IsSameSlot(left As MASLayoutSlot,
                                           right As MASLayoutSlot) As Boolean
            If left Is Nothing AndAlso right Is Nothing Then Return True
            If left Is Nothing OrElse right Is Nothing Then Return False

            Return IsSameRect(left.OuterBounds, right.OuterBounds) AndAlso
                   IsSameRect(left.VisualBounds, right.VisualBounds) AndAlso
                   IsSameRect(left.ContentBounds, right.ContentBounds) AndAlso
                   IsSameRect(left.HitBounds, right.HitBounds) AndAlso
                   IsSameRect(left.FocusBounds, right.FocusBounds) AndAlso
                   IsSameRect(left.FloatAnchorBounds, right.FloatAnchorBounds) AndAlso
                   Math.Abs(left.Baseline - right.Baseline) <= 0.01F AndAlso
                   left.ZOrderHint = right.ZOrderHint AndAlso
                   left.NavigationOrder = right.NavigationOrder
        End Function


        Private Shared Function IsSameNullableRect(left As System.Nullable(Of SKRect),
                                                   right As System.Nullable(Of SKRect)) As Boolean
            If left.HasValue <> right.HasValue Then Return False
            If Not left.HasValue Then Return True
            Return IsSameRect(left.Value, right.Value)
        End Function


        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 Function CreateDirectPlacementSlot(control As MASControlBase,
                                                   bounds As SKRect) As MASLayoutSlot
            Dim themeContext As MASThemeContext = RootHost.ThemeHostCore.TryGetContext()
            Dim integration As MASSizeLayoutIntegrationContext =
                MASSizeLayoutIntegrationGateway.Create(themeContext, MASRuntimeSizeProfileService.Current, RootHost.GetDpi())

            Return MASLayoutSlotFactory.CreateForControl(control, bounds, integration, control.SizeIntent)
        End Function


        ''' <summary>
        ''' Places a direct MAS control through the official controls gateway. Unlike assigning BoundsLogical
        ''' from consumer code, this uses the framework's layout-owned placement path and participates in
        ''' PerformUpdate batching, so resize/layout passes emit at most one host invalidation.
        ''' </summary>
        <Global.System.ComponentModel.EditorBrowsable(Global.System.ComponentModel.EditorBrowsableState.Advanced)>
        Friend Sub Place(control As MASControlBase, bounds As SKRect)
            If control Is Nothing Then Return
            ReleaseRetainedLayoutForDirectMutationInternal()
            If bounds.Left > bounds.Right OrElse bounds.Top > bounds.Bottom Then
                bounds = New SKRect(
                    Math.Min(bounds.Left, bounds.Right),
                    Math.Min(bounds.Top, bounds.Bottom),
                    Math.Max(bounds.Left, bounds.Right),
                    Math.Max(bounds.Top, bounds.Bottom))
            End If

            Dim slot As MASLayoutSlot = CreateDirectPlacementSlot(control, bounds)
            If IsSamePlacement(control.LastLayoutSlotInternal, slot, control.ClipBoundsLogicalInternal, control.ClipBoundsLogicalInternal) Then Return

            control.SetLayoutPlacementInternal(slot, control.ClipBoundsLogicalInternal)
            Layer.RequestVisualUpdateInternal()
        End Sub


        ''' <summary>
        ''' Places a direct MAS control using logical x/y/width/height values through the official controls gateway.
        ''' </summary>
        <Global.System.ComponentModel.EditorBrowsable(Global.System.ComponentModel.EditorBrowsableState.Advanced)>
        Friend Sub Place(control As MASControlBase,
                         x As Single,
                         y As Single,
                         width As Single,
                         height As Single)

            Place(control, New SKRect(
                x,
                y,
                x + Math.Max(0.0F, width),
                y + Math.Max(0.0F, height)))
        End Sub
    End Class

End Namespace
