Imports System.Xml.Serialization

<Serializable>
Public Class MapTile

    <XmlIgnore>
    Public Property Image As Image

    Public Property AnimationFrames As List(Of Image) = Nothing
    Public Property IsAnimated As Boolean = False
    Public Property AnimationInterval As Integer = 200

    Private currentAnimationFrameIndex As Integer = 0

    Private lastAnimationUpdateTime As Date = Date.Now

    Public Property ImageFilePath As String
    Public Property Position As Point
    Public Property Size As Size
    Public Property OriginalSize As Size

    Public Property RotationAngle As Integer = 0

    Public Property IsFlippedHorizontally As Boolean = False
    Public Property IsFlippedVertically As Boolean = False

    Public Property IsLocked As Boolean = False

    <XmlIgnore>
    Public Property Selected As Boolean = False

    <XmlIgnore>
    Public Property InitialPosition As Point

    <XmlIgnore>
    Public Property IsResizing As Boolean = False

    <XmlIgnore>
    Public Property CurrentResizeDirection As ResizeDirections = ResizeDirections.None

    <Flags>
    Public Enum ResizeDirections
        None = 0
        Left = 1
        Right = 2
        Top = 4
        Bottom = 8
    End Enum

    Public Property FrameWidth As Integer

    Public Property FrameHeight As Integer

    Public Function Clone() As MapTile
        Dim clonedTile As New MapTile With {
            .ImageFilePath = ImageFilePath,
            .Position = Position,
            .Size = Size,
            .OriginalSize = OriginalSize,
            .Selected = Selected,
            .IsAnimated = IsAnimated,
            .AnimationInterval = AnimationInterval,
            .RotationAngle = RotationAngle,
            .IsFlippedHorizontally = IsFlippedHorizontally,
            .IsFlippedVertically = IsFlippedVertically,
            .IsLocked = IsLocked,
            .FrameWidth = FrameWidth,
            .FrameHeight = FrameHeight
        }

        If IsAnimated AndAlso AnimationFrames IsNot Nothing Then
            clonedTile.AnimationFrames = New List(Of Image)
            For Each img In AnimationFrames
                clonedTile.AnimationFrames.Add(CType(img.Clone(), Image))
            Next
        ElseIf Image IsNot Nothing Then
            clonedTile.Image = CType(Image.Clone(), Image)
        End If

        Return clonedTile
    End Function

    Public Sub Dispose()
        If IsAnimated AndAlso AnimationFrames IsNot Nothing Then
            For Each img In AnimationFrames
                img.Dispose()
            Next
            AnimationFrames.Clear()
            AnimationFrames = Nothing
        ElseIf Image IsNot Nothing Then
            Image.Dispose()
            Image = Nothing
        End If
    End Sub

    Public Function GetCurrentFrame() As Image
        If IsAnimated AndAlso AnimationFrames IsNot Nothing AndAlso AnimationFrames.Count > 0 Then
            Dim currentTime As Date = Date.Now
            If (currentTime - lastAnimationUpdateTime).TotalMilliseconds >= AnimationInterval Then
                currentAnimationFrameIndex = (currentAnimationFrameIndex + 1) Mod AnimationFrames.Count
                lastAnimationUpdateTime = currentTime
            End If
            Return AnimationFrames(currentAnimationFrameIndex)
        Else
            Return Image
        End If
    End Function

End Class

Public Class MapTileData
    Public Property ImageFilePath As String
    Public Property Position As Point
    Public Property Size As Size
    Public Property OriginalSize As Size

    Public Property IsAnimated As Boolean

    Public Property AnimationInterval As Integer

    Public Property RotationAngle As Integer

    Public Property IsFlippedHorizontally As Boolean
    Public Property IsFlippedVertically As Boolean

    Public Property IsLocked As Boolean

    Public Property FrameWidth As Integer

    Public Property FrameHeight As Integer

    Public Property ImageData As String

End Class

<Serializable>
Public Class CanvasData
    Public Property MapTiles As List(Of MapTileData)
    Public Property GridSize As Integer
    Public Property MapSize As Size
End Class
Imports System.Collections.Concurrent
Imports System.Drawing.Drawing2D
Imports System.Drawing.Imaging
Imports System.IO
Imports System.Runtime.InteropServices
Imports System.Threading
Imports System.Xml.Serialization

Public Class FrmWorld
    Private dragBoxFromMouseDown As Rectangle
    Private currentFileName As String = ""
    Private isModified As Boolean = False
    Private recentSearches As New List(Of String)()
    Private Const MaxSearches As Integer = 10
    Private isUpdatingComboBox As Boolean = False
    Private searchCancellationTokenSource As CancellationTokenSource
    Private Const MaxImagesToDisplay As Integer = 500
    Private ReadOnly imageExtensions() As String = {".jpg", ".jpeg", ".png", ".bmp", ".gif"}
    Private undoStack As New Stack(Of MapAction)
    Private redoStack As New Stack(Of MapAction)

#Region " Initiate "

    Private Sub FrmWorld_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        PicCanvas = New MapCanvas()
        PicCanvas.Dock = DockStyle.Fill
        Me.Controls.Add(PicCanvas)
        PopulateTreeView(GetImagesDirectory())
        PicCanvas.AllowDrop = True

        AddHandler CboDimensions.SelectedIndexChanged, AddressOf CboDimensions_SelectedIndexChanged

        CboDimensions.SelectedItem = "32 x 32"

        Dim defaultMapSize As String = "15000 x 10000"

        If Not CboMapSize.Items.Contains(defaultMapSize) Then
            Dim unused = CboMapSize.Items.Add(defaultMapSize)
        End If

        CboMapSize.SelectedItem = defaultMapSize

        AddHandler PicCanvas.CanvasModified, AddressOf CanvasModified_Handler
        AddHandler cboSearch.KeyDown, AddressOf CboSearch_KeyDown
        AddHandler cboSearch.SelectedIndexChanged, AddressOf CboSearch_SelectedIndexChanged
        AddHandler PicCanvas.TileSelected, AddressOf PicCanvas_TileSelected
        AddHandler PicCanvas.TileSizeChanged, AddressOf PicCanvas_TileSizeChanged

        LoadTreeViewState()
        LoadRecentSearches()
    End Sub

    Private Sub LoadRecentSearches()
        Dim searchesFile As String = "RecentSearches.xml"
        If File.Exists(searchesFile) Then
            Dim serializer As New XmlSerializer(GetType(List(Of String)))
            Using reader As New StreamReader(searchesFile)
                recentSearches = CType(serializer.Deserialize(reader), List(Of String))
            End Using
            cboSearch.Items.Clear()
            For Each searchTerm As String In recentSearches
                Dim unused = cboSearch.Items.Add(searchTerm)
            Next
        End If
    End Sub

    Private Sub LoadTreeViewState()
        If File.Exists("TreeViewState.xml") Then
            Dim serializer As New XmlSerializer(GetType(TreeViewState))
            Using reader As New StreamReader("TreeViewState.xml")
                Dim state As TreeViewState = CType(serializer.Deserialize(reader), TreeViewState)

                For Each nodePath As String In state.ExpandedNodes
                    Dim node As TreeNode = FindNodeByPath(TrvFiles.Nodes, nodePath)
                    node?.Expand()
                Next

                If Not String.IsNullOrEmpty(state.SelectedNodePath) Then
                    Dim selectedNode As TreeNode = FindNodeByPath(TrvFiles.Nodes, state.SelectedNodePath)
                    If selectedNode IsNot Nothing Then
                        TrvFiles.SelectedNode = selectedNode
                        selectedNode.EnsureVisible()
                    End If
                End If
            End Using
        End If
    End Sub

    Private Function FindNodeByPath(nodes As TreeNodeCollection, path As String) As TreeNode
        Dim parts() As String = path.Split("\"c)
        Return FindNodeByPathRecursive(nodes, parts, 0)
    End Function

    Private Function FindNodeByPathRecursive(nodes As TreeNodeCollection, parts() As String, index As Integer) As TreeNode
        For Each node As TreeNode In nodes
            If node.Text = parts(index) Then
                Return If(index = parts.Length - 1, node, FindNodeByPathRecursive(node.Nodes, parts, index + 1))
            End If
        Next
        Return Nothing
    End Function

#End Region

#Region " Populate TreeView and FlowPanel "

    Private Async Function LoadImagesAsync(directoryInfo As DirectoryInfo) As Task
        Try
            FLPImages.Controls.Clear()

            Dim imageExtensions() As String = {".jpg", ".jpeg", ".png", ".bmp", ".gif"}
            Dim imageFiles As New List(Of FileInfo)()

            For Each extension As String In imageExtensions
                imageFiles.AddRange(directoryInfo.GetFiles($"*{extension}", SearchOption.TopDirectoryOnly))
            Next

            Dim isInBossesFolder As Boolean = IsInBossesDirectory(directoryInfo)
            Dim isInPeopleFolder As Boolean = IsInPeopleDirectory(directoryInfo)

            For Each imageFile As FileInfo In imageFiles
                Try
                    Dim spriteSheetImage As Image = Await Task.Run(Function() Image.FromFile(imageFile.FullName))

                    Dim frameWidth As Integer
                    Dim frameHeight As Integer

                    If isInBossesFolder Then
                        frameWidth = 96
                        frameHeight = 96

                        Dim firstFrame As Image = ExtractFirstFrame(spriteSheetImage, frameWidth, frameHeight, frameIndex:=1)

                        spriteSheetImage.Dispose()

                        Dim displayName As String = imageFile.Name

                        Invoke(Sub()
                                   Dim imageItem As New ImageItem() With {
                               .Filename = displayName,
                               .FilePath = imageFile.FullName,
                               .Width = frameWidth + 10,
                               .Height = frameHeight + 30,
                               .Image = firstFrame,
                               .IsSpriteSheet = True,
                               .FrameWidth = frameWidth,
                               .FrameHeight = frameHeight
                           }

                                   AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                                   AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                                   AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                                   FLPImages.Controls.Add(imageItem)
                               End Sub)
                    ElseIf isInPeopleFolder Then
                        frameWidth = 32
                        frameHeight = 32

                        Dim firstFrame As Image = ExtractFirstFrame(spriteSheetImage, frameWidth, frameHeight)

                        spriteSheetImage.Dispose()

                        Dim displayName As String = imageFile.Name

                        Invoke(Sub()
                                   Dim imageItem As New ImageItem() With {
                               .Filename = displayName,
                               .FilePath = imageFile.FullName,
                               .Width = 100,
                               .Height = 120,
                               .Image = firstFrame,
                               .IsSpriteSheet = True,
                               .FrameWidth = frameWidth,
                               .FrameHeight = frameHeight
                           }

                                   AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                                   AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                                   AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                                   FLPImages.Controls.Add(imageItem)
                               End Sub)
                    Else
                        frameWidth = spriteSheetImage.Width
                        frameHeight = spriteSheetImage.Height

                        Dim firstFrame As Image = New Bitmap(spriteSheetImage)

                        spriteSheetImage.Dispose()

                        Dim displayName As String = imageFile.Name

                        Invoke(Sub()
                                   Dim imageItem As New ImageItem() With {
                               .Filename = displayName,
                               .FilePath = imageFile.FullName,
                               .Width = 100,
                               .Height = 120,
                               .Image = firstFrame,
                               .IsSpriteSheet = False,
                               .FrameWidth = frameWidth,
                               .FrameHeight = frameHeight
                           }

                                   AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                                   AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                                   AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                                   FLPImages.Controls.Add(imageItem)
                               End Sub)
                    End If
                Catch ex As Exception
                    Dim unused1 = MessageBox.Show("Error processing image " & imageFile.Name & ": " & ex.Message)
                End Try
            Next
        Catch ex As Exception
            Dim unused = MessageBox.Show("Error loading images: " & ex.Message)
        End Try
    End Function

    Private Function IsInBossesDirectory(directory As DirectoryInfo) As Boolean
        Dim bossesDirectoryPath As String = Path.Combine(GetImagesDirectory(), "People", "Boss")
        bossesDirectoryPath = Path.GetFullPath(bossesDirectoryPath).TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()
        Dim currentDirectoryPath As String = directory.FullName.TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()
        Return String.Equals(currentDirectoryPath, bossesDirectoryPath, StringComparison.OrdinalIgnoreCase)
    End Function

    Private Function IsInPeopleDirectory(directory As DirectoryInfo) As Boolean
        Dim peopleDirectoryPath As String = Path.Combine(GetImagesDirectory(), "People")
        peopleDirectoryPath = Path.GetFullPath(peopleDirectoryPath).TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()
        Dim currentDirectoryPath As String = directory.FullName.TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()
        Return currentDirectoryPath.StartsWith(peopleDirectoryPath, StringComparison.OrdinalIgnoreCase)
    End Function

    Private Function ExtractFirstFrame(spriteSheet As Image, frameWidth As Integer, frameHeight As Integer, Optional frameIndex As Integer = 0) As Image
        Dim framesInRow As Integer = spriteSheet.Width \ frameWidth

        If frameIndex < 0 OrElse frameIndex >= framesInRow Then
            frameIndex = 0
        End If

        Dim sourceRect As New Rectangle(frameIndex * frameWidth, 0, frameWidth, frameHeight)

        Dim firstFrame As New Bitmap(frameWidth, frameHeight, PixelFormat.Format32bppArgb)
        Using g As Graphics = Graphics.FromImage(firstFrame)
            g.DrawImage(spriteSheet, New Rectangle(0, 0, frameWidth, frameHeight), sourceRect, GraphicsUnit.Pixel)
        End Using

        Return firstFrame
    End Function

    Private Function ExtractFrameAt(spriteSheet As Image, frameWidth As Integer, frameHeight As Integer, frameIndex As Integer) As Image
        Dim framesInRow As Integer = spriteSheet.Width \ frameWidth

        If frameIndex < 0 OrElse frameIndex >= framesInRow Then
            frameIndex = 0
        End If

        Dim sourceRect As New Rectangle(frameIndex * frameWidth, 0, frameWidth, frameHeight)

        Dim frameImage As New Bitmap(frameWidth, frameHeight)
        Using g As Graphics = Graphics.FromImage(frameImage)
            g.DrawImage(spriteSheet, New Rectangle(0, 0, frameWidth, frameHeight), sourceRect, GraphicsUnit.Pixel)
        End Using

        Return frameImage
    End Function

    Private Sub PopulateTreeView(rootDirectory As String)
        TrvFiles.Nodes.Clear()
        Dim rootDirectoryInfo As New DirectoryInfo(rootDirectory)
        Dim rootNode As New TreeNode(rootDirectoryInfo.Name) With {.Tag = rootDirectoryInfo}
        Dim unused = TrvFiles.Nodes.Add(rootNode)
        AddDirectories(rootDirectoryInfo, rootNode)
    End Sub

    Private Sub AddDirectories(directoryInfo As DirectoryInfo, node As TreeNode)
        For Each directory As DirectoryInfo In directoryInfo.GetDirectories()
            Dim directoryNode As New TreeNode(directory.Name) With {.Tag = directory}
            Dim unused = node.Nodes.Add(directoryNode)
            AddDirectories(directory, directoryNode)
        Next
    End Sub

    Private Async Sub TrvFiles_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TrvFiles.AfterSelect
        FLPImages.Controls.Clear()
        Dim selectedNode As TreeNode = e.Node
        If TypeOf selectedNode.Tag Is DirectoryInfo Then
            Dim directoryInfo As DirectoryInfo = CType(selectedNode.Tag, DirectoryInfo)
            Await LoadImagesAsync(directoryInfo)
        End If
    End Sub

#End Region

#Region " Mouse Events for Dragging Images "

    Private Sub ImageItem_MouseDown(sender As Object, e As MouseEventArgs)
        If e.Button = MouseButtons.Left Then
            Dim dragSize As Size = SystemInformation.DragSize
            dragBoxFromMouseDown = New Rectangle(New Point(e.X - (dragSize.Width \ 2),
                                                       e.Y - (dragSize.Height \ 2)),
                                             dragSize)
        Else
            dragBoxFromMouseDown = Rectangle.Empty
        End If
    End Sub

    Private Sub ImageItem_MouseMove(sender As Object, e As MouseEventArgs)
        If (e.Button And MouseButtons.Left) = MouseButtons.Left Then
            If dragBoxFromMouseDown <> Rectangle.Empty AndAlso Not dragBoxFromMouseDown.Contains(e.Location) Then
                Dim imageItem As ImageItem = CType(sender, ImageItem)
                Dim image As Image = imageItem.Image

                If image IsNot Nothing AndAlso image.Width > 0 AndAlso image.Height > 0 Then
                    Try
                        Dim cursor As Cursor = CreateCursorFromImage(image)
                        Cursor.Current = cursor

                        Dim dataObj As New DataObject()
                        dataObj.SetData(GetType(Bitmap), image)
                        Dim imageFilePath As String = imageItem.FilePath
                        dataObj.SetData("ImageFilePath", imageFilePath)
                        dataObj.SetData("IsSpriteSheet", imageItem.IsSpriteSheet)

                        dataObj.SetData("FrameWidth", imageItem.FrameWidth)
                        dataObj.SetData("FrameHeight", imageItem.FrameHeight)

                        Dim unused2 = imageItem.DoDragDrop(dataObj, DragDropEffects.Copy)
                        Cursor.Current = Cursors.Default

                        cursor.Dispose()
                    Catch ex As Exception
                        Dim unused1 = MessageBox.Show("Error creating cursor: " & ex.Message)
                        Return
                    End Try
                Else
                    Dim unused = MessageBox.Show("Error: The image is invalid or has been disposed.")
                    Return
                End If
                dragBoxFromMouseDown = Rectangle.Empty
            End If
        End If
    End Sub

    Private Sub ImageItem_MouseUp(sender As Object, e As MouseEventArgs)
        dragBoxFromMouseDown = Rectangle.Empty
    End Sub

    Private Function CreateCursorFromImage(image As Image) As Cursor
        Try
            If image Is Nothing Then
                Throw New ArgumentNullException(NameOf(image), "Image cannot be null.")
            End If

            If image.Width = 0 OrElse image.Height = 0 Then
                Throw New ArgumentException("Image dimensions are invalid.")
            End If

            Dim cursorSize As Integer = 32
            Using bitmap As New Bitmap(cursorSize, cursorSize, PixelFormat.Format32bppArgb)
                Using g As Graphics = Graphics.FromImage(bitmap)
                    g.InterpolationMode = InterpolationMode.HighQualityBicubic
                    g.DrawImage(image, New Rectangle(0, 0, cursorSize, cursorSize))
                End Using

                Dim iconHandle As IntPtr = bitmap.GetHicon()

                Dim cursor As New Cursor(iconHandle)

                Dim unused1 = DestroyIcon(iconHandle)

                Return cursor
            End Using
        Catch ex As Exception
            Dim unused = MessageBox.Show("Error creating cursor: " & ex.Message)
            Return Cursors.Default
        End Try
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto)>
    Private Shared Function DestroyIcon(ByVal hIcon As IntPtr) As Boolean
    End Function

    Private Sub CanvasModified_Handler()
        isModified = True
    End Sub

#End Region

#Region " CTMTrvFIles "

    Private Sub MnuCollapse_Click(sender As Object, e As EventArgs) Handles MnuCollapse.Click
        Dim selectedNode As TreeNode = TrvFiles.SelectedNode
        If selectedNode IsNot Nothing Then
            CollapseAllNodes(selectedNode)
        Else
            Dim unused = MessageBox.Show("Please select a node to collapse.", "Collapse Node", MessageBoxButtons.OK, MessageBoxIcon.Information)
        End If
    End Sub

    Private Sub MnuExpand_Click(sender As Object, e As EventArgs) Handles MnuExpand.Click
        Dim selectedNode As TreeNode = TrvFiles.SelectedNode
        If selectedNode IsNot Nothing Then
            ExpandAllNodes(selectedNode)
        Else
            Dim unused = MessageBox.Show("Please select a node to expand.", "Expand Node", MessageBoxButtons.OK, MessageBoxIcon.Information)
        End If
    End Sub

    Private Sub ExpandAllNodes(node As TreeNode)
        node.Expand()
        For Each childNode As TreeNode In node.Nodes
            ExpandAllNodes(childNode)
        Next
    End Sub

    Private Sub CollapseAllNodes(node As TreeNode)
        For Each childNode As TreeNode In node.Nodes
            CollapseAllNodes(childNode)
        Next
        node.Collapse()
    End Sub

#End Region

#Region " Search "

    Private Async Sub CboSearch_KeyDown(sender As Object, e As KeyEventArgs)
        If e.KeyCode = Keys.Enter Then
            Dim searchTerm As String = cboSearch.Text.Trim()
            If Not String.IsNullOrEmpty(searchTerm) Then
                Await PerformSearch(searchTerm)
                e.Handled = True
            End If
        End If
    End Sub

    Private Async Sub CboSearch_SelectedIndexChanged(sender As Object, e As EventArgs)
        If isUpdatingComboBox Then
            Return
        End If
        Dim searchTerm As String = cboSearch.SelectedItem.ToString()
        If Not String.IsNullOrEmpty(searchTerm) Then
            Await PerformSearch(searchTerm)
        End If
    End Sub

    Private Async Function PerformSearch(searchTerm As String) As Task
        If searchCancellationTokenSource IsNot Nothing AndAlso Not searchCancellationTokenSource.IsCancellationRequested Then
            searchCancellationTokenSource.Cancel()
        End If

        searchCancellationTokenSource = New CancellationTokenSource()
        Dim token As CancellationToken = searchCancellationTokenSource.Token

        FLPImages.Controls.Clear()

        PrgSearchProgress.Visible = True
        PrgSearchProgress.Style = ProgressBarStyle.Marquee

        Try
            Dim matchingFiles As New List(Of FileInfo)
            Await Task.Run(Sub()
                               SearchAllDirectories(searchTerm, matchingFiles, token)
                           End Sub)

            PrgSearchProgress.Style = ProgressBarStyle.Continuous
            PrgSearchProgress.Minimum = 0
            PrgSearchProgress.Maximum = Math.Min(matchingFiles.Count, MaxImagesToDisplay)
            PrgSearchProgress.Value = 0

            Dim progressHandler As New Progress(Of ImageItem)(Sub(imageItem)
                                                                  FLPImages.Controls.Add(imageItem)
                                                              End Sub)

            Await LoadImagesFromFilesAsync(matchingFiles, token, progressHandler)

            If Not token.IsCancellationRequested Then
                AddSearchToRecent(searchTerm)
            End If
        Catch ex As OperationCanceledException
        Finally
            PrgSearchProgress.Visible = False
        End Try
    End Function

    Private Sub SearchAllDirectories(searchTerm As String, matchingFiles As List(Of FileInfo), token As CancellationToken)
        Dim directoriesToSearch As New ConcurrentBag(Of DirectoryInfo)

        For Each rootNode As TreeNode In TrvFiles.Nodes
            If TypeOf rootNode.Tag Is DirectoryInfo Then
                directoriesToSearch.Add(CType(rootNode.Tag, DirectoryInfo))
            End If
        Next

        Dim unused = Parallel.ForEach(directoriesToSearch, Sub(directory)
                                                               If token.IsCancellationRequested Then Return
                                                               SearchDirectory(directory, searchTerm, matchingFiles, token)
                                                           End Sub)
    End Sub

    Private Sub SearchDirectory(directory As DirectoryInfo, searchTerm As String, matchingFiles As List(Of FileInfo), token As CancellationToken)
        If token.IsCancellationRequested Then Return

        Try
            Dim imageFiles As New List(Of FileInfo)
            For Each extension As String In imageExtensions
                imageFiles.AddRange(directory.GetFiles($"*{extension}", SearchOption.TopDirectoryOnly))
            Next

            For Each imageFile As FileInfo In imageFiles
                If token.IsCancellationRequested Then Return
                If imageFile.Name.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0 Then
                    SyncLock matchingFiles
                        matchingFiles.Add(imageFile)
                    End SyncLock
                End If
            Next

            For Each subDirectory As DirectoryInfo In directory.GetDirectories()
                If token.IsCancellationRequested Then Return
                SearchDirectory(subDirectory, searchTerm, matchingFiles, token)
            Next
        Catch ex As UnauthorizedAccessException
        End Try
    End Sub

    Private Sub SearchTreeNode(node As TreeNode, searchTerm As String, matchingFiles As List(Of FileInfo))
        If TypeOf node.Tag Is DirectoryInfo Then
            Dim directoryInfo As DirectoryInfo = CType(node.Tag, DirectoryInfo)
            Dim imageFiles As List(Of FileInfo) = GetImageFilesFromDirectory(directoryInfo)

            For Each imageFile As FileInfo In imageFiles
                If imageFile.Name.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0 Then
                    matchingFiles.Add(imageFile)
                End If
            Next

            For Each childNode As TreeNode In node.Nodes
                SearchTreeNode(childNode, searchTerm, matchingFiles)
            Next
        End If
    End Sub

    Private Function GetImageFilesFromDirectory(directoryInfo As DirectoryInfo) As List(Of FileInfo)
        Dim imageExtensions() As String = {".jpg", ".jpeg", ".png", ".bmp", ".gif"}
        Dim imageFiles As New List(Of FileInfo)()

        For Each extension As String In imageExtensions
            imageFiles.AddRange(directoryInfo.GetFiles($"*{extension}", SearchOption.TopDirectoryOnly))
        Next

        Return imageFiles
    End Function

    Private Async Function LoadImagesFromFilesAsync(imageFiles As List(Of FileInfo), token As CancellationToken, progress As IProgress(Of ImageItem)) As Task
        Dim imagesToLoad = imageFiles.Take(MaxImagesToDisplay).ToList()
        Dim loadedImagesCount As Integer = 0

        Invoke(Sub()
                   PrgSearchProgress.Maximum = imagesToLoad.Count
                   PrgSearchProgress.Value = 0
               End Sub)

        For Each imageFile As FileInfo In imagesToLoad
            If token.IsCancellationRequested Then Exit For
            Try
                Dim image As Image = Await Task.Run(Function() Image.FromFile(imageFile.FullName), token)

                Dim imageItem As New ImageItem() With {
                .Image = image,
                .Filename = imageFile.Name,
                .FilePath = imageFile.FullName,
                .Width = 100,
                .Height = 120
            }

                AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                progress.Report(imageItem)

                loadedImagesCount += 1
                Invoke(Sub()
                           PrgSearchProgress.Value = loadedImagesCount
                       End Sub)
            Catch ex As Exception
            End Try
        Next
    End Function

    Private Sub AddSearchToRecent(searchTerm As String)
        If recentSearches.Contains(searchTerm) Then
            Dim unused1 = recentSearches.Remove(searchTerm)
        End If
        recentSearches.Insert(0, searchTerm)

        If recentSearches.Count > MaxSearches Then
            recentSearches.RemoveAt(recentSearches.Count - 1)
        End If

        isUpdatingComboBox = True
        cboSearch.Items.Clear()
        For Each term As String In recentSearches
            Dim unused = cboSearch.Items.Add(term)
        Next
        isUpdatingComboBox = False
    End Sub

#End Region

#Region " Zoom "

    Private Sub PicCanvas_ZoomLevelChanged(zoomLevel As Single) Handles PicCanvas.ZoomLevelChanged
        lblZoom.Text = "Zoom: " & (zoomLevel * 100).ToString("0.##") & "%"
    End Sub

#End Region

#Region " ToolStrip "

    Private Sub BtnZoomToFit_Click(sender As Object, e As EventArgs) Handles BtnZoomToFit.Click
        PicCanvas.ZoomToFit()
    End Sub

    Private Sub CboDimensions_SelectedIndexChanged(sender As Object, e As EventArgs)
        Dim selectedText As String = CboDimensions.SelectedItem.ToString()
        Dim dimensions() As String = selectedText.Split("x"c)
        If dimensions.Length = 2 Then
            Dim size As Integer
            If Integer.TryParse(dimensions(0).Trim(), size) Then
                PicCanvas.GridSize = size
                PicCanvas.SnapAllTilesToGrid()
            End If
        End If
    End Sub

    Private Sub CboMapSize_SelectedIndexChanged(sender As Object, e As EventArgs) Handles CboMapSize.SelectedIndexChanged
        Dim selectedText As String = CboMapSize.SelectedItem.ToString()
        Dim dimensions() As String = selectedText.Split("x"c)
        If dimensions.Length = 2 Then
            Dim width As Integer
            Dim height As Integer
            If Integer.TryParse(dimensions(0).Trim(), width) AndAlso Integer.TryParse(dimensions(1).Trim(), height) Then
                PicCanvas.SetMapSize(width, height)
            End If
        End If
    End Sub

#End Region

#Region " Save and Load Functionality "

    Private Sub SaveCanvas(fileName As String)
        Dim canvasData As New CanvasData With {
        .GridSize = PicCanvas.GridSize,
        .MapSize = PicCanvas.MapSize,
        .MapTiles = New List(Of MapTileData)()
    }

        For Each tile As MapTile In PicCanvas.Tiles
            Dim tileData As New MapTileData With {
            .ImageFilePath = tile.ImageFilePath,
            .Position = tile.Position,
            .Size = tile.Size,
            .OriginalSize = tile.OriginalSize,
            .IsAnimated = tile.IsAnimated,
            .AnimationInterval = tile.AnimationInterval,
            .RotationAngle = tile.RotationAngle,
            .IsFlippedHorizontally = tile.IsFlippedHorizontally,
            .IsFlippedVertically = tile.IsFlippedVertically,
            .IsLocked = tile.IsLocked,
            .FrameWidth = tile.FrameWidth,
            .FrameHeight = tile.FrameHeight
        }

            If String.IsNullOrEmpty(tile.ImageFilePath) Then
                If tile.IsAnimated AndAlso tile.AnimationFrames IsNot Nothing Then
                    Dim frameDataList As New List(Of String)
                    For Each frame As Image In tile.AnimationFrames
                        frameDataList.Add(ImageToBase64(frame))
                    Next
                    tileData.ImageData = String.Join("|", frameDataList)
                ElseIf tile.Image IsNot Nothing Then
                    tileData.ImageData = ImageToBase64(tile.Image)
                End If
            End If

            canvasData.MapTiles.Add(tileData)
        Next

        Dim serializer As New XmlSerializer(GetType(CanvasData))

        Using writer As New StreamWriter(fileName)
            serializer.Serialize(writer, canvasData)
        End Using

        isModified = False
    End Sub

    Private Function ImageToBase64(image As Image) As String
        Using ms As New MemoryStream()
            image.Save(ms, ImageFormat.Png)
            Return Convert.ToBase64String(ms.ToArray())
        End Using
    End Function

    Private Sub LoadCanvas(fileName As String)
        Try
            Dim serializer As New XmlSerializer(GetType(CanvasData))
            Using reader As New StreamReader(fileName)
                Dim canvasData As CanvasData = CType(serializer.Deserialize(reader), CanvasData)

                PicCanvas.ClearTiles()

                PicCanvas.GridSize = canvasData.GridSize
                PicCanvas.SetMapSize(canvasData.MapSize.Width, canvasData.MapSize.Height)

                For Each tileData As MapTileData In canvasData.MapTiles
                    Dim mapTile As MapTile

                    If Not String.IsNullOrEmpty(tileData.ImageFilePath) AndAlso File.Exists(tileData.ImageFilePath) Then
                        If tileData.IsAnimated Then
                            Dim spriteSheetImage As Image = Image.FromFile(tileData.ImageFilePath)

                            Dim frameWidth As Integer = tileData.FrameWidth
                            Dim frameHeight As Integer = tileData.FrameHeight
                            If frameWidth <= 0 OrElse frameHeight <= 0 Then
                                Throw New ArgumentException("Frame width and height must be greater than zero.")
                            End If

                            Dim animationFrames As List(Of Image) = ExtractAnimationFrames(spriteSheetImage, frameWidth, frameHeight)

                            For Each frame As Image In animationFrames
                                Select Case tileData.RotationAngle Mod 360
                                    Case 90
                                        frame.RotateFlip(RotateFlipType.Rotate90FlipNone)
                                    Case 180
                                        frame.RotateFlip(RotateFlipType.Rotate180FlipNone)
                                    Case 270
                                        frame.RotateFlip(RotateFlipType.Rotate270FlipNone)
                                End Select

                                If tileData.IsFlippedHorizontally AndAlso tileData.IsFlippedVertically Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipXY)
                                ElseIf tileData.IsFlippedHorizontally Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipX)
                                ElseIf tileData.IsFlippedVertically Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipY)
                                End If
                            Next

                            spriteSheetImage.Dispose()

                            mapTile = New MapTile With {
                            .AnimationFrames = animationFrames,
                            .IsAnimated = True,
                            .ImageFilePath = tileData.ImageFilePath,
                            .Position = tileData.Position,
                            .Size = tileData.Size,
                            .OriginalSize = tileData.OriginalSize,
                            .AnimationInterval = tileData.AnimationInterval,
                            .RotationAngle = tileData.RotationAngle,
                            .IsFlippedHorizontally = tileData.IsFlippedHorizontally,
                            .IsFlippedVertically = tileData.IsFlippedVertically,
                            .IsLocked = tileData.IsLocked,
                            .FrameWidth = tileData.FrameWidth,
                            .FrameHeight = tileData.FrameHeight
                        }

                            PicCanvas.AddTile(mapTile)
                        Else
                            Dim image As Image = Image.FromFile(tileData.ImageFilePath)
                            Dim tileImage As New Bitmap(image, tileData.Size)
                            image.Dispose()

                            Select Case tileData.RotationAngle Mod 360
                                Case 90
                                    tileImage.RotateFlip(RotateFlipType.Rotate90FlipNone)
                                Case 180
                                    tileImage.RotateFlip(RotateFlipType.Rotate180FlipNone)
                                Case 270
                                    tileImage.RotateFlip(RotateFlipType.Rotate270FlipNone)
                            End Select

                            If tileData.IsFlippedHorizontally AndAlso tileData.IsFlippedVertically Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipXY)
                            ElseIf tileData.IsFlippedHorizontally Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipX)
                            ElseIf tileData.IsFlippedVertically Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipY)
                            End If

                            mapTile = New MapTile With {
                            .Image = tileImage,
                            .ImageFilePath = tileData.ImageFilePath,
                            .Position = tileData.Position,
                            .Size = tileData.Size,
                            .OriginalSize = tileData.OriginalSize,
                            .RotationAngle = tileData.RotationAngle,
                            .IsFlippedHorizontally = tileData.IsFlippedHorizontally,
                            .IsFlippedVertically = tileData.IsFlippedVertically,
                            .IsLocked = tileData.IsLocked
                        }

                            PicCanvas.AddTile(mapTile)
                        End If
                    ElseIf Not String.IsNullOrEmpty(tileData.ImageData) Then
                        If tileData.IsAnimated Then
                            Dim frameDataList As String() = tileData.ImageData.Split("|"c)
                            Dim animationFrames As New List(Of Image)
                            For Each frameData As String In frameDataList
                                Dim frame As Image = Base64ToImage(frameData)
                                Select Case tileData.RotationAngle Mod 360
                                    Case 90
                                        frame.RotateFlip(RotateFlipType.Rotate90FlipNone)
                                    Case 180
                                        frame.RotateFlip(RotateFlipType.Rotate180FlipNone)
                                    Case 270
                                        frame.RotateFlip(RotateFlipType.Rotate270FlipNone)
                                End Select

                                If tileData.IsFlippedHorizontally AndAlso tileData.IsFlippedVertically Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipXY)
                                ElseIf tileData.IsFlippedHorizontally Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipX)
                                ElseIf tileData.IsFlippedVertically Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipY)
                                End If

                                animationFrames.Add(frame)
                            Next

                            mapTile = New MapTile With {
                            .AnimationFrames = animationFrames,
                            .IsAnimated = True,
                            .ImageFilePath = tileData.ImageFilePath,
                            .Position = tileData.Position,
                            .Size = tileData.Size,
                            .OriginalSize = tileData.OriginalSize,
                            .AnimationInterval = tileData.AnimationInterval,
                            .RotationAngle = tileData.RotationAngle,
                            .IsFlippedHorizontally = tileData.IsFlippedHorizontally,
                            .IsFlippedVertically = tileData.IsFlippedVertically,
                            .IsLocked = tileData.IsLocked
                        }

                            PicCanvas.AddTile(mapTile)
                        Else
                            Dim tileImage As Image = Base64ToImage(tileData.ImageData)
                            If tileImage.Size <> tileData.Size Then
                                Dim resizedImage As New Bitmap(tileImage, tileData.Size)
                                tileImage.Dispose()
                                tileImage = resizedImage
                            End If

                            Select Case tileData.RotationAngle Mod 360
                                Case 90
                                    tileImage.RotateFlip(RotateFlipType.Rotate90FlipNone)
                                Case 180
                                    tileImage.RotateFlip(RotateFlipType.Rotate180FlipNone)
                                Case 270
                                    tileImage.RotateFlip(RotateFlipType.Rotate270FlipNone)
                            End Select

                            If tileData.IsFlippedHorizontally AndAlso tileData.IsFlippedVertically Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipXY)
                            ElseIf tileData.IsFlippedHorizontally Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipX)
                            ElseIf tileData.IsFlippedVertically Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipY)
                            End If

                            mapTile = New MapTile With {
                            .Image = tileImage,
                            .ImageFilePath = tileData.ImageFilePath,
                            .Position = tileData.Position,
                            .Size = tileData.Size,
                            .OriginalSize = tileData.OriginalSize,
                            .RotationAngle = tileData.RotationAngle,
                            .IsFlippedHorizontally = tileData.IsFlippedHorizontally,
                            .IsFlippedVertically = tileData.IsFlippedVertically,
                            .IsLocked = tileData.IsLocked
                        }

                            PicCanvas.AddTile(mapTile)
                        End If
                    Else
                        ' Handle missing image data
                    End If
                Next
            End Using

            isModified = False
        Catch ex As Exception
            ' Handle exceptions
        End Try
    End Sub

    Private Function Base64ToImage(base64String As String) As Image
        Dim imageBytes() As Byte = Convert.FromBase64String(base64String)
        Using ms As New MemoryStream(imageBytes)
            Return Image.FromStream(ms)
        End Using
    End Function

    Private Sub MnuNew_Click(sender As Object, e As EventArgs) Handles MnuNew.Click
        If isModified Then
            Dim result = MessageBox.Show("Do you want to save changes?", "Save Changes", MessageBoxButtons.YesNoCancel)
            If result = DialogResult.Yes Then
                MnuSave_Click(sender, e)
            ElseIf result = DialogResult.Cancel Then
                Return
            End If
        End If

        PicCanvas.ClearTiles()
        currentFileName = ""
        isModified = False
    End Sub

    Private Sub MnuSave_Click(sender As Object, e As EventArgs) Handles MnuSave.Click
        If String.IsNullOrEmpty(currentFileName) Then
            MnuSaveAs_Click(sender, e)
        Else
            SaveCanvas(currentFileName)
        End If
    End Sub

    Private Sub MnuSaveAs_Click(sender As Object, e As EventArgs) Handles MnuSaveAs.Click
        Using sfd As New SaveFileDialog()
            sfd.Filter = "Map Files (*.map)|*.map|All Files (*.*)|*.*"
            If sfd.ShowDialog() = DialogResult.OK Then
                currentFileName = sfd.FileName
                SaveCanvas(currentFileName)
            End If
        End Using
    End Sub

    Private Sub MnuLoad_Click(sender As Object, e As EventArgs) Handles MnuLoad.Click
        If isModified Then
            Dim result = MessageBox.Show("Do you want to save changes?", "Save Changes", MessageBoxButtons.YesNoCancel)
            If result = DialogResult.Yes Then
                MnuSave_Click(sender, e)
            ElseIf result = DialogResult.Cancel Then
                Return
            End If
        End If

        Using ofd As New OpenFileDialog()
            ofd.Filter = "Map Files (*.map)|*.map|All Files (*.*)|*.*"
            If ofd.ShowDialog() = DialogResult.OK Then
                Try
                    currentFileName = ofd.FileName
                    LoadCanvas(currentFileName)
                Catch ex As Exception
                    ' Handle exceptions
                End Try
            End If
        End Using
    End Sub

    Private Sub MnuExit_Click(sender As Object, e As EventArgs) Handles MnuExit.Click
        If isModified Then
            Dim result = MessageBox.Show("Do you want to save changes?", "Save Changes", MessageBoxButtons.YesNoCancel)
            If result = DialogResult.Yes Then
                MnuSave_Click(sender, e)
            ElseIf result = DialogResult.Cancel Then
                Return
            End If
        End If
        Close()
    End Sub

#End Region

#Region " Zoom "

    Private Sub Mnu50_Click(sender As Object, e As EventArgs) Handles Mnu50.Click
        SetZoom(0.5F)
    End Sub

    Private Sub Mnu100_Click(sender As Object, e As EventArgs) Handles Mnu100.Click
        SetZoom(1.0F)
    End Sub

    Private Sub Mnu250_Click(sender As Object, e As EventArgs) Handles Mnu250.Click
        SetZoom(2.5F)
    End Sub

    Private Sub Mnu500_Click(sender As Object, e As EventArgs) Handles Mnu500.Click
        SetZoom(5.0F)
    End Sub

    Private Sub Mnu1000_Click(sender As Object, e As EventArgs) Handles Mnu1000.Click
        SetZoom(10.0F)
    End Sub

    Private Sub SetZoom(ByVal zoomLevel As Single)
        PicCanvas.ZoomLevel = zoomLevel
    End Sub

    Private Sub TrvFiles_NodeMouseClick(sender As Object, e As TreeNodeMouseClickEventArgs) Handles TrvFiles.NodeMouseClick
        If e.Button = MouseButtons.Right Then
            TrvFiles.SelectedNode = e.Node
        End If
    End Sub

    Private Sub MnuOpenInExplorer_Click(sender As Object, e As EventArgs) Handles MnuOpenInExplorer.Click
        Dim selectedNode As TreeNode = TrvFiles.SelectedNode
        If selectedNode IsNot Nothing AndAlso TypeOf selectedNode.Tag Is DirectoryInfo Then
            Dim directoryInfo As DirectoryInfo = CType(selectedNode.Tag, DirectoryInfo)
            If directoryInfo.Exists Then
                Try
                    Dim unused3 = Process.Start("explorer.exe", directoryInfo.FullName)
                Catch ex As Exception
                    Dim unused2 = MessageBox.Show("Failed to open folder: " & ex.Message)
                End Try
            Else
                Dim unused1 = MessageBox.Show("Directory does not exist.")
            End If
        Else
            Dim unused = MessageBox.Show("No directory selected.")
        End If
    End Sub

    Private Sub PicCanvas_MouseMove(sender As Object, e As MouseEventArgs) Handles PicCanvas.MouseMove
        Dim canvasPoint As Point = PicCanvas.ClientToCanvas(e.Location)
        STBMouseCoords.Text = $"X: {canvasPoint.X}, Y: {canvasPoint.Y}"
    End Sub

#End Region

#Region " Closing "

    Private Sub FrmWorld_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
        SaveTreeViewState()
        SaveRecentSearches()
    End Sub

    Private Sub SaveRecentSearches()
        Dim serializer As New XmlSerializer(GetType(List(Of String)))
        Using writer As New StreamWriter("RecentSearches.xml")
            serializer.Serialize(writer, recentSearches)
        End Using
    End Sub

    Private Sub SaveTreeViewState()
        Dim state As New TreeViewState()

        For Each node As TreeNode In TrvFiles.Nodes
            SaveExpandedNodes(node, state)
        Next

        If TrvFiles.SelectedNode IsNot Nothing Then
            state.SelectedNodePath = TrvFiles.SelectedNode.FullPath
        End If

        Dim serializer As New XmlSerializer(GetType(TreeViewState))
        Using writer As New StreamWriter("TreeViewState.xml")
            serializer.Serialize(writer, state)
        End Using
    End Sub

    Private Sub SaveExpandedNodes(node As TreeNode, state As TreeViewState)
        If node.IsExpanded Then
            state.ExpandedNodes.Add(node.FullPath)
        End If
        For Each childNode As TreeNode In node.Nodes
            SaveExpandedNodes(childNode, state)
        Next
    End Sub

#End Region

#Region " Undo Redo "

    Private Sub BtnUndo_Click(sender As Object, e As EventArgs) Handles BtnUndo.Click
        PicCanvas.Undo()
    End Sub

    Private Sub BtnRedo_Click(sender As Object, e As EventArgs) Handles BtnRedo.Click
        PicCanvas.Redo()
    End Sub
    Private Sub AddAction(action As MapAction)
        undoStack.Push(action)
        redoStack.Clear()
    End Sub

    Private Sub Undo()
        If undoStack.Count > 0 Then
            Dim action As MapAction = undoStack.Pop()
            action.Undo()
            redoStack.Push(action)
        End If
    End Sub

    Private Sub Redo()
        If redoStack.Count > 0 Then
            Dim action As MapAction = redoStack.Pop()
            action.Execute()
            undoStack.Push(action)
        End If
    End Sub

#End Region

#Region " Other "

    Private Sub PicCanvas_TileSelected(selectedTile As MapTile)
        STBSize.Text = If(selectedTile IsNot Nothing,
            $"Original Size {selectedTile.OriginalSize.Width}, {selectedTile.OriginalSize.Height}: Current Size {selectedTile.Size.Width}, {selectedTile.Size.Height}",
            "")
        UpdateSTBSize(selectedTile)
    End Sub

    Private Sub PicCanvas_TileSizeChanged(selectedTile As MapTile)
        UpdateSTBSize(selectedTile)
        AddAction(New MoveTileAction(selectedTile, PicCanvas, selectedTile.Position, selectedTile.Position))
    End Sub

    Private Sub UpdateSTBSize(selectedTile As MapTile)
        STBSize.Text = If(selectedTile IsNot Nothing,
            $"Original Size {selectedTile.OriginalSize.Width}, {selectedTile.OriginalSize.Height}: Current Size {selectedTile.Size.Width}, {selectedTile.Size.Height}",
            "")
    End Sub

    Public Function GetImagesDirectory() As String
        Dim startupPath As String = Application.StartupPath
        Dim dirInfo As New DirectoryInfo(startupPath)

        While dirInfo IsNot Nothing AndAlso dirInfo.Name <> "Bizarria New"
            dirInfo = dirInfo.Parent
        End While

        If dirInfo Is Nothing Then
            Throw New DirectoryNotFoundException("Could not find the 'Bizarria New' directory in the path hierarchy.")
        End If

        Dim imagesDir As String = Path.Combine(dirInfo.FullName, "Data", "Decor")

        If Not Directory.Exists(imagesDir) Then
            Throw New DirectoryNotFoundException($"The images directory does not exist: {imagesDir}")
        End If

        Return imagesDir
    End Function

#End Region

End Class
Imports System.Collections.Concurrent
Imports System.Drawing.Drawing2D
Imports System.Drawing.Imaging
Imports System.IO
Imports System.Runtime.InteropServices
Imports System.Threading
Imports System.Xml.Serialization

Public Class FrmWorld
    Private dragBoxFromMouseDown As Rectangle
    Private currentFileName As String = ""
    Private isModified As Boolean = False
    Private recentSearches As New List(Of String)()
    Private Const MaxSearches As Integer = 10
    Private isUpdatingComboBox As Boolean = False
    Private searchCancellationTokenSource As CancellationTokenSource
    Private Const MaxImagesToDisplay As Integer = 500
    Private ReadOnly imageExtensions() As String = {".jpg", ".jpeg", ".png", ".bmp", ".gif"}
    Private undoStack As New Stack(Of MapAction)
    Private redoStack As New Stack(Of MapAction)

#Region " Initiate "

    Private Sub FrmWorld_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        PicCanvas = New MapCanvas()
        PicCanvas.Dock = DockStyle.Fill
        Me.Controls.Add(PicCanvas)
        PopulateTreeView(GetImagesDirectory())
        PicCanvas.AllowDrop = True

        AddHandler CboDimensions.SelectedIndexChanged, AddressOf CboDimensions_SelectedIndexChanged

        CboDimensions.SelectedItem = "32 x 32"

        Dim defaultMapSize As String = "15000 x 10000"

        If Not CboMapSize.Items.Contains(defaultMapSize) Then
            Dim unused = CboMapSize.Items.Add(defaultMapSize)
        End If

        CboMapSize.SelectedItem = defaultMapSize

        AddHandler PicCanvas.CanvasModified, AddressOf CanvasModified_Handler
        AddHandler cboSearch.KeyDown, AddressOf CboSearch_KeyDown
        AddHandler cboSearch.SelectedIndexChanged, AddressOf CboSearch_SelectedIndexChanged
        AddHandler PicCanvas.TileSelected, AddressOf PicCanvas_TileSelected
        AddHandler PicCanvas.TileSizeChanged, AddressOf PicCanvas_TileSizeChanged

        LoadTreeViewState()
        LoadRecentSearches()
    End Sub

    Private Sub LoadRecentSearches()
        Dim searchesFile As String = "RecentSearches.xml"
        If File.Exists(searchesFile) Then
            Dim serializer As New XmlSerializer(GetType(List(Of String)))
            Using reader As New StreamReader(searchesFile)
                recentSearches = CType(serializer.Deserialize(reader), List(Of String))
            End Using
            cboSearch.Items.Clear()
            For Each searchTerm As String In recentSearches
                Dim unused = cboSearch.Items.Add(searchTerm)
            Next
        End If
    End Sub

    Private Sub LoadTreeViewState()
        If File.Exists("TreeViewState.xml") Then
            Dim serializer As New XmlSerializer(GetType(TreeViewState))
            Using reader As New StreamReader("TreeViewState.xml")
                Dim state As TreeViewState = CType(serializer.Deserialize(reader), TreeViewState)

                For Each nodePath As String In state.ExpandedNodes
                    Dim node As TreeNode = FindNodeByPath(TrvFiles.Nodes, nodePath)
                    node?.Expand()
                Next

                If Not String.IsNullOrEmpty(state.SelectedNodePath) Then
                    Dim selectedNode As TreeNode = FindNodeByPath(TrvFiles.Nodes, state.SelectedNodePath)
                    If selectedNode IsNot Nothing Then
                        TrvFiles.SelectedNode = selectedNode
                        selectedNode.EnsureVisible()
                    End If
                End If
            End Using
        End If
    End Sub

    Private Function FindNodeByPath(nodes As TreeNodeCollection, path As String) As TreeNode
        Dim parts() As String = path.Split("\"c)
        Return FindNodeByPathRecursive(nodes, parts, 0)
    End Function

    Private Function FindNodeByPathRecursive(nodes As TreeNodeCollection, parts() As String, index As Integer) As TreeNode
        For Each node As TreeNode In nodes
            If node.Text = parts(index) Then
                Return If(index = parts.Length - 1, node, FindNodeByPathRecursive(node.Nodes, parts, index + 1))
            End If
        Next
        Return Nothing
    End Function

#End Region

#Region " Populate TreeView and FlowPanel "

    Private Async Function LoadImagesAsync(directoryInfo As DirectoryInfo) As Task
        Try
            FLPImages.Controls.Clear()

            Dim imageExtensions() As String = {".jpg", ".jpeg", ".png", ".bmp", ".gif"}
            Dim imageFiles As New List(Of FileInfo)()

            For Each extension As String In imageExtensions
                imageFiles.AddRange(directoryInfo.GetFiles($"*{extension}", SearchOption.TopDirectoryOnly))
            Next

            Dim isInBossesFolder As Boolean = IsInBossesDirectory(directoryInfo)
            Dim isInPeopleFolder As Boolean = IsInPeopleDirectory(directoryInfo)

            For Each imageFile As FileInfo In imageFiles
                Try
                    Dim spriteSheetImage As Image = Await Task.Run(Function() Image.FromFile(imageFile.FullName))

                    Dim frameWidth As Integer
                    Dim frameHeight As Integer

                    If isInBossesFolder Then
                        frameWidth = 96
                        frameHeight = 96

                        Dim firstFrame As Image = ExtractFirstFrame(spriteSheetImage, frameWidth, frameHeight, frameIndex:=1)

                        spriteSheetImage.Dispose()

                        Dim displayName As String = imageFile.Name

                        Invoke(Sub()
                                   Dim imageItem As New ImageItem() With {
                               .Filename = displayName,
                               .FilePath = imageFile.FullName,
                               .Width = frameWidth + 10,
                               .Height = frameHeight + 30,
                               .Image = firstFrame,
                               .IsSpriteSheet = True,
                               .FrameWidth = frameWidth,
                               .FrameHeight = frameHeight
                           }

                                   AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                                   AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                                   AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                                   FLPImages.Controls.Add(imageItem)
                               End Sub)
                    ElseIf isInPeopleFolder Then
                        frameWidth = 32
                        frameHeight = 32

                        Dim firstFrame As Image = ExtractFirstFrame(spriteSheetImage, frameWidth, frameHeight)

                        spriteSheetImage.Dispose()

                        Dim displayName As String = imageFile.Name

                        Invoke(Sub()
                                   Dim imageItem As New ImageItem() With {
                               .Filename = displayName,
                               .FilePath = imageFile.FullName,
                               .Width = 100,
                               .Height = 120,
                               .Image = firstFrame,
                               .IsSpriteSheet = True,
                               .FrameWidth = frameWidth,
                               .FrameHeight = frameHeight
                           }

                                   AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                                   AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                                   AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                                   FLPImages.Controls.Add(imageItem)
                               End Sub)
                    Else
                        frameWidth = spriteSheetImage.Width
                        frameHeight = spriteSheetImage.Height

                        Dim firstFrame As Image = New Bitmap(spriteSheetImage)

                        spriteSheetImage.Dispose()

                        Dim displayName As String = imageFile.Name

                        Invoke(Sub()
                                   Dim imageItem As New ImageItem() With {
                               .Filename = displayName,
                               .FilePath = imageFile.FullName,
                               .Width = 100,
                               .Height = 120,
                               .Image = firstFrame,
                               .IsSpriteSheet = False,
                               .FrameWidth = frameWidth,
                               .FrameHeight = frameHeight
                           }

                                   AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                                   AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                                   AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                                   FLPImages.Controls.Add(imageItem)
                               End Sub)
                    End If
                Catch ex As Exception
                    Dim unused1 = MessageBox.Show("Error processing image " & imageFile.Name & ": " & ex.Message)
                End Try
            Next
        Catch ex As Exception
            Dim unused = MessageBox.Show("Error loading images: " & ex.Message)
        End Try
    End Function

    Private Function IsInBossesDirectory(directory As DirectoryInfo) As Boolean
        Dim bossesDirectoryPath As String = Path.Combine(GetImagesDirectory(), "People", "Boss")
        bossesDirectoryPath = Path.GetFullPath(bossesDirectoryPath).TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()
        Dim currentDirectoryPath As String = directory.FullName.TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()
        Return String.Equals(currentDirectoryPath, bossesDirectoryPath, StringComparison.OrdinalIgnoreCase)
    End Function

    Private Function IsInPeopleDirectory(directory As DirectoryInfo) As Boolean
        Dim peopleDirectoryPath As String = Path.Combine(GetImagesDirectory(), "People")
        peopleDirectoryPath = Path.GetFullPath(peopleDirectoryPath).TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()
        Dim currentDirectoryPath As String = directory.FullName.TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()
        Return currentDirectoryPath.StartsWith(peopleDirectoryPath, StringComparison.OrdinalIgnoreCase)
    End Function

    Private Function ExtractFirstFrame(spriteSheet As Image, frameWidth As Integer, frameHeight As Integer, Optional frameIndex As Integer = 0) As Image
        Dim framesInRow As Integer = spriteSheet.Width \ frameWidth

        If frameIndex < 0 OrElse frameIndex >= framesInRow Then
            frameIndex = 0
        End If

        Dim sourceRect As New Rectangle(frameIndex * frameWidth, 0, frameWidth, frameHeight)

        Dim firstFrame As New Bitmap(frameWidth, frameHeight, PixelFormat.Format32bppArgb)
        Using g As Graphics = Graphics.FromImage(firstFrame)
            g.DrawImage(spriteSheet, New Rectangle(0, 0, frameWidth, frameHeight), sourceRect, GraphicsUnit.Pixel)
        End Using

        Return firstFrame
    End Function

    Private Function ExtractFrameAt(spriteSheet As Image, frameWidth As Integer, frameHeight As Integer, frameIndex As Integer) As Image
        Dim framesInRow As Integer = spriteSheet.Width \ frameWidth

        If frameIndex < 0 OrElse frameIndex >= framesInRow Then
            frameIndex = 0
        End If

        Dim sourceRect As New Rectangle(frameIndex * frameWidth, 0, frameWidth, frameHeight)

        Dim frameImage As New Bitmap(frameWidth, frameHeight)
        Using g As Graphics = Graphics.FromImage(frameImage)
            g.DrawImage(spriteSheet, New Rectangle(0, 0, frameWidth, frameHeight), sourceRect, GraphicsUnit.Pixel)
        End Using

        Return frameImage
    End Function

    Private Sub PopulateTreeView(rootDirectory As String)
        TrvFiles.Nodes.Clear()
        Dim rootDirectoryInfo As New DirectoryInfo(rootDirectory)
        Dim rootNode As New TreeNode(rootDirectoryInfo.Name) With {.Tag = rootDirectoryInfo}
        Dim unused = TrvFiles.Nodes.Add(rootNode)
        AddDirectories(rootDirectoryInfo, rootNode)
    End Sub

    Private Sub AddDirectories(directoryInfo As DirectoryInfo, node As TreeNode)
        For Each directory As DirectoryInfo In directoryInfo.GetDirectories()
            Dim directoryNode As New TreeNode(directory.Name) With {.Tag = directory}
            Dim unused = node.Nodes.Add(directoryNode)
            AddDirectories(directory, directoryNode)
        Next
    End Sub

    Private Async Sub TrvFiles_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TrvFiles.AfterSelect
        FLPImages.Controls.Clear()
        Dim selectedNode As TreeNode = e.Node
        If TypeOf selectedNode.Tag Is DirectoryInfo Then
            Dim directoryInfo As DirectoryInfo = CType(selectedNode.Tag, DirectoryInfo)
            Await LoadImagesAsync(directoryInfo)
        End If
    End Sub

#End Region

#Region " Mouse Events for Dragging Images "

    Private Sub ImageItem_MouseDown(sender As Object, e As MouseEventArgs)
        If e.Button = MouseButtons.Left Then
            Dim dragSize As Size = SystemInformation.DragSize
            dragBoxFromMouseDown = New Rectangle(New Point(e.X - (dragSize.Width \ 2),
                                                       e.Y - (dragSize.Height \ 2)),
                                             dragSize)
        Else
            dragBoxFromMouseDown = Rectangle.Empty
        End If
    End Sub

    Private Sub ImageItem_MouseMove(sender As Object, e As MouseEventArgs)
        If (e.Button And MouseButtons.Left) = MouseButtons.Left Then
            If dragBoxFromMouseDown <> Rectangle.Empty AndAlso Not dragBoxFromMouseDown.Contains(e.Location) Then
                Dim imageItem As ImageItem = CType(sender, ImageItem)
                Dim image As Image = imageItem.Image

                If image IsNot Nothing AndAlso image.Width > 0 AndAlso image.Height > 0 Then
                    Try
                        Dim cursor As Cursor = CreateCursorFromImage(image)
                        Cursor.Current = cursor

                        Dim dataObj As New DataObject()
                        dataObj.SetData(GetType(Bitmap), image)
                        Dim imageFilePath As String = imageItem.FilePath
                        dataObj.SetData("ImageFilePath", imageFilePath)
                        dataObj.SetData("IsSpriteSheet", imageItem.IsSpriteSheet)

                        dataObj.SetData("FrameWidth", imageItem.FrameWidth)
                        dataObj.SetData("FrameHeight", imageItem.FrameHeight)

                        Dim unused2 = imageItem.DoDragDrop(dataObj, DragDropEffects.Copy)
                        Cursor.Current = Cursors.Default

                        cursor.Dispose()
                    Catch ex As Exception
                        Dim unused1 = MessageBox.Show("Error creating cursor: " & ex.Message)
                        Return
                    End Try
                Else
                    Dim unused = MessageBox.Show("Error: The image is invalid or has been disposed.")
                    Return
                End If
                dragBoxFromMouseDown = Rectangle.Empty
            End If
        End If
    End Sub

    Private Sub ImageItem_MouseUp(sender As Object, e As MouseEventArgs)
        dragBoxFromMouseDown = Rectangle.Empty
    End Sub

    Private Function CreateCursorFromImage(image As Image) As Cursor
        Try
            If image Is Nothing Then
                Throw New ArgumentNullException(NameOf(image), "Image cannot be null.")
            End If

            If image.Width = 0 OrElse image.Height = 0 Then
                Throw New ArgumentException("Image dimensions are invalid.")
            End If

            Dim cursorSize As Integer = 32
            Using bitmap As New Bitmap(cursorSize, cursorSize, PixelFormat.Format32bppArgb)
                Using g As Graphics = Graphics.FromImage(bitmap)
                    g.InterpolationMode = InterpolationMode.HighQualityBicubic
                    g.DrawImage(image, New Rectangle(0, 0, cursorSize, cursorSize))
                End Using

                Dim iconHandle As IntPtr = bitmap.GetHicon()

                Dim cursor As New Cursor(iconHandle)

                Dim unused1 = DestroyIcon(iconHandle)

                Return cursor
            End Using
        Catch ex As Exception
            Dim unused = MessageBox.Show("Error creating cursor: " & ex.Message)
            Return Cursors.Default
        End Try
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto)>
    Private Shared Function DestroyIcon(ByVal hIcon As IntPtr) As Boolean
    End Function

    Private Sub CanvasModified_Handler()
        isModified = True
    End Sub

#End Region

#Region " CTMTrvFIles "

    Private Sub MnuCollapse_Click(sender As Object, e As EventArgs) Handles MnuCollapse.Click
        Dim selectedNode As TreeNode = TrvFiles.SelectedNode
        If selectedNode IsNot Nothing Then
            CollapseAllNodes(selectedNode)
        Else
            Dim unused = MessageBox.Show("Please select a node to collapse.", "Collapse Node", MessageBoxButtons.OK, MessageBoxIcon.Information)
        End If
    End Sub

    Private Sub MnuExpand_Click(sender As Object, e As EventArgs) Handles MnuExpand.Click
        Dim selectedNode As TreeNode = TrvFiles.SelectedNode
        If selectedNode IsNot Nothing Then
            ExpandAllNodes(selectedNode)
        Else
            Dim unused = MessageBox.Show("Please select a node to expand.", "Expand Node", MessageBoxButtons.OK, MessageBoxIcon.Information)
        End If
    End Sub

    Private Sub ExpandAllNodes(node As TreeNode)
        node.Expand()
        For Each childNode As TreeNode In node.Nodes
            ExpandAllNodes(childNode)
        Next
    End Sub

    Private Sub CollapseAllNodes(node As TreeNode)
        For Each childNode As TreeNode In node.Nodes
            CollapseAllNodes(childNode)
        Next
        node.Collapse()
    End Sub

#End Region

#Region " Search "

    Private Async Sub CboSearch_KeyDown(sender As Object, e As KeyEventArgs)
        If e.KeyCode = Keys.Enter Then
            Dim searchTerm As String = cboSearch.Text.Trim()
            If Not String.IsNullOrEmpty(searchTerm) Then
                Await PerformSearch(searchTerm)
                e.Handled = True
            End If
        End If
    End Sub

    Private Async Sub CboSearch_SelectedIndexChanged(sender As Object, e As EventArgs)
        If isUpdatingComboBox Then
            Return
        End If
        Dim searchTerm As String = cboSearch.SelectedItem.ToString()
        If Not String.IsNullOrEmpty(searchTerm) Then
            Await PerformSearch(searchTerm)
        End If
    End Sub

    Private Async Function PerformSearch(searchTerm As String) As Task
        If searchCancellationTokenSource IsNot Nothing AndAlso Not searchCancellationTokenSource.IsCancellationRequested Then
            searchCancellationTokenSource.Cancel()
        End If

        searchCancellationTokenSource = New CancellationTokenSource()
        Dim token As CancellationToken = searchCancellationTokenSource.Token

        FLPImages.Controls.Clear()

        PrgSearchProgress.Visible = True
        PrgSearchProgress.Style = ProgressBarStyle.Marquee

        Try
            Dim matchingFiles As New List(Of FileInfo)
            Await Task.Run(Sub()
                               SearchAllDirectories(searchTerm, matchingFiles, token)
                           End Sub)

            PrgSearchProgress.Style = ProgressBarStyle.Continuous
            PrgSearchProgress.Minimum = 0
            PrgSearchProgress.Maximum = Math.Min(matchingFiles.Count, MaxImagesToDisplay)
            PrgSearchProgress.Value = 0

            Dim progressHandler As New Progress(Of ImageItem)(Sub(imageItem)
                                                                  FLPImages.Controls.Add(imageItem)
                                                              End Sub)

            Await LoadImagesFromFilesAsync(matchingFiles, token, progressHandler)

            If Not token.IsCancellationRequested Then
                AddSearchToRecent(searchTerm)
            End If
        Catch ex As OperationCanceledException
        Finally
            PrgSearchProgress.Visible = False
        End Try
    End Function

    Private Sub SearchAllDirectories(searchTerm As String, matchingFiles As List(Of FileInfo), token As CancellationToken)
        Dim directoriesToSearch As New ConcurrentBag(Of DirectoryInfo)

        For Each rootNode As TreeNode In TrvFiles.Nodes
            If TypeOf rootNode.Tag Is DirectoryInfo Then
                directoriesToSearch.Add(CType(rootNode.Tag, DirectoryInfo))
            End If
        Next

        Dim unused = Parallel.ForEach(directoriesToSearch, Sub(directory)
                                                               If token.IsCancellationRequested Then Return
                                                               SearchDirectory(directory, searchTerm, matchingFiles, token)
                                                           End Sub)
    End Sub

    Private Sub SearchDirectory(directory As DirectoryInfo, searchTerm As String, matchingFiles As List(Of FileInfo), token As CancellationToken)
        If token.IsCancellationRequested Then Return

        Try
            Dim imageFiles As New List(Of FileInfo)
            For Each extension As String In imageExtensions
                imageFiles.AddRange(directory.GetFiles($"*{extension}", SearchOption.TopDirectoryOnly))
            Next

            For Each imageFile As FileInfo In imageFiles
                If token.IsCancellationRequested Then Return
                If imageFile.Name.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0 Then
                    SyncLock matchingFiles
                        matchingFiles.Add(imageFile)
                    End SyncLock
                End If
            Next

            For Each subDirectory As DirectoryInfo In directory.GetDirectories()
                If token.IsCancellationRequested Then Return
                SearchDirectory(subDirectory, searchTerm, matchingFiles, token)
            Next
        Catch ex As UnauthorizedAccessException
        End Try
    End Sub

    Private Sub SearchTreeNode(node As TreeNode, searchTerm As String, matchingFiles As List(Of FileInfo))
        If TypeOf node.Tag Is DirectoryInfo Then
            Dim directoryInfo As DirectoryInfo = CType(node.Tag, DirectoryInfo)
            Dim imageFiles As List(Of FileInfo) = GetImageFilesFromDirectory(directoryInfo)

            For Each imageFile As FileInfo In imageFiles
                If imageFile.Name.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0 Then
                    matchingFiles.Add(imageFile)
                End If
            Next

            For Each childNode As TreeNode In node.Nodes
                SearchTreeNode(childNode, searchTerm, matchingFiles)
            Next
        End If
    End Sub

    Private Function GetImageFilesFromDirectory(directoryInfo As DirectoryInfo) As List(Of FileInfo)
        Dim imageExtensions() As String = {".jpg", ".jpeg", ".png", ".bmp", ".gif"}
        Dim imageFiles As New List(Of FileInfo)()

        For Each extension As String In imageExtensions
            imageFiles.AddRange(directoryInfo.GetFiles($"*{extension}", SearchOption.TopDirectoryOnly))
        Next

        Return imageFiles
    End Function

    Private Async Function LoadImagesFromFilesAsync(imageFiles As List(Of FileInfo), token As CancellationToken, progress As IProgress(Of ImageItem)) As Task
        Dim imagesToLoad = imageFiles.Take(MaxImagesToDisplay).ToList()
        Dim loadedImagesCount As Integer = 0

        Invoke(Sub()
                   PrgSearchProgress.Maximum = imagesToLoad.Count
                   PrgSearchProgress.Value = 0
               End Sub)

        For Each imageFile As FileInfo In imagesToLoad
            If token.IsCancellationRequested Then Exit For
            Try
                Dim image As Image = Await Task.Run(Function() Image.FromFile(imageFile.FullName), token)

                Dim imageItem As New ImageItem() With {
                .Image = image,
                .Filename = imageFile.Name,
                .FilePath = imageFile.FullName,
                .Width = 100,
                .Height = 120
            }

                AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                progress.Report(imageItem)

                loadedImagesCount += 1
                Invoke(Sub()
                           PrgSearchProgress.Value = loadedImagesCount
                       End Sub)
            Catch ex As Exception
            End Try
        Next
    End Function

    Private Sub AddSearchToRecent(searchTerm As String)
        If recentSearches.Contains(searchTerm) Then
            Dim unused1 = recentSearches.Remove(searchTerm)
        End If
        recentSearches.Insert(0, searchTerm)

        If recentSearches.Count > MaxSearches Then
            recentSearches.RemoveAt(recentSearches.Count - 1)
        End If

        isUpdatingComboBox = True
        cboSearch.Items.Clear()
        For Each term As String In recentSearches
            Dim unused = cboSearch.Items.Add(term)
        Next
        isUpdatingComboBox = False
    End Sub

#End Region

#Region " Zoom "

    Private Sub PicCanvas_ZoomLevelChanged(zoomLevel As Single) Handles PicCanvas.ZoomLevelChanged
        lblZoom.Text = "Zoom: " & (zoomLevel * 100).ToString("0.##") & "%"
    End Sub

#End Region

#Region " ToolStrip "

    Private Sub BtnZoomToFit_Click(sender As Object, e As EventArgs) Handles BtnZoomToFit.Click
        PicCanvas.ZoomToFit()
    End Sub

    Private Sub CboDimensions_SelectedIndexChanged(sender As Object, e As EventArgs)
        Dim selectedText As String = CboDimensions.SelectedItem.ToString()
        Dim dimensions() As String = selectedText.Split("x"c)
        If dimensions.Length = 2 Then
            Dim size As Integer
            If Integer.TryParse(dimensions(0).Trim(), size) Then
                PicCanvas.GridSize = size
                PicCanvas.SnapAllTilesToGrid()
            End If
        End If
    End Sub

    Private Sub CboMapSize_SelectedIndexChanged(sender As Object, e As EventArgs) Handles CboMapSize.SelectedIndexChanged
        Dim selectedText As String = CboMapSize.SelectedItem.ToString()
        Dim dimensions() As String = selectedText.Split("x"c)
        If dimensions.Length = 2 Then
            Dim width As Integer
            Dim height As Integer
            If Integer.TryParse(dimensions(0).Trim(), width) AndAlso Integer.TryParse(dimensions(1).Trim(), height) Then
                PicCanvas.SetMapSize(width, height)
            End If
        End If
    End Sub

#End Region

#Region " Save and Load Functionality "

    Private Sub SaveCanvas(fileName As String)
        Dim canvasData As New CanvasData With {
        .GridSize = PicCanvas.GridSize,
        .MapSize = PicCanvas.MapSize,
        .MapTiles = New List(Of MapTileData)()
    }

        For Each tile As MapTile In PicCanvas.Tiles
            Dim tileData As New MapTileData With {
            .ImageFilePath = tile.ImageFilePath,
            .Position = tile.Position,
            .Size = tile.Size,
            .OriginalSize = tile.OriginalSize,
            .IsAnimated = tile.IsAnimated,
            .AnimationInterval = tile.AnimationInterval,
            .RotationAngle = tile.RotationAngle,
            .IsFlippedHorizontally = tile.IsFlippedHorizontally,
            .IsFlippedVertically = tile.IsFlippedVertically,
            .IsLocked = tile.IsLocked,
            .FrameWidth = tile.FrameWidth,
            .FrameHeight = tile.FrameHeight
        }

            If String.IsNullOrEmpty(tile.ImageFilePath) Then
                If tile.IsAnimated AndAlso tile.AnimationFrames IsNot Nothing Then
                    Dim frameDataList As New List(Of String)
                    For Each frame As Image In tile.AnimationFrames
                        frameDataList.Add(ImageToBase64(frame))
                    Next
                    tileData.ImageData = String.Join("|", frameDataList)
                ElseIf tile.Image IsNot Nothing Then
                    tileData.ImageData = ImageToBase64(tile.Image)
                End If
            End If

            canvasData.MapTiles.Add(tileData)
        Next

        Dim serializer As New XmlSerializer(GetType(CanvasData))

        Using writer As New StreamWriter(fileName)
            serializer.Serialize(writer, canvasData)
        End Using

        isModified = False
    End Sub

    Private Function ImageToBase64(image As Image) As String
        Using ms As New MemoryStream()
            image.Save(ms, ImageFormat.Png)
            Return Convert.ToBase64String(ms.ToArray())
        End Using
    End Function

    Private Sub LoadCanvas(fileName As String)
        Try
            Dim serializer As New XmlSerializer(GetType(CanvasData))
            Using reader As New StreamReader(fileName)
                Dim canvasData As CanvasData = CType(serializer.Deserialize(reader), CanvasData)

                PicCanvas.ClearTiles()

                PicCanvas.GridSize = canvasData.GridSize
                PicCanvas.SetMapSize(canvasData.MapSize.Width, canvasData.MapSize.Height)

                For Each tileData As MapTileData In canvasData.MapTiles
                    Dim mapTile As MapTile

                    If Not String.IsNullOrEmpty(tileData.ImageFilePath) AndAlso File.Exists(tileData.ImageFilePath) Then
                        If tileData.IsAnimated Then
                            Dim spriteSheetImage As Image = Image.FromFile(tileData.ImageFilePath)

                            Dim frameWidth As Integer = tileData.FrameWidth
                            Dim frameHeight As Integer = tileData.FrameHeight
                            If frameWidth <= 0 OrElse frameHeight <= 0 Then
                                Throw New ArgumentException("Frame width and height must be greater than zero.")
                            End If

                            Dim animationFrames As List(Of Image) = ExtractAnimationFrames(spriteSheetImage, frameWidth, frameHeight)

                            For Each frame As Image In animationFrames
                                Select Case tileData.RotationAngle Mod 360
                                    Case 90
                                        frame.RotateFlip(RotateFlipType.Rotate90FlipNone)
                                    Case 180
                                        frame.RotateFlip(RotateFlipType.Rotate180FlipNone)
                                    Case 270
                                        frame.RotateFlip(RotateFlipType.Rotate270FlipNone)
                                End Select

                                If tileData.IsFlippedHorizontally AndAlso tileData.IsFlippedVertically Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipXY)
                                ElseIf tileData.IsFlippedHorizontally Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipX)
                                ElseIf tileData.IsFlippedVertically Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipY)
                                End If
                            Next

                            spriteSheetImage.Dispose()

                            mapTile = New MapTile With {
                            .AnimationFrames = animationFrames,
                            .IsAnimated = True,
                            .ImageFilePath = tileData.ImageFilePath,
                            .Position = tileData.Position,
                            .Size = tileData.Size,
                            .OriginalSize = tileData.OriginalSize,
                            .AnimationInterval = tileData.AnimationInterval,
                            .RotationAngle = tileData.RotationAngle,
                            .IsFlippedHorizontally = tileData.IsFlippedHorizontally,
                            .IsFlippedVertically = tileData.IsFlippedVertically,
                            .IsLocked = tileData.IsLocked,
                            .FrameWidth = tileData.FrameWidth,
                            .FrameHeight = tileData.FrameHeight
                        }

                            PicCanvas.AddTile(mapTile)
                        Else
                            Dim image As Image = Image.FromFile(tileData.ImageFilePath)
                            Dim tileImage As New Bitmap(image, tileData.Size)
                            image.Dispose()

                            Select Case tileData.RotationAngle Mod 360
                                Case 90
                                    tileImage.RotateFlip(RotateFlipType.Rotate90FlipNone)
                                Case 180
                                    tileImage.RotateFlip(RotateFlipType.Rotate180FlipNone)
                                Case 270
                                    tileImage.RotateFlip(RotateFlipType.Rotate270FlipNone)
                            End Select

                            If tileData.IsFlippedHorizontally AndAlso tileData.IsFlippedVertically Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipXY)
                            ElseIf tileData.IsFlippedHorizontally Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipX)
                            ElseIf tileData.IsFlippedVertically Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipY)
                            End If

                            mapTile = New MapTile With {
                            .Image = tileImage,
                            .ImageFilePath = tileData.ImageFilePath,
                            .Position = tileData.Position,
                            .Size = tileData.Size,
                            .OriginalSize = tileData.OriginalSize,
                            .RotationAngle = tileData.RotationAngle,
                            .IsFlippedHorizontally = tileData.IsFlippedHorizontally,
                            .IsFlippedVertically = tileData.IsFlippedVertically,
                            .IsLocked = tileData.IsLocked
                        }

                            PicCanvas.AddTile(mapTile)
                        End If
                    ElseIf Not String.IsNullOrEmpty(tileData.ImageData) Then
                        If tileData.IsAnimated Then
                            Dim frameDataList As String() = tileData.ImageData.Split("|"c)
                            Dim animationFrames As New List(Of Image)
                            For Each frameData As String In frameDataList
                                Dim frame As Image = Base64ToImage(frameData)
                                Select Case tileData.RotationAngle Mod 360
                                    Case 90
                                        frame.RotateFlip(RotateFlipType.Rotate90FlipNone)
                                    Case 180
                                        frame.RotateFlip(RotateFlipType.Rotate180FlipNone)
                                    Case 270
                                        frame.RotateFlip(RotateFlipType.Rotate270FlipNone)
                                End Select

                                If tileData.IsFlippedHorizontally AndAlso tileData.IsFlippedVertically Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipXY)
                                ElseIf tileData.IsFlippedHorizontally Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipX)
                                ElseIf tileData.IsFlippedVertically Then
                                    frame.RotateFlip(RotateFlipType.RotateNoneFlipY)
                                End If

                                animationFrames.Add(frame)
                            Next

                            mapTile = New MapTile With {
                            .AnimationFrames = animationFrames,
                            .IsAnimated = True,
                            .ImageFilePath = tileData.ImageFilePath,
                            .Position = tileData.Position,
                            .Size = tileData.Size,
                            .OriginalSize = tileData.OriginalSize,
                            .AnimationInterval = tileData.AnimationInterval,
                            .RotationAngle = tileData.RotationAngle,
                            .IsFlippedHorizontally = tileData.IsFlippedHorizontally,
                            .IsFlippedVertically = tileData.IsFlippedVertically,
                            .IsLocked = tileData.IsLocked
                        }

                            PicCanvas.AddTile(mapTile)
                        Else
                            Dim tileImage As Image = Base64ToImage(tileData.ImageData)
                            If tileImage.Size <> tileData.Size Then
                                Dim resizedImage As New Bitmap(tileImage, tileData.Size)
                                tileImage.Dispose()
                                tileImage = resizedImage
                            End If

                            Select Case tileData.RotationAngle Mod 360
                                Case 90
                                    tileImage.RotateFlip(RotateFlipType.Rotate90FlipNone)
                                Case 180
                                    tileImage.RotateFlip(RotateFlipType.Rotate180FlipNone)
                                Case 270
                                    tileImage.RotateFlip(RotateFlipType.Rotate270FlipNone)
                            End Select

                            If tileData.IsFlippedHorizontally AndAlso tileData.IsFlippedVertically Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipXY)
                            ElseIf tileData.IsFlippedHorizontally Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipX)
                            ElseIf tileData.IsFlippedVertically Then
                                tileImage.RotateFlip(RotateFlipType.RotateNoneFlipY)
                            End If

                            mapTile = New MapTile With {
                            .Image = tileImage,
                            .ImageFilePath = tileData.ImageFilePath,
                            .Position = tileData.Position,
                            .Size = tileData.Size,
                            .OriginalSize = tileData.OriginalSize,
                            .RotationAngle = tileData.RotationAngle,
                            .IsFlippedHorizontally = tileData.IsFlippedHorizontally,
                            .IsFlippedVertically = tileData.IsFlippedVertically,
                            .IsLocked = tileData.IsLocked
                        }

                            PicCanvas.AddTile(mapTile)
                        End If
                    Else
                        ' Handle missing image data
                    End If
                Next
            End Using

            isModified = False
        Catch ex As Exception
            ' Handle exceptions
        End Try
    End Sub

    Private Function Base64ToImage(base64String As String) As Image
        Dim imageBytes() As Byte = Convert.FromBase64String(base64String)
        Using ms As New MemoryStream(imageBytes)
            Return Image.FromStream(ms)
        End Using
    End Function

    Private Sub MnuNew_Click(sender As Object, e As EventArgs) Handles MnuNew.Click
        If isModified Then
            Dim result = MessageBox.Show("Do you want to save changes?", "Save Changes", MessageBoxButtons.YesNoCancel)
            If result = DialogResult.Yes Then
                MnuSave_Click(sender, e)
            ElseIf result = DialogResult.Cancel Then
                Return
            End If
        End If

        PicCanvas.ClearTiles()
        currentFileName = ""
        isModified = False
    End Sub

    Private Sub MnuSave_Click(sender As Object, e As EventArgs) Handles MnuSave.Click
        If String.IsNullOrEmpty(currentFileName) Then
            MnuSaveAs_Click(sender, e)
        Else
            SaveCanvas(currentFileName)
        End If
    End Sub

    Private Sub MnuSaveAs_Click(sender As Object, e As EventArgs) Handles MnuSaveAs.Click
        Using sfd As New SaveFileDialog()
            sfd.Filter = "Map Files (*.map)|*.map|All Files (*.*)|*.*"
            If sfd.ShowDialog() = DialogResult.OK Then
                currentFileName = sfd.FileName
                SaveCanvas(currentFileName)
            End If
        End Using
    End Sub

    Private Sub MnuLoad_Click(sender As Object, e As EventArgs) Handles MnuLoad.Click
        If isModified Then
            Dim result = MessageBox.Show("Do you want to save changes?", "Save Changes", MessageBoxButtons.YesNoCancel)
            If result = DialogResult.Yes Then
                MnuSave_Click(sender, e)
            ElseIf result = DialogResult.Cancel Then
                Return
            End If
        End If

        Using ofd As New OpenFileDialog()
            ofd.Filter = "Map Files (*.map)|*.map|All Files (*.*)|*.*"
            If ofd.ShowDialog() = DialogResult.OK Then
                Try
                    currentFileName = ofd.FileName
                    LoadCanvas(currentFileName)
                Catch ex As Exception
                    ' Handle exceptions
                End Try
            End If
        End Using
    End Sub

    Private Sub MnuExit_Click(sender As Object, e As EventArgs) Handles MnuExit.Click
        If isModified Then
            Dim result = MessageBox.Show("Do you want to save changes?", "Save Changes", MessageBoxButtons.YesNoCancel)
            If result = DialogResult.Yes Then
                MnuSave_Click(sender, e)
            ElseIf result = DialogResult.Cancel Then
                Return
            End If
        End If
        Close()
    End Sub

#End Region

#Region " Zoom "

    Private Sub Mnu50_Click(sender As Object, e As EventArgs) Handles Mnu50.Click
        SetZoom(0.5F)
    End Sub

    Private Sub Mnu100_Click(sender As Object, e As EventArgs) Handles Mnu100.Click
        SetZoom(1.0F)
    End Sub

    Private Sub Mnu250_Click(sender As Object, e As EventArgs) Handles Mnu250.Click
        SetZoom(2.5F)
    End Sub

    Private Sub Mnu500_Click(sender As Object, e As EventArgs) Handles Mnu500.Click
        SetZoom(5.0F)
    End Sub

    Private Sub Mnu1000_Click(sender As Object, e As EventArgs) Handles Mnu1000.Click
        SetZoom(10.0F)
    End Sub

    Private Sub SetZoom(ByVal zoomLevel As Single)
        PicCanvas.ZoomLevel = zoomLevel
    End Sub

    Private Sub TrvFiles_NodeMouseClick(sender As Object, e As TreeNodeMouseClickEventArgs) Handles TrvFiles.NodeMouseClick
        If e.Button = MouseButtons.Right Then
            TrvFiles.SelectedNode = e.Node
        End If
    End Sub

    Private Sub MnuOpenInExplorer_Click(sender As Object, e As EventArgs) Handles MnuOpenInExplorer.Click
        Dim selectedNode As TreeNode = TrvFiles.SelectedNode
        If selectedNode IsNot Nothing AndAlso TypeOf selectedNode.Tag Is DirectoryInfo Then
            Dim directoryInfo As DirectoryInfo = CType(selectedNode.Tag, DirectoryInfo)
            If directoryInfo.Exists Then
                Try
                    Dim unused3 = Process.Start("explorer.exe", directoryInfo.FullName)
                Catch ex As Exception
                    Dim unused2 = MessageBox.Show("Failed to open folder: " & ex.Message)
                End Try
            Else
                Dim unused1 = MessageBox.Show("Directory does not exist.")
            End If
        Else
            Dim unused = MessageBox.Show("No directory selected.")
        End If
    End Sub

    Private Sub PicCanvas_MouseMove(sender As Object, e As MouseEventArgs) Handles PicCanvas.MouseMove
        Dim canvasPoint As Point = PicCanvas.ClientToCanvas(e.Location)
        STBMouseCoords.Text = $"X: {canvasPoint.X}, Y: {canvasPoint.Y}"
    End Sub

#End Region

#Region " Closing "

    Private Sub FrmWorld_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
        SaveTreeViewState()
        SaveRecentSearches()
    End Sub

    Private Sub SaveRecentSearches()
        Dim serializer As New XmlSerializer(GetType(List(Of String)))
        Using writer As New StreamWriter("RecentSearches.xml")
            serializer.Serialize(writer, recentSearches)
        End Using
    End Sub

    Private Sub SaveTreeViewState()
        Dim state As New TreeViewState()

        For Each node As TreeNode In TrvFiles.Nodes
            SaveExpandedNodes(node, state)
        Next

        If TrvFiles.SelectedNode IsNot Nothing Then
            state.SelectedNodePath = TrvFiles.SelectedNode.FullPath
        End If

        Dim serializer As New XmlSerializer(GetType(TreeViewState))
        Using writer As New StreamWriter("TreeViewState.xml")
            serializer.Serialize(writer, state)
        End Using
    End Sub

    Private Sub SaveExpandedNodes(node As TreeNode, state As TreeViewState)
        If node.IsExpanded Then
            state.ExpandedNodes.Add(node.FullPath)
        End If
        For Each childNode As TreeNode In node.Nodes
            SaveExpandedNodes(childNode, state)
        Next
    End Sub

#End Region

#Region " Undo Redo "

    Private Sub BtnUndo_Click(sender As Object, e As EventArgs) Handles BtnUndo.Click
        PicCanvas.Undo()
    End Sub

    Private Sub BtnRedo_Click(sender As Object, e As EventArgs) Handles BtnRedo.Click
        PicCanvas.Redo()
    End Sub
    Private Sub AddAction(action As MapAction)
        undoStack.Push(action)
        redoStack.Clear()
    End Sub

    Private Sub Undo()
        If undoStack.Count > 0 Then
            Dim action As MapAction = undoStack.Pop()
            action.Undo()
            redoStack.Push(action)
        End If
    End Sub

    Private Sub Redo()
        If redoStack.Count > 0 Then
            Dim action As MapAction = redoStack.Pop()
            action.Execute()
            undoStack.Push(action)
        End If
    End Sub

#End Region

#Region " Other "

    Private Sub PicCanvas_TileSelected(selectedTile As MapTile)
        STBSize.Text = If(selectedTile IsNot Nothing,
            $"Original Size {selectedTile.OriginalSize.Width}, {selectedTile.OriginalSize.Height}: Current Size {selectedTile.Size.Width}, {selectedTile.Size.Height}",
            "")
        UpdateSTBSize(selectedTile)
    End Sub

    Private Sub PicCanvas_TileSizeChanged(selectedTile As MapTile)
        UpdateSTBSize(selectedTile)
        AddAction(New MoveTileAction(selectedTile, PicCanvas, selectedTile.Position, selectedTile.Position))
    End Sub

    Private Sub UpdateSTBSize(selectedTile As MapTile)
        STBSize.Text = If(selectedTile IsNot Nothing,
            $"Original Size {selectedTile.OriginalSize.Width}, {selectedTile.OriginalSize.Height}: Current Size {selectedTile.Size.Width}, {selectedTile.Size.Height}",
            "")
    End Sub

    Public Function GetImagesDirectory() As String
        Dim startupPath As String = Application.StartupPath
        Dim dirInfo As New DirectoryInfo(startupPath)

        While dirInfo IsNot Nothing AndAlso dirInfo.Name <> "Bizarria New"
            dirInfo = dirInfo.Parent
        End While

        If dirInfo Is Nothing Then
            Throw New DirectoryNotFoundException("Could not find the 'Bizarria New' directory in the path hierarchy.")
        End If

        Dim imagesDir As String = Path.Combine(dirInfo.FullName, "Data", "Decor")

        If Not Directory.Exists(imagesDir) Then
            Throw New DirectoryNotFoundException($"The images directory does not exist: {imagesDir}")
        End If

        Return imagesDir
    End Function

#End Region

End Class


MnuLoad isn't working...  I added (and now removed) a load of debugging messages to see if the file was loading. And it is.. But nothing is being drawn. Can yuo see why?