Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Windows.Forms
Imports Nexamas.UI.Controls
Imports Nexamas.UI.Host
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Components
Imports SkiaSharp

Namespace Nexamas.UI.Application


    ''' <summary>
    ''' Retained Application layout session for application surfaces. It computes application-surface regions from
    ''' semantic intent, then delegates each region to the existing MASLayout engine; no controls are placed
    ''' directly and no second engine owns measurement or arrangement.
    ''' </summary>
    Friend NotInheritable Class MASApplicationControlsApplicationSurfaceLayoutSession
        Private ReadOnly _rootHost As MASSkiaRootHost
        Private ReadOnly _controls As MASApplicationWindowControls
        Private ReadOnly _buildAction As Action(Of MASApplicationSurfaceLayoutBuilder)
        Private ReadOnly _fixedSizeProfile As MASSizeProfile
        Private ReadOnly _engine As New MASLayoutEngine()
        Private ReadOnly _backgrounds As New MASApplicationSurfaceBackgroundRegistry()
        Private ReadOnly _applicationBackground As New MASBackground()
        Private ReadOnly _backgroundPainter As New MASCardSurfacePainter()
        Private ReadOnly _workspaceScroll As MASApplicationWorkspaceScrollState
        Private _currentBackgroundEntries As IReadOnlyList(Of MASApplicationSurfaceBackgroundEntry) = Array.Empty(Of MASApplicationSurfaceBackgroundEntry)()
        Private _disposed As Boolean
        Private _isApplying As Boolean

        Friend Sub New(rootHost As MASSkiaRootHost,
                       controls As MASApplicationWindowControls,
                       buildAction As Action(Of MASApplicationSurfaceLayoutBuilder),
                       fixedSizeProfile As MASSizeProfile)
            If rootHost Is Nothing Then Throw New ArgumentNullException(NameOf(rootHost))
            If controls Is Nothing Then Throw New ArgumentNullException(NameOf(controls))
            If buildAction Is Nothing Then Throw New ArgumentNullException(NameOf(buildAction))

            _rootHost = rootHost
            _controls = controls
            _buildAction = buildAction
            _fixedSizeProfile = fixedSizeProfile
            _workspaceScroll = New MASApplicationWorkspaceScrollState(_rootHost, Sub() ApplyCore(throwOnError:=False, refreshReason:="WorkspaceScroll"))

            _rootHost.AttachApplicationScrollPointerRouter(
                owner:=Me,
                pointerDown:=AddressOf ApplicationScroll_PointerDown,
                pointerMove:=AddressOf ApplicationScroll_PointerMove,
                pointerUp:=AddressOf ApplicationScroll_PointerUp,
                pointerWheel:=AddressOf ApplicationScroll_PointerWheel,
                pointerLeave:=AddressOf ApplicationScroll_PointerLeave,
                pointerCancel:=AddressOf ApplicationScroll_PointerCancel,
                wantsPointer:=AddressOf ApplicationScroll_WantsPointer,
                hasCapture:=AddressOf ApplicationScroll_HasCapture)

            AddHandler _rootHost.SurfaceMeasured, AddressOf RootHost_SurfaceMeasured
            AddHandler _rootHost.SizeLayoutRefreshRequested, AddressOf RootHost_SizeLayoutRefreshRequested
            AddHandler _rootHost.RenderBeforeControls, AddressOf RootHost_RenderBeforeControls
            AddHandler _rootHost.RenderAfterControls, AddressOf RootHost_RenderAfterControls
        End Sub

        Friend Function ApplyNow() As Boolean
            Return ApplyCore(throwOnError:=True, refreshReason:="InitialApply")
        End Function

        Friend Sub Dispose()
            If _disposed Then Return

            Dim requiresBackgroundClearRepaint As Boolean = ShouldInvalidateAfterApplicationSurfaceDispose(_currentBackgroundEntries)
            _disposed = True
            RemoveHandler _rootHost.SurfaceMeasured, AddressOf RootHost_SurfaceMeasured
            RemoveHandler _rootHost.SizeLayoutRefreshRequested, AddressOf RootHost_SizeLayoutRefreshRequested
            RemoveHandler _rootHost.RenderBeforeControls, AddressOf RootHost_RenderBeforeControls
            RemoveHandler _rootHost.RenderAfterControls, AddressOf RootHost_RenderAfterControls
            _rootHost.ClearApplicationScrollPointerRouter(Me)

            _currentBackgroundEntries = Array.Empty(Of MASApplicationSurfaceBackgroundEntry)()
            If requiresBackgroundClearRepaint AndAlso Not _rootHost.IsDisposed Then
                _rootHost.RequestInvalidate()
            End If

            Try
                _workspaceScroll.Dispose()
            Catch masCaughtScrollException As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtScrollException)
            End Try

            Try
                _backgroundPainter.Dispose()
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException1)
            End Try

            Try
                _applicationBackground.Dispose()
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException2)
            End Try
        End Sub

        ''' <summary>
        ''' Returns whether disposing an application-surface session removes renderer-owned background facts
        ''' and therefore must trigger a repaint even if the next layout plan keeps identical
        ''' control geometry. This is an internal visual-state cleanup contract, not SDK API.
        ''' </summary>
        Friend Shared Function ShouldInvalidateAfterApplicationSurfaceDispose(entries As IReadOnlyList(Of MASApplicationSurfaceBackgroundEntry)) As Boolean
            Return entries IsNot Nothing AndAlso entries.Count > 0
        End Function

        Private Sub RootHost_SurfaceMeasured(sizePx As SKSizeI)
            If _disposed Then Return
            ApplyCore(throwOnError:=False, refreshReason:="SurfaceMeasured")
        End Sub

        Private Sub RootHost_SizeLayoutRefreshRequested(reason As String)
            If _disposed Then Return
            ApplyCore(throwOnError:=False, refreshReason:=NormalizeRefreshReason(reason))
        End Sub

        Private Shared Function NormalizeRefreshReason(reason As String) As String
            If String.IsNullOrWhiteSpace(reason) Then Return "SizeLayoutRefreshRequested"
            Return reason.Trim()
        End Function

        Private Function ResolveSizeProfile() As MASSizeProfile
            If _fixedSizeProfile IsNot Nothing Then Return MASSizeProfile.Normalize(_fixedSizeProfile)
            Return MASRuntimeSizeProfileService.Current
        End Function

        Private Function ReplaceBackgroundEntries(entries As IReadOnlyList(Of MASApplicationSurfaceBackgroundEntry)) As Boolean
            Dim safeEntries As IReadOnlyList(Of MASApplicationSurfaceBackgroundEntry) = If(entries, Array.Empty(Of MASApplicationSurfaceBackgroundEntry)())
            Dim changed As Boolean = Not AreSameBackgroundEntries(_currentBackgroundEntries, safeEntries)
            _currentBackgroundEntries = safeEntries
            Return changed
        End Function

        Private Shared Function AreSameBackgroundEntries(left As IReadOnlyList(Of MASApplicationSurfaceBackgroundEntry),
                                                         right As IReadOnlyList(Of MASApplicationSurfaceBackgroundEntry)) As Boolean
            If Object.ReferenceEquals(left, right) Then Return True
            If left Is Nothing Then left = Array.Empty(Of MASApplicationSurfaceBackgroundEntry)()
            If right Is Nothing Then right = Array.Empty(Of MASApplicationSurfaceBackgroundEntry)()
            If left.Count <> right.Count Then Return False

            For i As Integer = 0 To left.Count - 1
                Dim leftEntry As MASApplicationSurfaceBackgroundEntry = left(i)
                Dim rightEntry As MASApplicationSurfaceBackgroundEntry = right(i)
                If leftEntry Is Nothing AndAlso rightEntry Is Nothing Then Continue For
                If leftEntry Is Nothing OrElse rightEntry Is Nothing Then Return False
                If leftEntry.VisualStyle <> rightEntry.VisualStyle Then Return False
                If Not String.Equals(leftEntry.SurfaceMaterialKey, rightEntry.SurfaceMaterialKey, StringComparison.OrdinalIgnoreCase) Then Return False
                If Not IsSameRect(leftEntry.BoundsLogical, rightEntry.BoundsLogical) Then Return False
            Next

            Return True
        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 Sub RootHost_RenderBeforeControls(canvas As SKCanvas,
                                                  info As SKImageInfo,
                                                  ctx As MASThemeContext)
            If _disposed Then Return
            If canvas Is Nothing OrElse ctx Is Nothing Then Return

            Try
                _applicationBackground.SurfaceMaterial = _rootHost.ApplicationSurfaceMaterialKeyCore
                _applicationBackground.Render(canvas, ctx, info.Width, info.Height)
            Catch masCaughtApplicationBackgroundException As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRenderFallback(masCaughtApplicationBackgroundException, "Application.Controls.LayoutApplicationSurface.ApplicationBackgroundRender")
            End Try

            Dim entries As IReadOnlyList(Of MASApplicationSurfaceBackgroundEntry) = _currentBackgroundEntries
            If entries Is Nothing OrElse entries.Count = 0 Then Return

            Try
                Dim dpi As Single = _rootHost.GetDpi()
                If dpi <= 0.0F OrElse Single.IsNaN(dpi) OrElse Single.IsInfinity(dpi) Then dpi = ctx.Dpi
                If dpi <= 0.0F OrElse Single.IsNaN(dpi) OrElse Single.IsInfinity(dpi) Then dpi = 1.0F

                For Each entry As MASApplicationSurfaceBackgroundEntry In entries
                    If entry Is Nothing Then Continue For
                    If entry.BoundsLogical.IsEmpty OrElse entry.BoundsLogical.Width <= 1.0F OrElse entry.BoundsLogical.Height <= 1.0F Then Continue For

                    Dim boundsPx As SKRect = _rootHost.LogicalToPx(entry.BoundsLogical)
                    If boundsPx.IsEmpty OrElse boundsPx.Width <= 1.0F OrElse boundsPx.Height <= 1.0F Then Continue For

                    _backgroundPainter.Draw(
                        canvas:=canvas,
                        ctx:=ctx,
                        rPx:=boundsPx,
                        dpi:=dpi,
                        visualStyle:=entry.VisualStyle,
                        surfaceMaterialKey:=entry.SurfaceMaterialKey)
                Next
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRenderFallback(ex, "Application.Controls.LayoutApplicationSurface.ApplicationSurfaceBackgroundRender")
            End Try
        End Sub

        Private Sub RootHost_RenderAfterControls(canvas As SKCanvas,
                                                 info As SKImageInfo,
                                                 ctx As MASThemeContext)
            If _disposed Then Return
            _workspaceScroll.Render(canvas, ctx)
        End Sub

        Private Function ApplicationScroll_PointerDown(xPx As Integer, yPx As Integer, button As MouseButtons) As Boolean
            If _disposed Then Return False
            Return _workspaceScroll.PointerDown(xPx, yPx, button)
        End Function

        Private Function ApplicationScroll_PointerMove(xPx As Integer, yPx As Integer) As Boolean
            If _disposed Then Return False
            Return _workspaceScroll.PointerMove(xPx, yPx)
        End Function

        Private Function ApplicationScroll_PointerUp(xPx As Integer, yPx As Integer, button As MouseButtons) As Boolean
            If _disposed Then Return False
            Return _workspaceScroll.PointerUp(xPx, yPx, button)
        End Function

        Private Function ApplicationScroll_PointerWheel(xPx As Integer, yPx As Integer, delta As Integer) As Boolean
            If _disposed Then Return False
            Return _workspaceScroll.PointerWheel(xPx, yPx, delta)
        End Function

        Private Function ApplicationScroll_PointerLeave() As Boolean
            If _disposed Then Return False
            Return _workspaceScroll.PointerLeaveOrCancel()
        End Function

        Private Function ApplicationScroll_PointerCancel() As Boolean
            If _disposed Then Return False
            Return _workspaceScroll.PointerLeaveOrCancel()
        End Function

        Private Function ApplicationScroll_WantsPointer(xPx As Integer, yPx As Integer) As Boolean
            If _disposed Then Return False
            Return _workspaceScroll.WantsPointer(xPx, yPx)
        End Function

        Private Function ApplicationScroll_HasCapture() As Boolean
            Return Not _disposed AndAlso _workspaceScroll.HasPointerCapture
        End Function

        Private Function ApplyCore(throwOnError As Boolean, refreshReason As String) As Boolean
            If _disposed OrElse _isApplying OrElse _rootHost.IsDisposed Then Return False

            _isApplying = True

            Try
                _rootHost.EnsureThemeContextForComposition()

                Dim viewportPx As SKRect = _rootHost.GetSurfaceViewportPx()
                Dim boundsLogical As SKRect = _rootHost.PxToLogical(viewportPx)
                If boundsLogical.IsEmpty OrElse boundsLogical.Width <= 1.0F OrElse boundsLogical.Height <= 1.0F Then Return False

                Dim diagnostics As New MASSizeLayoutDiagnosticBag()
                Dim themeContext As MASThemeContext = _rootHost.ThemeHostCore.TryGetContext()
                Dim sizeProfile As MASSizeProfile = ResolveSizeProfile()
                Dim integration As MASSizeLayoutIntegrationContext = MASSizeLayoutIntegrationGateway.Create(themeContext, sizeProfile, _rootHost.GetDpi())

                Dim applicationSurface As New MASApplicationSurfaceLayoutBuilder(_backgrounds)
                _controls.BeginRetainedLayoutBuildAdmissionInternal()
                Try
                    _buildAction.Invoke(applicationSurface)
                Finally
                    _controls.EndRetainedLayoutBuildAdmissionInternal()
                End Try

                Dim applicationSurfaceModel As MASApplicationSurfaceLayoutModel = applicationSurface.BuildLayoutModel(boundsLogical, sizeProfile)
                Dim pendingBackgroundEntries As IReadOnlyList(Of MASApplicationSurfaceBackgroundEntry) = applicationSurfaceModel.BackgroundEntries

                Dim hostedControls As New List(Of MASControlBase)()
                applicationSurfaceModel.CollectControls(hostedControls)

                Dim mergedSlots As New Dictionary(Of MASControlBase, MASLayoutSlot)()
                Dim clipBounds As New Dictionary(Of MASControlBase, SKRect)()
                Dim scrollStateChanged As Boolean = False

                For Each entry As MASApplicationLayoutRegionEntry In applicationSurfaceModel.RegionEntries
                    If entry Is Nothing Then Continue For
                    If entry.BoundsLogical.IsEmpty OrElse entry.BoundsLogical.Width <= 0.0F OrElse entry.BoundsLogical.Height <= 0.0F Then Continue For

                    If _workspaceScroll.ArrangeScrollableRegion(entry, sizeProfile, integration, diagnostics, _engine, mergedSlots, clipBounds, scrollStateChanged) Then
                        Continue For
                    End If

                    Dim regionContext As New MASLayoutContext(entry.BoundsLogical, integration, diagnostics)
                    Dim regionPlan As MASLayoutPlan = _engine.Arrange(entry.BuildNode(sizeProfile), regionContext)
                    If MASApplicationWindowControls.ShouldBlockMissingApplicationRegionLayoutPlan(
                        regionPlan,
                        diagnostics,
                        "MASLayout.ApplicationSurfaceRegionPlanMissing",
                        "A semantic application-surface region did not produce a MASLayout plan.") Then
                        Continue For
                    End If

                    For Each pair As KeyValuePair(Of MASControlBase, MASLayoutSlot) In regionPlan.Slots
                        If pair.Key Is Nothing OrElse pair.Value Is Nothing Then Continue For
                        If mergedSlots.ContainsKey(pair.Key) Then
                            diagnostics.AddError("MASLayout.ApplicationSurfaceDuplicateControl", "The same control was assigned to more than one semantic application-surface region.", "Type=" & pair.Key.GetType().Name)
                            Continue For
                        End If

                        mergedSlots(pair.Key) = pair.Value
                    Next
                Next

                If diagnostics.HasErrors Then
                    _controls.RecordLayoutRefreshDiagnosticsFailureInternal("LayoutApplicationSurface", refreshReason, "ArrangeApplicationSurface", diagnostics)
                    Return False
                End If

                Dim backgroundsChanged As Boolean = ReplaceBackgroundEntries(pendingBackgroundEntries)
                Dim mergedPlan As New MASLayoutPlan(New SKSize(boundsLogical.Width, boundsLogical.Height), mergedSlots)
                Dim geometryChanged As Boolean = _controls.TryApplySizeLayoutPlanInternal(mergedPlan, diagnostics, clipBounds)
                If diagnostics.HasErrors Then
                    _controls.RecordLayoutRefreshDiagnosticsFailureInternal("LayoutApplicationSurface", refreshReason, "ApplyApplicationSurface", diagnostics)
                    Return False
                End If
                Dim ownershipChanged As Boolean = _controls.ReconcileLayoutHostedControlsInternal(hostedControls)
                If geometryChanged OrElse backgroundsChanged OrElse ownershipChanged OrElse scrollStateChanged Then _rootHost.RequestInvalidate()
                _controls.ClearLayoutRefreshFailureInternal("LayoutApplicationSurface")
                Return True
            Catch ex As Exception
                _controls.RecordLayoutRefreshExceptionFailureInternal("LayoutApplicationSurface", refreshReason, "BuildOrApply", ex)

                If throwOnError Then
                    Throw New InvalidOperationException("MASApplicationWindow.Controls.LayoutApplicationSurface failed while building or applying the official MASLayout application-surface plan.", ex)
                End If

                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(ex, "Application.Controls.LayoutApplicationSurface.ResizeApply")
                Return False
            Finally
                _isApplying = False
            End Try
        End Function
    End Class

End Namespace
