Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Windows.Forms
Imports Nexamas.UI.FloatRuntime
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Motion
Imports SkiaSharp

Namespace Nexamas.UI.Controls

    Friend NotInheritable Class MASToastFloatContent
        Implements IMASFloatContent
        Implements IMASFloatRenderableContent
        Implements IMASFloatRoutedInputContent
        Implements IMASFloatPointerMoveChangeAwareContent
        Implements IMASFloatInputPolicy
        Implements IMASFloatSurfaceSizeContract
        Implements IDisposable

        Private ReadOnly _state As New MASToastStateController()
        Private ReadOnly _tracker As New MASToastInteractionTracker()
        Private ReadOnly _renderer As New MASToastRenderer()
        Private ReadOnly _invalidate As Action
        Private ReadOnly _motionFrameClock As MASMotionFrameClock

        Private _session As IMASFloatSession
        Private _motionRegistered As Boolean
        Private _disposed As Boolean

        Private _placement As MASToastPlacement = MASToastPlacement.BottomRight
        Private _maxVisible As Integer = 4
        Private _maxQueue As Integer = 16
        Private _queueWhenFull As Boolean = True

        Friend Sub New(invalidate As Action,
                       Optional motionFrameClock As MASMotionFrameClock = Nothing)
            _invalidate = If(invalidate, Sub()
                                         End Sub)
            _motionFrameClock = motionFrameClock
        End Sub

        Public ReadOnly Property Name As String Implements IMASFloatContent.Name
            Get
                Return "MAS.Toast.FloatContent"
            End Get
        End Property

        Friend ReadOnly Property HasAnyToast As Boolean
            Get
                Return _state.HasAnyToast
            End Get
        End Property

        Friend Function ShowToast(options As MASToastOptions) As Integer

            If _disposed Then Return 0
            If options Is Nothing Then Throw New ArgumentNullException(NameOf(options))

            Dim id As Integer =
                _state.ShowToast(
                    options:=options,
                    maxVisible:=_maxVisible,
                    maxQueue:=_maxQueue,
                    queueWhenFull:=_queueWhenFull)

            StartMotionPump()
            _invalidate.Invoke()
            Return id

        End Function

        Friend Function Dismiss(toastId As Integer) As Boolean

            If _disposed Then Return False

            Dim changed As Boolean =
                _state.Dismiss(
                    toastId:=toastId,
                    maxVisible:=_maxVisible)

            If changed Then
                StartMotionPump()
                _invalidate.Invoke()
            End If

            Return changed

        End Function

        Friend Sub ClearAll()

            If _disposed Then Return

            _state.ClearAllImmediate()
            _tracker.Reset(_state.Items)
            StopMotionPump()
            _invalidate.Invoke()

        End Sub

        Public Sub AttachSession(session As IMASFloatSession) Implements IMASFloatContent.AttachSession

            _session = session
            StartMotionPump()

        End Sub

        Public Sub DetachSession() Implements IMASFloatContent.DetachSession

            _session = Nothing
            _tracker.Reset(_state.Items)
            StopMotionPump()

        End Sub

        Friend Sub RenderFloat(canvas As SKCanvas,
                               ctx As MASThemeContext,
                               boundsPx As SKRect) Implements IMASFloatRenderableContent.RenderFloat

            If _disposed Then Return
            If canvas Is Nothing OrElse ctx Is Nothing Then Return

            Dim layouts As Dictionary(Of Integer, MASToastLayoutInfo) =
                _renderer.Arrange(
                    ctx:=ctx,
                    viewportPx:=boundsPx,
                    items:=_state.Items,
                    placement:=_placement)

            _tracker.UpdateLayouts(_state.Items, layouts)

            _renderer.Render(
                canvas:=canvas,
                ctx:=ctx,
                items:=_state.Items,
                layouts:=layouts)

        End Sub

        Private Function AdvanceToastMotionFrame(deltaMs As Integer) As Boolean

            If _disposed Then Return False

            Dim changed As Boolean = TickCore(deltaMs)
            Dim keepAlive As Boolean = _state.HasActiveMotion(_maxVisible)

            If changed Then
                _invalidate.Invoke()
            End If

            If Not keepAlive Then
                _motionRegistered = False
            End If

            Return keepAlive

        End Function

        Private Function TickCore(deltaMs As Integer) As Boolean

            Dim safeAnimationDeltaMs As Integer = MASMotionSystem.NormalizeFrameDeltaMs(deltaMs)
            Dim safeLifetimeDeltaMs As Double = Math.Max(1.0R, CDbl(deltaMs))

            Dim changed As Boolean =
                _state.Tick(
                    animationDtMs:=safeAnimationDeltaMs,
                    lifetimeDtMs:=safeLifetimeDeltaMs,
                    maxVisible:=_maxVisible)

            Dim closedIds As List(Of Integer) =
                _state.PurgeClosed(_maxVisible)

            If changed OrElse closedIds.Count > 0 Then
                _tracker.SyncAgainstItems(_state.Items)
            End If

            Return changed OrElse closedIds.Count > 0

        End Function

        Private Sub StartMotionPump()

            If _disposed Then Return
            If _motionRegistered Then Return
            If Not _state.HasActiveMotion(_maxVisible) Then Return

            _motionRegistered = True
            MASMotionSystem.RegisterFrameConsumer(
                owner:=Me,
                consumerName:="MASToast.FloatContent",
                requestFrame:=_invalidate,
                advanceFrame:=AddressOf AdvanceToastMotionFrame,
                motionFrameClock:=_motionFrameClock)

        End Sub

        Private Sub StopMotionPump()

            If Not _motionRegistered Then Return

            _motionRegistered = False
            MASMotionSystem.UnregisterFrameConsumer(Me)

        End Sub


#Region "Size/Layout Surface Contract"

        Private Function MeasureFloatSurface(context As MASSizeContext,
                                             availableSize As SKSize,
                                             intent As MASSize) As MASSizeResult _
            Implements IMASFloatSurfaceSizeContract.MeasureFloatSurface

            Return MASFloatSurfaceSizeResolver.ResolveStandardSurface(
                kind:=MASFloatSurfaceKind.ToastStack,
                context:=context,
                availableSize:=availableSize,
                intent:=MASSize.Normalize(intent))
        End Function

#End Region

#Region "Input Policy"

        Public ReadOnly Property AcceptsPointerInput As Boolean Implements IMASFloatInputPolicy.AcceptsPointerInput
            Get
                Return True
            End Get
        End Property

        Public ReadOnly Property AcceptsKeyboardInput As Boolean Implements IMASFloatInputPolicy.AcceptsKeyboardInput
            Get
                Return False
            End Get
        End Property

        Public ReadOnly Property AcceptsTextInput As Boolean Implements IMASFloatInputPolicy.AcceptsTextInput
            Get
                Return False
            End Get
        End Property

        Public ReadOnly Property CloseOnOutsidePointer As Boolean Implements IMASFloatInputPolicy.CloseOnOutsidePointer
            Get
                Return False
            End Get
        End Property

        Public ReadOnly Property SwallowOutsidePointer As Boolean Implements IMASFloatInputPolicy.SwallowOutsidePointer
            Get
                Return False
            End Get
        End Property

        Public ReadOnly Property CloseOnEscape As Boolean Implements IMASFloatInputPolicy.CloseOnEscape
            Get
                Return False
            End Get
        End Property

#End Region

#Region "Routed Input"

        Friend Function HitTestFloat(ctx As MASThemeContext,
                                     boundsPx As SKRect,
                                     ptPx As SKPoint) As Boolean Implements IMASFloatRoutedInputContent.HitTestFloat

            If _disposed Then Return False
            Return _tracker.HitTest(ptPx)

        End Function

        Friend Sub PointerMoveFloat(ctx As MASThemeContext,
                                    ptPx As SKPoint) Implements IMASFloatRoutedInputContent.PointerMoveFloat

            If PointerMoveFloatVisualChanged(ctx, ptPx) Then
                _invalidate.Invoke()
            End If

        End Sub

        Friend Function PointerMoveFloatVisualChanged(ctx As MASThemeContext,
                                                      ptPx As SKPoint) As Boolean Implements IMASFloatPointerMoveChangeAwareContent.PointerMoveFloatVisualChanged

            If _disposed Then Return False
            Return _tracker.UpdateHover(_state.Items, ptPx)
        End Function

        Friend Function PointerDownFloat(ctx As MASThemeContext,
                                         ptPx As SKPoint,
                                         button As MouseButtons) As Boolean Implements IMASFloatRoutedInputContent.PointerDownFloat

            If _disposed Then Return False
            If button <> MouseButtons.Left Then Return False

            Dim handled As Boolean =
                _tracker.BeginPress(_state.Items, ptPx)

            If handled Then
                _invalidate.Invoke()
            End If

            Return handled

        End Function

        Friend Function PointerUpFloat(ctx As MASThemeContext,
                                       ptPx As SKPoint,
                                       button As MouseButtons) As Boolean Implements IMASFloatRoutedInputContent.PointerUpFloat

            If _disposed Then Return False
            If button <> MouseButtons.Left Then Return False

            Dim pressed As MASToastHitInfo = MASToastHitInfo.Empty
            Dim released As MASToastHitInfo = MASToastHitInfo.Empty

            Dim hadPress As Boolean =
                _tracker.EndPress(
                    items:=_state.Items,
                    ptPx:=ptPx,
                    pressed:=pressed,
                    released:=released)

            If Not hadPress Then
                _invalidate.Invoke()
                Return False
            End If

            If pressed.ToastId = released.ToastId AndAlso
               pressed.Part = released.Part Then

                HandleClick(released)
                StartMotionPump()
            End If

            _invalidate.Invoke()
            Return True

        End Function

        Private Sub HandleClick(hit As MASToastHitInfo)

            If _disposed Then Return
            If hit.IsEmpty Then Return

            Dim item As MASToastItem =
                _state.FindItem(hit.ToastId)

            If item Is Nothing Then Return

            Select Case hit.Part

                Case MASToastHitPart.CloseButton

                    If item.Dismissible Then
                        _state.Dismiss(item.Id, _maxVisible)
                    End If

                Case MASToastHitPart.UndoButton

                    If item.UndoCallback IsNot Nothing Then
                        Try
                            item.UndoCallback.Invoke()
                        Catch masCaughtException2 As Exception
                            Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException2)
                        End Try
                    End If

                    _state.Dismiss(item.Id, _maxVisible)

                Case MASToastHitPart.ActionButton

                    If item.ActionCallback IsNot Nothing Then
                        Try
                            item.ActionCallback.Invoke()
                        Catch masCaughtException1 As Exception
                            Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException1)
                        End Try
                    End If

                    _state.Dismiss(item.Id, _maxVisible)

            End Select

        End Sub

        Friend Function PointerWheelFloat(ctx As MASThemeContext,
                                          ptPx As SKPoint,
                                          delta As Integer) As Boolean Implements IMASFloatRoutedInputContent.PointerWheelFloat

            Return False

        End Function

        Friend Function KeyDownFloat(ctx As MASThemeContext,
                                     keyCode As Keys) As Boolean Implements IMASFloatRoutedInputContent.KeyDownFloat

            Return False

        End Function

        Friend Function KeyUpFloat(ctx As MASThemeContext,
                                   keyCode As Keys) As Boolean Implements IMASFloatRoutedInputContent.KeyUpFloat

            Return False

        End Function

        Friend Function TextInputFloat(ctx As MASThemeContext,
                                       text As String) As Boolean Implements IMASFloatRoutedInputContent.TextInputFloat

            Return False

        End Function

        Friend Function Win32ImeMessageFloat(ctx As MASThemeContext,
                                             hwnd As IntPtr,
                                             message As Integer,
                                             wParam As IntPtr,
                                             lParam As IntPtr) As Boolean Implements IMASFloatRoutedInputContent.Win32ImeMessageFloat

            Return False

        End Function

#End Region

        Public Sub Dispose() Implements IDisposable.Dispose

            If _disposed Then Return

            StopMotionPump()
            _disposed = True

            Try
                _renderer.Dispose()
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException2)
            End Try

            _tracker.Reset(_state.Items)
            _state.Clear()
            _session = Nothing

        End Sub

    End Class

End Namespace