Imports System
Imports System.IO
Imports System.Security.Cryptography
Imports SkiaSharp

Namespace Nexamas.UI.Verification

    ''' <summary>
    ''' Owns the approved/current/diff folder contract for screenshot verification assets.
    ''' The store is deliberately path-based and deterministic so CI, local machines, and future Showcase tooling share one layout.
    ''' </summary>
    Friend NotInheritable Class MASRenderBaselineStore

        Friend Sub New(rootDirectory As String)
            If String.IsNullOrWhiteSpace(rootDirectory) Then Throw New ArgumentException("A render verification root directory is required.", NameOf(rootDirectory))
            Me.RootDirectory = Path.GetFullPath(rootDirectory)
        End Sub

        Friend ReadOnly Property RootDirectory As String

        Friend ReadOnly Property BaselineDirectory As String
            Get
                Return Path.Combine(RootDirectory, "baselines")
            End Get
        End Property

        Friend ReadOnly Property CurrentDirectory As String
            Get
                Return Path.Combine(RootDirectory, "current")
            End Get
        End Property

        Friend ReadOnly Property DifferenceDirectory As String
            Get
                Return Path.Combine(RootDirectory, "diff")
            End Get
        End Property

        Friend Sub EnsureDirectories()
            Directory.CreateDirectory(BaselineDirectory)
            Directory.CreateDirectory(CurrentDirectory)
            Directory.CreateDirectory(DifferenceDirectory)
        End Sub

        Friend Function GetBaselinePath(scenario As MASRenderVerificationScenario) As String
            Return Path.Combine(BaselineDirectory, BuildFileName(scenario))
        End Function

        Friend Function GetCurrentPath(scenario As MASRenderVerificationScenario) As String
            Return Path.Combine(CurrentDirectory, BuildFileName(scenario))
        End Function

        Friend Function GetDifferencePath(scenario As MASRenderVerificationScenario) As String
            Return Path.Combine(DifferenceDirectory, BuildFileName(scenario))
        End Function

        Friend Function BaselineExists(scenario As MASRenderVerificationScenario) As Boolean
            Return File.Exists(GetBaselinePath(scenario))
        End Function

        Friend Function LoadBaseline(scenario As MASRenderVerificationScenario) As SKBitmap
            Dim path = GetBaselinePath(scenario)
            If Not File.Exists(path) Then Return Nothing
            Return SKBitmap.Decode(path)
        End Function

        Friend Function SaveBaseline(scenario As MASRenderVerificationScenario, bitmap As SKBitmap) As MASRenderSnapshotResult
            Return SaveBitmap(GetBaselinePath(scenario), scenario, bitmap)
        End Function

        Friend Function SaveBaselineFromCurrent(scenario As MASRenderVerificationScenario,
                                                currentSnapshot As MASRenderSnapshotResult) As MASRenderSnapshotResult
            If scenario Is Nothing Then Throw New ArgumentNullException(NameOf(scenario))
            If currentSnapshot Is Nothing Then Throw New ArgumentNullException(NameOf(currentSnapshot))
            If String.IsNullOrWhiteSpace(currentSnapshot.Path) OrElse Not File.Exists(currentSnapshot.Path) Then
                Throw New InvalidOperationException("The current render verification snapshot file is required before seeding a baseline.")
            End If

            Dim baselinePath As String = GetBaselinePath(scenario)
            Dim parentDirectory As String = Path.GetDirectoryName(baselinePath)
            If Not String.IsNullOrWhiteSpace(parentDirectory) Then Directory.CreateDirectory(parentDirectory)
            File.Copy(currentSnapshot.Path, baselinePath, overwrite:=True)
            Return MASRenderSnapshotResult.FromPersistedFile(scenario, baselinePath, currentSnapshot.Width, currentSnapshot.Height, currentSnapshot.Hash)
        End Function

        Friend Function SaveCurrent(scenario As MASRenderVerificationScenario, bitmap As SKBitmap) As MASRenderSnapshotResult
            Return SaveBitmap(GetCurrentPath(scenario), scenario, bitmap)
        End Function

        Friend Function ComputeBaselineFileHash(scenario As MASRenderVerificationScenario) As String
            Return ComputeFileHash(GetBaselinePath(scenario))
        End Function

        Private Shared Function SaveBitmap(filePath As String,
                                           scenario As MASRenderVerificationScenario,
                                           bitmap As SKBitmap) As MASRenderSnapshotResult
            If bitmap Is Nothing Then Throw New ArgumentNullException(NameOf(bitmap))
            If String.IsNullOrWhiteSpace(filePath) Then Throw New ArgumentException("A render verification bitmap path is required.", NameOf(filePath))

            Dim parentDirectory As String = Path.GetDirectoryName(filePath)
            If Not String.IsNullOrWhiteSpace(parentDirectory) Then Directory.CreateDirectory(parentDirectory)

            Using data As SKData = bitmap.Encode(SKEncodedImageFormat.Png, 100)
                If data Is Nothing Then Throw New InvalidOperationException("Unable to encode render verification bitmap as PNG.")

                Using stream As FileStream = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.None)
                    data.SaveTo(stream)
                End Using

                Return MASRenderSnapshotResult.FromEncodedData(scenario, filePath, bitmap.Width, bitmap.Height, ComputeStableHash(data))
            End Using
        End Function

        Private Shared Function BuildFileName(scenario As MASRenderVerificationScenario) As String
            If scenario Is Nothing Then Throw New ArgumentNullException(NameOf(scenario))
            Return Sanitize(scenario.Id) & ".png"
        End Function

        Private Shared Function Sanitize(value As String) As String
            Dim clean = If(value, String.Empty).Trim()
            For Each ch In Path.GetInvalidFileNameChars()
                clean = clean.Replace(ch, "_"c)
            Next
            If clean.Length = 0 Then clean = "unnamed"
            Return clean
        End Function

        Friend Shared Function ComputeStableHash(bitmap As SKBitmap) As String
            If bitmap Is Nothing Then Return String.Empty
            Using data As SKData = bitmap.Encode(SKEncodedImageFormat.Png, 100)
                If data Is Nothing Then Return String.Empty
                Return ComputeStableHash(data)
            End Using
        End Function

        Friend Shared Function ComputeStableHash(data As SKData) As String
            If data Is Nothing Then Return String.Empty
            Using stream As Stream = data.AsStream()
                Using sha As SHA256 = SHA256.Create()
                    Dim hash As Byte() = sha.ComputeHash(stream)
                    Return BitConverter.ToString(hash).Replace("-", String.Empty).ToLowerInvariant()
                End Using
            End Using
        End Function

        Private Shared Function ComputeFileHash(filePath As String) As String
            If String.IsNullOrWhiteSpace(filePath) OrElse Not File.Exists(filePath) Then Return String.Empty
            Using stream As FileStream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)
                Using sha As SHA256 = SHA256.Create()
                    Dim hash As Byte() = sha.ComputeHash(stream)
                    Return BitConverter.ToString(hash).Replace("-", String.Empty).ToLowerInvariant()
                End Using
            End Using
        End Function

    End Class

End Namespace
