Imports System.IO
Imports System.Threading.Tasks

Public Class FrmWorld
    Private dragBoxFromMouseDown As Rectangle ' Variable to track the drag rectangle

#Region " Initiate "
    Private Sub FrmWorld_Load(sender As Object, e As EventArgs) Handles MyBase.Load

        PopulateTreeView("D:\01 - Grags\Visual Studio\Bizarria New\Data\")
        ' 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 = "16 x 16"
    End Sub
#End Region

#Region " Populate TreeView and FlowPanel "
    Private Async Function LoadImagesAsync(directoryInfo As DirectoryInfo) As Task
        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

        For Each imageFile As FileInfo In imageFiles
            Await Task.Run(Sub()
                               Dim pictureBox As New PictureBox() With {
                                   .Image = Image.FromFile(imageFile.FullName),
                                   .SizeMode = PictureBoxSizeMode.Zoom,
                                   .Width = 100,
                                   .Height = 100,
                                   .Tag = imageFile.FullName ' Store the file path
                               }
                               ' Add event handlers for drag initiation
                               AddHandler pictureBox.MouseDown, AddressOf PictureBox_MouseDown
                               AddHandler pictureBox.MouseMove, AddressOf PictureBox_MouseMove
                               AddHandler pictureBox.MouseUp, AddressOf PictureBox_MouseUp
                               FLPImages.Invoke(Sub() FLPImages.Controls.Add(pictureBox))
                           End Sub)
        Next
    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}
        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}
            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 PictureBox in FLPImages
    Private Sub PictureBox_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 PictureBox in FLPImages
    Private Sub PictureBox_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.X, e.Y) Then
                Dim pictureBox As PictureBox = CType(sender, PictureBox)
                ' Get the image from the PictureBox
                Dim image As Image = pictureBox.Image
                If image IsNot Nothing Then
                    ' Use custom data format to pass the image
                    Dim dataObj As New DataObject()
                    dataObj.SetData("ImageFormat", image)
                    ' Optionally pass size information
                    dataObj.SetData("ImageSize", New Size(PicCanvas.GridSize, PicCanvas.GridSize)) ' Adjust tile size as needed
                    ' Start the drag-and-drop operation
                    pictureBox.DoDragDrop(dataObj, DragDropEffects.Copy)
                End If
                ' Reset drag rectangle
                dragBoxFromMouseDown = Rectangle.Empty
            End If
        End If
    End Sub

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

    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 Class

Public Class MapCanvas
    Inherits UserControl

    Private tiles As New List(Of MapTile)
    Private _gridSize As Integer = 16 ' Default grid size (can be made adjustable)
    Private snappingEnabled As Boolean = True
    Private _showGrid As Boolean = True
    Private selectedTile As MapTile = Nothing
    Private dragging As Boolean = False
    Private dragStartPoint As Point
    Private resizing As Boolean = False
    Private resizeStartPoint As Point

    Public Sub New()
        Me.DoubleBuffered = True
        Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or
                    ControlStyles.OptimizedDoubleBuffer Or
                    ControlStyles.ResizeRedraw Or
                    ControlStyles.UserPaint, True)
        Me.UpdateStyles()
    End Sub

    ' 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
                Me.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
            Me.Invalidate()
        End Set
    End Property

    ' Method to add a tile
    Public Sub AddTile(tile As MapTile)
        tiles.Add(tile)
        Me.Invalidate()
    End Sub

    ' Override OnPaint to draw everything
    Protected Overrides Sub OnPaint(e As PaintEventArgs)
        MyBase.OnPaint(e)

        ' Draw the grid if enabled
        If _showGrid Then
            DrawGrid(e.Graphics)
        End If

        ' Draw the tiles
        For Each tile In tiles
            e.Graphics.DrawImage(tile.Image, New Rectangle(tile.Position, tile.Size))

            ' Draw selection rectangle and resize handles
            If tile.Selected Then
                Using pen As New Pen(Color.Blue)
                    pen.DashStyle = Drawing2D.DashStyle.Dash
                    e.Graphics.DrawRectangle(pen, New Rectangle(tile.Position, tile.Size))
                End Using

                ' Draw resize handles at corners
                Dim handleSize As Integer = 6
                Dim rect = New Rectangle(tile.Position, tile.Size)

                Using brush As New SolidBrush(Color.White)
                    Using pen As New Pen(Color.Black)
                        ' Top-left corner
                        e.Graphics.FillRectangle(brush,
                                             CInt(rect.Left - handleSize / 2),
                                             CInt(rect.Top - handleSize / 2),
                                             handleSize,
                                             handleSize)
                        e.Graphics.DrawRectangle(pen,
                                             CInt(rect.Left - handleSize / 2),
                                             CInt(rect.Top - handleSize / 2),
                                             handleSize,
                                             handleSize)

                        ' Top-right corner
                        e.Graphics.FillRectangle(brush,
                                             CInt(rect.Right - handleSize / 2),
                                             CInt(rect.Top - handleSize / 2),
                                             handleSize,
                                             handleSize)
                        e.Graphics.DrawRectangle(pen,
                                             CInt(rect.Right - handleSize / 2),
                                             CInt(rect.Top - handleSize / 2),
                                             handleSize,
                                             handleSize)

                        ' Bottom-left corner
                        e.Graphics.FillRectangle(brush,
                                             CInt(rect.Left - handleSize / 2),
                                             CInt(rect.Bottom - handleSize / 2),
                                             handleSize,
                                             handleSize)
                        e.Graphics.DrawRectangle(pen,
                                             CInt(rect.Left - handleSize / 2),
                                             CInt(rect.Bottom - handleSize / 2),
                                             handleSize,
                                             handleSize)

                        ' Bottom-right corner
                        e.Graphics.FillRectangle(brush,
                                             CInt(rect.Right - handleSize / 2),
                                             CInt(rect.Bottom - handleSize / 2),
                                             handleSize,
                                             handleSize)
                        e.Graphics.DrawRectangle(pen,
                                             CInt(rect.Right - handleSize / 2),
                                             CInt(rect.Bottom - handleSize / 2),
                                             handleSize,
                                             handleSize)
                    End Using
                End Using
            End If
        Next
    End Sub
    Private Sub DrawGrid(g As Graphics)
        Using pen As New Pen(Color.LightGray)
            For x As Integer = 0 To Me.Width Step _gridSize
                g.DrawLine(pen, x, 0, x, Me.Height)
            Next
            For y As Integer = 0 To Me.Height Step _gridSize
                g.DrawLine(pen, 0, y, Me.Width, y)
            Next
        End Using
    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 clickedPoint As Point = e.Location
            Dim clickedTile As MapTile = GetTileAtPoint(clickedPoint)

            If clickedTile IsNot Nothing Then
                ' Check if over resize handles (we'll use the corners)
                Dim direction As MapTile.ResizeDirections = GetResizeDirection(clickedTile, clickedPoint)
                If direction <> MapTile.ResizeDirections.None Then
                    ' Start resizing
                    resizing = True
                    clickedTile.IsResizing = True
                    clickedTile.CurrentResizeDirection = direction
                    selectedTile = clickedTile
                    resizeStartPoint = clickedPoint
                Else
                    ' Start dragging/moving
                    If selectedTile IsNot Nothing Then
                        selectedTile.Selected = False
                    End If
                    selectedTile = clickedTile
                    selectedTile.Selected = True
                    dragging = True
                    dragStartPoint = clickedPoint
                End If
                Me.Invalidate()
            Else
                ' Deselect any selected tile
                If selectedTile IsNot Nothing Then
                    selectedTile.Selected = False
                    selectedTile = Nothing
                    Me.Invalidate()
                End If
            End If
        End If
    End Sub

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

        If resizing AndAlso selectedTile IsNot Nothing Then
            HandleResizing(e.Location)
        ElseIf dragging AndAlso selectedTile IsNot Nothing Then
            HandleDragging(e.Location)
        Else
            ' Update cursor based on hover over resize handles
            Dim tile = GetTileAtPoint(e.Location)
            If tile IsNot Nothing Then
                Dim direction As MapTile.ResizeDirections = GetResizeDirection(tile, e.Location)
                Me.Cursor = GetCursorForDirection(direction)
            Else
                Me.Cursor = Cursors.Default
            End If
        End If
    End Sub

    Protected Overrides Sub OnMouseUp(e As MouseEventArgs)
        MyBase.OnMouseUp(e)
        dragging = False
        resizing = False
        If selectedTile IsNot Nothing Then
            selectedTile.IsResizing = False
            selectedTile.CurrentResizeDirection = MapTile.ResizeDirections.None
        End If
    End Sub
    ' Helper method to get the tile at a specific point
    Private Function GetTileAtPoint(point As Point) As MapTile
        For Each tile In tiles
            Dim rect As New Rectangle(tile.Position, tile.Size)
            If rect.Contains(point) Then
                Return tile
            End If
        Next
        Return Nothing
    End Function

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

    Protected Overrides Sub OnDragEnter(drgevent As DragEventArgs)
        MyBase.OnDragEnter(drgevent)
        If drgevent.Data.GetDataPresent("ImageFormat") Then
            drgevent.Effect = DragDropEffects.Copy
        Else
            drgevent.Effect = DragDropEffects.None
        End If
    End Sub

    Protected Overrides Sub OnDragDrop(drgevent As DragEventArgs)
        MyBase.OnDragDrop(drgevent)
        If drgevent.Data.GetDataPresent("ImageFormat") Then
            Dim image As Image = CType(drgevent.Data.GetData("ImageFormat"), Image)
            ' 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
            Dim point As Point = Me.PointToClient(New Point(drgevent.X, drgevent.Y))

            ' Apply snapping to position if 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,
            .Position = point,
            .Size = resizedImage.Size
        }
            Me.AddTile(newTile)
        End If
    End Sub

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

        ' Update tile position
        Dim newPos As Point = New Point(selectedTile.Position.X + deltaX, selectedTile.Position.Y + deltaY)

        ' Apply snapping if 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

        selectedTile.Position = newPos
        dragStartPoint = currentPoint
        Me.Invalidate()
    End Sub

    ' Handle resizing
    Private Sub HandleResizing(currentPoint As Point)
        Dim deltaX As Integer = currentPoint.X - resizeStartPoint.X
        Dim deltaY As Integer = currentPoint.Y - resizeStartPoint.Y

        Dim newSize As Size = selectedTile.Size
        Dim newPosition As Point = selectedTile.Position

        ' Check each direction and adjust size and position
        If selectedTile.CurrentResizeDirection.HasFlag(MapTile.ResizeDirections.Right) Then
            newSize.Width += deltaX
        End If
        If selectedTile.CurrentResizeDirection.HasFlag(MapTile.ResizeDirections.Bottom) Then
            newSize.Height += deltaY
        End If
        If selectedTile.CurrentResizeDirection.HasFlag(MapTile.ResizeDirections.Left) Then
            newSize.Width -= deltaX
            newPosition.X += deltaX
        End If
        If selectedTile.CurrentResizeDirection.HasFlag(MapTile.ResizeDirections.Top) Then
            newSize.Height -= deltaY
            newPosition.Y += deltaY
        End If

        ' Ensure size is not negative or zero
        If newSize.Width <= 0 Then newSize.Width = 1
        If newSize.Height <= 0 Then newSize.Height = 1

        ' Apply snapping if 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
        resizeStartPoint = currentPoint
        Me.Invalidate()
    End Sub
    ' Determine if mouse is over resize handles
    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 and sides
        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
    ' 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
    Private ReadOnly Property IsShiftKeyPressed As Boolean
        Get
            Return (Control.ModifierKeys And Keys.Shift) = Keys.Shift
        End Get
    End Property

    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
        Me.Invalidate()
    End Sub

    Public Sub SetMapSize(width As Integer, height As Integer)
        Me.AutoScrollMinSize = New Size(width, height)
        Me.Invalidate()
    End Sub
End Class


When I drag a picture from FLPImages, the image doesn't draw until I place it on PicCanvas
When I move pictures or resize pictures. They do not follow my cursor making it impossible to fine tune it. 
When I scroll away from Images I've placed on the grid the grid contorts. When I return to where my images are the images have disappeared until I click on them.