Imports System.Xml.Serialization

<Serializable>
Public Class MapTile

    ' Existing properties
    <XmlIgnore>
    Public Property Image As Image ' For static tiles

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

    ' Variables for animation
    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

    ' Transformation properties
    Public Property RotationAngle As Integer = 0 ' In degrees

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

    ' Lock state
    Public Property IsLocked As Boolean = False

    <XmlIgnore>
    Public Property Selected As Boolean = False

    ' Property for group movement
    <XmlIgnore>
    Public Property InitialPosition As Point

    ' Resizing properties
    <XmlIgnore>
    Public Property IsResizing As Boolean = False

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

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

    ' New properties for frame dimensions
    Public Property FrameWidth As Integer

    Public Property FrameHeight As Integer

    ' Clone method
    Public Function Clone() As MapTile
        ' Clone the tile, including any images
        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,        ' Include frame dimensions
            .FrameHeight = FrameHeight
        }

        ' Clone the Image or AnimationFrames
        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

    ' Dispose method
    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

    ' GetCurrentFrame method
    Public Function GetCurrentFrame() As Image
        If IsAnimated AndAlso AnimationFrames IsNot Nothing AndAlso AnimationFrames.Count > 0 Then
            ' Update frame if enough time has passed
            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

' Updated MapTileData class
Public Class MapTileData
    Public Property ImageFilePath As String
    Public Property Position As Point
    Public Property Size As Size
    Public Property OriginalSize As Size

    ' Animation properties
    Public Property IsAnimated As Boolean

    Public Property AnimationInterval As Integer

    ' Transformation properties
    Public Property RotationAngle As Integer          ' In degrees (e.g., 0, 90, 180, 270)

    Public Property IsFlippedHorizontally As Boolean
    Public Property IsFlippedVertically As Boolean

    ' Lock state
    Public Property IsLocked As Boolean

    ' New properties to store frame dimensions
    Public Property FrameWidth As Integer

    Public Property FrameHeight As Integer

    ' New property to store image data for tiles without a file path
    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

Public Class ImageItem

    Public Sub New()
        InitializeComponent()

        ' Set PictureBox SizeMode to Normal to display images at their actual size
        PicImage.SizeMode = PictureBoxSizeMode.Zoom

        ' Adjust PicImage Dock to fill the available space
        PicImage.Dock = DockStyle.Fill

        ' Set PicImage size to match the image
        'PicImage.Height = Me.Height - LblFileName.Height

        ' Ensure that LblFileName is placed correctly
        ' LblFileName.Dock = DockStyle.Bottom
    End Sub

    Public Property BossIndex As Integer = -1 ' Default to -1 if not a boss
    Public Property IsSpriteSheet As Boolean = False
    Public Property FrameWidth As Integer
    Public Property FrameHeight As Integer

    Public Property Image As Image
        Get
            Return PicImage.Image
        End Get
        Set(value As Image)
            PicImage.Image = value
        End Set
    End Property

    ' Expose the Filename property
    Public Property Filename As String
        Get
            Return LblFileName.Text
        End Get
        Set(value As String)
            LblFileName.Text = value
        End Set
    End Property

    ' Expose the FilePath property
    Public Property FilePath As String

    ' Declare events for mouse actions (avoid naming conflicts)
    Public Event ImageItemMouseDownEvent(sender As Object, e As MouseEventArgs)

    Public Event ImageItemMouseMoveEvent(sender As Object, e As MouseEventArgs)

    Public Event ImageItemMouseUpEvent(sender As Object, e As MouseEventArgs)

    ' Event handlers for PicImage
    Private Sub PicImage_MouseDownHandler(sender As Object, e As MouseEventArgs) Handles PicImage.MouseDown
        RaiseEvent ImageItemMouseDownEvent(Me, e)
    End Sub

    Private Sub PicImage_MouseMoveHandler(sender As Object, e As MouseEventArgs) Handles PicImage.MouseMove
        RaiseEvent ImageItemMouseMoveEvent(Me, e)
    End Sub

    Private Sub PicImage_MouseUpHandler(sender As Object, e As MouseEventArgs) Handles PicImage.MouseUp
        RaiseEvent ImageItemMouseUpEvent(Me, e)
    End Sub

    ' Event handlers for LblFilename (if needed)
    Private Sub LblFilename_MouseDownHandler(sender As Object, e As MouseEventArgs) Handles LblFileName.MouseDown
        RaiseEvent ImageItemMouseDownEvent(Me, e)
    End Sub

    Private Sub LblFilename_MouseMoveHandler(sender As Object, e As MouseEventArgs) Handles LblFileName.MouseMove
        RaiseEvent ImageItemMouseMoveEvent(Me, e)
    End Sub

    Private Sub LblFilename_MouseUpHandler(sender As Object, e As MouseEventArgs) Handles LblFileName.MouseUp
        RaiseEvent ImageItemMouseUpEvent(Me, e)
    End Sub

    ' Optionally, handle events for the entire control
    Private Sub ImageItem_MouseDownHandler(sender As Object, e As MouseEventArgs) Handles MyBase.MouseDown
        RaiseEvent ImageItemMouseDownEvent(Me, e)
    End Sub

    Private Sub ImageItem_MouseMoveHandler(sender As Object, e As MouseEventArgs) Handles MyBase.MouseMove
        RaiseEvent ImageItemMouseMoveEvent(Me, e)
    End Sub

    Private Sub ImageItem_MouseUpHandler(sender As Object, e As MouseEventArgs) Handles MyBase.MouseUp
        RaiseEvent ImageItemMouseUpEvent(Me, e)
    End Sub

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 ' Variable to track the drag 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"}

#Region " Initiate "

    Private Sub FrmWorld_Load(sender As Object, e As EventArgs) Handles MyBase.Load

        PopulateTreeView(GetImagesDirectory())
        ' Ensure PicCanvas can accept drops (if not already set in MapCanvas class)
        PicCanvas.AllowDrop = True

        ' Add event handler for grid size selection
        AddHandler CboDimensions.SelectedIndexChanged, AddressOf CboDimensions_SelectedIndexChanged

        ' Set default grid size
        CboDimensions.SelectedItem = "32 x 32"

        ' Add "10000 x 10000" to the combo box if not already present
        Dim defaultMapSize As String = "15000 x 10000"

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

        ' Set default map size
        CboMapSize.SelectedItem = defaultMapSize

        ' Handle CanvasModified event
        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
            ' Populate the ComboBox
            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)

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

                ' Restore selected node
                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
            ' Clear previous images
            FLPImages.Controls.Clear()

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

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

            ' Identify folder type
            Dim isInBossesFolder As Boolean = IsInBossesDirectory(directoryInfo)
            Dim isInPeopleFolder As Boolean = IsInPeopleDirectory(directoryInfo)

            For Each imageFile As FileInfo In imageFiles
                Try
                    ' Load the sprite sheet asynchronously
                    Dim spriteSheetImage As Image = Await Task.Run(Function() Image.FromFile(imageFile.FullName))

                    ' Determine frame size
                    Dim frameWidth As Integer
                    Dim frameHeight As Integer

                    If isInBossesFolder Then
                        frameWidth = 96
                        frameHeight = 96

                        ' Extract the middle frame (index 1)
                        Dim firstFrame As Image = ExtractFirstFrame(spriteSheetImage, frameWidth, frameHeight, frameIndex:=1)

                        ' Dispose of the sprite sheet image
                        spriteSheetImage.Dispose()

                        ' Determine the display name
                        Dim displayName As String = imageFile.Name

                        ' Create the ImageItem on the UI thread
                        Invoke(Sub()
                                   Dim imageItem As New ImageItem() With {
                                   .Filename = displayName,
                                   .FilePath = imageFile.FullName,
                                   .Width = frameWidth + 10,
                                   .Height = frameHeight + 30, ' Adjust height to accommodate the label
                                   .Image = firstFrame,
                                   .IsSpriteSheet = True,
                                   .FrameWidth = frameWidth,
                                   .FrameHeight = frameHeight
                               }

                                   ' Add event handlers
                                   AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                                   AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                                   AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

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

                        ' Extract the first frame (index 0)
                        Dim firstFrame As Image = ExtractFirstFrame(spriteSheetImage, frameWidth, frameHeight)

                        ' Dispose of the sprite sheet image
                        spriteSheetImage.Dispose()

                        ' Determine the display name
                        Dim displayName As String = imageFile.Name

                        ' Create the ImageItem on the UI thread
                        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
                               }

                                   ' Add event handlers
                                   AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                                   AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                                   AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                                   ' Add to FLPImages
                                   FLPImages.Controls.Add(imageItem)
                               End Sub)
                    Else
                        ' Handle other images (if any)
                        frameWidth = spriteSheetImage.Width
                        frameHeight = spriteSheetImage.Height

                        ' Use the entire image
                        Dim firstFrame As Image = New Bitmap(spriteSheetImage)

                        ' Dispose of the sprite sheet image
                        spriteSheetImage.Dispose()

                        ' Determine the display name
                        Dim displayName As String = imageFile.Name

                        ' Create the ImageItem on the UI thread
                        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
                               }

                                   ' Add event handlers
                                   AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                                   AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                                   AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                                   ' Add to FLPImages
                                   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", "Bosses")
        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
        ' Calculate the number of frames in the sprite sheet
        Dim framesInRow As Integer = spriteSheet.Width \ frameWidth

        ' Ensure frameIndex is within bounds
        If frameIndex < 0 OrElse frameIndex >= framesInRow Then
            frameIndex = 0  ' Default to first frame
        End If

        ' Calculate the source rectangle
        Dim sourceRect As New Rectangle(frameIndex * frameWidth, 0, frameWidth, frameHeight)

        ' Create a bitmap to hold the frame
        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
        ' Calculate the number of frames in a row
        Dim framesInRow As Integer = spriteSheet.Width \ frameWidth

        ' Ensure frameIndex is within bounds
        If frameIndex < 0 OrElse frameIndex >= framesInRow Then
            frameIndex = 0  ' Default to first frame
        End If

        ' Calculate the source rectangle
        Dim sourceRect As New Rectangle(frameIndex * frameWidth, 0, frameWidth, frameHeight)

        ' Create a bitmap to hold the frame
        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 Function ExtractAnimationFrames(spriteSheet As Image, frameWidth As Integer, frameHeight As Integer, isBossImage As Boolean) As List(Of Image)
        Dim frames As New List(Of Image)

        Dim framesInRow As Integer = spriteSheet.Width \ frameWidth
        Dim numberOfRows As Integer = spriteSheet.Height \ frameHeight

        Dim rowsToExtract As Integer = If(isBossImage, 1, numberOfRows)

        For row As Integer = 0 To rowsToExtract - 1
            For col As Integer = 0 To framesInRow - 1
                Dim frameRect As New Rectangle(col * frameWidth, row * frameHeight, 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), frameRect, GraphicsUnit.Pixel)
                End Using
                frames.Add(frameImage)
            Next
        Next

        Return frames
    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 "

    ' MouseDown event handler for ImageItem in FLPImages
    Private Sub ImageItem_MouseDown(sender As Object, e As MouseEventArgs)
        If e.Button = MouseButtons.Left Then
            ' Remember the point where the mouse down occurred
            Dim dragSize As Size = SystemInformation.DragSize
            ' Create a rectangle representing the area the mouse can move before a drag starts
            dragBoxFromMouseDown = New Rectangle(New Point(e.X - (dragSize.Width \ 2),
                                                       e.Y - (dragSize.Height \ 2)),
                                             dragSize)
        Else
            ' Reset drag rectangle
            dragBoxFromMouseDown = Rectangle.Empty
        End If
    End Sub

    ' MouseMove event handler for ImageItem in FLPImages
    Private Sub ImageItem_MouseMove(sender As Object, e As MouseEventArgs)
        If (e.Button And MouseButtons.Left) = MouseButtons.Left Then
            ' Check if the mouse has moved outside the drag rectangle
            If dragBoxFromMouseDown <> Rectangle.Empty AndAlso Not dragBoxFromMouseDown.Contains(e.Location) Then
                Dim imageItem As ImageItem = CType(sender, ImageItem)
                ' Get the image from the ImageItem
                Dim image As Image = imageItem.Image

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

                        ' Start the drag-and-drop operation
                        Dim dataObj As New DataObject()
                        dataObj.SetData(GetType(Bitmap), image) ' Set the image data with the Bitmap type
                        Dim imageFilePath As String = imageItem.FilePath
                        dataObj.SetData("ImageFilePath", imageFilePath) ' Set the image file path
                        dataObj.SetData("IsSpriteSheet", imageItem.IsSpriteSheet) ' Indicate if it's a sprite sheet

                        ' Include FrameWidth and FrameHeight
                        dataObj.SetData("FrameWidth", imageItem.FrameWidth)
                        dataObj.SetData("FrameHeight", imageItem.FrameHeight)

                        ' Start the drag-and-drop operation
                        Dim unused2 = imageItem.DoDragDrop(dataObj, DragDropEffects.Copy)
                        ' Reset cursor after dragging
                        Cursor.Current = Cursors.Default

                        ' Dispose of the custom cursor to release resources
                        cursor.Dispose()
                    Catch ex As Exception
                        ' Handle exception by logging or showing a message
                        Dim unused1 = MessageBox.Show("Error creating cursor: " & ex.Message)
                        ' Prevent the drag operation
                        Return
                    End Try
                Else
                    ' Handle the case where the image is invalid
                    Dim unused = MessageBox.Show("Error: The image is invalid or has been disposed.")
                    ' Prevent the drag operation
                    Return
                End If
                ' Reset drag rectangle
                dragBoxFromMouseDown = Rectangle.Empty
            End If
        End If
    End Sub

    ' MouseUp event handler for ImageItem in FLPImages
    Private Sub ImageItem_MouseUp(sender As Object, e As MouseEventArgs)
        ' Reset drag rectangle
        dragBoxFromMouseDown = Rectangle.Empty
    End Sub

    ' Helper function to convert Image to Cursor
    Private Function CreateCursorFromImage(image As Image) As Cursor
        Try
            ' Check if the image is valid
            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

            ' Resize the image to a reasonable cursor size, e.g., 32x32
            Dim cursorSize As Integer = 32
            ' Create a new bitmap with 32bppArgb pixel format
            Using bitmap As New Bitmap(cursorSize, cursorSize, PixelFormat.Format32bppArgb)
                Using g As Graphics = Graphics.FromImage(bitmap)
                    ' Set interpolation mode
                    g.InterpolationMode = InterpolationMode.HighQualityBicubic
                    ' Draw the image scaled to the cursor size
                    g.DrawImage(image, New Rectangle(0, 0, cursorSize, cursorSize))
                End Using

                ' Now create a cursor from the bitmap
                Dim iconHandle As IntPtr = bitmap.GetHicon()

                ' Create a cursor from the icon handle
                Dim cursor As New Cursor(iconHandle)

                ' It's important to destroy the icon handle to prevent memory leaks
                Dim unused1 = DestroyIcon(iconHandle)

                Return cursor
            End Using
        Catch ex As Exception
            ' Handle exception by logging or showing a message
            Dim unused = MessageBox.Show("Error creating cursor: " & ex.Message)
            ' Return the default cursor as a fallback
            Return Cursors.Default
        End Try
    End Function

    ' PInvoke declaration to destroy the icon handle and prevent memory leaks
    <DllImport("user32.dll", CharSet:=CharSet.Auto)>
    Private Shared Function DestroyIcon(ByVal hIcon As IntPtr) As Boolean
    End Function

    ' Event handler for CanvasModified event
    Private Sub CanvasModified_Handler()
        isModified = True
    End Sub

#End Region

#Region " CTMTrvFIles "

    ' Event handler for MnuCollapse Click
    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

    ' Event handler for MnuExpand Click
    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

    ' Recursive method to expand all child nodes of a given node
    Private Sub ExpandAllNodes(node As TreeNode)
        node.Expand()
        For Each childNode As TreeNode In node.Nodes
            ExpandAllNodes(childNode)
        Next
    End Sub

    ' Recursive method to collapse all child nodes of a given node
    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
        ' Cancel any existing search
        If searchCancellationTokenSource IsNot Nothing AndAlso Not searchCancellationTokenSource.IsCancellationRequested Then
            searchCancellationTokenSource.Cancel()
        End If

        ' Initialize a new CancellationTokenSource
        searchCancellationTokenSource = New CancellationTokenSource()
        Dim token As CancellationToken = searchCancellationTokenSource.Token

        ' Clear previous search results
        FLPImages.Controls.Clear()

        ' Make the progress bar and cancel button visible
        PrgSearchProgress.Visible = True
        PrgSearchProgress.Style = ProgressBarStyle.Marquee ' Use marquee style

        Try
            ' Perform the search asynchronously
            Dim matchingFiles As New List(Of FileInfo)
            Await Task.Run(Sub()
                               ' Search logic here
                               SearchAllDirectories(searchTerm, matchingFiles, token)
                           End Sub)

            ' Update progress bar to determinate style
            PrgSearchProgress.Style = ProgressBarStyle.Continuous
            PrgSearchProgress.Minimum = 0
            ' Limit the maximum progress to the MaxImagesToDisplay
            PrgSearchProgress.Maximum = Math.Min(matchingFiles.Count, MaxImagesToDisplay)
            PrgSearchProgress.Value = 0

            ' Create a Progress handler
            Dim progressHandler As New Progress(Of ImageItem)(Sub(imageItem)
                                                                  ' This code runs on the UI thread
                                                                  FLPImages.Controls.Add(imageItem)
                                                              End Sub)

            ' Load matching images asynchronously, passing the progress handler
            Await LoadImagesFromFilesAsync(matchingFiles, token, progressHandler)

            If Not token.IsCancellationRequested Then
                AddSearchToRecent(searchTerm)
            End If
        Catch ex As OperationCanceledException
            ' Search was canceled
        Finally
            ' Hide the progress bar and cancel button
            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

    ' Recursive method to search directories
    Private Sub SearchDirectory(directory As DirectoryInfo, searchTerm As String, matchingFiles As List(Of FileInfo), token As CancellationToken)
        If token.IsCancellationRequested Then Return

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

            ' Search for matching files
            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

            ' Recursively search subdirectories
            For Each subDirectory As DirectoryInfo In directory.GetDirectories()
                If token.IsCancellationRequested Then Return
                SearchDirectory(subDirectory, searchTerm, matchingFiles, token)
            Next
        Catch ex As UnauthorizedAccessException
            ' Handle or log if necessary
        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)
            ' Get image files in this directory
            Dim imageFiles As List(Of FileInfo) = GetImageFilesFromDirectory(directoryInfo)

            ' Check if file name contains the search term
            For Each imageFile As FileInfo In imageFiles
                If imageFile.Name.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0 Then
                    matchingFiles.Add(imageFile)
                End If
            Next

            ' Now search child nodes
            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

    ' Load images from files asynchronously
    Private Async Function LoadImagesFromFilesAsync(imageFiles As List(Of FileInfo), token As CancellationToken, progress As IProgress(Of ImageItem)) As Task
        ' Limit the number of images to display
        Dim imagesToLoad = imageFiles.Take(MaxImagesToDisplay).ToList()
        Dim loadedImagesCount As Integer = 0

        ' Update progress bar maximum on the UI thread
        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
                ' Load the image in a background task
                Dim image As Image = Await Task.Run(Function() Image.FromFile(imageFile.FullName), token)

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

                ' Add event handlers
                AddHandler imageItem.ImageItemMouseDownEvent, AddressOf ImageItem_MouseDown
                AddHandler imageItem.ImageItemMouseMoveEvent, AddressOf ImageItem_MouseMove
                AddHandler imageItem.ImageItemMouseUpEvent, AddressOf ImageItem_MouseUp

                ' Report progress to update UI
                progress.Report(imageItem)

                ' Update the progress bar on the UI thread
                loadedImagesCount += 1
                Invoke(Sub()
                           PrgSearchProgress.Value = loadedImagesCount
                       End Sub)
            Catch ex As Exception
                ' Handle exceptions if necessary
            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

        ' Update the ComboBox cboSearch
        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
        ' Update the lblZoom label
        lblZoom.Text = "Zoom: " & (zoomLevel * 100).ToString("0.##") & "%"
    End Sub

#End Region

#Region " ToolStrip "

    ' Click event handler for the "Zoom to Fit" button
    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
                ' Update grid size in PicCanvas
                PicCanvas.GridSize = size
                ' Optionally, update existing tiles to snap to new grid
                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
                ' Update map size
                PicCanvas.SetMapSize(width, height)
            End If
        End If
    End Sub

#End Region

#Region " Save and Load Functionality "

    ' SaveCanvas method
    Private Sub SaveCanvas(fileName As String)
        ' Create CanvasData object
        Dim canvasData As New CanvasData With {
        .GridSize = PicCanvas.GridSize,
        .MapSize = PicCanvas.MapSize, ' Use the public MapSize property
        .MapTiles = New List(Of MapTileData)()
    }

        ' Map tiles
        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
        }

            ' Handle tiles without an ImageFilePath (e.g., grouped tiles)
            If String.IsNullOrEmpty(tile.ImageFilePath) Then
                If tile.IsAnimated AndAlso tile.AnimationFrames IsNot Nothing Then
                    ' Serialize animation frames
                    Dim frameDataList As New List(Of String)
                    For Each frame As Image In tile.AnimationFrames
                        frameDataList.Add(ImageToBase64(frame))
                    Next
                    ' Concatenate frame data with a separator
                    tileData.ImageData = String.Join("|", frameDataList)
                ElseIf tile.Image IsNot Nothing Then
                    ' Serialize the image
                    tileData.ImageData = ImageToBase64(tile.Image)
                End If
            End If

            canvasData.MapTiles.Add(tileData)
        Next

        ' Serialize to XML
        Dim serializer As New XmlSerializer(GetType(CanvasData))

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

        isModified = False
    End Sub

    ' Helper method to convert an Image to a Base64 string
    Private Function ImageToBase64(image As Image) As String
        Using ms As New MemoryStream()
            image.Save(ms, ImageFormat.Png) ' You can adjust the image format as needed
            Return Convert.ToBase64String(ms.ToArray())
        End Using
    End Function

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

            ' Clear existing tiles
            PicCanvas.ClearTiles()

            ' Set grid size and map size
            PicCanvas.GridSize = canvasData.GridSize
            PicCanvas.SetMapSize(canvasData.MapSize.Width, canvasData.MapSize.Height)

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

                If Not String.IsNullOrEmpty(tileData.ImageFilePath) AndAlso File.Exists(tileData.ImageFilePath) Then
                    ' Load image from file path
                    If tileData.IsAnimated Then
                        ' Handle animated tile
                        ' Load the sprite sheet
                        Dim spriteSheetImage As Image = Image.FromFile(tileData.ImageFilePath)

                        ' Extract the frames
                        Dim frameWidth As Integer = tileData.FrameWidth
                        Dim frameHeight As Integer = tileData.FrameHeight
                        Dim isBossImage As Boolean = frameWidth = 96 AndAlso frameHeight = 96

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

                        ' Apply transformations to animation frames
                        For Each frame As Image In animationFrames
                            ' Apply rotation first
                            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

                            ' Then apply flips
                            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

                        ' Dispose of the sprite sheet image
                        spriteSheetImage.Dispose()

                        ' Create the animated tile
                        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
                    }

                        ' Add the tile to the canvas
                        PicCanvas.AddTile(mapTile)
                    Else
                        ' Handle static tile
                        ' Load the image
                        Dim image As Image = Image.FromFile(tileData.ImageFilePath)
                        ' Resize the image to the saved size
                        Dim tileImage As New Bitmap(image, tileData.Size)
                        ' Dispose of the original image
                        image.Dispose()

                        ' Apply transformations
                        ' Apply rotation first
                        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

                        ' Then apply flips
                        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

                        ' Create the map tile
                        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
                    }

                        ' Add the tile to the canvas
                        PicCanvas.AddTile(mapTile)
                    End If
                ElseIf Not String.IsNullOrEmpty(tileData.ImageData) Then
                    ' Handle tiles with embedded image data (e.g., grouped tiles or tiles without a file path)
                    If tileData.IsAnimated Then
                        ' Handle animated tile with embedded frames
                        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)
                            ' Apply transformations to frame
                            ' Apply rotation first
                            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

                            ' Then apply flips
                            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

                        ' Create the animated tile
                        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
                    }

                        ' Add the tile to the canvas
                        PicCanvas.AddTile(mapTile)
                    Else
                        ' Handle static tile with embedded image
                        Dim tileImage As Image = Base64ToImage(tileData.ImageData)
                        ' Resize the image to the saved size
                        If tileImage.Size <> tileData.Size Then
                            Dim resizedImage As New Bitmap(tileImage, tileData.Size)
                            tileImage.Dispose()
                            tileImage = resizedImage
                        End If

                        ' Apply transformations
                        ' Apply rotation first
                        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

                        ' Then apply flips
                        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

                        ' Create the map tile
                        mapTile = New MapTile With {
                        .Image = tileImage,
                        .ImageFilePath = tileData.ImageFilePath, ' This may be empty
                        .Position = tileData.Position,
                        .Size = tileData.Size,
                        .OriginalSize = tileData.OriginalSize,
                        .RotationAngle = tileData.RotationAngle,
                        .IsFlippedHorizontally = tileData.IsFlippedHorizontally,
                        .IsFlippedVertically = tileData.IsFlippedVertically,
                        .IsLocked = tileData.IsLocked
                    }

                        ' Add the tile to the canvas
                        PicCanvas.AddTile(mapTile)
                    End If
                Else
                    ' Handle missing image data
                    Dim unused = MessageBox.Show("Image data not found for a tile.")
                End If
            Next
        End Using

        isModified = False
    End Sub

    ' Helper method to convert a Base64 string to an Image
    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
                currentFileName = ofd.FileName
                LoadCanvas(currentFileName)
            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)
        ' Set the zoom level on PicCanvas
        PicCanvas.ZoomLevel = zoomLevel
    End Sub

    Private Sub TrvFiles_NodeMouseClick(sender As Object, e As TreeNodeMouseClickEventArgs) Handles TrvFiles.NodeMouseClick
        If e.Button = MouseButtons.Right Then
            ' Select the node under the mouse cursor
            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
                    ' Open the folder in Windows Explorer
                    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
        ' Convert the mouse position to canvas coordinates
        Dim canvasPoint As Point = PicCanvas.ClientToCanvas(e.Location)
        ' Update the status strip label
        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
        ' Save TreeView state
        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()

        ' Collect expanded nodes
        For Each node As TreeNode In TrvFiles.Nodes
            SaveExpandedNodes(node, state)
        Next

        ' Save selected node
        If TrvFiles.SelectedNode IsNot Nothing Then
            state.SelectedNodePath = TrvFiles.SelectedNode.FullPath
        End If

        ' Serialize to XML
        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 " Other "

    Private Sub PicCanvas_TileSelected(selectedTile As MapTile)
        If selectedTile IsNot Nothing Then
            ' Update the STBSize label with the original and current size
            STBSize.Text = $"Original Size {selectedTile.OriginalSize.Width}, {selectedTile.OriginalSize.Height}: Current Size {selectedTile.Size.Width}, {selectedTile.Size.Height}"
        Else
            ' Clear the STBSize label
            STBSize.Text = ""
        End If
        UpdateSTBSize(selectedTile)
    End Sub

    Private Sub PicCanvas_TileSizeChanged(selectedTile As MapTile)
        ' Update the STBSize label with the new size
        UpdateSTBSize(selectedTile)
    End Sub

    Private Sub UpdateSTBSize(selectedTile As MapTile)
        If selectedTile IsNot Nothing Then
            ' Update the STBSize label with the original and current size
            STBSize.Text = $"Original Size {selectedTile.OriginalSize.Width}, {selectedTile.OriginalSize.Height}: Current Size {selectedTile.Size.Width}, {selectedTile.Size.Height}"
        Else
            ' Clear the STBSize label
            STBSize.Text = ""
        End If
    End Sub

    Public Function GetImagesDirectory() As String
        ' Get the startup path of the application
        Dim startupPath As String = Application.StartupPath
        Dim dirInfo As New DirectoryInfo(startupPath)

        ' Traverse up the directory tree until we find the 'Bizarria New' directory
        While dirInfo IsNot Nothing AndAlso dirInfo.Name <> "Bizarria New"
            dirInfo = dirInfo.Parent
        End While

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

        ' Combine the path to reach 'Data\Decor' under 'Bizarria New'
        Dim imagesDir As String = Path.Combine(dirInfo.FullName, "Data", "Decor")

        ' Ensure the directory exists
        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.Drawing.Drawing2D
Imports System.Drawing.Imaging

Public Class MapCanvas
    Inherits UserControl

    ' Constructor
    Public Sub New()
        InitializeComponent()
        InitializeControl()
    End Sub

#Region " Variables And Properties "

    Private ctrlClickTile As MapTile = Nothing

    Private Const DragThreshold As Integer = 4 ' pixels
    Private isCtrlClick As Boolean = False
    Private mouseDownLocation As Point

    Public Event TileSizeChanged(selectedTile As MapTile)

    Public Event TileSelected(selectedTile As MapTile)

    Private contextMenuTile As MapTile = Nothing
    Private animationTimer As System.Windows.Forms.Timer

    ' Event to notify when the zoom level changes
    Public Event ZoomLevelChanged(zoomLevel As Single)

    ' Variables for dragging and resizing
    Private selectedTile As MapTile = Nothing

    Private dragging As Boolean = False
    Private resizing As Boolean = False
    Private initialTilePosition As Point
    Private initialTileSize As Size
    Private initialMousePosition As Point

    ' Variables for selection rectangle
    Private isSelecting As Boolean = False

    Private selectionStartPoint As Point
    Private selectionEndPoint As Point
    Private selectionRectangle As Rectangle

    ' List to store selected tiles
    Private selectedTiles As New List(Of MapTile)

    ' Variables for group resizing
    Private initialSelectedTilesBounds As Rectangle

    Private initialSelectedTilesPositionsAndSizes As New List(Of Tuple(Of MapTile, Point, Size))

    ' Variables for right-click panning
    Private isPanning As Boolean = False

    Private panStartPoint As Point
    Private scrollStartPoint As Point
    Private hasPanned As Boolean = False

    ' List of all tiles on the canvas
    Private _tiles As New List(Of MapTile)

    ' Grid size (default is 32)
    Private _gridSize As Integer = 32

    ' Flag to enable or disable snapping
    Private snappingEnabled As Boolean = True

    ' Flag to show or hide the grid
    Private _showGrid As Boolean = True

    ' Map dimensions
    Private _mapWidth As Integer = 15000

    Private _mapHeight As Integer = 10000

    ' Zoom level (default is 1.0)
    Private _zoomLevel As Single = 1.0F

    ' Event to notify when the canvas is modified
    Public Event CanvasModified()

    ' Property to adjust grid size
    Public Property GridSize As Integer
        Get
            Return _gridSize
        End Get
        Set(value As Integer)
            If value > 0 Then
                _gridSize = value
                Invalidate()
            End If
        End Set
    End Property

    ' Property to show or hide the grid
    Public Property ShowGrid As Boolean
        Get
            Return _showGrid
        End Get
        Set(value As Boolean)
            _showGrid = value
            Invalidate()
        End Set
    End Property

    ' Property to access the tiles
    Public ReadOnly Property Tiles As List(Of MapTile)
        Get
            Return _tiles
        End Get
    End Property

    ' Property to adjust the zoom level
    Public Property ZoomLevel As Single
        Get
            Return _zoomLevel
        End Get
        Set(value As Single)
            ' Remove lower limit constraint or adjust it
            If value <= 0 Then ' Ensure zoom level is positive
                value = 0.01F ' Set to a very small positive value
            End If

            If _zoomLevel <> value Then
                Dim oldZoomLevel As Single = _zoomLevel
                _zoomLevel = value
                AdjustScrollPosition(oldZoomLevel, Nothing)
                UpdateScrollBars()
                Invalidate()
                ' Raise the ZoomLevelChanged event
                RaiseEvent ZoomLevelChanged(_zoomLevel)
            End If
        End Set
    End Property

    Public ReadOnly Property MapWidth As Integer
        Get
            Return _mapWidth
        End Get
    End Property

    Public ReadOnly Property MapHeight As Integer
        Get
            Return _mapHeight
        End Get
    End Property

    ' Add this property to the MapCanvas class
    Public ReadOnly Property MapSize As Size
        Get
            Return New Size(_mapWidth, _mapHeight)
        End Get
    End Property

#End Region

#Region " Initialization and Setup "

    Private Sub InitializeControl()
        ' Enable double buffering for smoother rendering
        DoubleBuffered = True

        ' Set control styles for optimized painting and user interaction
        SetStyle(ControlStyles.AllPaintingInWmPaint Or
             ControlStyles.OptimizedDoubleBuffer Or
             ControlStyles.ResizeRedraw Or
             ControlStyles.UserPaint Or
             ControlStyles.Selectable, True)

        ' Apply the styles to the control
        UpdateStyles()

        ' Enable AutoScroll to allow scrolling if the content is larger than the control
        AutoScroll = True

        ' Allow the control to receive focus
        TabStop = True

        ' Ensure no default context menu is set
        ContextMenuStrip = Nothing

        ' Initialize the animation timer
        animationTimer = New System.Windows.Forms.Timer With {
            .Interval = 50  ' Adjust as needed for smooth animation
            }
        AddHandler animationTimer.Tick, AddressOf AnimationTimer_Tick
        animationTimer.Start()
    End Sub

#End Region

#Region " Drawing Methods "

    Protected Overrides Sub OnPaint(e As PaintEventArgs)
        MyBase.OnPaint(e)

        Dim g As Graphics = e.Graphics

        ' Fill the background with a solid color
        g.Clear(Color.White) ' Choose any color you prefer

        ' Set graphics options to prevent gaps
        g.InterpolationMode = InterpolationMode.NearestNeighbor
        g.PixelOffsetMode = PixelOffsetMode.Half
        g.SmoothingMode = SmoothingMode.None

        ' Save the current state of the Graphics object
        Dim state As GraphicsState = g.Save()

        ' Apply transformations
        g.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y)
        g.ScaleTransform(ZoomLevel, ZoomLevel)

        ' Snap to the nearest whole pixel after scaling
        g.PixelOffsetMode = PixelOffsetMode.None

        ' Draw the grid and tiles
        DrawGrid(g)
        DrawTiles(g)

        ' Restore the graphics state to undo the transforms
        g.Restore(state)

        ' Draw selection rectangle and handles in client coordinates
        If isSelecting Then
            DrawSelectionRectangle(g)
        End If

        If selectedTiles.Count > 0 Then
            If selectedTiles.Count = 1 Then
                DrawResizeHandles(g, selectedTiles(0))
            Else
                DrawGroupResizeHandles(g)
            End If
        End If
    End Sub

    ' Draw the grid
    Private Sub DrawGrid(g As Graphics)
        If Not _showGrid Then Exit Sub

        ' Set up the pen for grid lines
        Using pen As New Pen(Color.LightGray)
            ' Adjust pen width based on zoom level to maintain consistent appearance
            pen.Width = 1 / ZoomLevel

            ' Determine the visible area in canvas coordinates
            Dim visibleRect As Rectangle = GetVisibleCanvasRectangle()

            ' Adjust starting positions to align with the grid
            Dim startX As Integer = visibleRect.Left - (visibleRect.Left Mod _gridSize)
            Dim startY As Integer = visibleRect.Top - (visibleRect.Top Mod _gridSize)

            ' Draw vertical grid lines
            For x As Integer = startX To visibleRect.Right Step _gridSize
                g.DrawLine(pen, x, visibleRect.Top, x, visibleRect.Bottom)
            Next

            ' Draw horizontal grid lines
            For y As Integer = startY To visibleRect.Bottom Step _gridSize
                g.DrawLine(pen, visibleRect.Left, y, visibleRect.Right, y)
            Next
        End Using

        ' Draw the border around the grid
        DrawGridBorder(g)
    End Sub

    ' Draw a thick black border around the grid edges
    Private Sub DrawGridBorder(g As Graphics)
        ' Create a rectangle representing the grid area
        Dim gridRect As New Rectangle(0, 0, _mapWidth, _mapHeight)

        ' Draw the border
        Using pen As New Pen(Color.Black)
            ' Adjust pen width based on zoom level
            pen.Width = 4 / ZoomLevel
            g.DrawRectangle(pen, gridRect)
        End Using
    End Sub

    ' Draw the tiles
    Private Sub DrawTiles(g As Graphics)
        For Each tile In _tiles
            ' Get the current frame to draw
            Dim tileImage As Image = tile.GetCurrentFrame()

            ' Ensure integer positions and sizes
            Dim tileRect As New Rectangle(Math.Round(tile.Position.X),
                                      Math.Round(tile.Position.Y),
                                      Math.Round(tile.Size.Width),
                                      Math.Round(tile.Size.Height))

            ' Draw tile
            g.DrawImage(tileImage, tileRect)

            ' Draw selection rectangle if selected
            If tile.Selected Then
                Using pen As New Pen(Color.Blue)
                    pen.DashStyle = DashStyle.Dash
                    g.DrawRectangle(pen, tileRect)
                End Using
            End If
        Next
    End Sub

    ' Draw selection rectangle
    Private Sub DrawSelectionRectangle(g As Graphics)
        Using selectionPen As New Pen(Color.Red)
            selectionPen.DashStyle = Drawing2D.DashStyle.Dash
            Dim rect As Rectangle = GetSelectionRectangle()
            Dim screenRect As Rectangle = CanvasToClientRect(rect)
            g.DrawRectangle(selectionPen, screenRect)
        End Using
    End Sub

    ' Draw resize handles for a single tile
    Private Sub DrawResizeHandles(g As Graphics, tile As MapTile)
        Dim handleSize As Integer = 6
        Dim tileRect As New Rectangle(tile.Position, tile.Size)
        Dim screenRect As Rectangle = CanvasToClientRect(tileRect)

        Using brush As New SolidBrush(Color.White)
            Using pen As New Pen(Color.Black)
                ' Top-left corner
                DrawHandle(g, brush, pen, screenRect.Left - (handleSize \ 2), screenRect.Top - (handleSize \ 2), handleSize)
                ' Top-right corner
                DrawHandle(g, brush, pen, screenRect.Right - (handleSize \ 2), screenRect.Top - (handleSize \ 2), handleSize)
                ' Bottom-left corner
                DrawHandle(g, brush, pen, screenRect.Left - (handleSize \ 2), screenRect.Bottom - (handleSize \ 2), handleSize)
                ' Bottom-right corner
                DrawHandle(g, brush, pen, screenRect.Right - (handleSize \ 2), screenRect.Bottom - (handleSize \ 2), handleSize)
            End Using
        End Using
    End Sub

    ' Draw resize handles for group of tiles
    Private Sub DrawGroupResizeHandles(g As Graphics)
        Dim handleSize As Integer = 6
        Dim bounds = GetSelectedTilesBounds()
        Dim screenRect As Rectangle = CanvasToClientRect(bounds)

        Using brush As New SolidBrush(Color.White)
            Using pen As New Pen(Color.Black)
                ' Top-left corner
                DrawHandle(g, brush, pen, screenRect.Left - (handleSize \ 2), screenRect.Top - (handleSize \ 2), handleSize)
                ' Top-right corner
                DrawHandle(g, brush, pen, screenRect.Right - (handleSize \ 2), screenRect.Top - (handleSize \ 2), handleSize)
                ' Bottom-left corner
                DrawHandle(g, brush, pen, screenRect.Left - (handleSize \ 2), screenRect.Bottom - (handleSize \ 2), handleSize)
                ' Bottom-right corner
                DrawHandle(g, brush, pen, screenRect.Right - (handleSize \ 2), screenRect.Bottom - (handleSize \ 2), handleSize)
            End Using
        End Using
    End Sub

    ' Helper method to draw a resize handle
    Private Sub DrawHandle(g As Graphics, brush As Brush, pen As Pen, x As Integer, y As Integer, size As Integer)
        g.FillRectangle(brush, x, y, size, size)
        g.DrawRectangle(pen, x, y, size, size)
    End Sub

#End Region

#Region " Mouse Event Handlers "

    Private Sub HandleLeftMouseDown(e As MouseEventArgs, currentPoint As Point, clickedTile As MapTile)
        If clickedTile IsNot Nothing AndAlso Not clickedTile.IsLocked Then
            ' First, check if over resize handles of the tile
            Dim direction As MapTile.ResizeDirections = GetResizeDirection(clickedTile, currentPoint)
            If direction <> MapTile.ResizeDirections.None Then
                ' Start resizing the tile
                StartSingleTileResizing(clickedTile, currentPoint, direction)
            Else
                If selectedTiles.Contains(clickedTile) Then
                    ' The clicked tile is among the selected tiles
                    ' Start dragging the selected tiles
                    For Each tile In selectedTiles
                        tile.InitialPosition = tile.Position
                    Next
                    initialMousePosition = currentPoint
                    dragging = True
                    Refresh()
                Else
                    ' The clicked tile is not among the selected tiles
                    DeselectAllTiles()
                    selectedTile = clickedTile
                    selectedTile.Selected = True
                    selectedTiles.Clear()
                    selectedTiles.Add(selectedTile)

                    ' Start dragging the clicked tile
                    selectedTile.InitialPosition = selectedTile.Position
                    initialMousePosition = currentPoint
                    dragging = True
                    Refresh()
                End If
            End If
        Else
            ' No tile clicked or tile is locked - start selection rectangle
            StartSelection(currentPoint)
        End If
    End Sub

    ' Handle right mouse button up
    Private Sub HandleRightMouseUp(e As MouseEventArgs)
        ' Adjust current point for scrolling and zoom
        Dim currentPoint As Point = ClientToCanvas(e.Location)
        ' Include locked tiles when detecting right-clicks
        contextMenuTile = GetTileAtPoint(currentPoint, includeLockedTiles:=True)

        If contextMenuTile IsNot Nothing Then
            ' If the tile is not already selected, select it
            If Not contextMenuTile.Selected Then
                ' Deselect all other tiles
                DeselectAllTiles()
                ' Select the context menu tile
                contextMenuTile.Selected = True
                selectedTiles.Clear()
                selectedTiles.Add(contextMenuTile)
                selectedTile = contextMenuTile

                ' Raise the TileSelected event
                RaiseEvent TileSelected(selectedTile)
            End If

            ' Show the context menu at the mouse location
            CTMMenu.Show(Me, e.Location)
        Else
            ' Deselect all tiles
            DeselectAllTiles()
        End If
        Refresh()
    End Sub

    ' Handle dragging
    Private Sub HandleDragging(currentPoint As Point)
        Dim deltaX As Integer = currentPoint.X - initialMousePosition.X
        Dim deltaY As Integer = currentPoint.Y - initialMousePosition.Y

        ' Move all selected tiles
        For Each tile In selectedTiles
            Dim initialPos As Point = tile.InitialPosition
            Dim newPos As New Point(initialPos.X + deltaX, initialPos.Y + deltaY)

            ' Apply snapping if enabled and Shift is NOT pressed
            If snappingEnabled AndAlso Not IsShiftKeyPressed Then
                newPos.X = CInt(Math.Round(newPos.X / _gridSize)) * _gridSize
                newPos.Y = CInt(Math.Round(newPos.Y / _gridSize)) * _gridSize
            End If

            ' Clamp the position to be within the map boundaries
            newPos.X = Math.Max(0, Math.Min(newPos.X, _mapWidth - tile.Size.Width))
            newPos.Y = Math.Max(0, Math.Min(newPos.Y, _mapHeight - tile.Size.Height))

            tile.Position = newPos
        Next

        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    ' Handle resizing
    Private Sub HandleResizing(currentPoint As Point)
        If selectedTiles.Count = 1 Then
            ' Single tile resizing
            ResizeSingleTile(currentPoint)
        Else
            ' Group resizing
            ResizeGroupTiles(currentPoint)
        End If
    End Sub

    Private Sub UpdateCursor(currentPoint As Point)
        ' Convert the mouse point to canvas coordinates
        Dim canvasPoint As Point = ClientToCanvas(currentPoint)

        Dim cursorSet As Boolean = False

        ' Check if over group resize handles
        If selectedTiles.Count > 1 Then
            Dim groupDirection As MapTile.ResizeDirections = GetGroupResizeDirection(canvasPoint)
            If groupDirection <> MapTile.ResizeDirections.None Then
                Cursor = GetCursorForDirection(groupDirection)
                cursorSet = True
            End If
        End If

        ' Check if over resize handles of any tile
        If Not cursorSet Then
            For Each tile In _tiles
                Dim direction As MapTile.ResizeDirections = GetResizeDirection(tile, canvasPoint)
                If direction <> MapTile.ResizeDirections.None Then
                    Cursor = GetCursorForDirection(direction)
                    cursorSet = True
                    Exit For
                End If
            Next
        End If

        If Not cursorSet Then
            Cursor = Cursors.Default
        End If
    End Sub

    ' Handle mouse events for selecting and moving tiles
    Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
        MyBase.OnMouseDown(e)

        If e.Button = MouseButtons.Left Then
            Dim currentPoint As Point = ClientToCanvas(e.Location)
            Dim clickedTile As MapTile = GetTileAtPoint(currentPoint)

            If IsCtrlKeyPressed Then
                isCtrlClick = True
                mouseDownLocation = e.Location
                ctrlClickTile = clickedTile

                If clickedTile IsNot Nothing Then
                    ' Handle multi-selection
                    If selectedTiles.Contains(clickedTile) Then
                        ' If tile is already selected, remove it from selection
                        clickedTile.Selected = False
                        Dim unused = selectedTiles.Remove(clickedTile)
                    Else
                        ' Add tile to selection
                        clickedTile.Selected = True
                        selectedTiles.Add(clickedTile)
                    End If

                    Refresh()
                End If
            Else
                ctrlClickTile = Nothing
                HandleLeftMouseDown(e, currentPoint, clickedTile)
                isCtrlClick = False
            End If
        ElseIf e.Button = MouseButtons.Right Then
            ' Handle right-click panning
            Dim currentPoint As Point = e.Location
            isPanning = True
            panStartPoint = currentPoint
            scrollStartPoint = New Point(AutoScrollPosition.X, AutoScrollPosition.Y)
            hasPanned = False
            Cursor = Cursors.Hand
        End If
    End Sub

    Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
        MyBase.OnMouseMove(e)

        Dim currentPoint As Point = e.Location

        If resizing Then
            Dim canvasPoint As Point = ClientToCanvas(currentPoint)
            HandleResizing(canvasPoint)
        ElseIf dragging AndAlso selectedTiles.Count > 0 Then
            Dim canvasPoint As Point = ClientToCanvas(currentPoint)
            HandleDragging(canvasPoint)
        ElseIf isSelecting Then
            Dim canvasPoint As Point = ClientToCanvas(currentPoint)
            selectionEndPoint = canvasPoint
            Refresh()
        ElseIf isPanning AndAlso e.Button = MouseButtons.Right Then
            HandlePanning(currentPoint)
        ElseIf isCtrlClick Then
            ' Check if the mouse has moved beyond the drag threshold
            If Math.Abs(e.Location.X - mouseDownLocation.X) > DragThreshold OrElse Math.Abs(e.Location.Y - mouseDownLocation.Y) > DragThreshold Then
                ' Include the clicked tile in the selection if not already selected
                If ctrlClickTile IsNot Nothing AndAlso Not selectedTiles.Contains(ctrlClickTile) Then
                    ctrlClickTile.Selected = True
                    selectedTiles.Add(ctrlClickTile)
                End If

                ' Start duplication
                DuplicateSelectedTiles(ClientToCanvas(e.Location))
                dragging = True
                isCtrlClick = False
                ctrlClickTile = Nothing
            End If
        Else
            UpdateCursor(currentPoint)
        End If
    End Sub

    Protected Overrides Sub OnMouseUp(e As MouseEventArgs)
        If e.Button <> MouseButtons.Right Then
            MyBase.OnMouseUp(e)
        End If

        ' Handle selection rectangle completion
        If isSelecting Then
            isSelecting = False
            selectionRectangle = GetSelectionRectangle()
            SelectTilesInRectangle(selectionRectangle)
            Refresh()
        End If

        ' Stop dragging and resizing
        dragging = False
        resizing = False
        If selectedTile IsNot Nothing Then
            selectedTile.IsResizing = False
            selectedTile.CurrentResizeDirection = MapTile.ResizeDirections.None
        End If
        For Each tile In selectedTiles
            tile.CurrentResizeDirection = MapTile.ResizeDirections.None
        Next

        If e.Button = MouseButtons.Left Then
            If isCtrlClick Then
                ' Mouse was clicked without dragging while CTRL was pressed
                isCtrlClick = False
                ctrlClickTile = Nothing
            End If
        End If

        If e.Button = MouseButtons.Right Then
            If isPanning Then
                isPanning = False
                Cursor = Cursors.Default
                If hasPanned Then
                    hasPanned = False
                Else
                    HandleRightMouseUp(e)
                End If
            Else
                HandleRightMouseUp(e)
            End If
        End If
    End Sub

    ' Enable dropping
    Protected Overrides Sub OnCreateControl()
        MyBase.OnCreateControl()
        AllowDrop = True
    End Sub

    ' Handle drag enter event
    Protected Overrides Sub OnDragEnter(drgevent As DragEventArgs)
        MyBase.OnDragEnter(drgevent)
        If drgevent.Data.GetDataPresent(GetType(Bitmap)) Then
            drgevent.Effect = DragDropEffects.Copy
        ElseIf drgevent.Data.GetDataPresent(DataFormats.FileDrop) Then
            drgevent.Effect = DragDropEffects.Copy
        Else
            drgevent.Effect = DragDropEffects.None
        End If
    End Sub

    ' Handle drag drop event
    Protected Overrides Sub OnDragDrop(drgevent As DragEventArgs)
        MyBase.OnDragDrop(drgevent)
        If drgevent.Data.GetDataPresent(GetType(Bitmap)) Then
            ' Retrieve the image
            Dim image As Image = CType(drgevent.Data.GetData(GetType(Bitmap)), Image)
            ' Retrieve the image file path if available
            Dim imageFilePath As String = Nothing
            If drgevent.Data.GetDataPresent("ImageFilePath") Then
                imageFilePath = CType(drgevent.Data.GetData("ImageFilePath"), String)
            End If
            ' Handle the dropped image
            HandleImageDrop(image, imageFilePath, drgevent)
        ElseIf drgevent.Data.GetDataPresent(DataFormats.FileDrop) Then
            ' Handle file drops (e.g., dragging an image from Explorer)
            Dim files() As String = CType(drgevent.Data.GetData(DataFormats.FileDrop), String())
            For Each file In files
                If IO.File.Exists(file) Then
                    Try
                        Dim image As Image = Image.FromFile(file)
                        HandleImageDrop(image, file, drgevent)
                    Catch ex As Exception
                        ' Handle exception if file is not an image
                    End Try
                End If
            Next
        End If
    End Sub

    Protected Overrides Sub OnMouseEnter(e As EventArgs)
        MyBase.OnMouseEnter(e)
        ' Set focus only if the control does not already have it
        If Not Focused Then
            Dim unused = Focus()
        End If
    End Sub

    Protected Overrides Sub OnMouseWheel(e As MouseEventArgs)
        ' Ensure the control has focus to receive keyboard and mouse wheel events
        Dim unused = Focus()

        ' Capture the mouse position
        Dim mousePosition As Point = e.Location

        If e.Delta > 0 Then
            ZoomIn(mousePosition)
        ElseIf e.Delta < 0 Then
            ZoomOut(mousePosition)
        End If
    End Sub

#End Region

#Region " Helper Methods "

    Private Sub HandleImageDrop(image As Image, imageFilePath As String, drgevent As DragEventArgs)
        ' Check if the dragged item is a sprite sheet
        Dim isSpriteSheet As Boolean = False
        If drgevent.Data.GetDataPresent("IsSpriteSheet") Then
            isSpriteSheet = CBool(drgevent.Data.GetData("IsSpriteSheet"))
        End If

        ' Get frameWidth and frameHeight if available
        Dim frameWidth As Integer = 0
        Dim frameHeight As Integer = 0
        If drgevent.Data.GetDataPresent("FrameWidth") Then
            frameWidth = CInt(drgevent.Data.GetData("FrameWidth"))
        End If
        If drgevent.Data.GetDataPresent("FrameHeight") Then
            frameHeight = CInt(drgevent.Data.GetData("FrameHeight"))
        End If

        ' Determine if it's a boss image
        Dim isBossImage As Boolean = frameWidth = 96 AndAlso frameHeight = 96

        If isSpriteSheet AndAlso isBossImage Then
            ' Handle boss sprite sheet
            ' Load the sprite sheet from the image
            Dim spriteSheetImage As Image = image

            ' Extract the frames for the boss image
            Dim animationFrames As List(Of Image) = ExtractAnimationFrames(spriteSheetImage, frameWidth, frameHeight, True) ' True indicates it's a boss image

            ' Dispose of the sprite sheet image
            spriteSheetImage.Dispose()

            ' Use the frame size
            Dim frameSize As New Size(frameWidth, frameHeight)

            ' Get the drop coordinates
            Dim point As Point = PointToClient(New Point(drgevent.X, drgevent.Y))
            point = ClientToCanvas(point)

            ' Apply snapping if enabled and Shift is NOT pressed
            If snappingEnabled AndAlso Not IsShiftKeyPressed Then
                point.X = CInt(Math.Round(point.X / _gridSize)) * _gridSize
                point.Y = CInt(Math.Round(point.Y / _gridSize)) * _gridSize
            End If

            ' Clamp the position within the map boundaries
            point.X = Math.Max(0, Math.Min(point.X, _mapWidth - frameSize.Width))
            point.Y = Math.Max(0, Math.Min(point.Y, _mapHeight - frameSize.Height))

            ' Create a new animated tile
            Dim newTile As New MapTile With {
        .AnimationFrames = animationFrames,
        .IsAnimated = True,
        .ImageFilePath = imageFilePath,
        .Position = point,
        .Size = frameSize,
        .OriginalSize = frameSize,
        .FrameWidth = frameWidth,
        .FrameHeight = frameHeight
    }

            AddTile(newTile)

        ElseIf isSpriteSheet Then
            ' Handle general sprite sheet (e.g., from "People" folder)
            Dim spriteSheetImage As Image = image

            ' Default frame size if not provided
            If frameWidth = 0 Then frameWidth = 32
            If frameHeight = 0 Then frameHeight = 32

            ' Extract the frames
            Dim animationFrames As List(Of Image) = ExtractAnimationFrames(spriteSheetImage, frameWidth, frameHeight, False)

            ' Dispose of the sprite sheet image
            spriteSheetImage.Dispose()

            ' Double the frame size
            Dim scaledFrameWidth As Integer = frameWidth * 2
            Dim scaledFrameHeight As Integer = frameHeight * 2

            ' Resize each frame
            For i As Integer = 0 To animationFrames.Count - 1
                Dim resizedFrame As New Bitmap(scaledFrameWidth, scaledFrameHeight)
                Using g As Graphics = Graphics.FromImage(resizedFrame)
                    g.InterpolationMode = InterpolationMode.NearestNeighbor
                    g.PixelOffsetMode = PixelOffsetMode.HighQuality
                    g.SmoothingMode = SmoothingMode.None
                    g.DrawImage(animationFrames(i), New Rectangle(0, 0, scaledFrameWidth, scaledFrameHeight))
                End Using
                animationFrames(i).Dispose()
                animationFrames(i) = resizedFrame
            Next

            ' Frame size after resizing
            Dim frameSize As New Size(scaledFrameWidth, scaledFrameHeight)

            ' Get the drop coordinates
            Dim point As Point = PointToClient(New Point(drgevent.X, drgevent.Y))
            point = ClientToCanvas(point)

            ' Apply snapping if enabled and Shift is NOT pressed
            If snappingEnabled AndAlso Not IsShiftKeyPressed Then
                point.X = CInt(Math.Round(point.X / _gridSize)) * _gridSize
                point.Y = CInt(Math.Round(point.Y / _gridSize)) * _gridSize
            End If

            ' Clamp the position within the map boundaries
            point.X = Math.Max(0, Math.Min(point.X, _mapWidth - frameSize.Width))
            point.Y = Math.Max(0, Math.Min(point.Y, _mapHeight - frameSize.Height))

            ' Create a new animated tile
            Dim newTile As New MapTile With {
            .AnimationFrames = animationFrames,
            .IsAnimated = True,
            .AnimationInterval = 200,  ' Set desired frame interval
            .ImageFilePath = imageFilePath,
            .Position = point,
            .Size = frameSize,
            .OriginalSize = frameSize,
            .FrameWidth = scaledFrameWidth,
            .FrameHeight = scaledFrameHeight
        }

            AddTile(newTile)
        Else
            ' Handle non-sprite sheet images (static images)
            ' Clone the image to avoid shared references
            Dim imageClone As Image = CType(image.Clone(), Image)

            ' Apply scaling to reduce size to 50%
            Dim originalSize As Size = imageClone.Size

            ' Calculate the new size by scaling to 50%
            Dim scaledWidth As Double = originalSize.Width * 0.5
            Dim scaledHeight As Double = originalSize.Height * 0.5

            ' Adjust the new size to be exact multiples of the grid size
            Dim newWidth As Integer = CInt(Math.Ceiling(scaledWidth / _gridSize)) * _gridSize
            Dim newHeight As Integer = CInt(Math.Ceiling(scaledHeight / _gridSize)) * _gridSize

            ' Clamp the size to not exceed map boundaries
            newWidth = Math.Min(newWidth, _mapWidth)
            newHeight = Math.Min(newHeight, _mapHeight)

            ' Create a resized image that fits the grid exactly
            Dim resizedImage As New Bitmap(newWidth, newHeight, PixelFormat.Format32bppArgb)
            Using g As Graphics = Graphics.FromImage(resizedImage)
                ' Set graphics options
                g.InterpolationMode = InterpolationMode.NearestNeighbor
                g.PixelOffsetMode = PixelOffsetMode.Half
                g.SmoothingMode = SmoothingMode.None

                g.Clear(Color.Transparent)
                g.DrawImage(imageClone, New Rectangle(0, 0, newWidth, newHeight))
            End Using

            ' Dispose of the cloned image as it's no longer needed
            imageClone.Dispose()

            ' Get the drop coordinates
            Dim point As Point = PointToClient(New Point(drgevent.X, drgevent.Y))
            point = ClientToCanvas(point)

            ' Apply snapping if enabled and Shift is NOT pressed
            If snappingEnabled AndAlso Not IsShiftKeyPressed Then
                point.X = CInt(Math.Round(point.X / _gridSize)) * _gridSize
                point.Y = CInt(Math.Round(point.Y / _gridSize)) * _gridSize
            End If

            ' Clamp the position to be within the map boundaries
            point.X = Math.Max(0, Math.Min(point.X, _mapWidth - newWidth))
            point.Y = Math.Max(0, Math.Min(point.Y, _mapHeight - newHeight))

            ' Create a new tile and add it to the canvas
            Dim newTile As New MapTile With {
        .Image = resizedImage,
        .ImageFilePath = imageFilePath,
        .Position = point,
        .Size = New Size(newWidth, newHeight),
        .OriginalSize = image.Size
    }
            AddTile(newTile)
        End If
    End Sub

    Private Function ExtractAnimationFrames(spriteSheet As Image, frameWidth As Integer, frameHeight As Integer, isBossImage As Boolean) As List(Of Image)
        Dim frames As New List(Of Image)

        Dim framesInRow As Integer = spriteSheet.Width \ frameWidth
        Dim numberOfRows As Integer = spriteSheet.Height \ frameHeight

        Dim rowsToExtract As Integer = If(isBossImage, 1, numberOfRows)

        For row As Integer = 0 To rowsToExtract - 1
            For col As Integer = 0 To framesInRow - 1
                Dim frameRect As New Rectangle(col * frameWidth, row * frameHeight, 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),
                            frameRect,
                            GraphicsUnit.Pixel)
                End Using
                frames.Add(frameImage)
            Next
        Next

        Return frames
    End Function

    Private Function ExtractBossAnimationFrames(spriteSheet As Image, bossIndex As Integer) As List(Of Image)
        Dim frames As New List(Of Image)

        ' Each frame is 96x96 pixels
        Dim frameWidth As Integer = 96
        Dim frameHeight As Integer = 96

        ' Number of frames per boss
        Dim framesPerBoss As Integer = 3

        ' Starting position in the sprite sheet
        Dim row As Integer = bossIndex ' 0-based index
        Dim yOffset As Integer = row * frameHeight

        For col As Integer = 0 To framesPerBoss - 1
            ' Rectangle for the frame
            Dim frameRect As New Rectangle(col * frameWidth, yOffset, frameWidth, frameHeight)

            ' Create bitmap for the frame
            Dim frameImage As New Bitmap(frameWidth, frameHeight)
            Using g As Graphics = Graphics.FromImage(frameImage)
                g.DrawImage(spriteSheet, New Rectangle(0, 0, frameWidth, frameHeight), frameRect, GraphicsUnit.Pixel)
            End Using

            frames.Add(frameImage)
        Next

        Return frames
    End Function

    ' Handle panning
    Private Sub HandlePanning(currentMousePosition As Point)
        ' Indicate that panning has occurred
        hasPanned = True

        ' Calculate the delta movement
        Dim deltaX As Integer = currentMousePosition.X - panStartPoint.X
        Dim deltaY As Integer = currentMousePosition.Y - panStartPoint.Y

        ' Calculate the new scroll position
        Dim newScrollPosition As New Point(scrollStartPoint.X + deltaX, scrollStartPoint.Y + deltaY)

        ' Calculate the scaled map size
        Dim scaledMapWidth As Integer = _mapWidth * ZoomLevel
        Dim scaledMapHeight As Integer = _mapHeight * ZoomLevel

        ' Calculate maximum scroll positions
        Dim maxScrollX As Integer = Math.Max(0, scaledMapWidth - ClientSize.Width)
        Dim maxScrollY As Integer = Math.Max(0, scaledMapHeight - ClientSize.Height)

        ' Adjust offsetX and offsetY for centering when map is smaller than viewport
        Dim offsetX As Integer = 0
        Dim offsetY As Integer = 0

        If scaledMapWidth < ClientSize.Width Then
            offsetX = (ClientSize.Width - scaledMapWidth) \ 2
        End If
        If scaledMapHeight < ClientSize.Height Then
            offsetY = (ClientSize.Height - scaledMapHeight) \ 2
        End If

        ' Adjust clamping to account for centering
        Dim minScrollX As Integer = -offsetX
        Dim minScrollY As Integer = -offsetY
        Dim maxScrollXWithOffset As Integer = maxScrollX + offsetX
        Dim maxScrollYWithOffset = maxScrollY + offsetY

        ' Clamp the scroll position to prevent scrolling beyond the content boundaries
        newScrollPosition.X = Math.Max(-maxScrollXWithOffset, Math.Min(-offsetX, newScrollPosition.X))
        newScrollPosition.Y = Math.Max(-maxScrollYWithOffset, Math.Min(-offsetY, newScrollPosition.Y))

        ' Update the scroll position (AutoScrollPosition uses positive values)
        AutoScrollPosition = New Point(-newScrollPosition.X, -newScrollPosition.Y)

        ' Refresh the control to update the display
        Refresh()
    End Sub

    ' Convert client point to canvas point
    Public Function ClientToCanvas(point As Point) As Point
        Return New Point((point.X - AutoScrollPosition.X) / ZoomLevel, (point.Y - AutoScrollPosition.Y) / ZoomLevel)
    End Function

    ' Convert canvas rectangle to client rectangle
    Private Function CanvasToClientRect(rect As Rectangle) As Rectangle
        Dim x As Integer = (rect.X * ZoomLevel) + AutoScrollPosition.X
        Dim y As Integer = (rect.Y * ZoomLevel) + AutoScrollPosition.Y
        Dim width As Integer = rect.Width * ZoomLevel
        Dim height As Integer = rect.Height * ZoomLevel
        Return New Rectangle(x, y, width, height)
    End Function

    ' Adjust scroll bars based on zoom level
    Private Sub UpdateScrollBars()
        ' Calculate the scaled map size
        Dim scaledWidth As Integer = _mapWidth * ZoomLevel
        Dim scaledHeight As Integer = _mapHeight * ZoomLevel

        ' Set the AutoScrollMinSize to the scaled map size
        AutoScrollMinSize = New Size(scaledWidth, scaledHeight)

        ' Calculate offsets for centering
        Dim offsetX As Integer = 0
        Dim offsetY As Integer = 0

        If scaledWidth < ClientSize.Width Then
            offsetX = (ClientSize.Width - scaledWidth) \ 2
        End If
        If scaledHeight < ClientSize.Height Then
            offsetY = (ClientSize.Height - scaledHeight) \ 2
        End If

        ' Adjust scroll position to keep within bounds
        Dim maxScrollX As Integer = Math.Max(0, scaledWidth - ClientSize.Width)
        Dim maxScrollY As Integer = Math.Max(0, scaledHeight - ClientSize.Height)

        Dim minScrollX As Integer = -offsetX
        Dim minScrollY As Integer = -offsetY
        Dim maxScrollXWithOffset As Integer = maxScrollX + offsetX
        Dim maxScrollYWithOffset As Integer = maxScrollY + offsetY

        Dim scrollX As Integer = AutoScrollPosition.X
        Dim scrollY As Integer = AutoScrollPosition.Y

        scrollX = Math.Max(-maxScrollXWithOffset, Math.Min(-offsetX, scrollX))
        scrollY = Math.Max(-maxScrollYWithOffset, Math.Min(-offsetY, scrollY))

        AutoScrollPosition = New Point(scrollX, scrollY)
    End Sub

    Private Sub AdjustScrollPosition(oldZoomLevel As Single, ByVal focusPoint As Point?)
        ' By default, use the center of the control as the focus point
        Dim focus As Point = If(focusPoint, New Point(ClientSize.Width / 2, ClientSize.Height / 2))

        ' Convert focus point to canvas coordinates before zoom
        Dim scrollPos As Point = AutoScrollPosition ' Note: AutoScrollPosition values are negative
        Dim offsetX As Integer = 0
        Dim offsetY As Integer = 0

        ' Calculate the scaled map size
        Dim scaledMapWidth As Integer = _mapWidth * ZoomLevel
        Dim scaledMapHeight As Integer = _mapHeight * ZoomLevel

        If scaledMapWidth < ClientSize.Width Then
            offsetX = (ClientSize.Width - scaledMapWidth) \ 2
        End If
        If scaledMapHeight < ClientSize.Height Then
            offsetY = (ClientSize.Height - scaledMapHeight) \ 2
        End If

        Dim focusX As Single = (-scrollPos.X + focus.X - offsetX) / oldZoomLevel
        Dim focusY As Single = (-scrollPos.Y + focus.Y - offsetY) / oldZoomLevel

        ' Update scrollbars to get new AutoScrollMinSize
        UpdateScrollBars()

        ' Recalculate offsets after zoom change
        offsetX = 0
        offsetY = 0
        scaledMapWidth = CInt(_mapWidth * ZoomLevel)
        scaledMapHeight = CInt(_mapHeight * ZoomLevel)

        If scaledMapWidth < ClientSize.Width Then
            offsetX = (ClientSize.Width - scaledMapWidth) \ 2
        End If
        If scaledMapHeight < ClientSize.Height Then
            offsetY = (ClientSize.Height - scaledMapHeight) \ 2
        End If

        ' Calculate new scroll positions to keep focus point at the same canvas coordinates
        Dim newScrollX As Integer = -(focusX * ZoomLevel) + focus.X - offsetX
        Dim newScrollY As Integer = -(focusY * ZoomLevel) + focus.Y - offsetY

        ' Calculate maximum scroll positions
        Dim maxScrollX As Integer = Math.Max(0, scaledMapWidth - ClientSize.Width)
        Dim maxScrollY As Integer = Math.Max(0, scaledMapHeight - ClientSize.Height)

        Dim minScrollX As Integer = -offsetX
        Dim minScrollY As Integer = -offsetY
        Dim maxScrollXWithOffset As Integer = maxScrollX + offsetX
        Dim maxScrollYWithOffset As Integer = maxScrollY + offsetY

        ' Clamp the scroll positions within allowable ranges
        newScrollX = Math.Max(-maxScrollXWithOffset, Math.Min(-offsetX, newScrollX))
        newScrollY = Math.Max(-maxScrollYWithOffset, Math.Min(-offsetY, newScrollY))

        ' Update AutoScrollPosition (note that setting AutoScrollPosition uses positive values)
        AutoScrollPosition = New Point(-newScrollX, -newScrollY)
    End Sub

    ' Method to bring a tile to the front
    Private Sub BringTileToFront(tile As MapTile)
        If _tiles.Remove(tile) Then
            _tiles.Add(tile)
            Invalidate()
        End If
    End Sub

    ' Method to send a tile to the back
    Private Sub SendTileToBack(tile As MapTile)
        If _tiles.Remove(tile) Then
            _tiles.Insert(0, tile)
            Invalidate()
        End If
    End Sub

    ' Method to delete selected tiles
    Private Sub DeleteSelectedTiles()
        For Each tile In selectedTiles
            ' Dispose images
            tile.Dispose()
            Dim unused = _tiles.Remove(tile)
        Next
        DeselectAllTiles()
        Invalidate()
        RaiseEvent CanvasModified()
    End Sub

    ' Get the tile at a specific point
    Private Function GetTileAtPoint(point As Point, Optional includeLockedTiles As Boolean = False) As MapTile
        For i As Integer = _tiles.Count - 1 To 0 Step -1 ' Start from topmost tile
            Dim tile = _tiles(i)

            ' Skip if tile is locked and we're not including locked tiles
            If tile.IsLocked AndAlso Not includeLockedTiles Then
                Continue For
            End If

            ' Check if point is within tile rectangle
            Dim rect As New Rectangle(tile.Position, tile.Size)
            If rect.Contains(point) Then
                Return tile
            Else
                ' Check if point is within the tile's resize handles
                If IsPointOverTileHandles(tile, point) Then
                    Return tile
                End If
            End If
        Next
        Return Nothing
    End Function

    Private Function IsPointOverTileHandles(tile As MapTile, point As Point) As Boolean
        Dim handleSize As Integer = 6 ' Same as the handle size used in drawing
        Dim adjustedHandleSize As Single = handleSize / ZoomLevel

        Dim rect = New Rectangle(tile.Position, tile.Size)

        ' Define rectangles for the handles
        Dim topLeftHandle As New RectangleF(rect.Left - (adjustedHandleSize / 2), rect.Top - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)
        Dim topRightHandle As New RectangleF(rect.Right - (adjustedHandleSize / 2), rect.Top - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)
        Dim bottomLeftHandle As New RectangleF(rect.Left - (adjustedHandleSize / 2), rect.Bottom - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)
        Dim bottomRightHandle As New RectangleF(rect.Right - (adjustedHandleSize / 2), rect.Bottom - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)

        ' Check if point is within any of the handles
        Return topLeftHandle.Contains(point) OrElse topRightHandle.Contains(point) OrElse bottomLeftHandle.Contains(point) OrElse bottomRightHandle.Contains(point)
    End Function

    ' Determine if mouse is over resize handles of a tile
    Private Function GetResizeDirection(tile As MapTile, canvasPoint As Point) As MapTile.ResizeDirections
        Dim handleSize As Integer = 6 ' Should match the handle size used in drawing

        ' Adjust handle size based on zoom level to match the display
        Dim adjustedHandleSize As Single = handleSize / ZoomLevel

        Dim rect = New Rectangle(tile.Position, tile.Size)
        Dim direction As MapTile.ResizeDirections = MapTile.ResizeDirections.None

        ' Define rectangles for the handles
        Dim topLeftHandle As New RectangleF(rect.Left - (adjustedHandleSize / 2), rect.Top - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)
        Dim topRightHandle As New RectangleF(rect.Right - (adjustedHandleSize / 2), rect.Top - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)
        Dim bottomLeftHandle As New RectangleF(rect.Left - (adjustedHandleSize / 2), rect.Bottom - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)
        Dim bottomRightHandle As New RectangleF(rect.Right - (adjustedHandleSize / 2), rect.Bottom - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)

        ' Check which handle contains the point
        If topLeftHandle.Contains(canvasPoint) Then
            direction = MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Top
        ElseIf topRightHandle.Contains(canvasPoint) Then
            direction = MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Top
        ElseIf bottomLeftHandle.Contains(canvasPoint) Then
            direction = MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Bottom
        ElseIf bottomRightHandle.Contains(canvasPoint) Then
            direction = MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Bottom
        End If

        Return direction
    End Function

    ' Determine if mouse is over group resize handles
    Private Function GetGroupResizeDirection(canvasPoint As Point) As MapTile.ResizeDirections
        If selectedTiles.Count <= 1 Then Return MapTile.ResizeDirections.None

        Dim handleSize As Integer = 6 ' Should match the handle size used in drawing

        ' Adjust handle size based on zoom level
        Dim adjustedHandleSize As Single = handleSize / ZoomLevel

        Dim bounds = GetSelectedTilesBounds()

        Dim direction As MapTile.ResizeDirections = MapTile.ResizeDirections.None

        ' Define rectangles for the handles
        Dim topLeftHandle As New RectangleF(bounds.Left - (adjustedHandleSize / 2), bounds.Top - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)
        Dim topRightHandle As New RectangleF(bounds.Right - (adjustedHandleSize / 2), bounds.Top - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)
        Dim bottomLeftHandle As New RectangleF(bounds.Left - (adjustedHandleSize / 2), bounds.Bottom - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)
        Dim bottomRightHandle As New RectangleF(bounds.Right - (adjustedHandleSize / 2), bounds.Bottom - (adjustedHandleSize / 2), adjustedHandleSize, adjustedHandleSize)

        ' Check which handle contains the point
        If topLeftHandle.Contains(canvasPoint) Then
            direction = MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Top
        ElseIf topRightHandle.Contains(canvasPoint) Then
            direction = MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Top
        ElseIf bottomLeftHandle.Contains(canvasPoint) Then
            direction = MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Bottom
        ElseIf bottomRightHandle.Contains(canvasPoint) Then
            direction = MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Bottom
        End If

        Return direction
    End Function

    ' Get the bounding rectangle of selected tiles
    Private Function GetSelectedTilesBounds() As Rectangle
        If selectedTiles.Count = 0 Then Return Rectangle.Empty

        Dim minX As Integer = Integer.MaxValue
        Dim minY As Integer = Integer.MaxValue
        Dim maxX As Integer = Integer.MinValue
        Dim maxY As Integer = Integer.MinValue

        For Each tile In selectedTiles
            minX = Math.Min(minX, tile.Position.X)
            minY = Math.Min(minY, tile.Position.Y)
            maxX = Math.Max(maxX, tile.Position.X + tile.Size.Width)
            maxY = Math.Max(maxY, tile.Position.Y + tile.Size.Height)
        Next

        Return New Rectangle(minX, minY, maxX - minX, maxY - minY)
    End Function

    ' Get cursor based on resize direction
    Private Function GetCursorForDirection(direction As MapTile.ResizeDirections) As Cursor
        Select Case direction
            Case MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Top,
                 MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Bottom
                Return Cursors.SizeNWSE
            Case MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Top,
                 MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Bottom
                Return Cursors.SizeNESW
            Case MapTile.ResizeDirections.Left, MapTile.ResizeDirections.Right
                Return Cursors.SizeWE
            Case MapTile.ResizeDirections.Top, MapTile.ResizeDirections.Bottom
                Return Cursors.SizeNS
            Case Else
                Return Cursors.Default
        End Select
    End Function

    ' Helper methods for selection rectangle
    Private Function GetSelectionRectangle() As Rectangle
        Dim x As Integer = Math.Min(selectionStartPoint.X, selectionEndPoint.X)
        Dim y As Integer = Math.Min(selectionStartPoint.Y, selectionEndPoint.Y)
        Dim width As Integer = Math.Abs(selectionStartPoint.X - selectionEndPoint.X)
        Dim height As Integer = Math.Abs(selectionStartPoint.Y - selectionEndPoint.Y)
        Return New Rectangle(x, y, width, height)
    End Function

    Private Sub SelectTilesInRectangle(rect As Rectangle)
        selectedTiles.Clear()
        For Each tile In _tiles
            If tile.IsLocked Then
                tile.Selected = False ' Ensure locked tiles are not selected
                Continue For
            End If
            If rect.IntersectsWith(New Rectangle(tile.Position, tile.Size)) Then
                tile.Selected = True
                selectedTiles.Add(tile)
            Else
                tile.Selected = False
            End If
        Next
    End Sub

    Private Sub DeselectAllTiles()
        For Each tile In _tiles
            tile.Selected = False
            tile.CurrentResizeDirection = MapTile.ResizeDirections.None
        Next
        selectedTiles.Clear()
    End Sub

    ' Method to set the map size
    Public Sub SetMapSize(width As Integer, height As Integer)
        _mapWidth = width
        _mapHeight = height

        ' Adjust tiles that are outside new boundaries
        For Each tile In _tiles
            ' Adjust position
            Dim newX As Integer = Math.Max(0, Math.Min(tile.Position.X, _mapWidth - tile.Size.Width))
            Dim newY As Integer = Math.Max(0, Math.Min(tile.Position.Y, _mapHeight - tile.Size.Height))
            tile.Position = New Point(newX, newY)

            ' Adjust size if necessary
            If tile.Position.X + tile.Size.Width > _mapWidth Then
                tile.Size = New Size(_mapWidth - tile.Position.X, tile.Size.Height)
            End If
            If tile.Position.Y + tile.Size.Height > _mapHeight Then
                tile.Size = New Size(tile.Size.Width, _mapHeight - tile.Position.Y)
            End If

            ' Ensure minimum size
            If tile.Size.Width < 1 Then tile.Size = New Size(1, tile.Size.Height)
            If tile.Size.Height < 1 Then tile.Size = New Size(tile.Size.Width, 1)
        Next

        UpdateScrollBars()
        Invalidate()
    End Sub

    ' Method to snap all tiles to grid
    Public Sub SnapAllTilesToGrid()
        For Each tile In _tiles
            tile.Position = New Point(CInt(tile.Position.X / _gridSize) * _gridSize,
                                      CInt(tile.Position.Y / _gridSize) * _gridSize)
            tile.Size = New Size(CInt(tile.Size.Width / _gridSize) * _gridSize,
                                 CInt(tile.Size.Height / _gridSize) * _gridSize)
        Next
        Invalidate()
    End Sub

    ' ReadOnly properties for key detection
    Private ReadOnly Property IsShiftKeyPressed As Boolean
        Get
            Return (ModifierKeys And Keys.Shift) = Keys.Shift
        End Get
    End Property

    Private ReadOnly Property IsCtrlKeyPressed As Boolean
        Get
            Return (Control.ModifierKeys And Keys.Control) = Keys.Control
        End Get
    End Property

    Private Function GetVisibleCanvasRectangle() As Rectangle
        Dim left As Integer = -AutoScrollPosition.X / ZoomLevel
        Dim top As Integer = -AutoScrollPosition.Y / ZoomLevel
        Dim width As Integer = ClientSize.Width / ZoomLevel
        Dim height As Integer = ClientSize.Height / ZoomLevel
        Return New Rectangle(left, top, width, height)
    End Function

#End Region

#Region " Zoom Methods "

    ' Zoom in method
    Private Sub ZoomIn(ByVal focusPoint As Point?)
        ' Increase zoom level
        ZoomLevel *= 1.1F ' Increase by 10%
    End Sub

    Private Sub ZoomOut(ByVal focusPoint As Point?)
        ' Decrease zoom level
        ZoomLevel /= 1.1F ' Decrease by approx 10%
    End Sub

    ' Method to zoom out to fit the entire map within the viewport
    Public Sub ZoomToFit()
        ' Calculate the zoom levels needed to fit the width and height
        Dim zoomX As Single = ClientSize.Width / _mapWidth
        Dim zoomY As Single = ClientSize.Height / _mapHeight

        ' Choose the smaller zoom level to ensure the entire map fits
        Dim newZoomLevel As Single = Math.Min(zoomX, zoomY)

        ' Ensure zoom level is positive and non-zero
        If newZoomLevel <= 0 Then
            newZoomLevel = 0.01F ' Set to a very small positive value
        End If

        ' Set the new zoom level
        Dim oldZoomLevel As Single = _zoomLevel
        _zoomLevel = newZoomLevel
        AdjustScrollPosition(oldZoomLevel, New Point(ClientSize.Width / 2, ClientSize.Height / 2))
        UpdateScrollBars()
        Invalidate()
        ' Raise the ZoomLevelChanged event
        RaiseEvent ZoomLevelChanged(_zoomLevel)
    End Sub

#End Region

#Region " CTMMenu "

    ' Event handler for MnuLock Click
    Private Sub MnuFlipHorizontal_Click(sender As Object, e As EventArgs) Handles MnuFlipHorizontal.Click
        If selectedTiles.Count > 0 Then
            FlipSelectedTiles(RotateFlipType.RotateNoneFlipX)
        ElseIf contextMenuTile IsNot Nothing Then
            FlipTile(contextMenuTile, RotateFlipType.RotateNoneFlipX)
        End If
        contextMenuTile = Nothing
        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    Private Sub MnuFlipVertical_Click(sender As Object, e As EventArgs) Handles MnuFlipVertical.Click
        If selectedTiles.Count > 0 Then
            FlipSelectedTiles(RotateFlipType.RotateNoneFlipY)
        ElseIf contextMenuTile IsNot Nothing Then
            FlipTile(contextMenuTile, RotateFlipType.RotateNoneFlipY)
        End If
        contextMenuTile = Nothing
        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    ' Method to flip selected tiles
    Private Sub FlipSelectedTiles(flipType As RotateFlipType)
        For Each tile In selectedTiles
            FlipTile(tile, flipType)
        Next
    End Sub

    ' Method to flip a single tile
    Private Sub FlipTile(tile As MapTile, flipType As RotateFlipType)
        If tile.IsAnimated AndAlso tile.AnimationFrames IsNot Nothing Then
            ' Flip each frame in the animation
            For i As Integer = 0 To tile.AnimationFrames.Count - 1
                tile.AnimationFrames(i).RotateFlip(flipType)
            Next
        ElseIf tile.Image IsNot Nothing Then
            tile.Image.RotateFlip(flipType)
        End If

        ' Update flip properties based on flipType
        Select Case flipType
            Case RotateFlipType.RotateNoneFlipX
                tile.IsFlippedHorizontally = Not tile.IsFlippedHorizontally
            Case RotateFlipType.RotateNoneFlipY
                tile.IsFlippedVertically = Not tile.IsFlippedVertically
            Case RotateFlipType.RotateNoneFlipXY
                tile.IsFlippedHorizontally = Not tile.IsFlippedHorizontally
                tile.IsFlippedVertically = Not tile.IsFlippedVertically
            Case Else
                ' Handle other cases if necessary
        End Select
    End Sub

    Private Sub MnuLock_Click(sender As Object, e As EventArgs) Handles MnuLock.Click
        If selectedTiles.Count > 0 Then
            For Each tile In selectedTiles
                tile.IsLocked = True
                tile.Selected = False ' Optionally deselect after locking
            Next
            selectedTiles.Clear()
        ElseIf contextMenuTile IsNot Nothing Then
            contextMenuTile.IsLocked = True
            ' Do not change selection state
        End If
        contextMenuTile = Nothing
        Refresh()
    End Sub

    ' Event handler for MnuUnlock Click
    Private Sub MnuUnlock_Click(sender As Object, e As EventArgs) Handles MnuUnlock.Click
        If selectedTiles.Count > 0 Then
            For Each tile In selectedTiles
                tile.IsLocked = False
            Next
            Refresh()
        ElseIf contextMenuTile IsNot Nothing Then
            contextMenuTile.IsLocked = False
            Refresh()
        Else
            Dim unused = MessageBox.Show("No tile selected to unlock.", "Unlock Tile", MessageBoxButtons.OK, MessageBoxIcon.Information)
        End If
        contextMenuTile = Nothing
        ' Raise the CanvasModified event if necessary
        RaiseEvent CanvasModified()
    End Sub

    ' Event handler for Mnu100 Click
    Private Sub Mnu100_Click(sender As Object, e As EventArgs) Handles Mnu100.Click
        If selectedTiles.Count > 0 Then
            ResizeSelectedTilesToPercentage(100)
        End If
    End Sub

    ' Event handler for Mnu50 Click
    Private Sub Mnu50_Click(sender As Object, e As EventArgs) Handles Mnu50.Click
        If selectedTiles.Count > 0 Then
            ResizeSelectedTilesToPercentage(50)
        End If
    End Sub

    ' Event handler for Mnu25 Click
    Private Sub Mnu25_Click(sender As Object, e As EventArgs) Handles Mnu25.Click
        If selectedTiles.Count > 0 Then
            ResizeSelectedTilesToPercentage(25)
        End If
    End Sub

    ' Method to resize selected tiles to a percentage of their original size
    Private Sub ResizeSelectedTilesToPercentage(percentage As Integer)
        For Each tile In selectedTiles
            ' Get the original size
            Dim originalSize As Size = tile.OriginalSize

            ' Calculate new size
            Dim newWidth As Integer = originalSize.Width * percentage / 100
            Dim newHeight As Integer = originalSize.Height * percentage / 100

            ' Apply snapping if necessary
            If snappingEnabled AndAlso Not IsShiftKeyPressed Then
                newWidth = CInt(Math.Ceiling(newWidth / _gridSize)) * _gridSize
                newHeight = CInt(Math.Ceiling(newHeight / _gridSize)) * _gridSize
            End If

            ' Ensure minimum size
            If newWidth <= 1 Then newWidth = 1
            If newHeight <= 1 Then newHeight = 1

            ' Update the tile's size
            tile.Size = New Size(newWidth, newHeight)
            ' Clamp position and size to map boundaries
            Dim newPosition As Point = tile.Position

            ' Make sure the tile stays within the bounds
            If newPosition.X + newWidth > _mapWidth Then
                newPosition.X = _mapWidth - newWidth
                If newPosition.X < 0 Then
                    newPosition.X = 0
                    newWidth = _mapWidth
                End If
            End If

            If newPosition.Y + newHeight > _mapHeight Then
                newPosition.Y = _mapHeight - newHeight
                If newPosition.Y < 0 Then
                    newPosition.Y = 0
                    newHeight = _mapHeight
                End If
            End If

            tile.Position = newPosition
            tile.Size = New Size(newWidth, newHeight)

            ' Raise the TileSizeChanged event
            RaiseEvent TileSizeChanged(tile)
        Next
        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    ' Event handler for Send to Back menu item
    Private Sub MnuSendToBack_Click(sender As Object, e As EventArgs) Handles MnuSendToBack.Click
        If selectedTiles.Count > 0 Then
            For Each tile In selectedTiles
                SendTileToBack(tile)
            Next
            Refresh()
        End If
    End Sub

    ' Event handler for Bring to Front menu item
    Private Sub MnuBringToFront_Click(sender As Object, e As EventArgs) Handles MnuBringToFront.Click
        If selectedTiles.Count > 0 Then
            For Each tile In selectedTiles
                BringTileToFront(tile)
            Next
            Refresh()
        End If
    End Sub

    ' Event handler for Delete menu item
    Private Sub MnuDelete_Click(sender As Object, e As EventArgs) Handles MnuDelete.Click
        If selectedTiles.Count > 0 Then
            DeleteSelectedTiles()
        End If
    End Sub

    ' Event handler for Rotate 90° menu item
    Private Sub MnuRotate90_Click(sender As Object, e As EventArgs) Handles MnuRotate90.Click
        If selectedTiles.Count > 0 Then
            RotateSelectedTiles(90)
        ElseIf contextMenuTile IsNot Nothing Then
            RotateTile(contextMenuTile, 90)
        End If
        contextMenuTile = Nothing
        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    Private Sub MnuRotate180_Click(sender As Object, e As EventArgs) Handles MnuRotate180.Click
        If selectedTiles.Count > 0 Then
            RotateSelectedTiles(180)
        ElseIf contextMenuTile IsNot Nothing Then
            RotateTile(contextMenuTile, 180)
        End If
        contextMenuTile = Nothing
        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    Private Sub MnuRotate270_Click(sender As Object, e As EventArgs) Handles MnuRotate270.Click
        If selectedTiles.Count > 0 Then
            RotateSelectedTiles(270)
        ElseIf contextMenuTile IsNot Nothing Then
            RotateTile(contextMenuTile, 270)
        End If
        contextMenuTile = Nothing
        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    ' Event handler for Group menu item
    Private Sub MnuGroup_Click(sender As Object, e As EventArgs) Handles MnuGroup.Click
        If selectedTiles.Count > 1 Then
            GroupSelectedTiles()
            Refresh()
        Else
            Dim unused = MessageBox.Show("Please select at least two tiles to group.", "Group Tiles", MessageBoxButtons.OK, MessageBoxIcon.Information)
        End If
    End Sub

    Private Sub GroupSelectedTiles()
        ' Check if any selected tile is animated
        For Each tile In selectedTiles
            If tile.IsAnimated Then
                Dim unused1 = MessageBox.Show("Animated tiles cannot be grouped.", "Grouping Not Allowed", MessageBoxButtons.OK, MessageBoxIcon.Information)
                Return
            End If
        Next

        ' Get the bounding rectangle of selected tiles
        Dim bounds As Rectangle = GetSelectedTilesBounds()

        ' Create a new bitmap of the size of the bounding rectangle
        Dim mergedImage As New Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb)
        Using g As Graphics = Graphics.FromImage(mergedImage)
            ' Set graphics options
            g.InterpolationMode = InterpolationMode.NearestNeighbor
            g.PixelOffsetMode = PixelOffsetMode.Half
            g.SmoothingMode = SmoothingMode.None

            ' Clear the bitmap to transparent
            g.Clear(Color.Transparent)

            ' Draw the selected tiles in order of their z-index (layering order)
            ' To preserve layering, draw from the bottom up

            ' Get the selected tiles in their current z-index order
            Dim selectedTilesInOrder = _tiles.Where(Function(t) t.Selected).ToList()

            ' Draw the tiles from back to front
            For Each tile In selectedTilesInOrder
                ' Compute the offset position relative to the bounding rectangle
                Dim offsetX As Integer = tile.Position.X - bounds.X
                Dim offsetY As Integer = tile.Position.Y - bounds.Y

                ' Draw the tile's image onto the merged image
                g.DrawImage(tile.Image, New Rectangle(offsetX, offsetY, tile.Size.Width, tile.Size.Height))
            Next
        End Using

        ' Create a new MapTile with the merged image
        Dim newTile As New MapTile With {
            .Image = mergedImage,
            .Position = New Point(bounds.X, bounds.Y),
            .Size = mergedImage.Size,
            .Selected = True
        }

        ' Remove the selected tiles from the Tiles list
        Dim unused = _tiles.RemoveAll(Function(t) t.Selected)

        ' Add the new tile to the Tiles list
        _tiles.Add(newTile)

        ' Clear the selectedTiles list and add the new tile to it
        selectedTiles.Clear()
        selectedTiles.Add(newTile)
        selectedTile = newTile

        ' Raise CanvasModified event
        RaiseEvent CanvasModified()
    End Sub

#End Region

#Region " Tile Manipulation Methods "

    ' Rotate selected tiles by specified angle
    Private Sub RotateSelectedTiles(angle As Single)
        For Each tile In selectedTiles
            RotateTile(tile, angle)
        Next
    End Sub

    ' Method to rotate a single tile by specified angle
    ' Method to rotate a tile by specified angle
    Private Sub RotateTile(tile As MapTile, angle As Integer)
        Dim rotateFlipType As RotateFlipType
        Select Case angle Mod 360
            Case 90
                rotateFlipType = RotateFlipType.Rotate90FlipNone
            Case 180
                rotateFlipType = RotateFlipType.Rotate180FlipNone
            Case 270
                rotateFlipType = RotateFlipType.Rotate270FlipNone
            Case Else
                ' For other angles, you might need to implement custom rotation logic
                ' For simplicity, we'll handle multiples of 90 degrees
                Return
        End Select

        If tile.IsAnimated AndAlso tile.AnimationFrames IsNot Nothing Then
            ' Rotate each frame in the animation
            For i As Integer = 0 To tile.AnimationFrames.Count - 1
                tile.AnimationFrames(i).RotateFlip(rotateFlipType)
            Next
            ' Swap Width and Height if rotated 90 or 270 degrees
            If angle Mod 180 <> 0 Then
                ' Swap size dimensions
                tile.Size = New Size(tile.Size.Height, tile.Size.Width)
            End If
        ElseIf tile.Image IsNot Nothing Then
            tile.Image.RotateFlip(rotateFlipType)
            ' Swap Width and Height if rotated 90 or 270 degrees
            If angle Mod 180 <> 0 Then
                ' Swap size dimensions
                tile.Size = New Size(tile.Size.Height, tile.Size.Width)
            End If
        End If

        ' Update the rotation angle
        tile.RotationAngle = (tile.RotationAngle + angle) Mod 360

        ' Recalculate position to maintain center
        Dim oldCenter As New Point(tile.Position.X + (tile.Size.Width \ 2), tile.Position.Y + (tile.Size.Height \ 2))
        tile.Position = New Point(oldCenter.X - (tile.Size.Width \ 2), oldCenter.Y - (tile.Size.Height \ 2))

        ' Apply snapping if necessary
        If snappingEnabled AndAlso Not IsShiftKeyPressed Then
            tile.Position = New Point(CInt(Math.Round(tile.Position.X / _gridSize)) * _gridSize,
                                  CInt(Math.Round(tile.Position.Y / _gridSize)) * _gridSize)
        End If
    End Sub

    ' Function to rotate an image by a specified angle
    Private Function RotateImage(img As Image, angle As Single) As Image
        ArgumentNullException.ThrowIfNull(img)

        ' Create a new empty bitmap to hold rotated image
        Dim rotatedBmp As New Bitmap(img.Height, img.Width)
        rotatedBmp.SetResolution(img.HorizontalResolution, img.VerticalResolution)

        ' Make a graphics object from the empty bitmap
        Using g As Graphics = Graphics.FromImage(rotatedBmp)
            ' Move rotation point to center of image
            g.TranslateTransform(rotatedBmp.Width / 2, rotatedBmp.Height / 2)
            ' Rotate
            g.RotateTransform(angle)
            ' Move image back
            g.TranslateTransform(-img.Width / 2, -img.Height / 2)
            ' Draw passed in image onto graphics object
            g.DrawImage(img, New Point(0, 0))
        End Using

        Return rotatedBmp
    End Function

    Private Sub HandleTileClick(e As MouseEventArgs, currentPoint As Point, clickedTile As MapTile)
        ' This method is now simplified as selection logic is handled in OnMouseDown
        ' No need to handle CTRL key here
        If clickedTile IsNot Nothing AndAlso Not clickedTile.IsLocked Then
            If selectedTiles.Contains(clickedTile) Then
                ' Start dragging the selected tiles
                For Each tile In selectedTiles
                    tile.InitialPosition = tile.Position
                Next
                initialMousePosition = currentPoint
                dragging = True
                Refresh()
            Else
                ' Deselect all tiles and select the clicked tile
                DeselectAllTiles()
                selectedTile = clickedTile
                selectedTile.Selected = True
                selectedTiles.Clear()
                selectedTiles.Add(selectedTile)

                ' Start dragging the clicked tile
                selectedTile.InitialPosition = selectedTile.Position
                initialMousePosition = currentPoint
                dragging = True
                Refresh()
            End If
        Else
            ' No tile clicked or tile is locked - start selection rectangle
            StartSelection(currentPoint)
        End If
    End Sub

    Private Sub DuplicateSelectedTiles(currentPoint As Point)
        ' List to hold duplicates
        Dim duplicates As New List(Of MapTile)

        ' Clone all selected tiles
        For Each tile In selectedTiles
            Dim duplicateTile As MapTile = tile.Clone()
            _tiles.Add(duplicateTile)
            duplicates.Add(duplicateTile)
        Next

        ' Deselect all tiles
        For Each tile In _tiles
            tile.Selected = False
        Next

        ' Set the duplicates as the selected tiles
        selectedTiles = duplicates
        For Each tile In selectedTiles
            tile.Selected = True
            tile.InitialPosition = tile.Position
        Next

        initialMousePosition = currentPoint

        ' Start dragging the duplicates
        dragging = True
        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    ' Start selection rectangle
    Private Sub StartSelection(currentPoint As Point)
        isSelecting = True
        selectionStartPoint = currentPoint
        selectionEndPoint = currentPoint
        DeselectAllTiles()
        Refresh()
    End Sub

    ' Start group resizing
    Private Sub StartGroupResizing(currentPoint As Point, groupDirection As MapTile.ResizeDirections)
        resizing = True
        ' Record initial mouse position
        initialMousePosition = currentPoint

        ' Record initial bounds of selected tiles
        initialSelectedTilesBounds = GetSelectedTilesBounds()
        initialSelectedTilesPositionsAndSizes = New List(Of Tuple(Of MapTile, Point, Size))
        For Each tile In selectedTiles
            initialSelectedTilesPositionsAndSizes.Add(Tuple.Create(tile, tile.Position, tile.Size))
        Next

        ' Set current resize direction
        For Each tile In selectedTiles
            tile.CurrentResizeDirection = groupDirection
        Next

        Refresh()
    End Sub

    ' Start resizing a single tile
    Private Sub StartSingleTileResizing(clickedTile As MapTile, currentPoint As Point, direction As MapTile.ResizeDirections)
        DeselectAllTiles()
        selectedTile = clickedTile
        selectedTile.Selected = True
        selectedTiles.Clear()
        selectedTiles.Add(selectedTile)

        resizing = True
        selectedTile.IsResizing = True
        selectedTile.CurrentResizeDirection = direction

        ' Initialize resizing variables
        initialTilePosition = selectedTile.Position
        initialTileSize = selectedTile.Size
        initialMousePosition = currentPoint
        Refresh()
    End Sub

    ' Duplicate a tile when CTRL + Click
    Private Sub DuplicateTile(clickedTile As MapTile, currentPoint As Point)
        Dim duplicateTile As MapTile = clickedTile.Clone()
        _tiles.Add(duplicateTile)
        DeselectAllTiles()
        selectedTile = duplicateTile
        selectedTile.Selected = True
        selectedTiles.Clear()
        selectedTiles.Add(selectedTile)

        ' Set initial positions for dragging
        initialTilePosition = duplicateTile.Position
        duplicateTile.InitialPosition = duplicateTile.Position
        initialMousePosition = currentPoint

        dragging = True
        Refresh()
    End Sub

    Private Sub ResizeSingleTile(currentPoint As Point)
        Dim deltaX As Integer = currentPoint.X - initialMousePosition.X
        Dim deltaY As Integer = currentPoint.Y - initialMousePosition.Y

        Dim newSize As Size = initialTileSize
        Dim newPosition As Point = initialTilePosition

        ' Adjust size and position based on resize direction
        If selectedTile.CurrentResizeDirection.HasFlag(MapTile.ResizeDirections.Right) Then
            newSize.Width = initialTileSize.Width + deltaX
        End If
        If selectedTile.CurrentResizeDirection.HasFlag(MapTile.ResizeDirections.Bottom) Then
            newSize.Height = initialTileSize.Height + deltaY
        End If
        If selectedTile.CurrentResizeDirection.HasFlag(MapTile.ResizeDirections.Left) Then
            newSize.Width = initialTileSize.Width - deltaX
            newPosition.X = initialTilePosition.X + deltaX
        End If
        If selectedTile.CurrentResizeDirection.HasFlag(MapTile.ResizeDirections.Top) Then
            newSize.Height = initialTileSize.Height - deltaY
            newPosition.Y = initialTilePosition.Y + deltaY
        End If

        ' Ensure minimum size
        If newSize.Width <= 1 Then newSize.Width = 1
        If newSize.Height <= 1 Then newSize.Height = 1

        ' Apply snapping if enabled and Shift is NOT pressed
        If snappingEnabled AndAlso Not IsShiftKeyPressed Then
            newSize.Width = CInt(Math.Ceiling(newSize.Width / _gridSize)) * _gridSize
            newSize.Height = CInt(Math.Ceiling(newSize.Height / _gridSize)) * _gridSize
            newPosition.X = CInt(Math.Round(newPosition.X / _gridSize)) * _gridSize
            newPosition.Y = CInt(Math.Round(newPosition.Y / _gridSize)) * _gridSize
        End If

        ' Clamp position and size to map boundaries
        If newPosition.X < 0 Then
            newSize.Width += newPosition.X ' Reduce width by the amount it goes over the left edge
            newPosition.X = 0
        End If
        If newPosition.Y < 0 Then
            newSize.Height += newPosition.Y ' Reduce height by the amount it goes over the top edge
            newPosition.Y = 0
        End If
        If newPosition.X + newSize.Width > _mapWidth Then
            newSize.Width = _mapWidth - newPosition.X
        End If
        If newPosition.Y + newSize.Height > _mapHeight Then
            newSize.Height = _mapHeight - newPosition.Y
        End If

        ' Ensure minimum size after clamping
        If newSize.Width <= 1 Then newSize.Width = 1
        If newSize.Height <= 1 Then newSize.Height = 1

        selectedTile.Size = newSize
        selectedTile.Position = newPosition

        ' Raise the TileSizeChanged event
        RaiseEvent TileSizeChanged(selectedTile)

        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    Private Sub ResizeGroupTiles(currentPoint As Point)
        Dim deltaX As Integer = currentPoint.X - initialMousePosition.X
        Dim deltaY As Integer = currentPoint.Y - initialMousePosition.Y

        Dim newBounds As Rectangle = initialSelectedTilesBounds

        ' Adjust bounds based on resize direction
        Dim resizeDirection = selectedTiles(0).CurrentResizeDirection

        If resizeDirection.HasFlag(MapTile.ResizeDirections.Right) Then
            newBounds.Width = initialSelectedTilesBounds.Width + deltaX
        End If
        If resizeDirection.HasFlag(MapTile.ResizeDirections.Bottom) Then
            newBounds.Height = initialSelectedTilesBounds.Height + deltaY
        End If
        If resizeDirection.HasFlag(MapTile.ResizeDirections.Left) Then
            newBounds.Width = initialSelectedTilesBounds.Width - deltaX
            newBounds.X = initialSelectedTilesBounds.X + deltaX
        End If
        If resizeDirection.HasFlag(MapTile.ResizeDirections.Top) Then
            newBounds.Height = initialSelectedTilesBounds.Height - deltaY
            newBounds.Y = initialSelectedTilesBounds.Y + deltaY
        End If

        ' Ensure minimum size
        If newBounds.Width <= 1 Then newBounds.Width = 1
        If newBounds.Height <= 1 Then newBounds.Height = 1

        ' Clamp the new bounds within map boundaries
        If newBounds.X < 0 Then
            newBounds.Width += newBounds.X ' Reduce width
            newBounds.X = 0
        End If
        If newBounds.Y < 0 Then
            newBounds.Height += newBounds.Y ' Reduce height
            newBounds.Y = 0
        End If
        If newBounds.X + newBounds.Width > _mapWidth Then
            newBounds.Width = _mapWidth - newBounds.X
        End If
        If newBounds.Y + newBounds.Height > _mapHeight Then
            newBounds.Height = _mapHeight - newBounds.Y
        End If

        ' Ensure minimum size after clamping
        If newBounds.Width <= 1 Then newBounds.Width = 1
        If newBounds.Height <= 1 Then newBounds.Height = 1

        ' Calculate scale factors
        Dim scaleX As Double = newBounds.Width / initialSelectedTilesBounds.Width
        Dim scaleY As Double = newBounds.Height / initialSelectedTilesBounds.Height

        ' Resize and reposition each tile
        For Each item In initialSelectedTilesPositionsAndSizes
            Dim tile = item.Item1
            Dim initialPos = item.Item2
            Dim initialSize = item.Item3

            ' Calculate relative position within the group
            Dim relX As Double = (initialPos.X - initialSelectedTilesBounds.X) / initialSelectedTilesBounds.Width
            Dim relY As Double = (initialPos.Y - initialSelectedTilesBounds.Y) / initialSelectedTilesBounds.Height

            ' New position
            Dim newPosX As Double = newBounds.X + (relX * newBounds.Width)
            Dim newPosY As Double = newBounds.Y + (relY * newBounds.Height)

            ' New size
            Dim newWidth As Double = initialSize.Width * scaleX
            Dim newHeight As Double = initialSize.Height * scaleY

            ' Ensure minimum size
            If newWidth <= 1 Then newWidth = 1
            If newHeight <= 1 Then newHeight = 1

            ' Apply snapping to individual tiles if needed
            If snappingEnabled AndAlso Not IsShiftKeyPressed Then
                newPosX = CInt(Math.Round(newPosX / _gridSize)) * _gridSize
                newPosY = CInt(Math.Round(newPosY / _gridSize)) * _gridSize
                newWidth = CInt(Math.Ceiling(newWidth / _gridSize)) * _gridSize
                newHeight = CInt(Math.Ceiling(newHeight / _gridSize)) * _gridSize
            End If

            ' Clamp individual tile positions and sizes within map boundaries
            newPosX = Math.Max(0, Math.Min(newPosX, _mapWidth - newWidth))
            newPosY = Math.Max(0, Math.Min(newPosY, _mapHeight - newHeight))
            newWidth = Math.Min(newWidth, _mapWidth - newPosX)
            newHeight = Math.Min(newHeight, _mapHeight - newPosY)

            tile.Position = New Point(newPosX, newPosY)
            tile.Size = New Size(newWidth, newHeight)
        Next

        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    ' Handle drag drop action
    Private Sub HandleDragDrop(drgevent As DragEventArgs)
        Dim image As Image = CType(drgevent.Data.GetData("ImageFormat"), Image)
        Dim imageFilePath As String = CType(drgevent.Data.GetData("ImageFilePath"), String)

        ' Original image size
        Dim originalSize As Size = image.Size

        ' Calculate the new size by snapping to the grid
        Dim newWidth As Integer = CInt(originalSize.Width / _gridSize) * _gridSize
        Dim newHeight As Integer = CInt(originalSize.Height / _gridSize) * _gridSize

        ' Ensure minimum size of one grid unit
        If newWidth < _gridSize Then newWidth = _gridSize
        If newHeight < _gridSize Then newHeight = _gridSize

        ' Create a resized image
        Dim resizedImage As New Bitmap(image, New Size(newWidth, newHeight))

        ' Get the drop coordinates and adjust for scroll position and zoom
        Dim point As Point = PointToClient(New Point(drgevent.X, drgevent.Y))
        point = ClientToCanvas(point)

        ' Apply snapping to position if snapping is enabled and Shift is NOT pressed
        If snappingEnabled AndAlso Not IsShiftKeyPressed Then
            point.X = CInt(point.X / _gridSize) * _gridSize
            point.Y = CInt(point.Y / _gridSize) * _gridSize
        End If

        ' Create a new tile and add it to the canvas
        Dim newTile As New MapTile With {
            .Image = resizedImage,
            .ImageFilePath = imageFilePath,
            .Position = point,
            .Size = resizedImage.Size
        }
        AddTile(newTile)
    End Sub

    ' Method to add a tile
    Public Sub AddTile(tile As MapTile)
        _tiles.Add(tile)
        Invalidate()
        ' Optionally, raise the CanvasModified event if needed
        RaiseEvent CanvasModified()
    End Sub

    ' Method to clear the tiles
    Public Sub ClearTiles()
        _tiles.Clear()
        Invalidate()
    End Sub

#End Region

#Region " Timers "

    ' Timer tick event handler
    Private Sub AnimationTimer_Tick(sender As Object, e As EventArgs)
        ' Redraw the canvas to update animations
        Invalidate()
    End Sub

    ' Add the Disposed event handler
    Private Sub MapCanvas_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed
        ' Dispose of the animation time
        If animationTimer IsNot Nothing Then
            animationTimer.Stop()
            animationTimer.Dispose()
            animationTimer = Nothing
        End If

        ' Dispose of other resources if necessary
        ' For example, dispose of all tiles
        If _tiles IsNot Nothing Then
            For Each tile In _tiles
                tile.Dispose()
            Next
            _tiles.Clear()
            _tiles = Nothing
        End If
    End Sub

#End Region

#Region " Key Down "

    ' Handle KeyDown event to detect when the Delete key is pressed
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        MyBase.OnKeyDown(e)

        If e.KeyCode = Keys.Delete Then
            ' Delete the selected tiles
            If selectedTiles.Count > 0 Then
                DeleteSelectedTiles()
            End If
        End If
    End Sub

#End Region

End Class

Why isn't my people animated?