Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports SkiaSharp

Namespace Nexamas.UI.Icons

    <Global.System.ComponentModel.EditorBrowsable(Global.System.ComponentModel.EditorBrowsableState.Advanced)>
    Friend NotInheritable Class MASSvgProjectIconProvider

        Private Sub New()
        End Sub

#Region "Data model"

        <Global.System.ComponentModel.EditorBrowsable(Global.System.ComponentModel.EditorBrowsableState.Advanced)>
        Private Enum IconColorMode
            PreserveLayerColors
            TintAll
        End Enum

        <Global.System.ComponentModel.EditorBrowsable(Global.System.ComponentModel.EditorBrowsableState.Advanced)>
        Private NotInheritable Class ColoredPath
            Friend Property Data As String
            Friend Property Fill As Nullable(Of SKColor)
            Friend Property Stroke As Nullable(Of SKColor)
            Friend Property StrokeWidthFactor As Single
            Friend Property BaseSize As Single = 24.0F
        End Class

#End Region

#Region "Optical tuning"

        Private Structure IconOpticalTuning
            Friend PadFactor As Single
            Friend ScaleFactor As Single
            Friend OffsetXDip As Single
            Friend OffsetYDip As Single
            Friend StrokeScale As Single

            Friend Shared ReadOnly Property [Default] As IconOpticalTuning
                Get
                    Return New IconOpticalTuning With {
                        .PadFactor = 0.06F,
                        .ScaleFactor = 1.0F,
                        .OffsetXDip = 0.0F,
                        .OffsetYDip = 0.0F,
                        .StrokeScale = 1.0F
                    }
                End Get
            End Property
        End Structure

        Private Shared Function GetOpticalTuning(kind As MASSvgProjectIconKind) As IconOpticalTuning
            Select Case kind
                Case MASSvgProjectIconKind.All
                    Return New IconOpticalTuning With {.PadFactor = 0.092F, .ScaleFactor = 0.885F, .OffsetXDip = 0.0F, .OffsetYDip = 0.0F, .StrokeScale = 0.98F}

                Case MASSvgProjectIconKind.UI
                    Return New IconOpticalTuning With {.PadFactor = 0.08F, .ScaleFactor = 0.915F, .OffsetXDip = 0.0F, .OffsetYDip = 0.0F, .StrokeScale = 0.98F}

                Case MASSvgProjectIconKind.Files
                    Return New IconOpticalTuning With {.PadFactor = 0.076F, .ScaleFactor = 0.935F, .OffsetXDip = 0.0F, .OffsetYDip = 0.04F, .StrokeScale = 1.0F}

                Case MASSvgProjectIconKind.Media
                    Return New IconOpticalTuning With {.PadFactor = 0.078F, .ScaleFactor = 0.93F, .OffsetXDip = 0.0F, .OffsetYDip = 0.02F, .StrokeScale = 1.02F}

                Case MASSvgProjectIconKind.Communication
                    Return New IconOpticalTuning With {.PadFactor = 0.078F, .ScaleFactor = 0.925F, .OffsetXDip = 0.0F, .OffsetYDip = 0.02F, .StrokeScale = 1.0F}

                Case MASSvgProjectIconKind.Security
                    Return New IconOpticalTuning With {.PadFactor = 0.076F, .ScaleFactor = 0.94F, .OffsetXDip = 0.0F, .OffsetYDip = 0.03F, .StrokeScale = 1.02F}

                Case MASSvgProjectIconKind.Network
                    Return New IconOpticalTuning With {.PadFactor = 0.078F, .ScaleFactor = 0.94F, .OffsetXDip = 0.0F, .OffsetYDip = 0.18F, .StrokeScale = 1.04F}

                Case MASSvgProjectIconKind.Devices
                    Return New IconOpticalTuning With {.PadFactor = 0.074F, .ScaleFactor = 0.945F, .OffsetXDip = 0.0F, .OffsetYDip = 0.08F, .StrokeScale = 1.0F}

                Case MASSvgProjectIconKind.System
                    Return New IconOpticalTuning With {.PadFactor = 0.082F, .ScaleFactor = 0.91F, .OffsetXDip = 0.0F, .OffsetYDip = 0.0F, .StrokeScale = 0.98F}

                Case MASSvgProjectIconKind.Unsorted
                    Return New IconOpticalTuning With {.PadFactor = 0.145F, .ScaleFactor = 0.62F, .OffsetXDip = 0.0F, .OffsetYDip = 0.0F, .StrokeScale = 0.86F}

                Case Else
                    Return IconOpticalTuning.Default
            End Select
        End Function

#End Region

#Region "Registry / cache"

        Private Shared ReadOnly Colored As New Dictionary(Of MASSvgProjectIconKind, List(Of ColoredPath))()

        Private NotInheritable Class IconCache
            Friend BaseSize As Single
            Friend Paths As List(Of ColoredPath)
            Friend Parsed As List(Of SKPath)
            Friend UnionBounds As SKRect
        End Class

        Private Shared ReadOnly _cache As New Dictionary(Of MASSvgProjectIconKind, IconCache)()

        Friend Shared Sub RegisterDefaultCatalog()
            Colored.Clear()
            ClearCache()

            SetIcon(MASSvgProjectIconKind.All, CreateAllIcon())
            SetIcon(MASSvgProjectIconKind.UI, CreateUIIcon())
            SetIcon(MASSvgProjectIconKind.Files, CreateFilesIcon())
            SetIcon(MASSvgProjectIconKind.Media, CreateMediaIcon())
            SetIcon(MASSvgProjectIconKind.Communication, CreateCommunicationIcon())
            SetIcon(MASSvgProjectIconKind.Security, CreateSecurityIcon())
            SetIcon(MASSvgProjectIconKind.Network, CreateNetworkIcon())
            SetIcon(MASSvgProjectIconKind.Devices, CreateDevicesIcon())
            SetIcon(MASSvgProjectIconKind.System, CreateSystemIcon())
            SetIcon(MASSvgProjectIconKind.Unsorted, CreateUnsortedIcon())
        End Sub

        Private Shared Sub SetIcon(kind As MASSvgProjectIconKind, layers As List(Of ColoredPath))
            Colored(kind) = If(layers, New List(Of ColoredPath)())
            _cache.Remove(kind)
        End Sub

        Friend Shared Sub ClearIcons()
            Colored.Clear()
            ClearCache()
        End Sub

        Friend Shared Sub ClearCache()
            For Each kv In _cache
                Dim cache As IconCache = kv.Value
                If cache Is Nothing OrElse cache.Parsed Is Nothing Then Continue For

                For Each p As SKPath In cache.Parsed
                    Try
                        If p IsNot Nothing Then p.Dispose()
                    Catch masCaughtException1 As Exception
                        Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException1)
                    End Try
                Next
            Next

            _cache.Clear()
        End Sub

        Private Shared Function GetCache(kind As MASSvgProjectIconKind) As IconCache
            Dim existing As IconCache = Nothing
            If _cache.TryGetValue(kind, existing) AndAlso existing IsNot Nothing Then Return existing

            Dim layers As List(Of ColoredPath) = Nothing
            If Not Colored.TryGetValue(kind, layers) OrElse layers Is Nothing OrElse layers.Count = 0 Then
                _cache(kind) = Nothing
                Return Nothing
            End If

            Dim built As IconCache = BuildCache(layers)
            _cache(kind) = built
            Return built
        End Function

        Private Shared Function BuildCache(layers As List(Of ColoredPath)) As IconCache
            If layers Is Nothing OrElse layers.Count = 0 Then Return Nothing

            Dim iconBase As Single = 24.0F
            For Each cp As ColoredPath In layers
                If cp IsNot Nothing AndAlso cp.BaseSize > 0.0F Then
                    iconBase = cp.BaseSize
                    Exit For
                End If
            Next

            Dim parsed As New List(Of SKPath)()
            Dim union As SKRect = SKRect.Empty
            Dim any As Boolean = False

            For Each cp As ColoredPath In layers
                If cp Is Nothing OrElse String.IsNullOrWhiteSpace(cp.Data) Then
                    parsed.Add(Nothing)
                    Continue For
                End If

                Dim d As String = cp.Data.Trim()

                If IsIgnorableSvgData(d) Then
                    parsed.Add(Nothing)
                    Continue For
                End If

                Dim path As SKPath = Nothing

                Try
                    path = SKPath.ParseSvgPathData(d)
                Catch masCaughtException3 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException3)
                    path = Nothing
                End Try

                If path Is Nothing Then
                    parsed.Add(Nothing)
                    Continue For
                End If

                parsed.Add(path)

                Dim b As SKRect = path.Bounds
                If Not b.IsEmpty AndAlso b.Width > 0.0001F AndAlso b.Height > 0.0001F Then
                    If Not any Then
                        union = b
                        any = True
                    Else
                        union = SKRect.Union(union, b)
                    End If
                End If
            Next

            If Not any Then
                For Each p As SKPath In parsed
                    Try
                        If p IsNot Nothing Then p.Dispose()
                    Catch masCaughtException2 As Exception
                        Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException2)
                    End Try
                Next

                Return Nothing
            End If

            Return New IconCache With {
                .BaseSize = iconBase,
                .Paths = layers,
                .Parsed = parsed,
                .UnionBounds = union
            }
        End Function

        Private Shared Function IsIgnorableSvgData(data As String) As Boolean
            If String.IsNullOrWhiteSpace(data) Then Return True

            Dim t As String = data.Trim()

            If t.StartsWith("new", StringComparison.OrdinalIgnoreCase) Then Return True
            If t.StartsWith("M0 0h24", StringComparison.OrdinalIgnoreCase) Then Return True
            If t.StartsWith("M0,0h24", StringComparison.OrdinalIgnoreCase) Then Return True
            If t.StartsWith("M0 0h20", StringComparison.OrdinalIgnoreCase) Then Return True
            If t.StartsWith("M0,0h20", StringComparison.OrdinalIgnoreCase) Then Return True

            Return False
        End Function

#End Region

#Region "Draw"

        Friend Shared Sub DrawIcon(canvas As SKCanvas,
                                   kind As MASSvgProjectIconKind,
                                   bounds As SKRect,
                                   tint As SKColor,
                                   dpi As Single,
                                   stroke As SKPaint,
                                   fill As SKPaint)

            DrawIcon(
                canvas:=canvas,
                kind:=kind,
                bounds:=bounds,
                tint:=tint,
                dpi:=dpi,
                stroke:=stroke,
                fill:=fill,
                colorMode:=IconColorMode.PreserveLayerColors
            )
        End Sub

        Private Shared Sub DrawIcon(canvas As SKCanvas,
                                    kind As MASSvgProjectIconKind,
                                    bounds As SKRect,
                                    tint As SKColor,
                                    dpi As Single,
                                    stroke As SKPaint,
                                    fill As SKPaint,
                                    colorMode As IconColorMode)

            Dim cache As IconCache = GetCache(kind)
            Dim tuning As IconOpticalTuning = GetOpticalTuning(kind)

            DrawIconCore(
                canvas:=canvas,
                cache:=cache,
                bounds:=bounds,
                tint:=tint,
                dpi:=dpi,
                stroke:=stroke,
                fill:=fill,
                tuning:=tuning,
                colorMode:=colorMode
            )
        End Sub

        Private Shared Sub DrawIconCore(canvas As SKCanvas,
                                        cache As IconCache,
                                        bounds As SKRect,
                                        tint As SKColor,
                                        dpi As Single,
                                        stroke As SKPaint,
                                        fill As SKPaint,
                                        tuning As IconOpticalTuning,
                                        colorMode As IconColorMode)

            If canvas Is Nothing Then Return
            If bounds.IsEmpty OrElse bounds.Width <= 1.0F OrElse bounds.Height <= 1.0F Then Return
            If stroke Is Nothing OrElse fill Is Nothing Then Return

            dpi = SafeDpi(dpi)

            If cache Is Nothing OrElse cache.Parsed Is Nothing OrElse cache.Parsed.Count = 0 Then
                DrawFallbackIcon(canvas, bounds, tint, fill)
                Return
            End If

            Dim ub As SKRect = cache.UnionBounds
            If ub.IsEmpty OrElse ub.Width <= 0.0001F OrElse ub.Height <= 0.0001F Then Return

            Dim padFactor As Single = Math.Max(0.0F, tuning.PadFactor)
            Dim pad As Single = Math.Max(1.5F * dpi, Math.Min(bounds.Width, bounds.Height) * padFactor)

            Dim targetW As Single = Math.Max(1.0F, bounds.Width - (pad * 2.0F))
            Dim targetH As Single = Math.Max(1.0F, bounds.Height - (pad * 2.0F))

            Dim scale As Single = Math.Min(targetW / ub.Width, targetH / ub.Height)
            scale *= Math.Max(0.01F, tuning.ScaleFactor)

            Dim tx As Single = bounds.MidX - (ub.MidX * scale) + (tuning.OffsetXDip * dpi)
            Dim ty As Single = bounds.MidY - (ub.MidY * scale) + (tuning.OffsetYDip * dpi)

            canvas.Save()

            Try
                canvas.Translate(tx, ty)
                canvas.Scale(scale, scale)

                For i As Integer = 0 To cache.Paths.Count - 1
                    If i >= cache.Parsed.Count Then Exit For

                    Dim cp As ColoredPath = cache.Paths(i)
                    Dim path As SKPath = cache.Parsed(i)

                    If cp Is Nothing OrElse path Is Nothing Then Continue For

                    DrawPathLayer(
                        canvas:=canvas,
                        path:=path,
                        cp:=cp,
                        baseSize:=cache.BaseSize,
                        tint:=tint,
                        stroke:=stroke,
                        fill:=fill,
                        tuning:=tuning,
                        colorMode:=colorMode
                    )
                Next

            Finally
                canvas.Restore()
            End Try
        End Sub

        Private Shared Sub DrawPathLayer(canvas As SKCanvas,
                                         path As SKPath,
                                         cp As ColoredPath,
                                         baseSize As Single,
                                         tint As SKColor,
                                         stroke As SKPaint,
                                         fill As SKPaint,
                                         tuning As IconOpticalTuning,
                                         colorMode As IconColorMode)

            Dim shouldDrawFill As Boolean
            Dim fillColor As SKColor

            ResolveFill(
                cp:=cp,
                tint:=tint,
                colorMode:=colorMode,
                shouldDrawFill:=shouldDrawFill,
                fillColor:=fillColor
            )

            If shouldDrawFill Then
                fill.Reset()
                fill.IsAntialias = True
                fill.IsDither = False
                fill.Style = SKPaintStyle.Fill
                fill.Color = fillColor
                canvas.DrawPath(path, fill)
            End If

            If cp.StrokeWidthFactor <= 0.0F Then Return

            stroke.Reset()
            stroke.IsAntialias = True
            stroke.IsDither = False
            stroke.Style = SKPaintStyle.Stroke
            stroke.StrokeCap = SKStrokeCap.Round
            stroke.StrokeJoin = SKStrokeJoin.Round
            stroke.StrokeWidth = Math.Max(0.01F, baseSize) *
                                 cp.StrokeWidthFactor *
                                 Math.Max(0.01F, tuning.StrokeScale)

            If colorMode = IconColorMode.TintAll Then
                stroke.Color = tint
            Else
                stroke.Color = If(cp.Stroke.HasValue, cp.Stroke.Value, tint)
            End If

            If stroke.Color.Alpha = 0 Then Return

            canvas.DrawPath(path, stroke)
        End Sub

        Private Shared Sub ResolveFill(cp As ColoredPath,
                                       tint As SKColor,
                                       colorMode As IconColorMode,
                                       ByRef shouldDrawFill As Boolean,
                                       ByRef fillColor As SKColor)

            shouldDrawFill = True
            fillColor = tint

            If cp Is Nothing Then
                shouldDrawFill = False
                Return
            End If

            If colorMode = IconColorMode.TintAll Then
                If cp.Fill.HasValue AndAlso cp.Fill.Value.Alpha = 0 Then
                    shouldDrawFill = False
                    Return
                End If

                fillColor = tint
                Return
            End If

            If cp.Fill.HasValue Then
                fillColor = cp.Fill.Value
                If fillColor.Alpha = 0 Then
                    shouldDrawFill = False
                End If
            End If
        End Sub

        Private Shared Sub DrawFallbackIcon(canvas As SKCanvas,
                                            bounds As SKRect,
                                            tint As SKColor,
                                            fill As SKPaint)

            Dim radius As Single = Math.Min(bounds.Width, bounds.Height) * 0.33F

            fill.Reset()
            fill.IsAntialias = True
            fill.IsDither = False
            fill.Style = SKPaintStyle.Fill
            fill.Color = tint

            canvas.DrawCircle(bounds.MidX, bounds.MidY, radius, fill)
        End Sub

        Private Shared Function SafeDpi(dpi As Single) As Single
            If dpi <= 0.0F OrElse Single.IsNaN(dpi) OrElse Single.IsInfinity(dpi) Then
                Return 1.0F
            End If

            Return dpi
        End Function

#End Region

#Region "Default SVG catalog"

        Private Shared Function CreateAllIcon() As List(Of ColoredPath)
            Return CPListTabler(
                2.0F,
                "M12 3v18",
                "M3 12h18",
                "M3 5a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2H5a2 2 0 0 1 -2 -2z"
            )
        End Function

        Private Shared Function CreateUIIcon() As List(Of ColoredPath)
            Return CPListTabler(
                2.0F,
                "M10 8h.01",
                "M12 12h.01",
                "M14 8h.01",
                "M16 12h.01",
                "M18 8h.01",
                "M6 8h.01",
                "M8 12h.01",
                "M7 16h10",
                "M2 6a2 2 0 0 1 2 -2h16a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-16a2 2 0 0 1 -2 -2z"
            )
        End Function

        Private Shared Function CreateFilesIcon() As List(Of ColoredPath)
            Return CPListTabler(
                2.0F,
                "M20 20a2 2 0 0 0 2 -2V8a2 2 0 0 0 -2 -2h-7.9a2 2 0 0 1 -1.69 -.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0 -2 2v13a2 2 0 0 0 2 2Z",
                "M2 10h20"
            )
        End Function

        Private Shared Function CreateMediaIcon() As List(Of ColoredPath)
            Return CPListTabler(
                1.75F,
                "M3 9a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2l0 -9",
                "M16 3l-4 4l-4 -4"
            )
        End Function

        Private Shared Function CreateCommunicationIcon() As List(Of ColoredPath)
            Return CPListTabler(
                2.0F,
                "M22 17a2 2 0 0 1 -2 2H6.828a2 2 0 0 0 -1.414 .586l-2.202 2.202A.71 .71 0 0 1 2 21.286V5a2 2 0 0 1 2 -2h16a2 2 0 0 1 2 2z",
                "M12 11h.01",
                "M16 11h.01",
                "M8 11h.01"
            )
        End Function

        Private Shared Function CreateSecurityIcon() As List(Of ColoredPath)
            Return CPListTabler(
                2.0F,
                "M20 13c0 5 -3.5 7.5 -7.66 8.95a1 1 0 0 1 -.67 -.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1 -1c2 0 4.5 -1.2 6.24 -2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",
                "M4.243 5.21l14.39 12.472"
            )
        End Function

        Private Shared Function CreateNetworkIcon() As List(Of ColoredPath)
            Return CPListTabler(
                2.0F,
                "M12 20h.01",
                "M2 8.82a15 15 0 0 1 20 0",
                "M5 12.859a10 10 0 0 1 14 0",
                "M8.5 16.429a5 5 0 0 1 7 0"
            )
        End Function

        Private Shared Function CreateDevicesIcon() As List(Of ColoredPath)
            Return CPListTabler(
                2.0F,
                "M3 6a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z",
                "M2 20h20"
            )
        End Function

        Private Shared Function CreateSystemIcon() As List(Of ColoredPath)
            Return CPListTabler(
                2.0F,
                "M9.671 4.136a2.34 2.34 0 0 1 4.659 0a2.34 2.34 0 0 0 3.319 1.915a2.34 2.34 0 0 1 2.33 4.033a2.34 2.34 0 0 0 0 3.831a2.34 2.34 0 0 1 -2.33 4.033a2.34 2.34 0 0 0 -3.319 1.915a2.34 2.34 0 0 1 -4.659 0a2.34 2.34 0 0 0 -3.32 -1.915a2.34 2.34 0 0 1 -2.33 -4.033a2.34 2.34 0 0 0 0 -3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319 -1.915",
                "M12 9a3 3 0 1 0 0 6a3 3 0 0 0 0 -6"
            )
        End Function

        Private Shared Function CreateUnsortedIcon() As List(Of ColoredPath)
            Return CPListTabler(
                1.75F,
                "M3 12h18",
                "M12 21v-18",
                "M7.5 7.5l9 9",
                "M7.5 16.5l9 -9"
            )
        End Function

#End Region

#Region "ColoredPath helpers"

        Private Shared Function CPFromDTabler(data As String,
                                              Optional strokeWidth As Single = 1.75F,
                                              Optional baseSize As Single = 24.0F) As ColoredPath

            Return New ColoredPath With {
                .Data = data,
                .Fill = SKColors.Transparent,
                .Stroke = Nothing,
                .StrokeWidthFactor = strokeWidth / Math.Max(0.001F, baseSize),
                .BaseSize = baseSize
            }
        End Function

        Private Shared Function CPListTabler(strokeWidth As Single,
                                             ParamArray paths() As String) As List(Of ColoredPath)

            Dim result As New List(Of ColoredPath)()

            If paths Is Nothing Then Return result

            For Each d As String In paths
                If String.IsNullOrWhiteSpace(d) Then Continue For
                result.Add(CPFromDTabler(d, strokeWidth, 24.0F))
            Next

            Return result
        End Function

#End Region

    End Class

End Namespace
