Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports Nexamas.UI.Motion
Imports SkiaSharp

Namespace Nexamas.UI.FloatRuntime

    ' ============================================================
    ' MASFloatRuntime Contract
    ' Class: MASFloatTransitionEngine
    '
    ' Purpose:
    '   Provides the official runtime-owned transition execution point for
    '   Float opening and closing.
    '
    ' Responsibility:
    '   Executes visual transition intent described by MASFloatPresentation
    '   through MASMotionTimelineRunner and the official MotionSystem
    '   frame-consumer gateway. Feature services are not allowed to own
    '   random timers or local transition loops.
    '
    ' Owns:
    '   Transition execution decision and per-record visual transition facts.
    '
    ' Does Not Own:
    '   Lifecycle authorization.
    '   Host insertion.
    '   Host removal.
    '   Result storage.
    '   Queue continuation.
    '   Input routing.
    '   Focus ownership.
    '   Drawing implementation.
    ' ============================================================
    Friend NotInheritable Class MASFloatTransitionEngine
        Implements IDisposable

        Private ReadOnly _requestFrame As Action
        Private ReadOnly _motionFrameClock As MASMotionFrameClock
        Private ReadOnly _syncRoot As New Object()
        Private ReadOnly _activeRunnersByRecord As New Dictionary(Of MASFloatLifecycleRecord, MASMotionTimelineRunner)()
        Private _shutdown As Boolean

        Friend Sub New(Optional requestFrame As Action = Nothing,
                       Optional motionFrameClock As MASMotionFrameClock = Nothing)
            If requestFrame Is Nothing Then
                _requestFrame = Sub()
                                End Sub
            Else
                _requestFrame = requestFrame
            End If

            _motionFrameClock = motionFrameClock
        End Sub

        Friend ReadOnly Property ActiveTransitionCount As Integer
            Get
                SyncLock _syncRoot
                    Return _activeRunnersByRecord.Count
                End SyncLock
            End Get
        End Property

        Friend Sub RunOpenTransition(record As MASFloatLifecycleRecord,
                                     onCompleted As Action)
            If record Is Nothing Then
                Complete(onCompleted)
                Return
            End If

            If IsShutdown Then
                Complete(onCompleted)
                Return
            End If

            Dim presentation As MASFloatPresentation = GetPresentation(record)
            Dim plan As MASMotionTransitionPlan = CreateMotionPlan(MASMotionTokenId.FloatOpen, presentation.DurationMs, presentation.Easing)

            If ShouldCompleteImmediately(presentation.Transition, plan.EffectiveDurationMs) Then
                CancelTransitionFor(record)
                ApplyTransitionVisual(record, presentation.Transition, progress:=1.0F, isOpening:=True)
                record.ClearTransitionVisual()
                Complete(onCompleted)
                Return
            End If

            ApplyTransitionVisual(record, presentation.Transition, progress:=0.0F, isOpening:=True)

            Dim runner As MASMotionTimelineRunner = Nothing
            runner = MASMotionSystem.CreateTimelineRunner(
                owner:=record,
                consumerName:="MASFloatTransitionEngine.Open",
                plan:=plan,
                requestFrame:=_requestFrame,
                applyFrame:=Sub(frame As MASMotionFrame)
                                If frame Is Nothing OrElse Not IsRunnerCurrent(record, runner) Then Return
                                ApplyTransitionVisual(record, presentation.Transition, frame.Value, isOpening:=True)
                            End Sub,
                completed:=Sub(frame As MASMotionFrame)
                               If Not RemoveRunnerIfCurrent(record, runner) Then Return
                               ApplyTransitionVisual(record, presentation.Transition, progress:=1.0F, isOpening:=True)
                               record.ClearTransitionVisual()
                               Complete(onCompleted)
                           End Sub,
                motionFrameClock:=_motionFrameClock)

            If Not AttachRunner(record, runner) Then
                runner.Dispose()
                Complete(onCompleted)
                Return
            End If

            runner.Start()
            If Not runner.IsRunning Then
                RemoveRunnerIfCurrent(record, runner)
            End If
        End Sub

        Friend Sub RunCloseTransition(record As MASFloatLifecycleRecord,
                                      onCompleted As Action)
            If record Is Nothing Then
                Complete(onCompleted)
                Return
            End If

            If IsShutdown Then
                Complete(onCompleted)
                Return
            End If

            Dim presentation As MASFloatPresentation = GetPresentation(record)
            Dim plan As MASMotionTransitionPlan = CreateMotionPlan(MASMotionTokenId.FloatClose, presentation.DurationMs, presentation.Easing)

            If ShouldCompleteImmediately(presentation.CloseTransition, plan.EffectiveDurationMs) Then
                CancelTransitionFor(record)
                ApplyTransitionVisual(record, presentation.CloseTransition, progress:=1.0F, isOpening:=False)
                Complete(onCompleted)
                Return
            End If

            ApplyTransitionVisual(record, presentation.CloseTransition, progress:=0.0F, isOpening:=False)

            Dim runner As MASMotionTimelineRunner = Nothing
            runner = MASMotionSystem.CreateTimelineRunner(
                owner:=record,
                consumerName:="MASFloatTransitionEngine.Close",
                plan:=plan,
                requestFrame:=_requestFrame,
                applyFrame:=Sub(frame As MASMotionFrame)
                                If frame Is Nothing OrElse Not IsRunnerCurrent(record, runner) Then Return
                                ApplyTransitionVisual(record, presentation.CloseTransition, frame.Value, isOpening:=False)
                            End Sub,
                completed:=Sub(frame As MASMotionFrame)
                               If Not RemoveRunnerIfCurrent(record, runner) Then Return
                               ApplyTransitionVisual(record, presentation.CloseTransition, progress:=1.0F, isOpening:=False)
                               Complete(onCompleted)
                           End Sub,
                motionFrameClock:=_motionFrameClock)

            If Not AttachRunner(record, runner) Then
                runner.Dispose()
                Complete(onCompleted)
                Return
            End If

            runner.Start()
            If Not runner.IsRunning Then
                RemoveRunnerIfCurrent(record, runner)
            End If
        End Sub

        Friend Sub CancelTransitionFor(record As MASFloatLifecycleRecord)
            If record Is Nothing Then Return

            Dim runner As MASMotionTimelineRunner = Nothing
            SyncLock _syncRoot
                If _activeRunnersByRecord.TryGetValue(record, runner) Then
                    _activeRunnersByRecord.Remove(record)
                End If
            End SyncLock

            If runner IsNot Nothing Then
                runner.Dispose()
            End If

            record.ClearTransitionVisual()
        End Sub

        Friend Sub Shutdown()
            Dim runners As List(Of MASMotionTimelineRunner) = Nothing
            Dim records As List(Of MASFloatLifecycleRecord) = Nothing

            SyncLock _syncRoot
                If _shutdown Then Return
                _shutdown = True
                runners = New List(Of MASMotionTimelineRunner)(_activeRunnersByRecord.Values)
                records = New List(Of MASFloatLifecycleRecord)(_activeRunnersByRecord.Keys)
                _activeRunnersByRecord.Clear()
            End SyncLock

            If runners IsNot Nothing Then
                For Each runner As MASMotionTimelineRunner In runners
                    If runner Is Nothing Then Continue For
                    Try
                        runner.Dispose()
                    Catch masCaughtException1 As Exception
                        Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(
                            masCaughtException1,
                            "MASFloatTransitionEngine.Shutdown")
                    End Try
                Next
            End If

            If records IsNot Nothing Then
                For Each record As MASFloatLifecycleRecord In records
                    If record Is Nothing Then Continue For
                    record.ClearTransitionVisual()
                Next
            End If
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            Shutdown()
        End Sub

        Private Function AttachRunner(record As MASFloatLifecycleRecord,
                                      runner As MASMotionTimelineRunner) As Boolean
            If record Is Nothing OrElse runner Is Nothing Then Return False

            Dim previous As MASMotionTimelineRunner = Nothing

            SyncLock _syncRoot
                If _shutdown Then Return False

                If _activeRunnersByRecord.TryGetValue(record, previous) Then
                    _activeRunnersByRecord.Remove(record)
                End If

                _activeRunnersByRecord(record) = runner
            End SyncLock

            If previous IsNot Nothing AndAlso Not Object.ReferenceEquals(previous, runner) Then
                previous.Dispose()
            End If

            Return True
        End Function

        Private Function IsRunnerCurrent(record As MASFloatLifecycleRecord,
                                         runner As MASMotionTimelineRunner) As Boolean
            If record Is Nothing OrElse runner Is Nothing Then Return False

            SyncLock _syncRoot
                If _shutdown Then Return False

                Dim current As MASMotionTimelineRunner = Nothing
                Return _activeRunnersByRecord.TryGetValue(record, current) AndAlso
                       Object.ReferenceEquals(current, runner)
            End SyncLock
        End Function

        Private Function RemoveRunnerIfCurrent(record As MASFloatLifecycleRecord,
                                               runner As MASMotionTimelineRunner) As Boolean
            If record Is Nothing OrElse runner Is Nothing Then Return False

            SyncLock _syncRoot
                Dim current As MASMotionTimelineRunner = Nothing
                If Not _activeRunnersByRecord.TryGetValue(record, current) Then Return False
                If Not Object.ReferenceEquals(current, runner) Then Return False

                _activeRunnersByRecord.Remove(record)
                Return Not _shutdown
            End SyncLock
        End Function

        Private ReadOnly Property IsShutdown As Boolean
            Get
                SyncLock _syncRoot
                    Return _shutdown
                End SyncLock
            End Get
        End Property

        Private Shared Function GetPresentation(record As MASFloatLifecycleRecord) As MASFloatPresentation
            If record Is Nothing OrElse
               record.ResolvedRequest Is Nothing OrElse
               record.ResolvedRequest.Presentation Is Nothing Then
                Return New MASFloatPresentation()
            End If

            Return record.ResolvedRequest.Presentation
        End Function

        Private Shared Function CreateMotionPlan(tokenId As MASMotionTokenId,
                                                 durationMs As Integer,
                                                 easing As MASFloatEasing) As MASMotionTransitionPlan
            Return MASMotionSystem.CreateTransitionPlan(tokenId, 0.0F, 1.0F, durationMs, ResolveMotionEasing(easing))
        End Function

        Private Shared Function ResolveMotionEasing(easing As MASFloatEasing) As MASMotionEasing
            Select Case easing
                Case MASFloatEasing.Linear
                    Return MASMotionEasing.Linear

                Case MASFloatEasing.Emphasized
                    Return MASMotionEasing.Emphasized

                Case MASFloatEasing.Decelerate
                    Return MASMotionEasing.Decelerate

                Case MASFloatEasing.Accelerate
                    Return MASMotionEasing.Accelerate

                Case Else
                    Return MASMotionEasing.Standard
            End Select
        End Function

        Private Shared Function ShouldCompleteImmediately(transition As MASFloatTransitionKind,
                                                          durationMs As Integer) As Boolean
            If transition = MASFloatTransitionKind.None Then Return True
            If durationMs <= 0 Then Return True
            Return False
        End Function

        Private Shared Sub ApplyTransitionVisual(record As MASFloatLifecycleRecord,
                                                 transition As MASFloatTransitionKind,
                                                 progress As Single,
                                                 isOpening As Boolean)
            If record Is Nothing Then Return

            Dim t As Single = MASMotionSystem.ClampProgress(progress)
            Dim visibleT As Single = If(isOpening, t, 1.0F - t)

            Dim opacity As Single = 1.0F
            If HasTransitionFlag(transition, MASFloatTransitionKind.Fade) Then
                opacity = visibleT
            End If

            Dim scale As Single = 1.0F
            If HasTransitionFlag(transition, MASFloatTransitionKind.Scale) Then
                Dim startScale As Single = ResolveStartScale(record)
                scale = startScale + ((1.0F - startScale) * visibleT)
            End If

            Dim offsetX As Single = 0.0F
            Dim offsetY As Single = 0.0F
            Dim slideDistance As Single = ResolveSlideDistancePx(record) * If(isOpening, 1.0F - t, t)

            If HasTransitionFlag(transition, MASFloatTransitionKind.SlideFromLeft) Then offsetX = -slideDistance
            If HasTransitionFlag(transition, MASFloatTransitionKind.SlideFromRight) Then offsetX = slideDistance
            If HasTransitionFlag(transition, MASFloatTransitionKind.SlideFromTop) Then offsetY = -slideDistance
            If HasTransitionFlag(transition, MASFloatTransitionKind.SlideFromBottom) Then offsetY = slideDistance

            Dim origin As SKPoint = ResolveTransitionOriginPx(record)
            Dim active As Boolean = t < 1.0F OrElse opacity < 0.999F OrElse Math.Abs(scale - 1.0F) > 0.0001F OrElse Math.Abs(offsetX) > 0.01F OrElse Math.Abs(offsetY) > 0.01F
            record.SetTransitionVisual(opacity, scale, offsetX, offsetY, active, origin.X, origin.Y)
        End Sub


        Private Shared Function ResolveStartScale(record As MASFloatLifecycleRecord) As Single
            Dim key As String = ResolveFloatKey(record)

            Select Case key
                Case MASFloatKeys.DropDownMenu,
                     MASFloatKeys.MenuBarDropDownMenu,
                     MASFloatKeys.ContextMenu,
                     MASFloatKeys.FileExplorerViewModeMenu,
                     MASFloatKeys.DropDownList
                    Return 0.965F

                Case MASFloatKeys.Tooltip,
                     MASFloatKeys.Toast,
                     MASFloatKeys.Progress
                    Return 0.985F

                Case Else
                    Return 0.98F
            End Select
        End Function

        Private Shared Function ResolveSlideDistancePx(record As MASFloatLifecycleRecord) As Single
            Dim key As String = ResolveFloatKey(record)
            Dim anchor As SKRect = ResolveAnchorRectPx(record)

            Select Case key
                Case MASFloatKeys.DropDownMenu,
                     MASFloatKeys.MenuBarDropDownMenu,
                     MASFloatKeys.FileExplorerViewModeMenu,
                     MASFloatKeys.DropDownList
                    If HasArea(anchor) Then
                        Return Math.Max(6.0F, Math.Min(12.0F, anchor.Height * 0.22F))
                    End If

                    Return 8.0F

                Case MASFloatKeys.Toast
                    Return 10.0F

                Case Else
                    Return 12.0F
            End Select
        End Function

        Private Shared Function ResolveTransitionOriginPx(record As MASFloatLifecycleRecord) As SKPoint
            Dim presentation As MASFloatPresentation = GetPresentation(record)
            Dim originKind As MASFloatTransitionOriginKind = presentation.TransitionOrigin

            If originKind = MASFloatTransitionOriginKind.Auto Then
                originKind = ResolveAutoOriginKind(record, presentation)
            End If

            Dim bounds As SKRect = ResolveBoundsRectPx(record)
            Dim anchor As SKRect = ResolveAnchorRectPx(record)

            Select Case originKind
                Case MASFloatTransitionOriginKind.AnchorTopLeft
                    If HasArea(anchor) Then Return New SKPoint(anchor.Left, anchor.Top)

                Case MASFloatTransitionOriginKind.AnchorTopCenter
                    If HasArea(anchor) Then Return New SKPoint(anchor.Left + (anchor.Width * 0.5F), anchor.Top)

                Case MASFloatTransitionOriginKind.AnchorBottomLeft
                    If HasArea(anchor) Then Return New SKPoint(anchor.Left, anchor.Bottom)

                Case MASFloatTransitionOriginKind.AnchorBottomCenter
                    If HasArea(anchor) Then Return New SKPoint(anchor.Left + (anchor.Width * 0.5F), anchor.Bottom)

                Case MASFloatTransitionOriginKind.BoundsTopLeft
                    If HasArea(bounds) Then Return New SKPoint(bounds.Left, bounds.Top)

                Case MASFloatTransitionOriginKind.BoundsTopCenter
                    If HasArea(bounds) Then Return New SKPoint(bounds.Left + (bounds.Width * 0.5F), bounds.Top)

                Case MASFloatTransitionOriginKind.BoundsBottomLeft
                    If HasArea(bounds) Then Return New SKPoint(bounds.Left, bounds.Bottom)

                Case MASFloatTransitionOriginKind.BoundsBottomCenter
                    If HasArea(bounds) Then Return New SKPoint(bounds.Left + (bounds.Width * 0.5F), bounds.Bottom)
            End Select

            If HasArea(bounds) Then
                Return New SKPoint(bounds.Left + (bounds.Width * 0.5F), bounds.Top + (bounds.Height * 0.5F))
            End If

            If HasArea(anchor) Then
                Return New SKPoint(anchor.Left + (anchor.Width * 0.5F), anchor.Top + (anchor.Height * 0.5F))
            End If

            Return New SKPoint(Single.NaN, Single.NaN)
        End Function

        Private Shared Function ResolveAutoOriginKind(record As MASFloatLifecycleRecord,
                                                      presentation As MASFloatPresentation) As MASFloatTransitionOriginKind
            Dim key As String = ResolveFloatKey(record)

            Select Case key
                Case MASFloatKeys.DropDownMenu,
                     MASFloatKeys.MenuBarDropDownMenu,
                     MASFloatKeys.FileExplorerViewModeMenu
                    Return MASFloatTransitionOriginKind.AnchorBottomLeft

                Case MASFloatKeys.ContextMenu
                    Return MASFloatTransitionOriginKind.AnchorTopLeft

                Case MASFloatKeys.DropDownList
                    Return MASFloatTransitionOriginKind.BoundsTopCenter

                Case MASFloatKeys.Toast
                    Return MASFloatTransitionOriginKind.BoundsBottomCenter

                Case MASFloatKeys.Tooltip
                    Return MASFloatTransitionOriginKind.AnchorTopCenter
            End Select

            If presentation IsNot Nothing Then
                Select Case presentation.Placement
                    Case MASFloatPlacement.AnchorBelow
                        Return MASFloatTransitionOriginKind.AnchorBottomLeft

                    Case MASFloatPlacement.AnchorAbove
                        Return MASFloatTransitionOriginKind.AnchorTopLeft

                    Case MASFloatPlacement.AnchorLeft
                        Return MASFloatTransitionOriginKind.AnchorTopLeft

                    Case MASFloatPlacement.AnchorRight
                        Return MASFloatTransitionOriginKind.AnchorTopLeft
                End Select
            End If

            Return MASFloatTransitionOriginKind.BoundsCenter
        End Function

        Private Shared Function ResolveFloatKey(record As MASFloatLifecycleRecord) As String
            If record Is Nothing OrElse
               record.ResolvedRequest Is Nothing Then
                Return String.Empty
            End If

            If record.ResolvedRequest.Descriptor IsNot Nothing AndAlso
               Not String.IsNullOrWhiteSpace(record.ResolvedRequest.Descriptor.Key) Then
                Return record.ResolvedRequest.Descriptor.Key.Trim()
            End If

            If record.ResolvedRequest.Request IsNot Nothing AndAlso
               Not String.IsNullOrWhiteSpace(record.ResolvedRequest.Request.FloatKey) Then
                Return record.ResolvedRequest.Request.FloatKey.Trim()
            End If

            Return String.Empty
        End Function

        Private Shared Function ResolveAnchorRectPx(record As MASFloatLifecycleRecord) As SKRect
            If record Is Nothing OrElse
               record.ResolvedRequest Is Nothing OrElse
               record.ResolvedRequest.Request Is Nothing Then
                Return SKRect.Empty
            End If

            Return NormalizeRect(record.ResolvedRequest.Request.AnchorRectPx)
        End Function

        Private Shared Function ResolveBoundsRectPx(record As MASFloatLifecycleRecord) As SKRect
            If record Is Nothing OrElse
               record.ResolvedRequest Is Nothing OrElse
               record.ResolvedRequest.Request Is Nothing Then
                Return SKRect.Empty
            End If

            Dim initial As SKRect = NormalizeRect(record.ResolvedRequest.Request.InitialBoundsPx)
            If HasArea(initial) Then Return initial

            Dim available As SKRect = NormalizeRect(record.ResolvedRequest.Request.AvailableBoundsPx)
            If HasArea(available) Then Return available

            Return SKRect.Empty
        End Function

        Private Shared Function NormalizeRect(rect As SKRect) As SKRect
            Dim left As Single = Math.Min(rect.Left, rect.Right)
            Dim right As Single = Math.Max(rect.Left, rect.Right)
            Dim top As Single = Math.Min(rect.Top, rect.Bottom)
            Dim bottom As Single = Math.Max(rect.Top, rect.Bottom)
            Return New SKRect(left, top, right, bottom)
        End Function

        Private Shared Function HasArea(rect As SKRect) As Boolean
            Return Not rect.IsEmpty AndAlso rect.Width > 0.5F AndAlso rect.Height > 0.5F
        End Function

        Private Shared Function HasTransitionFlag(value As MASFloatTransitionKind,
                                                  flag As MASFloatTransitionKind) As Boolean
            Return (value And flag) = flag
        End Function

        Private Shared Sub Complete(onCompleted As Action)
            If onCompleted IsNot Nothing Then onCompleted.Invoke()
        End Sub

    End Class

End Namespace
