Imports System.Drawing.Drawing2D

Public Class MapCanvas
    Inherits UserControl

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

#Region " Variables And Properties "
    ' 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))

    ' Context Menu and Menu Items
    Private ctmMenu As ContextMenuStrip

    ' Variables for right-click panning
    Private isPanning As Boolean = False
    Private panStartPoint As Point
    Private scrollStartPoint As Point
    Private hasPanned As Boolean = False

    Private WithEvents MnuSendToBack As ToolStripMenuItem
    Private WithEvents MnuDelete As ToolStripMenuItem
    Private WithEvents MnuRotate90 As ToolStripMenuItem

    ' 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 = 10000

    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)
            If value >= 0.1F AndAlso value <= 10.0F Then ' Limit zoom level between 0.1 and 10
                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 If
        End Set
    End Property
#End Region

#Region " Initialization and Setup "

    Private Sub InitializeControl()
        DoubleBuffered = True
        SetStyle(ControlStyles.AllPaintingInWmPaint Or
                ControlStyles.OptimizedDoubleBuffer Or
                ControlStyles.ResizeRedraw Or
                ControlStyles.UserPaint Or
                ControlStyles.Selectable, True)
        UpdateStyles()
        AutoScroll = True ' Enable AutoScroll
        TabStop = True    ' Allow control to receive focus
        ContextMenuStrip = Nothing ' Ensure no default context menu

        ' Initialize the context menu
        InitializeContextMenu()
    End Sub

    ' Initialize the context menu
    Private Sub InitializeContextMenu()
        ctmMenu = New ContextMenuStrip()
        MnuSendToBack = New ToolStripMenuItem("Send to Back") With {.Name = "MnuSendToBack"}
        MnuDelete = New ToolStripMenuItem("Delete") With {.Name = "MnuDelete"}
        MnuRotate90 = New ToolStripMenuItem("Rotate 90°") With {.Name = "MnuRotate90"}

        ctmMenu.Items.AddRange(New ToolStripItem() {mnuSendToBack, mnuDelete, mnuRotate90})
    End Sub

#End Region

#Region " Drawing Methods "

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

        ' Save the state of the graphics
        Dim g As Graphics = e.Graphics

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

        ' Apply translate transform for scrolling
        g.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y)
        ' Apply scaling
        g.ScaleTransform(ZoomLevel, ZoomLevel)

        ' Draw the grid and tiles in the scaled coordinate system
        DrawGrid(g)
        DrawTiles(g)

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

        ' Now 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
            ' Draw tile
            Dim tileRect As New Rectangle(tile.Position, tile.Size)
            g.DrawImage(tile.Image, tileRect)

            ' Draw selection rectangle
            If tile.Selected Then
                Using pen As New Pen(Color.Blue)
                    pen.DashStyle = Drawing2D.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 "

    ' Handle left mouse button down
    Private Sub HandleLeftMouseDown(e As MouseEventArgs, currentPoint As Point, clickedTile As MapTile)
        If selectedTiles.Count > 1 Then
            ' Check if over group resize handles first
            Dim groupDirection As MapTile.ResizeDirections = GetGroupResizeDirection(currentPoint)
            If groupDirection <> MapTile.ResizeDirections.None Then
                ' Start group resizing
                StartGroupResizing(currentPoint, groupDirection)
                Return
            End If
        End If

        ' If not over group resize handles, proceed
        If clickedTile IsNot Nothing Then
            HandleTileClick(e, currentPoint, clickedTile)
        Else
            ' No tile clicked - 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)
        Dim clickedTile As MapTile = GetTileAtPoint(currentPoint)

        If clickedTile IsNot Nothing Then
            ' Select the tile
            If Not selectedTiles.Contains(clickedTile) Then
                DeselectAllTiles()
                selectedTile = clickedTile
                selectedTile.Selected = True
                selectedTiles.Clear()
                selectedTiles.Add(selectedTile)
            End If

            ' Bring the selected tile to the front
            BringTileToFront(selectedTile)

            Refresh()
            ' Show context menu at mouse position
            ctmMenu?.Show(Me, e.Location)
        Else
            ' No tile was right-clicked; do not show context menu
            ' Optionally, deselect all tiles
            ' DeselectAllTiles()
            ' Refresh()
        End If
    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(newPos.X / _gridSize) * _gridSize
                newPos.Y = CInt(newPos.Y / _gridSize) * _gridSize
            End If

            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

    ' Update cursor based on mouse position
    Private Sub UpdateCursor(currentPoint As Point)
        Dim cursorSet As Boolean = False

        ' Check if over group resize handles
        If selectedTiles.Count > 1 Then
            Dim groupDirection As MapTile.ResizeDirections = GetGroupResizeDirection(currentPoint)
            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, currentPoint)
                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)

        Dim currentPoint As Point = ClientToCanvas(e.Location)
        Dim clickedTile As MapTile = GetTileAtPoint(currentPoint)

        If e.Button = MouseButtons.Left Then
            HandleLeftMouseDown(e, currentPoint, clickedTile)
        ElseIf e.Button = MouseButtons.Right Then
            ' Start panning
            isPanning = True
            panStartPoint = e.Location ' Store the mouse position in client coordinates
            scrollStartPoint = AutoScrollPosition
        ElseIf e.Button = MouseButtons.Right Then
            ' Start panning
            isPanning = True
            hasPanned = False ' Reset the flag
            panStartPoint = e.Location ' Store the mouse position in client coordinates
            scrollStartPoint = AutoScrollPosition
        End If
    End Sub

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

        Dim currentPoint As Point = ClientToCanvas(e.Location)

        If resizing Then
            HandleResizing(currentPoint)
        ElseIf dragging AndAlso selectedTiles.Count > 0 Then
            HandleDragging(currentPoint)
        ElseIf isSelecting Then
            ' Update selection rectangle
            selectionEndPoint = currentPoint
            Refresh()
        ElseIf isPanning AndAlso e.Button = MouseButtons.Right Then
            ' Handle panning
            HandlePanning(e.Location)
        Else
            ' Update cursor based on hover over resize handles
            UpdateCursor(currentPoint)
        End If
    End Sub
    Protected Overrides Sub OnMouseUp(e As MouseEventArgs)
        ' For right-click, prevent the base method from displaying the default context menu
        If e.Button <> MouseButtons.Right Then
            MyBase.OnMouseUp(e)
        End If

        ' Handle selection rectangle completion
        If isSelecting Then
            isSelecting = False
            ' Create the selection rectangle
            selectionRectangle = GetSelectionRectangle()
            ' Select tiles within the rectangle
            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

        ' Handle right mouse button up event
        If e.Button = MouseButtons.Right Then
            If isPanning Then
                ' Stop panning
                isPanning = False
                Cursor = Cursors.Default
                If hasPanned Then
                    ' If panning occurred, we don't show the context menu
                    hasPanned = False
                Else
                    ' No panning occurred, check if a tile was right-clicked
                    HandleRightMouseUp(e)
                End If
            Else
                ' Not panning, handle right-click
                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)
        drgevent.Effect = If(drgevent.Data.GetDataPresent("ImageFormat"), DragDropEffects.Copy, DragDropEffects.None)
    End Sub

    ' Handle drag drop event
    Protected Overrides Sub OnDragDrop(drgevent As DragEventArgs)
        MyBase.OnDragDrop(drgevent)
        If drgevent.Data.GetDataPresent("ImageFormat") Then
            HandleDragDrop(drgevent)
        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 Me.Focused Then
            Me.Focus()
        End If
    End Sub
    Protected Overrides Sub OnMouseWheel(e As MouseEventArgs)
        ' Ensure the control has focus to receive keyboard and mouse wheel events
        Me.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 "
    ' Handle panning
    Private Sub HandlePanning(currentMousePosition As Point)
        ' Check if mouse has moved beyond a small threshold to start panning
        Dim threshold As Integer = 2 ' Adjust this threshold as needed
        If Not hasPanned AndAlso (Math.Abs(currentMousePosition.X - panStartPoint.X) > threshold OrElse
                              Math.Abs(currentMousePosition.Y - panStartPoint.Y) > threshold) Then
            hasPanned = True
        End If

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

            ' Invert the delta values to move the canvas in the same direction as the mouse
            deltaX = -deltaX
            deltaY = -deltaY

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

            ' Clamp the scroll position to prevent scrolling beyond the content boundaries
            Dim maxScrollX As Integer = Math.Max(0, AutoScrollMinSize.Width - ClientSize.Width)
            Dim maxScrollY As Integer = Math.Max(0, AutoScrollMinSize.Height - ClientSize.Height)

            newScrollPosition.X = Math.Max(-maxScrollX, Math.Min(0, newScrollPosition.X))
            newScrollPosition.Y = Math.Max(-maxScrollY, Math.Min(0, 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 If
    End Sub
    ' Convert client point to canvas point
    Private 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()
        Dim scaledWidth As Integer = CInt(_mapWidth * ZoomLevel)
        Dim scaledHeight As Integer = CInt(_mapHeight * ZoomLevel)

        ' Ensure AutoScrollMinSize is at least as large as ClientSize
        Dim minWidth As Integer = Math.Max(scaledWidth, ClientSize.Width)
        Dim minHeight As Integer = Math.Max(scaledHeight, ClientSize.Height)

        ' Update AutoScrollMinSize
        AutoScrollMinSize = New Size(minWidth, minHeight)
    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 focusX As Single = (-scrollPos.X + focus.X) / oldZoomLevel
        Dim focusY As Single = (-scrollPos.Y + focus.Y) / oldZoomLevel

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

        ' Update scrollbars to get new AutoScrollMinSize
        UpdateScrollBars()

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

        ' Clamp the scroll positions within allowable ranges
        newScrollX = Math.Max(-maxScrollX, Math.Min(0, newScrollX))
        newScrollY = Math.Max(-maxScrollY, Math.Min(0, 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
            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) As MapTile
        For i As Integer = _tiles.Count - 1 To 0 Step -1 ' Start from topmost tile
            Dim tile = _tiles(i)
            Dim rect As New Rectangle(tile.Position, tile.Size)
            If rect.Contains(point) Then
                Return tile
            End If
        Next
        Return Nothing
    End Function

    ' Determine if mouse is over resize handles of a tile
    Private Function GetResizeDirection(tile As MapTile, point As Point) As MapTile.ResizeDirections
        Const handleSize As Integer = 10 ' Size of the resize handle area
        Dim rect = New Rectangle(tile.Position, tile.Size)
        Dim direction As MapTile.ResizeDirections = MapTile.ResizeDirections.None

        ' Check corners
        If New Rectangle(rect.Right - handleSize, rect.Bottom - handleSize, handleSize, handleSize).Contains(point) Then
            direction = MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Bottom ' Bottom-right corner
        ElseIf New Rectangle(rect.Left, rect.Bottom - handleSize, handleSize, handleSize).Contains(point) Then
            direction = MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Bottom ' Bottom-left corner
        ElseIf New Rectangle(rect.Right - handleSize, rect.Top, handleSize, handleSize).Contains(point) Then
            direction = MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Top ' Top-right corner
        ElseIf New Rectangle(rect.Left, rect.Top, handleSize, handleSize).Contains(point) Then
            direction = MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Top ' Top-left corner
        End If

        Return direction
    End Function

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

        Const handleSize As Integer = 10 ' Size of the resize handle area
        Dim bounds = GetSelectedTilesBounds()
        Dim direction As MapTile.ResizeDirections = MapTile.ResizeDirections.None

        ' Check corners
        If New Rectangle(bounds.Right - handleSize, bounds.Bottom - handleSize, handleSize, handleSize).Contains(point) Then
            direction = MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Bottom ' Bottom-right corner
        ElseIf New Rectangle(bounds.Left - (handleSize \ 2), bounds.Bottom - handleSize, handleSize, handleSize).Contains(point) Then
            direction = MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Bottom ' Bottom-left corner
        ElseIf New Rectangle(bounds.Right - handleSize, bounds.Top - (handleSize \ 2), handleSize, handleSize).Contains(point) Then
            direction = MapTile.ResizeDirections.Right Or MapTile.ResizeDirections.Top ' Top-right corner
        ElseIf New Rectangle(bounds.Left - (handleSize \ 2), bounds.Top - (handleSize \ 2), handleSize, handleSize).Contains(point) Then
            direction = MapTile.ResizeDirections.Left Or MapTile.ResizeDirections.Top ' Top-left corner
        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 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
        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 (Control.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
#End Region

#Region " Event Handlers for Context Menu "

    ' 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 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)
            Refresh()
            RaiseEvent CanvasModified()
        End If
    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
            ' Rotate the image
            Dim rotatedImage As Image = RotateImage(tile.Image, angle)

            ' Adjust the size
            Dim newSize As Size = rotatedImage.Size

            ' Calculate new position to maintain the center point
            Dim oldCenter As New Point(tile.Position.X + (tile.Size.Width / 2), tile.Position.Y + (tile.Size.Height / 2))
            Dim newPosition As New Point(oldCenter.X - (newSize.Width / 2), oldCenter.Y - (newSize.Height / 2))

            ' Apply snapping if necessary
            If snappingEnabled AndAlso Not IsShiftKeyPressed Then
                newPosition.X = CInt(newPosition.X / _gridSize) * _gridSize
                newPosition.Y = CInt(newPosition.Y / _gridSize) * _gridSize
            End If

            ' Update the tile's image, size, and position
            tile.Image = rotatedImage
            tile.Size = newSize
            tile.Position = newPosition
        Next
    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

    ' Handle tile click actions
    Private Sub HandleTileClick(e As MouseEventArgs, currentPoint As Point, clickedTile As MapTile)
        ' Check if over resize handles of a single tile
        Dim direction As MapTile.ResizeDirections = GetResizeDirection(clickedTile, currentPoint)
        If direction <> MapTile.ResizeDirections.None Then
            ' Start resizing single tile
            StartSingleTileResizing(clickedTile, currentPoint, direction)
        ElseIf IsCtrlKeyPressed Then
            ' CTRL + Click: Duplicate the tile
            DuplicateTile(clickedTile, currentPoint)
        Else
            If Not selectedTiles.Contains(clickedTile) Then
                ' Single tile selection
                DeselectAllTiles()
                selectedTile = clickedTile
                selectedTile.Selected = True
                selectedTiles.Clear()
                selectedTiles.Add(selectedTile)
            Else
                selectedTile = clickedTile ' Ensure clickedTile is set as selectedTile
            End If

            ' Initialize variables for dragging
            For Each tile In selectedTiles
                tile.InitialPosition = tile.Position
            Next
            initialMousePosition = currentPoint

            ' Bring the selected tile to the front
            BringTileToFront(selectedTile)

            dragging = True
            Refresh()
        End If
    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

        ' Bring the tile to the front
        BringTileToFront(selectedTile)

        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

        ' Bring the duplicated tile to the front
        BringTileToFront(selectedTile)

        dragging = True
        Refresh()
    End Sub

    ' Resize a single tile
    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(newSize.Width / _gridSize) * _gridSize
            newSize.Height = CInt(newSize.Height / _gridSize) * _gridSize
            newPosition.X = CInt(newPosition.X / _gridSize) * _gridSize
            newPosition.Y = CInt(newPosition.Y / _gridSize) * _gridSize
        End If

        selectedTile.Size = newSize
        selectedTile.Position = newPosition
        Refresh()
        RaiseEvent CanvasModified()
    End Sub

    ' Resize group of tiles
    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

        ' 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(newPosX / _gridSize) * _gridSize
                newPosY = CInt(newPosY / _gridSize) * _gridSize
                newWidth = CInt(newWidth / _gridSize) * _gridSize
                newHeight = CInt(newHeight / _gridSize) * _gridSize
            End If

            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()
        RaiseEvent CanvasModified()
    End Sub

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

#End Region

End Class


Can you invert the dragging please?

So when I drag the canvas goes in the direction I'm dragging instead of the other way?