Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Concurrent
Imports System.Threading
Imports System.Threading.Tasks
Imports Nexamas.UI.Performance
Imports SkiaSharp

Namespace Nexamas.UI.Icons

    Friend NotInheritable Class MASAsyncCachedIconResolver
        Implements IMASIconResolver, IMASIconInvalidationSource, IDisposable

        Private ReadOnly _inner As IMASIconResolver
        Private ReadOnly _keyProvider As IMASIconCacheKeyProvider
        Private ReadOnly _cache As New ConcurrentDictionary(Of String, MASIconResult)(StringComparer.OrdinalIgnoreCase)
        Private ReadOnly _pending As New ConcurrentDictionary(Of String, Byte)(StringComparer.OrdinalIgnoreCase)
        Private ReadOnly _semaphore As SemaphoreSlim
        Private ReadOnly _maxCacheCount As Integer
        Private ReadOnly _disposeLock As New Object()

        Private _cts As CancellationTokenSource
        Private _disposed As Boolean

        Public Event IconChanged As EventHandler _
            Implements IMASIconInvalidationSource.IconChanged

        Friend Sub New(inner As IMASIconResolver,
                       maxCacheCount As Integer,
                       maxConcurrentLoads As Integer)

            If inner Is Nothing Then Throw New ArgumentNullException(NameOf(inner))

            _inner = inner
            _keyProvider = TryCast(inner, IMASIconCacheKeyProvider)

            If _keyProvider Is Nothing Then
                Throw New ArgumentException("Inner resolver must implement IMASIconCacheKeyProvider.", NameOf(inner))
            End If

            If maxCacheCount < 64 Then maxCacheCount = 64
            If maxConcurrentLoads < 1 Then maxConcurrentLoads = 1
            If maxConcurrentLoads > 4 Then maxConcurrentLoads = 4

            _maxCacheCount = maxCacheCount
            _semaphore = New SemaphoreSlim(maxConcurrentLoads, maxConcurrentLoads)
            _cts = New CancellationTokenSource()
        End Sub

        Public Function ResolveIcon(request As MASIconRequest) As MASIconResult _
            Implements IMASIconResolver.ResolveIcon

            If _disposed Then Return MASIconResult.None()
            If request Is Nothing Then Return MASIconResult.None()

            Dim key As String = _keyProvider.BuildCacheKey(request)
            If String.IsNullOrWhiteSpace(key) Then Return MASIconResult.None()

            Dim cached As MASIconResult = Nothing

            If _cache.TryGetValue(key, cached) AndAlso cached IsNot Nothing AndAlso cached.HasIcon Then
                MASPerformanceSystem.RecordCacheHit(MASPerformanceCacheKind.IconResolver)
                Return cached
            End If

            MASPerformanceSystem.RecordCacheMiss(MASPerformanceCacheKind.IconResolver)
            QueueLoad(key, CloneRequest(request))

            Return MASIconResult.None()
        End Function

        Private Sub QueueLoad(key As String, request As MASIconRequest)
            If _disposed Then Return
            If String.IsNullOrWhiteSpace(key) Then Return
            If request Is Nothing Then Return
            If Not _pending.TryAdd(key, 0) Then Return

            MASPerformanceSystem.RecordAsyncLoadStarted()
            Dim token As CancellationToken = _cts.Token

            Task.Run(
                Async Function()
                    Await LoadAsync(key, request, token).ConfigureAwait(False)
                End Function,
                token)
        End Sub

        Private Async Function LoadAsync(key As String,
                                         request As MASIconRequest,
                                         token As CancellationToken) As Task

            If _disposed OrElse token.IsCancellationRequested Then
                MASPerformanceSystem.RecordAsyncLoadCancelled()
                Return
            End If

            Try
                Await _semaphore.WaitAsync(token).ConfigureAwait(False)
            Catch masCaughtException8 As Exception
                If token.IsCancellationRequested Then
                    MASPerformanceSystem.RecordAsyncLoadCancelled()
                Else
                    MASPerformanceSystem.RecordAsyncLoadFailed()
                End If
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException8)
                RemovePending(key)
                Return
            End Try

            Try
                If _disposed OrElse token.IsCancellationRequested Then Return
                If _cache.ContainsKey(key) Then Return

                Dim result As MASIconResult = Nothing

                Try
                    result = _inner.ResolveIcon(request)
                    MASPerformanceSystem.RecordAsyncLoadCompleted()
                Catch masCaughtException9 As Exception
                    MASPerformanceSystem.RecordAsyncLoadFailed()
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException9)
                    result = Nothing
                End Try

                If _disposed OrElse token.IsCancellationRequested Then
                    MASPerformanceSystem.RecordAsyncLoadCancelled()
                    DisposeResult(result)
                    Return
                End If

                If result IsNot Nothing AndAlso result.HasIcon Then
                    AddToCache(key, result)
                    RaiseIconChanged()
                End If

            Finally
                RemovePending(key)

                Try
                    _semaphore.Release()
                Catch masCaughtException1 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException1)
                End Try
            End Try
        End Function

        Private Sub AddToCache(key As String, result As MASIconResult)
            If result Is Nothing OrElse Not result.HasIcon Then Return

            If _disposed Then
                DisposeResult(result)
                Return
            End If

            If String.IsNullOrWhiteSpace(key) Then
                DisposeResult(result)
                Return
            End If

            If _cache.Count >= _maxCacheCount Then
                RemoveOneFromCache()
            End If

            If Not _cache.TryAdd(key, result) Then
                DisposeResult(result)
            End If
        End Sub

        Private Sub RemoveOneFromCache()
            For Each key As String In _cache.Keys
                Dim result As MASIconResult = Nothing

                If _cache.TryRemove(key, result) Then
                    MASPerformanceSystem.RecordCacheInvalidated(MASPerformanceCacheKind.IconResolver)
                    DisposeResult(result)
                End If

                Exit For
            Next
        End Sub

        Friend Sub ClearCache()
            For Each key As String In _cache.Keys
                Dim result As MASIconResult = Nothing

                If _cache.TryRemove(key, result) Then
                    MASPerformanceSystem.RecordCacheInvalidated(MASPerformanceCacheKind.IconResolver)
                    DisposeResult(result)
                End If
            Next
        End Sub

        Private Sub RemovePending(key As String)
            If String.IsNullOrWhiteSpace(key) Then Return

            Dim dummy As Byte = 0
            _pending.TryRemove(key, dummy)
        End Sub

        Private Shared Function CloneRequest(request As MASIconRequest) As MASIconRequest
            Return New MASIconRequest(
                path:=request.Path,
                displayName:=request.DisplayName,
                kind:=request.Kind,
                sizeDip:=request.SizeDip,
                dpi:=request.Dpi
            )
        End Function

        Private Shared Sub DisposeResult(result As MASIconResult)
            If result Is Nothing Then Return

            If result.HasSkImage AndAlso result.Image IsNot Nothing Then
                Try
                    result.Image.Dispose()
                Catch masCaughtException2 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException2)
                End Try
            End If
        End Sub

        Private Sub RaiseIconChanged()
            If _disposed Then Return

            Try
                RaiseEvent IconChanged(Me, EventArgs.Empty)
            Catch masCaughtException3 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException3)
            End Try
        End Sub

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

                If _cts IsNot Nothing Then
                    Try
                        _cts.Cancel()
                    Catch masCaughtException4 As Exception
                        Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException4)
                    End Try
                End If
            End SyncLock

            ClearCache()
            _pending.Clear()

            Dim disposable As IDisposable = TryCast(_inner, IDisposable)
            If disposable IsNot Nothing Then
                Try
                    disposable.Dispose()
                Catch masCaughtException5 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException5)
                End Try
            End If

            If _cts IsNot Nothing Then
                Try
                    _cts.Dispose()
                Catch masCaughtException6 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException6)
                End Try

                _cts = Nothing
            End If

            Try
                _semaphore.Dispose()
            Catch masCaughtException7 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException7)
            End Try
        End Sub

    End Class

End Namespace
