Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports Nexamas.UI.Controls
Imports Nexamas.UI.Layout

Namespace Nexamas.UI.Composition

    ''' <summary>
    ''' Phase 1 Apply engine.
    ''' This is the only Composition class allowed to mutate ControlsLayer and BoundsLogical.
    ''' </summary>
    Friend NotInheritable Class MASLayoutApplier
        Implements IDisposable

        Private Shared ReadOnly _globalOwners As New Dictionary(Of MASControlBase, MASLayoutApplier)()
        Private Shared ReadOnly _globalLock As New Object()

        Private ReadOnly _owned As New HashSet(Of MASControlBase)()
        Private ReadOnly _ownershipByControl As New Dictionary(Of MASControlBase, MASLayoutControlOwnership)()
        Private _disposed As Boolean

        Friend Function Apply(target As MASLayoutApplyTarget,
                              arrangeResult As MASLayoutArrangeResult,
                              Optional diagnostics As MASLayoutDiagnosticBag = Nothing) As Boolean

            If diagnostics Is Nothing Then diagnostics = New MASLayoutDiagnosticBag()

            If _disposed Then
                diagnostics.AddError("MASLayout.ApplyAfterDispose", "MASLayoutApplier failed: Apply was called after dispose.")
                Return False
            End If

            If target Is Nothing OrElse target.ControlsLayer Is Nothing Then
                diagnostics.AddError("MASLayout.MissingApplyTarget", "MASLayoutApplier failed: the target ControlsLayer is missing.")
                Return False
            End If

            If arrangeResult Is Nothing Then
                diagnostics.AddError("MASLayout.MissingArrangeResult", "MASLayoutApplier failed: arrange result is missing.")
                Return False
            End If

            If diagnostics.HasErrors Then Return False

            Dim placements As MASLayoutPlacement() = arrangeResult.Placements
            If Not ValidateNoDuplicateControls(placements, diagnostics) Then Return False
            If Not ValidateOwnership(target, placements, diagnostics) Then Return False
            If diagnostics.HasErrors Then Return False

            Dim desired As New HashSet(Of MASControlBase)(placements.Select(Function(p) p.Control))
            Dim changed As Boolean = False

            target.ControlsLayer.BeginUpdateInternal()

            Try
                For Each oldControl As MASControlBase In _owned.ToArray()
                    If Not desired.Contains(oldControl) Then
                        RemoveCompositionControl(target, oldControl)
                        changed = True
                    End If
                Next

                Dim layerSnapshot As New HashSet(Of MASControlBase)(target.ControlsLayer.GetSnapshot())

                For Each placement As MASLayoutPlacement In placements
                    If placement Is Nothing OrElse placement.Control Is Nothing Then Continue For

                    If Not _owned.Contains(placement.Control) Then
                        target.ControlsLayer.Add(placement.Control)
                        RegisterOwner(placement.Control)
                        _owned.Add(placement.Control)
                        SetControlOwnership(placement.Control, placement.ControlOwnership)
                        layerSnapshot.Add(placement.Control)
                        changed = True
                    Else
                        SetControlOwnership(placement.Control, placement.ControlOwnership)

                        If Not layerSnapshot.Contains(placement.Control) Then
                            target.ControlsLayer.Add(placement.Control)
                            layerSnapshot.Add(placement.Control)
                            changed = True
                        End If
                    End If

                    If Not IsSamePlacement(placement.Control.LastLayoutSlotInternal,
                                           placement.Slot,
                                           placement.Control.ClipBoundsLogicalInternal,
                                           placement.ClipLogical) Then
                        changed = True
                    End If

                    placement.Control.SetLayoutPlacementInternal(placement.Slot, placement.ClipLogical)
                Next
            Finally
                target.ControlsLayer.EndUpdateInternal()
            End Try

            If changed AndAlso target.RequestInvalidate IsNot Nothing Then target.RequestInvalidate.Invoke()
            Return Not diagnostics.HasErrors
        End Function

        ''' <summary>
        ''' Detaches every control currently owned by this applier from the supplied target.
        ''' This is the only safe teardown path because global ownership must not be cleared
        ''' until the ControlsLayer detach/dispose operation has actually succeeded.
        ''' </summary>
        Friend Function DetachAll(target As MASLayoutApplyTarget,
                                  Optional diagnostics As MASLayoutDiagnosticBag = Nothing) As Boolean

            If diagnostics Is Nothing Then diagnostics = New MASLayoutDiagnosticBag()

            If _disposed Then
                diagnostics.AddError("MASLayout.DetachAfterDispose", "MASLayoutApplier failed: DetachAll was called after dispose.")
                Return False
            End If

            If target Is Nothing OrElse target.ControlsLayer Is Nothing Then
                diagnostics.AddError("MASLayout.MissingDetachTarget", "MASLayoutApplier failed: detach target ControlsLayer is missing.")
                Return False
            End If

            Dim changed As Boolean = False

            target.ControlsLayer.BeginUpdateInternal()

            Try
                For Each oldControl As MASControlBase In _owned.ToArray()
                    RemoveCompositionControl(target, oldControl)
                    changed = True
                Next
            Catch masCaughtDetachException As Exception
                diagnostics.AddError(
                    code:="MASLayout.DetachAllException",
                    message:="MASLayoutApplier failed while detaching owned Composition controls.",
                    detail:=masCaughtDetachException.GetType().Name & ": " & masCaughtDetachException.Message)
            Finally
                target.ControlsLayer.EndUpdateInternal()
            End Try

            If changed AndAlso target.RequestInvalidate IsNot Nothing Then target.RequestInvalidate.Invoke()
            Return Not diagnostics.HasErrors AndAlso _owned.Count = 0
        End Function

        ''' <summary>
        ''' Target-aware disposal for retained Composition owners. It preserves ownership records
        ''' when detach fails, preventing a false release that would hide leaked/attached controls.
        ''' </summary>
        Friend Function DisposeWithTarget(target As MASLayoutApplyTarget,
                                          Optional diagnostics As MASLayoutDiagnosticBag = Nothing) As Boolean

            If diagnostics Is Nothing Then diagnostics = New MASLayoutDiagnosticBag()
            If _disposed Then Return True

            Dim detached As Boolean = DetachAll(target, diagnostics)
            If Not detached Then Return False

            _disposed = True
            _ownershipByControl.Clear()
            _owned.Clear()
            Return True
        End Function

        ''' <summary>
        ''' Root-shutdown-only quarantine release for composition ownership. Normal disposal must keep
        ''' global owners when detach fails, because the control may still be attached to a live host.
        ''' During root shutdown, however, the host and ControlsLayer are being destroyed; retaining
        ''' _globalOwners would leak the applier/control pair and block later diagnostic probes.
        ''' This method never hides the original detach failure: it appends an explicit diagnostic and
        ''' then clears only ownership records that belong to this applier.
        ''' </summary>
        Friend Sub ReleaseOwnershipAfterRootShutdownDetachFault(diagnostics As MASLayoutDiagnosticBag)
            If _disposed Then Return
            If diagnostics Is Nothing Then diagnostics = New MASLayoutDiagnosticBag()
            If _owned.Count = 0 Then
                _disposed = True
                _ownershipByControl.Clear()
                Return
            End If

            diagnostics.AddError(
                code:="MASLayout.RootShutdownOwnerQuarantineRelease",
                message:="MASLayoutApplier released composition ownership during root shutdown after a detach fault.",
                detail:="The original detach diagnostic remains recorded; this release is allowed only because the root host/scene is being disposed and must not keep static applier/control ownership alive.")

            For Each control As MASControlBase In _owned.ToArray()
                UnregisterOwner(control)
            Next

            _ownershipByControl.Clear()
            _owned.Clear()
            _disposed = True
        End Sub


        Private Sub SetControlOwnership(control As MASControlBase,
                                        ownership As MASLayoutControlOwnership)
            If control Is Nothing Then Return

            If ownership = MASLayoutControlOwnership.OwnedByComposition OrElse control.CompositionLifetimeOwnedInternal Then
                _ownershipByControl(control) = MASLayoutControlOwnership.OwnedByComposition
                Return
            End If

            _ownershipByControl(control) = MASLayoutControlOwnership.Borrowed
        End Sub

        Private Sub RemoveCompositionControl(target As MASLayoutApplyTarget,
                                             control As MASControlBase)
            If control Is Nothing Then Return

            Dim ownership As MASLayoutControlOwnership = MASLayoutControlOwnership.Borrowed
            If _ownershipByControl.ContainsKey(control) Then
                ownership = _ownershipByControl(control)
            End If

            If ownership = MASLayoutControlOwnership.OwnedByComposition Then
                target.ControlsLayer.RemoveAndDispose(control)
            Else
                target.ControlsLayer.Remove(control)
            End If

            UnregisterOwner(control)
            _ownershipByControl.Remove(control)
            _owned.Remove(control)
        End Sub

        Private Shared Function IsSamePlacement(currentSlot As MASLayoutSlot,
                                                newSlot As MASLayoutSlot,
                                                currentClip As System.Nullable(Of SkiaSharp.SKRect),
                                                newClip As System.Nullable(Of SkiaSharp.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 SkiaSharp.SKRect),
                                                   right As System.Nullable(Of SkiaSharp.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 SkiaSharp.SKRect,
                                           right As SkiaSharp.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 ValidateNoDuplicateControls(placements As MASLayoutPlacement(), diagnostics As MASLayoutDiagnosticBag) As Boolean
            Dim seen As New HashSet(Of MASControlBase)()

            For Each placement As MASLayoutPlacement In placements
                If placement Is Nothing OrElse placement.Control Is Nothing Then
                    diagnostics.AddError("MASLayout.NullPlacement", "MASLayoutApplier failed: arrange result contains an empty placement.")
                    Return False
                End If

                If seen.Contains(placement.Control) Then
                    diagnostics.AddError("MASLayout.DuplicateControl", "The same control instance was added more than once to the active composition.")
                    Return False
                End If

                seen.Add(placement.Control)
            Next

            Return True
        End Function

        Private Function ValidateOwnership(target As MASLayoutApplyTarget,
                                           placements As MASLayoutPlacement(),
                                           diagnostics As MASLayoutDiagnosticBag) As Boolean

            Dim layerSnapshot As New HashSet(Of MASControlBase)(target.ControlsLayer.GetSnapshot())

            SyncLock _globalLock
                For Each placement As MASLayoutPlacement In placements
                    Dim ctrl As MASControlBase = placement.Control

                    If _globalOwners.ContainsKey(ctrl) AndAlso Not Object.ReferenceEquals(_globalOwners(ctrl), Me) Then
                        diagnostics.AddError("MASLayout.ControlAlreadyOwned", "MASLayoutApplier failed: the control is already owned by another active composition.")
                        Return False
                    End If

                    If layerSnapshot.Contains(ctrl) AndAlso Not _owned.Contains(ctrl) Then
                        diagnostics.AddError("MASLayout.ControlAlreadyAttached", "MASLayoutApplier failed: the control is already attached to the target ControlsLayer outside this composition.")
                        Return False
                    End If
                Next
            End SyncLock

            Return True
        End Function

        Private Sub RegisterOwner(control As MASControlBase)
            If control Is Nothing Then Return
            SyncLock _globalLock
                _globalOwners(control) = Me
            End SyncLock
        End Sub

        Private Sub UnregisterOwner(control As MASControlBase)
            If control Is Nothing Then Return
            SyncLock _globalLock
                If _globalOwners.ContainsKey(control) AndAlso Object.ReferenceEquals(_globalOwners(control), Me) Then
                    _globalOwners.Remove(control)
                End If
            End SyncLock
        End Sub

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

            ' Targetless disposal cannot prove that ControlsLayer detach/dispose succeeded.
            ' Do not falsely release global ownership for attached controls here; retained
            ' Composition hosts must call DisposeWithTarget so ownership is cleared only
            ' after DetachAll has actually removed the controls.
            If _owned.Count = 0 Then
                _ownershipByControl.Clear()
            End If
        End Sub

    End Class

End Namespace
