Imports System.IO
Imports System.Text.Json
Imports System.Text.Json.Serialization
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.ComponentModel

Public Class frmGame

#Region "Variables"
    Private movingObjects As New List(Of MovingObject)()
    Private movingObjectsTimer As Timer

    Private CollectedCoins As Integer = 0
    Private playerX As Integer
    Private playerY As Integer
    Private hasKey As Boolean = False
    Private justWarped As Boolean = False
    Public Property RoomFilePath As String
    Private SelectedMusic As String

    Private Const INDENT As Integer = 20
    Private Const WALL_THICKNESS As Integer = 10

    Private SQCOUNTX As Integer
    Private SQCOUNTY As Integer

    Private SQSizeX As Integer
    Private SQSizeY As Integer

    Private wallsHatchStyle As HatchStyle
    Private wallsColor1 As Color
    Private wallsColor2 As Color
    Private floorsHatchStyle As HatchStyle
    Private floorsColor1 As Color
    Private floorsColor2 As Color

    Private verticalWalls()() As Boolean
    Private horizontalWalls()() As Boolean
    Private placedItems()() As String
    Private verticalWallItems()() As String
    Private horizontalWallItems()() As String
    Private floorsUseEdgeStyle()() As Boolean

    Private imageDictionary As New Dictionary(Of String, Image)
    Private animatedImages As New List(Of Image)

    Private animationTimer As Timer
    Private isRoomDataLoaded As Boolean = False

    Private ReadOnly projectDir As String = GetProjectDirectory()
    Private ReadOnly ImagesFolder As String = Path.Combine(projectDir, "Data", "Images")
    Private ReadOnly musicFolder As String = Path.Combine(projectDir, "Data", "Music")
    Private ReadOnly SFXFolder As String = Path.Combine(projectDir, "Data", "Sound Effects")

    Private musicManager As New MusicManager()
    Private soundManager As New SFXManager(SFXFolder)

    Private Class MovingObject
        Public Property X As Integer
        Public Property Y As Integer
        Public Property DeltaX As Integer
        Public Property DeltaY As Integer
        Public Property IsBall As Boolean
        Public Property IsBlock As Boolean
        Public Property MoveInterval As Integer
        Public Property NextMoveTime As DateTime
    End Class
#End Region

#Region "Movement"
    Private Sub FindPlayerStartPosition()
        For x As Integer = 0 To placedItems.Length - 1
            If placedItems(x) IsNot Nothing Then
                For y As Integer = 0 To placedItems(x).Length - 1
                    Dim imageKey As String = placedItems(x)(y)
                    If imageKey = "picStart" Then
                        playerX = x
                        playerY = y
                        placedItems(x)(y) = Nothing
                        Exit Sub
                    End If
                Next
            End If
        Next
    End Sub

    Private Sub frmGame_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown
        Dim moved As Boolean = False
        Select Case e.KeyCode
            Case Keys.Up, Keys.P
                moved = AttemptMove("Up")
            Case Keys.Down, Keys.L
                moved = AttemptMove("Down")
            Case Keys.Left, Keys.Z
                moved = AttemptMove("Left")
            Case Keys.Right, Keys.X
                moved = AttemptMove("Right")
        End Select
        If moved Then
            picTest.Invalidate()
        End If
    End Sub

    Private Function AttemptMove(direction As String) As Boolean
        Dim deltaX As Integer = 0
        Dim deltaY As Integer = 0
        Select Case direction
            Case "Up"
                deltaY = -1
            Case "Down"
                deltaY = 1
            Case "Left"
                deltaX = -1
            Case "Right"
                deltaX = 1
        End Select

        Dim newX As Integer = playerX + deltaX
        Dim newY As Integer = playerY + deltaY

        If newX < 0 OrElse newX >= SQCOUNTX OrElse newY < 0 OrElse newY >= SQCOUNTY Then
            Return False
        End If

        If Not CanMoveTo(newX, newY, deltaX, deltaY) Then
            Return False
        End If

        Dim targetItem As String = placedItems(newX)(newY)

        If String.IsNullOrEmpty(targetItem) Then
            playerX = newX
            playerY = newY
            soundManager.PlaySoundEffect("Move")
            CheckForWarp()
            Return True
        ElseIf targetItem = "picBlock1" Then
            Dim blockMoved As Boolean = AttemptPushBlock(newX, newY, deltaX, deltaY)
            If blockMoved Then
                playerX = newX
                playerY = newY
                soundManager.PlaySoundEffect("Move")
                CheckForWarp()
                Return True
            Else
                Return False
            End If
        ElseIf targetItem = "picBlockStopper1" Then
            playerX = newX
            playerY = newY
            soundManager.PlaySoundEffect("Move")
            CheckForWarp()
            Return True
        ElseIf targetItem = "picBall" Then
            Dim ballMoved As Boolean = AttemptPushBall(newX, newY, deltaX, deltaY)
            If ballMoved Then
                playerX = newX
                playerY = newY
                soundManager.PlaySoundEffect("Move")
                CheckForWarp()
                Return True
            Else
                Return False
            End If
        ElseIf targetItem = "picCoin" Then
            CollectedCoins += 1
            placedItems(newX)(newY) = Nothing
            playerX = newX
            playerY = newY
            soundManager.PlaySoundEffect("Coin")
            soundManager.PlaySoundEffect("Move")
            CheckForWarp()
            Return True
        ElseIf targetItem = "picKey" Then
            hasKey = True
            placedItems(newX)(newY) = Nothing
            playerX = newX
            playerY = newY
            soundManager.PlaySoundEffect("Key")
            soundManager.PlaySoundEffect("Move")
            CheckForWarp()
            Return True
        ElseIf targetItem = "picGate" Then
            If hasKey Then
                placedItems(newX)(newY) = Nothing
                playerX = newX
                playerY = newY
                soundManager.PlaySoundEffect("Door Open")
                soundManager.PlaySoundEffect("Move")
                CheckForWarp()
                Return True
            Else
                Return False
            End If
        ElseIf targetItem = "picFinish" Then
            playerX = newX
            playerY = newY
            soundManager.PlaySoundEffect("Move")
            GameWon()
            Return True
        ElseIf targetItem = "picWarp" Then
            playerX = newX
            playerY = newY
            soundManager.PlaySoundEffect("Move")
            HandleWarp("picWarp")
            Return True
        ElseIf targetItem = "picWarp2" Then
            playerX = newX
            playerY = newY
            soundManager.PlaySoundEffect("Move")
            HandleWarp("picWarp2")
            Return True
        Else
            Return False
        End If
    End Function

    Private Sub CheckForWarp()
        Dim currentItem As String = placedItems(playerX)(playerY)
        If currentItem = "picWarp" AndAlso Not justWarped Then
            HandleWarp("picWarp")
        ElseIf currentItem = "picWarp2" AndAlso Not justWarped Then
            HandleWarp("picWarp2")
        ElseIf currentItem <> "picWarp" AndAlso currentItem <> "picWarp2" Then
            justWarped = False
        End If
    End Sub

    Private Sub HandleWarp(warpType As String)
        If Not justWarped Then
            Dim warpPositions As New List(Of Point)
            For x As Integer = 0 To placedItems.Length - 1
                If placedItems(x) IsNot Nothing Then
                    For y As Integer = 0 To placedItems(x).Length - 1
                        If placedItems(x)(y) = warpType Then
                            warpPositions.Add(New Point(x, y))
                        End If
                    Next
                End If
            Next
            warpPositions.Remove(New Point(playerX, playerY))
            If warpPositions.Count = 1 Then
                justWarped = True
                soundManager.PlaySoundEffect("Warp")
                playerX = warpPositions(0).X
                playerY = warpPositions(0).Y
                CheckForWarp()
            End If
        End If
    End Sub

    Private Function AttemptPushBall(ballX As Integer, ballY As Integer, deltaX As Integer, deltaY As Integer) As Boolean
        Dim nextX As Integer = ballX + deltaX
        Dim nextY As Integer = ballY + deltaY

        If nextX < 0 OrElse nextX >= SQCOUNTX OrElse nextY < 0 OrElse nextY >= SQCOUNTY Then
            Return False
        End If

        If Not CanBallMoveTo(ballX, ballY, deltaX, deltaY) Then
            Return False
        End If

        Dim targetItem As String = placedItems(nextX)(nextY)
        Dim playerAtNextPos As Boolean = (nextX = playerX AndAlso nextY = playerY)

        If String.IsNullOrEmpty(targetItem) AndAlso Not playerAtNextPos Then
            placedItems(ballX)(ballY) = Nothing

            Dim newBall As New MovingObject With {
                .X = ballX,
                .Y = ballY,
                .DeltaX = deltaX,
                .DeltaY = deltaY,
                .IsBall = True,
                .IsBlock = False,
                .MoveInterval = 100,
                .NextMoveTime = DateTime.Now.AddMilliseconds(100)
            }

            movingObjects.Add(newBall)

            If movingObjectsTimer Is Nothing Then
                movingObjectsTimer = New Timer()
                AddHandler movingObjectsTimer.Tick, AddressOf OnMovingObjectsTimerTick
                movingObjectsTimer.Interval = 50
                movingObjectsTimer.Start()
            End If

            soundManager.PlaySoundEffect("Push Ball")

            Return True
        Else
            Return False
        End If
    End Function

    Private Sub CheckForFlipper(movingObject As MovingObject)
        Dim x As Integer = movingObject.X
        Dim y As Integer = movingObject.Y
        Dim onFlipper As Boolean = False

        ' Check Right wall (Flipper1: Push left)
        If x < verticalWallItems.Length - 1 AndAlso verticalWallItems(x + 1)(y) IsNot Nothing Then
            Dim item As String = verticalWallItems(x + 1)(y)
            If item = "picFlipper1" Then
                movingObject.DeltaX = -1
                movingObject.DeltaY = 0
                movingObject.MoveInterval = If(movingObject.IsBall, 100, 50)
                movingObject.NextMoveTime = DateTime.Now.AddMilliseconds(movingObject.MoveInterval)
                soundManager.PlaySoundEffect("Flipper")
                onFlipper = True
            End If
        End If

        ' Check Left wall (Flipper2: Push right)
        If x > 0 AndAlso verticalWallItems(x)(y) IsNot Nothing Then
            Dim item As String = verticalWallItems(x)(y)
            If item = "picFlipper2" Then
                movingObject.DeltaX = 1
                movingObject.DeltaY = 0
                movingObject.MoveInterval = If(movingObject.IsBall, 100, 50)
                movingObject.NextMoveTime = DateTime.Now.AddMilliseconds(movingObject.MoveInterval)
                soundManager.PlaySoundEffect("Flipper")
                onFlipper = True
            End If
        End If

        ' Check Bottom wall (Flipper3: Push up)
        If y < horizontalWallItems(x).Length - 1 AndAlso horizontalWallItems(x)(y + 1) IsNot Nothing Then
            Dim item As String = horizontalWallItems(x)(y + 1)
            If item = "picFlipper3" Then
                movingObject.DeltaX = 0
                movingObject.DeltaY = -1
                movingObject.MoveInterval = If(movingObject.IsBall, 100, 50)
                movingObject.NextMoveTime = DateTime.Now.AddMilliseconds(movingObject.MoveInterval)
                soundManager.PlaySoundEffect("Flipper")
                onFlipper = True
            End If
        End If

        ' Check Top wall (Flipper4: Push down)
        If y > 0 AndAlso horizontalWallItems(x)(y) IsNot Nothing Then
            Dim item As String = horizontalWallItems(x)(y)
            If item = "picFlipper4" Then
                movingObject.DeltaX = 0
                movingObject.DeltaY = 1
                movingObject.MoveInterval = If(movingObject.IsBall, 100, 50)
                movingObject.NextMoveTime = DateTime.Now.AddMilliseconds(movingObject.MoveInterval)
                soundManager.PlaySoundEffect("Flipper")
                onFlipper = True
            End If
        End If

        ' If the object is not on a flipper and doesn't have a direction yet, do nothing
        ' This allows the moving object to retain its current direction
    End Sub

    Private Sub OnMovingObjectsTimerTick(sender As Object, e As EventArgs)
        Dim currentTime As DateTime = DateTime.Now

        For i As Integer = movingObjects.Count - 1 To 0 Step -1
            Dim movingObject = movingObjects(i)

            If movingObject.NextMoveTime > currentTime Then
                Continue For
            End If

            Dim deltaX As Integer = movingObject.DeltaX
            Dim deltaY As Integer = movingObject.DeltaY

            Dim currentX As Integer = movingObject.X
            Dim currentY As Integer = movingObject.Y
            Dim objectStopped As Boolean = False

            Dim nextX As Integer = currentX + deltaX
            Dim nextY As Integer = currentY + deltaY

            If nextX < 0 OrElse nextX >= SQCOUNTX OrElse nextY < 0 OrElse nextY >= SQCOUNTY Then
                objectStopped = True
            Else
                If Not CanObjectMoveTo(movingObject, currentX, currentY, deltaX, deltaY) Then
                    objectStopped = True
                Else
                    Dim targetItem As String = placedItems(nextX)(nextY)
                    Dim playerAtNextPos As Boolean = (nextX = playerX AndAlso nextY = playerY)
                    Dim objectAtNextPos As Boolean = movingObjects.Any(Function(o) o.X = nextX AndAlso o.Y = nextY AndAlso o IsNot movingObject)

                    If String.IsNullOrEmpty(targetItem) AndAlso Not playerAtNextPos AndAlso Not objectAtNextPos Then
                        movingObject.X = nextX
                        movingObject.Y = nextY
                        movingObject.NextMoveTime = currentTime.AddMilliseconds(movingObject.MoveInterval)

                        ' After moving, check for flippers to possibly change direction
                        CheckForFlipper(movingObject)
                    Else
                        objectStopped = True
                    End If
                End If
            End If

            If objectStopped Then
                If IsObjectOnFlipper(movingObject) Then
                    ' The object is on a flipper, so we keep it in movingObjects
                    movingObject.NextMoveTime = currentTime.AddMilliseconds(movingObject.MoveInterval)
                    ' Update movement direction based on current flipper
                    CheckForFlipper(movingObject)
                Else
                    ' The object is not on a flipper, so we can stop tracking it
                    If movingObject.IsBall Then
                        placedItems(movingObject.X)(movingObject.Y) = "picBall"
                    ElseIf movingObject.IsBlock Then
                        placedItems(movingObject.X)(movingObject.Y) = "picBlock1"
                    End If
                    soundManager.PlaySoundEffect("Block Stop")
                    movingObjects.RemoveAt(i)
                End If
            End If
        Next

        ' Do not stop the timer; keep checking for objects on flippers
        picTest.Invalidate()
    End Sub

    Private Function IsObjectOnFlipper(movingObject As MovingObject) As Boolean
        Dim x As Integer = movingObject.X
        Dim y As Integer = movingObject.Y

        ' Check Right wall (Flipper1)
        If x < verticalWallItems.Length - 1 AndAlso verticalWallItems(x + 1)(y) IsNot Nothing Then
            Dim item As String = verticalWallItems(x + 1)(y)
            If item = "picFlipper1" Then
                Return True
            End If
        End If

        ' Check Left wall (Flipper2)
        If x > 0 AndAlso verticalWallItems(x)(y) IsNot Nothing Then
            Dim item As String = verticalWallItems(x)(y)
            If item = "picFlipper2" Then
                Return True
            End If
        End If

        ' Check Bottom wall (Flipper3)
        If y < horizontalWallItems(x).Length - 1 AndAlso horizontalWallItems(x)(y + 1) IsNot Nothing Then
            Dim item As String = horizontalWallItems(x)(y + 1)
            If item = "picFlipper3" Then
                Return True
            End If
        End If

        ' Check Top wall (Flipper4)
        If y > 0 AndAlso horizontalWallItems(x)(y) IsNot Nothing Then
            Dim item As String = horizontalWallItems(x)(y)
            If item = "picFlipper4" Then
                Return True
            End If
        End If

        Return False
    End Function
    Private Sub AddObjectsOnFlippersToMovingObjects()
        ' Loop through the grid to find blocks or balls that are on flippers
        For x As Integer = 0 To SQCOUNTX - 1
            For y As Integer = 0 To SQCOUNTY - 1
                Dim item As String = placedItems(x)(y)
                If item = "picBlock1" OrElse item = "picBall" Then
                    ' Check if this item is adjacent to a flipper
                    Dim movingObject As New MovingObject With {
                    .X = x,
                    .Y = y,
                    .IsBlock = (item = "picBlock1"),
                    .IsBall = (item = "picBall"),
                    .DeltaX = 0,
                    .DeltaY = 0
                }

                    ' Use CheckForFlipper to determine if the object is on a flipper and set its movement direction
                    CheckForFlipper(movingObject)

                    If movingObject.DeltaX <> 0 OrElse movingObject.DeltaY <> 0 Then
                        ' The object is on a flipper and will start moving
                        ' Remove it from placedItems
                        placedItems(x)(y) = Nothing
                        movingObject.MoveInterval = If(movingObject.IsBall, 100, 50)
                        movingObject.NextMoveTime = DateTime.Now.AddMilliseconds(movingObject.MoveInterval)
                        movingObjects.Add(movingObject)
                    End If
                End If
            Next
        Next

        ' Start the movingObjectsTimer if not already running
        If movingObjects.Count > 0 AndAlso movingObjectsTimer Is Nothing Then
            movingObjectsTimer = New Timer()
            AddHandler movingObjectsTimer.Tick, AddressOf OnMovingObjectsTimerTick
            movingObjectsTimer.Interval = 50
            movingObjectsTimer.Start()
        End If
    End Sub
    Private Function CanObjectMoveTo(movingObject As MovingObject, currentX As Integer, currentY As Integer, deltaX As Integer, deltaY As Integer) As Boolean
        Dim wallExists As Boolean = False
        Dim laserExists As Boolean = False
        Dim laserX As Integer = -1
        Dim laserY As Integer = -1
        Dim laserType As String = ""

        If deltaX = 1 Then
            wallExists = verticalWalls(currentX + 1)(currentY)
            If verticalWallItems(currentX + 1)(currentY) IsNot Nothing Then
                laserExists = True
                laserX = currentX + 1
                laserY = currentY
                laserType = verticalWallItems(laserX)(laserY)
            End If
        ElseIf deltaX = -1 Then
            wallExists = verticalWalls(currentX)(currentY)
            If verticalWallItems(currentX)(currentY) IsNot Nothing Then
                laserExists = True
                laserX = currentX
                laserY = currentY
                laserType = verticalWallItems(laserX)(laserY)
            End If
        ElseIf deltaY = 1 Then
            wallExists = horizontalWalls(currentX)(currentY + 1)
            If horizontalWallItems(currentX)(currentY + 1) IsNot Nothing Then
                laserExists = True
                laserX = currentX
                laserY = currentY + 1
                laserType = horizontalWallItems(laserX)(laserY)
            End If
        ElseIf deltaY = -1 Then
            wallExists = horizontalWalls(currentX)(currentY)
            If horizontalWallItems(currentX)(currentY) IsNot Nothing Then
                laserExists = True
                laserX = currentX
                laserY = currentY
                laserType = horizontalWallItems(laserX)(laserY)
            End If
        End If

        If wallExists Then
            Return False
        End If

        If laserExists Then
            Dim laserCanBeDestroyed As Boolean = False

            If (deltaX <> 0 AndAlso laserType = "picLazer1") Then
                laserCanBeDestroyed = True
            ElseIf (deltaY <> 0 AndAlso laserType = "picLazer2") Then
                laserCanBeDestroyed = True
            End If

            If laserCanBeDestroyed Then
                If deltaX <> 0 Then
                    verticalWallItems(laserX)(laserY) = Nothing
                ElseIf deltaY <> 0 Then
                    horizontalWallItems(laserX)(laserY) = Nothing
                End If
                soundManager.PlaySoundEffect("Break Lazer")
            Else
                Return False
            End If
        End If

        Return True
    End Function

    Private Function CanBallMoveTo(currentX As Integer, currentY As Integer, deltaX As Integer, deltaY As Integer) As Boolean
        Return CanObjectMoveTo(New MovingObject With {.IsBall = True}, currentX, currentY, deltaX, deltaY)
    End Function

    Private Function IsLaserOnVerticalWall(x As Integer, y As Integer) As Boolean
        If verticalWallItems(x)(y) IsNot Nothing Then
            Dim item As String = verticalWallItems(x)(y)
            Return item = "picLazer1" OrElse item = "picLazer2"
        End If
        Return False
    End Function

    Private Function IsLaserOnHorizontalWall(x As Integer, y As Integer) As Boolean
        If horizontalWallItems(x)(y) IsNot Nothing Then
            Dim item As String = horizontalWallItems(x)(y)
            Return item = "picLazer1" OrElse item = "picLazer2"
        End If
        Return False
    End Function

    Private Function AttemptPushBlock(blockX As Integer, blockY As Integer, deltaX As Integer, deltaY As Integer) As Boolean
        Dim blockNewX As Integer = blockX + deltaX
        Dim blockNewY As Integer = blockY + deltaY

        If blockNewX < 0 OrElse blockNewX >= SQCOUNTX OrElse blockNewY < 0 OrElse blockNewY >= SQCOUNTY Then
            Return False
        End If

        If Not CanBlockMoveTo(blockX, blockY, deltaX, deltaY) Then
            Return False
        End If

        Dim blockTargetItem As String = placedItems(blockNewX)(blockNewY)
        Dim playerAtNextPos As Boolean = (blockNewX = playerX AndAlso blockNewY = playerY)

        If String.IsNullOrEmpty(blockTargetItem) AndAlso Not playerAtNextPos Then
            placedItems(blockX)(blockY) = Nothing
            placedItems(blockNewX)(blockNewY) = "picBlock1"

            soundManager.PlaySoundEffect("Push Block")

            ' After moving the block, check if it should start moving due to a flipper
            Dim newBlock As New MovingObject With {
                .X = blockNewX,
                .Y = blockNewY,
                .IsBlock = True,
                .IsBall = False,
                .DeltaX = 0,
                .DeltaY = 0
            }

            CheckForFlipper(newBlock)
            If newBlock.DeltaX <> 0 OrElse newBlock.DeltaY <> 0 Then
                placedItems(blockNewX)(blockNewY) = Nothing
                newBlock.MoveInterval = 50
                newBlock.NextMoveTime = DateTime.Now.AddMilliseconds(50)
                movingObjects.Add(newBlock)

                If movingObjectsTimer Is Nothing Then
                    movingObjectsTimer = New Timer()
                    AddHandler movingObjectsTimer.Tick, AddressOf OnMovingObjectsTimerTick
                    movingObjectsTimer.Interval = 50
                    movingObjectsTimer.Start()
                End If
            End If

            Return True
        Else
            Return False
        End If
    End Function

    Private Function CanBlockMoveTo(blockX As Integer, blockY As Integer, deltaX As Integer, deltaY As Integer) As Boolean
        Return CanObjectMoveTo(New MovingObject With {.IsBlock = True}, blockX, blockY, deltaX, deltaY)
    End Function

    Private Function IsFlipperOnVerticalWall(x As Integer, y As Integer) As Boolean
        If verticalWallItems(x)(y) IsNot Nothing Then
            Dim item As String = verticalWallItems(x)(y)
            Return item = "picFlipper1" OrElse item = "picFlipper2"
        End If
        Return False
    End Function

    Private Function IsFlipperOnHorizontalWall(x As Integer, y As Integer) As Boolean
        If horizontalWallItems(x)(y) IsNot Nothing Then
            Dim item As String = horizontalWallItems(x)(y)
            Return item = "picFlipper3" OrElse item = "picFlipper4"
        End If
        Return False
    End Function

    Private Function CanMoveTo(newX As Integer, newY As Integer, deltaX As Integer, deltaY As Integer) As Boolean
        ' Check walls, lasers, and flippers between current and new position
        If deltaX = 1 Then
            ' Moving right
            If verticalWalls(playerX + 1)(playerY) Then Return False
            If IsLaserOnVerticalWall(playerX + 1, playerY) Then Return False
            If IsFlipperOnVerticalWall(playerX + 1, playerY) Then Return False
        ElseIf deltaX = -1 Then
            ' Moving left
            If verticalWalls(playerX)(playerY) Then Return False
            If IsLaserOnVerticalWall(playerX, playerY) Then Return False
            If IsFlipperOnVerticalWall(playerX, playerY) Then Return False
        ElseIf deltaY = 1 Then
            ' Moving down
            If horizontalWalls(playerX)(playerY + 1) Then Return False
            If IsLaserOnHorizontalWall(playerX, playerY + 1) Then Return False
            If IsFlipperOnHorizontalWall(playerX, playerY + 1) Then Return False
        ElseIf deltaY = -1 Then
            ' Moving up
            If horizontalWalls(playerX)(playerY) Then Return False
            If IsLaserOnHorizontalWall(playerX, playerY) Then Return False
            If IsFlipperOnHorizontalWall(playerX, playerY) Then Return False
        End If

        ' Check if the new position is occupied by a moving object
        If movingObjects.Any(Function(b) b.X = newX AndAlso b.Y = newY) Then
            Return False
        End If

        Return True
    End Function

    Private Sub GameWon()
        soundManager.PlaySoundEffect("Finish")
        MessageBox.Show($"Congratulations! You have completed the level.{Environment.NewLine}Coins Collected: {CollectedCoins}")
        Me.Close()
    End Sub
#End Region

#Region "Load"
    Private Sub frmGame_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' Initialize sound effects before any potential calls to PlaySoundEffect
        InitializeImages()

        ' Load the room data from file
        If Not String.IsNullOrEmpty(RoomFilePath) AndAlso File.Exists(RoomFilePath) Then
            LoadRoomFromFile(RoomFilePath)
        Else
            MessageBox.Show("Room file not found.")
            Me.Close()
            Return
        End If

        ' Start the animation timer
        animationTimer = New Timer() With {.Interval = 100}
        AddHandler animationTimer.Tick, AddressOf OnAnimationTick
        animationTimer.Start()

        picTest.Invalidate()

        Me.KeyPreview = True
        Me.Focus()
    End Sub

    Private Sub LoadRoomFromFile(filePath As String)
        Try
            Dim options As New JsonSerializerOptions With {
                .IncludeFields = True,
                .PropertyNameCaseInsensitive = True
            }
            Dim jsonString As String = File.ReadAllText(filePath)
            Dim roomData As RoomData = JsonSerializer.Deserialize(Of RoomData)(jsonString, options)

            If roomData Is Nothing Then
                MessageBox.Show("Error: Failed to deserialize room data.")
                Return
            End If

            RestoreRoomData(roomData)
        Catch ex As Exception
            MessageBox.Show("Error loading room: " & ex.ToString())
        End Try
    End Sub

    Private Sub RestoreRoomData(roomData As RoomData)
        Try
            SQCOUNTX = roomData.GridSizeX
            SQCOUNTY = roomData.GridSizeY

            wallsHatchStyle = CType(roomData.wallsHatchStyleValue, HatchStyle)
            wallsColor1 = Color.FromArgb(roomData.wallsColor1Argb)
            wallsColor2 = Color.FromArgb(roomData.wallsColor2Argb)
            floorsHatchStyle = CType(roomData.floorsHatchStyleValue, HatchStyle)
            floorsColor1 = Color.FromArgb(roomData.floorsColor1Argb)
            floorsColor2 = Color.FromArgb(roomData.floorsColor2Argb)

            verticalWalls = roomData.verticalWalls
            horizontalWalls = roomData.horizontalWalls
            floorsUseEdgeStyle = roomData.floorsUseEdgeStyle

            placedItems = RestorePlacedItems(roomData.placedItems)
            verticalWallItems = RestorePlacedItems(roomData.verticalWallItems)
            horizontalWallItems = RestorePlacedItems(roomData.horizontalWallItems)

            SelectedMusic = roomData.SelectedMusic
            lblName.Text = roomData.LevelName
            PlaySelectedMusic()

            isRoomDataLoaded = True
            picTest.Invalidate()
        Catch ex As Exception
            MessageBox.Show("Error restoring room data: " & ex.ToString())
        End Try

        FindPlayerStartPosition()
        AddObjectsOnFlippersToMovingObjects()

    End Sub

    Private Function RestorePlacedItems(data As PlacedItemData()()) As String()()
        If data Is Nothing Then
            Return Nothing
        End If

        Dim lengthOuter As Integer = data.Length
        Dim restoredItems As String()() = New String(lengthOuter - 1)() {}

        For x As Integer = 0 To lengthOuter - 1
            If data(x) IsNot Nothing Then
                Dim lengthInner As Integer = data(x).Length
                restoredItems(x) = New String(lengthInner - 1) {}
                For y As Integer = 0 To lengthInner - 1
                    If data(x)(y) IsNot Nothing Then
                        restoredItems(x)(y) = data(x)(y).ImageKey
                    Else
                        restoredItems(x)(y) = Nothing
                    End If
                Next
            Else
                restoredItems(x) = Nothing
            End If
        Next
        Return restoredItems
    End Function

    Private Sub InitializeImages()
        Try
            imageDictionary("picBlock1") = Image.FromFile(Path.Combine(ImagesFolder, "Block.png"))
            imageDictionary("picBall") = Image.FromFile(Path.Combine(ImagesFolder, "Ball.gif"))
            imageDictionary("picCoin") = Image.FromFile(Path.Combine(ImagesFolder, "Coin.gif"))
            imageDictionary("picFinish") = Image.FromFile(Path.Combine(ImagesFolder, "Finish.gif"))
            imageDictionary("picFlipper1") = Image.FromFile(Path.Combine(ImagesFolder, "Flipper1.gif"))
            imageDictionary("picFlipper2") = Image.FromFile(Path.Combine(ImagesFolder, "Flipper2.gif"))
            imageDictionary("picFlipper3") = Image.FromFile(Path.Combine(ImagesFolder, "Flipper3.gif"))
            imageDictionary("picFlipper4") = Image.FromFile(Path.Combine(ImagesFolder, "Flipper4.gif"))
            imageDictionary("picGate") = Image.FromFile(Path.Combine(ImagesFolder, "Gate.gif"))
            imageDictionary("picKey") = Image.FromFile(Path.Combine(ImagesFolder, "Key.gif"))
            imageDictionary("picLazer1") = Image.FromFile(Path.Combine(ImagesFolder, "Lazer1.gif"))
            imageDictionary("picLazer2") = Image.FromFile(Path.Combine(ImagesFolder, "Lazer2.gif"))
            imageDictionary("picWarp") = Image.FromFile(Path.Combine(ImagesFolder, "Warp.gif"))
            imageDictionary("picWarp2") = Image.FromFile(Path.Combine(ImagesFolder, "Warp2.gif"))
            imageDictionary("picStart") = Image.FromFile(Path.Combine(ImagesFolder, "Guy.gif"))
            imageDictionary("picBlockStopper1") = Image.FromFile(Path.Combine(ImagesFolder, "Debris1.png"))

            Dim animatedImageKeys As String() = {
                "picLazer1", "picLazer2", "picFlipper1", "picFlipper2",
                "picFlipper3", "picFlipper4", "picCoin", "picGate",
                "picKey", "picFinish", "picBall", "picWarp", "picWarp2", "picStart"
            }

            For Each imgKey In animatedImageKeys
                If imageDictionary.ContainsKey(imgKey) Then
                    Dim img As Image = imageDictionary(imgKey)
                    animatedImages.Add(img)
                    ImageAnimator.Animate(img, AddressOf OnFrameChanged)
                End If
            Next
        Catch ex As Exception
            MessageBox.Show("Error loading images: " & ex.Message)
        End Try
    End Sub

    Private Function GetProjectDirectory() As String
        Dim dir As DirectoryInfo = New DirectoryInfo(Application.StartupPath)
        For i As Integer = 1 To 4
            If dir.Parent IsNot Nothing Then
                dir = dir.Parent
            End If
        Next
        Return dir.FullName
    End Function
#End Region

#Region "Sound"
    Private Sub PlaySelectedMusic()
        If Not String.IsNullOrEmpty(SelectedMusic) Then
            Dim musicDetails As String() = SelectedMusic.Split(", ")
            Dim musicFileName As String = musicDetails(0)
            Dim volumeLevel As Single = Integer.Parse(musicDetails(1).Split(":")(1)) / 100.0F
            Dim gainLevel As Single = Integer.Parse(musicDetails(2).Split(":")(1)) / 10.0F

            Dim musicFilePath As String = Path.Combine(musicFolder, musicFileName)

            musicManager.PlaySelectedMusic(musicFilePath, volumeLevel, gainLevel)
        End If
    End Sub
#End Region

#Region "Draw"
    Private Sub OnAnimationTick(sender As Object, e As EventArgs)
        For Each img In animatedImages
            ImageAnimator.UpdateFrames(img)
        Next
        picTest.Invalidate()
    End Sub

    Private Sub OnFrameChanged(o As Object, e As EventArgs)
        ' Required but can be left empty
    End Sub

    Private Sub picTest_Paint(sender As Object, e As PaintEventArgs) Handles picTest.Paint
        If Not isRoomDataLoaded Then
            Return
        End If

        Using backgroundBrush As New HatchBrush(wallsHatchStyle, wallsColor1, wallsColor2)
            e.Graphics.FillRectangle(backgroundBrush, picTest.ClientRectangle)
        End Using

        CalculateSquareSizes(picTest.ClientSize.Width, picTest.ClientSize.Height)

        e.Graphics.SmoothingMode = SmoothingMode.None

        DrawGrid(e.Graphics)
    End Sub

    Private Sub CalculateSquareSizes(clientWidth As Integer, clientHeight As Integer)
        SQSizeX = (clientWidth - 2 * INDENT - (SQCOUNTX + 1) * WALL_THICKNESS) \ SQCOUNTX
        SQSizeY = (clientHeight - 2 * INDENT - (SQCOUNTY + 1) * WALL_THICKNESS) \ SQCOUNTY
    End Sub

    Private Sub DrawGrid(g As Graphics)
        Dim xOrigin As Integer = INDENT
        Dim yOrigin As Integer = INDENT

        ' Draw floor squares
        For x As Integer = 0 To SQCOUNTX - 1
            For y As Integer = 0 To SQCOUNTY - 1
                Dim floorRect As New Rectangle(
                    xOrigin + x * (SQSizeX + WALL_THICKNESS) + WALL_THICKNESS \ 2,
                    yOrigin + y * (SQSizeY + WALL_THICKNESS) + WALL_THICKNESS \ 2,
                    SQSizeX,
                    SQSizeY
                )

                If floorsUseEdgeStyle IsNot Nothing AndAlso floorsUseEdgeStyle.Length > x AndAlso
                   floorsUseEdgeStyle(x) IsNot Nothing AndAlso floorsUseEdgeStyle(x).Length > y AndAlso
                   floorsUseEdgeStyle(x)(y) Then

                    Using floorBrush As New HatchBrush(wallsHatchStyle, wallsColor1, wallsColor2)
                        g.FillRectangle(floorBrush, floorRect)
                    End Using
                Else
                    Using floorBrush As New HatchBrush(floorsHatchStyle, floorsColor1, floorsColor2)
                        g.FillRectangle(floorBrush, floorRect)
                    End Using
                End If
            Next
        Next

        ' Draw wall intersections
        For x As Integer = 0 To SQCOUNTX
            For y As Integer = 0 To SQCOUNTY
                Dim intersectionRect As New Rectangle(
                    xOrigin + x * (SQSizeX + WALL_THICKNESS) - WALL_THICKNESS \ 2,
                    yOrigin + y * (SQSizeY + WALL_THICKNESS) - WALL_THICKNESS \ 2,
                    WALL_THICKNESS,
                    WALL_THICKNESS
                )
                Using wallBrush As New HatchBrush(wallsHatchStyle, wallsColor1, wallsColor2)
                    g.FillRectangle(wallBrush, intersectionRect)
                End Using
            Next
        Next

        ' Draw vertical walls or floor
        For x As Integer = 0 To SQCOUNTX
            For y As Integer = 0 To SQCOUNTY - 1
                Dim wallRect As New Rectangle(
                    xOrigin + x * (SQSizeX + WALL_THICKNESS) - WALL_THICKNESS \ 2,
                    yOrigin + y * (SQSizeY + WALL_THICKNESS) + WALL_THICKNESS \ 2,
                    WALL_THICKNESS,
                    SQSizeY
                )

                If x >= 0 AndAlso x < verticalWalls.Length AndAlso
                   verticalWalls(x) IsNot Nothing AndAlso
                   y >= 0 AndAlso y < verticalWalls(x).Length Then

                    If verticalWalls(x)(y) Then
                        Using wallBrush As New HatchBrush(wallsHatchStyle, wallsColor1, wallsColor2)
                            g.FillRectangle(wallBrush, wallRect)
                        End Using
                    Else
                        Using floorBrush As New HatchBrush(floorsHatchStyle, floorsColor1, floorsColor2)
                            g.FillRectangle(floorBrush, wallRect)
                        End Using
                    End If
                End If
            Next
        Next

        ' Draw horizontal walls or floor
        For x As Integer = 0 To SQCOUNTX - 1
            For y As Integer = 0 To SQCOUNTY
                Dim wallRect As New Rectangle(
                    xOrigin + x * (SQSizeX + WALL_THICKNESS) + WALL_THICKNESS \ 2,
                    yOrigin + y * (SQSizeY + WALL_THICKNESS) - WALL_THICKNESS \ 2,
                    SQSizeX,
                    WALL_THICKNESS
                )

                If x >= 0 AndAlso x < horizontalWalls.Length AndAlso
                   horizontalWalls(x) IsNot Nothing AndAlso
                   y >= 0 AndAlso y < horizontalWalls(x).Length Then

                    If horizontalWalls(x)(y) Then
                        Using wallBrush As New HatchBrush(wallsHatchStyle, wallsColor1, wallsColor2)
                            g.FillRectangle(wallBrush, wallRect)
                        End Using
                    Else
                        Using floorBrush As New HatchBrush(floorsHatchStyle, floorsColor1, floorsColor2)
                            g.FillRectangle(floorBrush, wallRect)
                        End Using
                    End If
                End If
            Next
        Next

        DrawItems(g)
        DrawWallItems(g)
    End Sub

    Private Sub DrawItems(g As Graphics)
        If placedItems Is Nothing Then Return

        For x As Integer = 0 To placedItems.Length - 1
            If placedItems(x) IsNot Nothing Then
                For y As Integer = 0 To placedItems(x).Length - 1
                    Dim imageKey As String = placedItems(x)(y)
                    If Not String.IsNullOrEmpty(imageKey) AndAlso imageDictionary.ContainsKey(imageKey) Then
                        If imageKey = "picStart" Then
                            Continue For
                        End If
                        Dim img As Image = imageDictionary(imageKey)
                        Dim xPos As Integer = INDENT + x * (SQSizeX + WALL_THICKNESS) + WALL_THICKNESS \ 2
                        Dim yPos As Integer = INDENT + y * (SQSizeY + WALL_THICKNESS) + WALL_THICKNESS \ 2
                        Dim destRect As New Rectangle(xPos, yPos, SQSizeX, SQSizeY)

                        g.DrawImage(img, destRect)
                    End If
                Next
            End If
        Next

        ' Draw moving objects (balls and blocks)
        For Each movingObject In movingObjects
            Dim img As Image = Nothing
            If movingObject.IsBall Then
                img = imageDictionary("picBall")
            ElseIf movingObject.IsBlock Then
                img = imageDictionary("picBlock1")
            End If

            If img IsNot Nothing Then
                Dim xPos As Integer = INDENT + movingObject.X * (SQSizeX + WALL_THICKNESS) + WALL_THICKNESS \ 2
                Dim yPos As Integer = INDENT + movingObject.Y * (SQSizeY + WALL_THICKNESS) + WALL_THICKNESS \ 2
                Dim destRect As New Rectangle(xPos, yPos, SQSizeX, SQSizeY)

                g.DrawImage(img, destRect)
            End If
        Next

        DrawPlayer(g)
    End Sub

    Private Sub DrawPlayer(g As Graphics)
        Dim img As Image = imageDictionary("picStart")
        Dim xPos As Integer = INDENT + playerX * (SQSizeX + WALL_THICKNESS) + WALL_THICKNESS \ 2
        Dim yPos As Integer = INDENT + playerY * (SQSizeY + WALL_THICKNESS) + WALL_THICKNESS \ 2
        Dim destRect As New Rectangle(xPos, yPos, SQSizeX, SQSizeY)

        g.DrawImage(img, destRect)
    End Sub

    Private Sub DrawWallItems(g As Graphics)
        ' Draw vertical wall items
        If verticalWallItems IsNot Nothing Then
            For x As Integer = 0 To verticalWallItems.Length - 1
                If verticalWallItems(x) IsNot Nothing Then
                    For y As Integer = 0 To verticalWallItems(x).Length - 1
                        Dim imageKey As String = verticalWallItems(x)(y)
                        If Not String.IsNullOrEmpty(imageKey) AndAlso imageDictionary.ContainsKey(imageKey) Then
                            Dim img As Image = imageDictionary(imageKey)
                            ImageAnimator.UpdateFrames(img)
                            Dim wallX As Integer = INDENT + x * (SQSizeX + WALL_THICKNESS) - (WALL_THICKNESS \ 2)
                            Dim wallY As Integer = INDENT + y * (SQSizeY + WALL_THICKNESS) + (WALL_THICKNESS \ 2)
                            Dim wallWidth As Integer = WALL_THICKNESS
                            Dim wallHeight As Integer = SQSizeY

                            Dim newWidth As Integer = img.Width
                            Dim newHeight As Integer = wallHeight

                            Dim centeredX As Integer = wallX + (wallWidth - newWidth) \ 2

                            Dim drawRect As New Rectangle(centeredX, wallY, newWidth, newHeight)
                            g.DrawImage(img, drawRect)
                        End If
                    Next
                End If
            Next
        End If

        ' Draw horizontal wall items
        If horizontalWallItems IsNot Nothing Then
            For x As Integer = 0 To horizontalWallItems.Length - 1
                If horizontalWallItems(x) IsNot Nothing Then
                    For y As Integer = 0 To horizontalWallItems(x).Length - 1
                        Dim imageKey As String = horizontalWallItems(x)(y)
                        If Not String.IsNullOrEmpty(imageKey) AndAlso imageDictionary.ContainsKey(imageKey) Then
                            Dim img As Image = imageDictionary(imageKey)
                            ImageAnimator.UpdateFrames(img)
                            Dim wallX As Integer = INDENT + x * (SQSizeX + WALL_THICKNESS) + (WALL_THICKNESS \ 2)
                            Dim wallY As Integer = INDENT + y * (SQSizeY + WALL_THICKNESS) - (WALL_THICKNESS \ 2)
                            Dim wallWidth As Integer = SQSizeX
                            Dim wallHeight As Integer = WALL_THICKNESS

                            Dim newWidth As Integer = wallWidth
                            Dim newHeight As Integer = img.Height

                            Dim centeredY As Integer = wallY + (wallHeight - newHeight) \ 2

                            Dim drawRect As New Rectangle(wallX, centeredY, newWidth, newHeight)
                            g.DrawImage(img, drawRect)
                        End If
                    Next
                End If
            Next
        End If
    End Sub
#End Region

#Region "Closing"
    Private Sub frmGame_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
        ' Stop and dispose of animation timer
        If animationTimer IsNot Nothing Then
            animationTimer.Stop()
            animationTimer.Dispose()
            animationTimer = Nothing
        End If

        ' Stop and dispose of moving objects timer
        If movingObjectsTimer IsNot Nothing Then
            movingObjectsTimer.Stop()
            movingObjectsTimer.Dispose()
            movingObjectsTimer = Nothing
        End If

        ' Clear moving objects list
        movingObjects.Clear()

        ' Stop music playback
        musicManager.StopMusic()

        ' Stop sound effects
        soundManager.StopSoundEffects()
    End Sub

    Private Sub btnClose_Click(sender As Object, e As EventArgs) Handles btnClose.Click
        Me.Close()
    End Sub
#End Region
End Class

Imports NAudio.Wave
Imports NAudio.Wave.SampleProviders
Imports System.IO

Public Class SFXManager
    Private soundEffectFiles As New Dictionary(Of String, String)
    Private soundEffectMixer As MixingSampleProvider
    Private soundEffectWaveOut As WaveOutEvent

    Public Sub New(soundEffectsFolder As String)
        InitializeSoundEffects(soundEffectsFolder)
    End Sub

    Private Sub InitializeSoundEffects(soundEffectsFolder As String)
        Try
            soundEffectFiles("Break Lazer") = Path.Combine(soundEffectsFolder, "Break Lazer.wav")
            soundEffectFiles("Coin") = Path.Combine(soundEffectsFolder, "Coin.wav")
            soundEffectFiles("Finish") = Path.Combine(soundEffectsFolder, "Finish.wav")
            soundEffectFiles("Move") = Path.Combine(soundEffectsFolder, "Move.wav")
            soundEffectFiles("Push Block") = Path.Combine(soundEffectsFolder, "Push Block.wav")
            soundEffectFiles("Warp") = Path.Combine(soundEffectsFolder, "Warp.wav")
            soundEffectFiles("Key") = Path.Combine(soundEffectsFolder, "Key.wav")
            soundEffectFiles("Door Open") = Path.Combine(soundEffectsFolder, "Door Open.wav")
            soundEffectFiles("Push Ball") = Path.Combine(soundEffectsFolder, "Push Ball.wav")
            soundEffectFiles("Flipper") = Path.Combine(soundEffectsFolder, "Flipper.wav")
            soundEffectFiles("Block Stop") = Path.Combine(soundEffectsFolder, "BlockStop.wav")

            soundEffectMixer = New MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(44100, 2))
            soundEffectMixer.ReadFully = True
            soundEffectWaveOut = New WaveOutEvent()
            soundEffectWaveOut.Init(soundEffectMixer)
            soundEffectWaveOut.Play()
        Catch ex As Exception
            MessageBox.Show("Error initializing sound effects: " & ex.Message)
        End Try
    End Sub

    Public Sub PlaySoundEffect(soundKey As String)
        If soundEffectFiles.ContainsKey(soundKey) AndAlso soundEffectMixer IsNot Nothing Then
            Dim soundFilePath As String = soundEffectFiles(soundKey)
            If File.Exists(soundFilePath) Then
                Try
                    Dim reader As New AudioFileReader(soundFilePath)
                    ' Ensure the sound effect is played at its default volume
                    reader.Volume = 1.0F
                    soundEffectMixer.AddMixerInput(DirectCast(reader, ISampleProvider))
                Catch ex As Exception
                    ' Handle any errors here (e.g., sound file not found, can't play)
                End Try
            End If
        End If
    End Sub

    Public Sub StopSoundEffects()
        If soundEffectWaveOut IsNot Nothing Then
            soundEffectWaveOut.Stop()
            soundEffectWaveOut.Dispose()
            soundEffectWaveOut = Nothing
        End If

        If soundEffectMixer IsNot Nothing Then
            soundEffectMixer = Nothing
        End If
    End Sub
End Class

Public Class MusicManager
    Private musicWaveOut As WaveOutEvent
    Private musicReader As AudioFileReader
    Private musicEffectMixer As MixingSampleProvider

    Public Sub PlaySelectedMusic(musicFilePath As String, volumeLevel As Single, gainLevel As Single)
        StopMusic()

        If Not String.IsNullOrEmpty(musicFilePath) AndAlso File.Exists(musicFilePath) Then
            Try
                ' Create an AudioFileReader for the music file.
                musicReader = New AudioFileReader(musicFilePath)

                ' Create a Looping Wave Provider
                Dim loopingProvider As New LoopStream(musicReader)

                ' Create a WaveOutEvent for music playback
                musicWaveOut = New WaveOutEvent()
                musicWaveOut.Init(loopingProvider)

                ' Set volume and gain
                musicWaveOut.Volume = volumeLevel
                musicReader.Volume = gainLevel

                musicWaveOut.Play()
            Catch ex As Exception
                MessageBox.Show("Error playing music: " & ex.Message)
            End Try
        Else
            MessageBox.Show("Selected music file not found: " & musicFilePath)
        End If
    End Sub

    Public Sub StopMusic()
        If musicWaveOut IsNot Nothing Then
            musicWaveOut.Stop()
            musicWaveOut.Dispose()
            musicWaveOut = Nothing
        End If

        If musicReader IsNot Nothing Then
            musicReader.Dispose()
            musicReader = Nothing
        End If
    End Sub

    Private Class LoopStream
        Inherits WaveStream

        Private ReadOnly sourceStream As WaveStream

        Public Sub New(ByVal sourceStream As WaveStream)
            Me.sourceStream = sourceStream
        End Sub

        Public Overrides ReadOnly Property WaveFormat() As WaveFormat
            Get
                Return sourceStream.WaveFormat
            End Get
        End Property

        Public Overrides ReadOnly Property Length() As Long
            Get
                Return sourceStream.Length
            End Get
        End Property

        Public Overrides Property Position() As Long
            Get
                Return sourceStream.Position
            End Get
            Set(ByVal value As Long)
                sourceStream.Position = value
            End Set
        End Property

        Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer
            Dim totalBytesRead As Integer = 0

            While totalBytesRead < count
                Dim bytesRead As Integer = sourceStream.Read(buffer, offset + totalBytesRead, count - totalBytesRead)
                If bytesRead = 0 Then
                    ' Loop back to the beginning
                    sourceStream.Position = 0
                Else
                    totalBytesRead += bytesRead
                End If
            End While

            Return totalBytesRead
        End Function
    End Class
End Class


Why is volume effecting the sound effects as well as the music?

The way I've set this up, is...

for reference...

Option Explicit On
Option Strict On

Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.IO
Imports NAudio.Wave
Imports NAudio.Wave.SampleProviders
Imports System.Threading.Tasks

Public Class frmMusic
    ' Define fields for audio processing
    Private ReadOnly projectDir As String = GetProjectDirectory()
    Private ReadOnly musicFolder As String = Path.Combine(projectDir, "Data", "Music")
    Private ReadOnly IconsFolder As String = Path.Combine(projectDir, "Data", "Icons")

    Private waveOut As WaveOutEvent
    Private audioFileReader As AudioFileReader
    Private sampleChannel As SampleChannel

    ' Properties to return volume and gain levels
    Public Property AudioFile As String
    Public Property VolumeLevel As Integer
    Public Property GainLevel As Integer

    ' Fields for the moving line
    Private playbackTimer As New Timer()
    Private currentPosition As Double = 0
    Private totalDuration As Double = 0
    Private ReadOnly darkGreenPen As New Pen(Color.DarkGreen, 2)

    Private isPlaying As Boolean = False
    Private isRepeating As Boolean = False
    Private Function GetProjectDirectory() As String
        Dim dir As DirectoryInfo = New DirectoryInfo(Application.StartupPath)
        For i As Integer = 1 To 4
            If dir.Parent IsNot Nothing Then
                dir = dir.Parent
            End If
        Next
        Return dir.FullName
    End Function
    Private Sub frmMusic_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        btnPlayPause.Image = Image.FromFile(Path.Combine(IconsFolder, "Play.png"))

        ' Disable the "new row" at the bottom and allow a vertical scrollbar
        dgSound.AllowUserToAddRows = False
        dgSound.ScrollBars = ScrollBars.Vertical

        ' Initialize DataGridView columns
        dgSound.Columns.Clear()
        dgSound.Columns.Add("Filename", "Filename")
        dgSound.Columns.Add("Length", "Length")
        dgSound.Columns.Add("Type", "Type")

        ' Calculate the total available width for columns (account for vertical scrollbar width)
        Dim scrollbarWidth As Integer = SystemInformation.VerticalScrollBarWidth
        Dim availableWidth As Integer = dgSound.ClientSize.Width - scrollbarWidth

        ' Set column widths based on desired percentages
        dgSound.Columns("Filename").Width = CInt(availableWidth * 0.6)
        dgSound.Columns("Length").Width = CInt(availableWidth * 0.2)
        dgSound.Columns("Type").Width = CInt(availableWidth * 0.2)
        dgSound.RowTemplate.Height = 20
        dgSound.AllowUserToResizeRows = False

        ' Load music files into DataGridView
        LoadMusicFiles()

        ' Set up other controls
        TBVolume.Minimum = 0
        TBVolume.Maximum = 100
        TBVolume.Value = 50

        TBGain.Minimum = 0
        TBGain.Maximum = 30
        TBGain.Value = 10

        UpdateVolumeLabel()
        UpdateGainLabel()

        DrawCenterLine() ' Initialize with a center line

        playbackTimer.Interval = 500 ' Update every 500ms
        AddHandler playbackTimer.Tick, AddressOf PlaybackTimer_Tick

    End Sub

    Private Sub LoadMusicFiles()
        Dim allowedExtensions As String() = {".mp3", ".wav"}
        dgSound.Rows.Clear()

        If Directory.Exists(musicFolder) Then
            Dim musicFiles = Directory.GetFiles(musicFolder)
            For Each file In musicFiles
                If allowedExtensions.Contains(Path.GetExtension(file).ToLower()) Then
                    Dim fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file)
                    Dim fileType = Path.GetExtension(file).Replace(".", "").ToUpper()

                    Using reader As New AudioFileReader(file)
                        Dim duration As String = $"{reader.TotalTime.Minutes:D2}:{reader.TotalTime.Seconds:D2}"
                        dgSound.Rows.Add(fileNameWithoutExtension, duration, fileType)
                    End Using
                End If
            Next
        Else
            MessageBox.Show("Music folder doesn't exist.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        End If
    End Sub

    Private Sub dgSound_CellClick(sender As Object, e As DataGridViewCellEventArgs) Handles dgSound.CellClick
        If e.RowIndex >= 0 AndAlso e.RowIndex < dgSound.Rows.Count Then
            Dim selectedRow = dgSound.Rows(e.RowIndex)
            Dim fileNameWithoutExtension = selectedRow.Cells("Filename").Value.ToString()
            Dim fileType = selectedRow.Cells("Type").Value.ToString().ToLower()

            ' Call the method with the correct parameters
            LoadAndPlayAudio(fileNameWithoutExtension, fileType)

            ' Update buttons
            btnPlayPause.Image = Image.FromFile(Path.Combine(IconsFolder, "Pause.png"))
            isPlaying = True
            btnPlayPause.BorderStyle = BorderStyle.Fixed3D
            btnStop.BackColor = DefaultBackColor
            btnStop.BorderStyle = BorderStyle.None
            If isRepeating Then
                btnRepeat.BackColor = Color.LightGreen
                btnRepeat.BorderStyle = BorderStyle.Fixed3D
            Else
                btnRepeat.BackColor = DefaultBackColor
                btnRepeat.BorderStyle = BorderStyle.None
            End If
        End If
    End Sub

    Private Sub LoadAndPlayAudio(selectedFileName As String, fileType As String)
        playbackTimer.Stop()

        Try
            ' Ensure previous audio resources are disposed of safely
            If waveOut IsNot Nothing Then
                RemoveHandler waveOut.PlaybackStopped, AddressOf WaveOut_PlaybackStopped
                waveOut.Stop()
                waveOut.Dispose()
                waveOut = Nothing
            End If

            If audioFileReader IsNot Nothing Then
                audioFileReader.Dispose()
                audioFileReader = Nothing
            End If

            ' Construct the complete file path
            Dim completeFilePath As String = Path.Combine(musicFolder, selectedFileName & "." & fileType)

            If File.Exists(completeFilePath) Then
                audioFileReader = New AudioFileReader(completeFilePath)
                sampleChannel = New SampleChannel(audioFileReader, True)

                ' Apply gain
                Dim gainMultiplier As Single = TBGain.Value / 10.0F
                sampleChannel.Volume = gainMultiplier

                ' Initialize and start playback
                waveOut = New WaveOutEvent()
                waveOut.Init(New SampleToWaveProvider(sampleChannel))
                waveOut.Volume = TBVolume.Value / 100.0F

                totalDuration = audioFileReader.TotalTime.TotalSeconds
                currentPosition = 0 ' Reset

                ' Draw initial waveform
                DrawWaveform(completeFilePath, gainMultiplier)
                AudioFile = selectedFileName & "." & fileType

                AddHandler waveOut.PlaybackStopped, AddressOf WaveOut_PlaybackStopped

                waveOut.Play()
                playbackTimer.Start()
            Else
                MessageBox.Show("Selected audio file not found.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
                DrawCenterLine()
            End If
        Catch ex As Exception
            MessageBox.Show($"Error loading audio: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        End Try
    End Sub
    ' Ensure only one version of this method exists
    Private Sub WaveOut_PlaybackStopped(sender As Object, e As StoppedEventArgs)
        If Me.IsHandleCreated AndAlso Not Me.IsDisposed AndAlso audioFileReader IsNot Nothing Then
            ' Determine if track finished playing normally
            If Math.Abs(audioFileReader.CurrentTime.TotalSeconds - audioFileReader.TotalTime.TotalSeconds) < 0.1 Then
                currentPosition = totalDuration
                playbackTimer.Stop()
                UpdateTimerLabel()

                If isRepeating Then
                    Try
                        ' Reset and play again if repeating
                        audioFileReader.CurrentTime = TimeSpan.Zero
                        currentPosition = 0
                        waveOut.Play()
                        playbackTimer.Start()
                        btnPlayPause.BorderStyle = BorderStyle.Fixed3D
                    Catch ex As Exception
                        MessageBox.Show($"Error repeating audio: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
                    End Try
                Else
                    DrawCenterLine()
                    btnPlayPause.Image = Image.FromFile(Path.Combine(IconsFolder, "Play.png"))
                    isPlaying = False
                    btnPlayPause.BorderStyle = BorderStyle.FixedSingle
                End If
            End If
        End If
    End Sub

    Private Sub TBVolume_Scroll(sender As Object, e As EventArgs) Handles TBVolume.Scroll
        If waveOut IsNot Nothing Then
            waveOut.Volume = TBVolume.Value / 100.0F
        End If
        UpdateVolumeLabel()
    End Sub

    Private Sub TBGain_Scroll(sender As Object, e As EventArgs) Handles TBGain.Scroll
        If sampleChannel IsNot Nothing Then
            Dim gainMultiplier As Single = TBGain.Value / 10.0F
            sampleChannel.Volume = gainMultiplier
            DrawWaveform(Path.Combine(musicFolder, AudioFile), gainMultiplier)
        End If
        UpdateGainLabel()
    End Sub

    Private Sub UpdateVolumeLabel()
        lblVolume.Text = "Volume: " & TBVolume.Value
    End Sub

    Private Sub UpdateGainLabel()
        lblGain.Text = "Gain: " & TBGain.Value
    End Sub

    Private Async Sub DrawWaveform(filePath As String, gainMultiplier As Single)
        If Not Me.IsHandleCreated OrElse picWav.IsDisposed OrElse picWav.Width = 0 OrElse picWav.Height = 0 Then Exit Sub

        Await Task.Run(Sub()
                           Try
                               Using reader As New AudioFileReader(filePath)
                                   Dim buffer As Single() = New Single(16383) {}
                                   Dim allSamples As New List(Of Single)()
                                   Dim samplesRead As Integer

                                   Do
                                       samplesRead = reader.Read(buffer, 0, buffer.Length)
                                       allSamples.AddRange(buffer.Take(samplesRead))
                                   Loop While samplesRead > 0

                                   Dim bmp As New Bitmap(picWav.Width, picWav.Height)
                                   Using g As Graphics = Graphics.FromImage(bmp)
                                       g.Clear(Color.Black)
                                       Dim midY As Integer = picWav.Height \ 2
                                       Dim samplesPerPixel As Integer = Math.Max(1, allSamples.Count \ (picWav.Width * reader.WaveFormat.Channels))

                                       For x As Integer = 0 To picWav.Width - 1
                                           Dim startIndex As Integer = x * samplesPerPixel * reader.WaveFormat.Channels
                                           Dim maxAmplitude As Single = allSamples.Skip(startIndex).Take(samplesPerPixel).Max(Function(s) Math.Abs(s))
                                           maxAmplitude *= gainMultiplier
                                           maxAmplitude = Math.Min(maxAmplitude, 1.0F)

                                           Dim amplitudeHeight As Integer = CInt(maxAmplitude * picWav.Height / 2)
                                           g.DrawLine(Pens.White, x, midY - amplitudeHeight, x, midY + amplitudeHeight)
                                       Next

                                       ' Draw the green line
                                       Dim greenLineX As Integer = CInt((currentPosition / totalDuration) * picWav.Width)
                                       g.DrawLine(darkGreenPen, greenLineX, 0, greenLineX, picWav.Height)
                                   End Using

                                   If Me.IsHandleCreated Then
                                       Me.Invoke(Sub()
                                                     If Not picWav.IsDisposed Then
                                                         picWav.Image?.Dispose()
                                                         picWav.Image = bmp
                                                     End If
                                                 End Sub)
                                   End If
                               End Using
                           Catch ex As Exception
                               Console.WriteLine($"Drawing waveform error: {ex.Message}")
                           End Try
                       End Sub)
    End Sub

    Private Sub DrawCenterLine()
        If picWav.Width = 0 OrElse picWav.Height = 0 Then Exit Sub

        Dim bmp As New Bitmap(picWav.Width, picWav.Height)
        Using g As Graphics = Graphics.FromImage(bmp)
            g.Clear(Color.Black)
            Dim midY As Integer = CInt(picWav.Height / 2)
            g.DrawLine(Pens.White, 0, midY, picWav.Width, midY)
        End Using
        picWav.Image = bmp
    End Sub

    Private Sub frmMusic_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
        ' Stop timer and clean up resources
        playbackTimer.Stop()
        RemoveHandler playbackTimer.Tick, AddressOf PlaybackTimer_Tick

        Try
            If waveOut IsNot Nothing Then
                RemoveHandler waveOut.PlaybackStopped, AddressOf WaveOut_PlaybackStopped
                waveOut.Stop()
                waveOut.Dispose()
                waveOut = Nothing
            End If

            If audioFileReader IsNot Nothing Then
                audioFileReader.Dispose()
                audioFileReader = Nothing
            End If
        Catch ex As Exception
            Console.WriteLine($"Error during form closing: {ex.Message}")
        End Try

        VolumeLevel = TBVolume.Value
        GainLevel = TBGain.Value
        Me.DialogResult = DialogResult.OK
    End Sub
    Private Sub UpdateTimerLabel()
        If audioFileReader IsNot Nothing Then
            Dim elapsed As TimeSpan = audioFileReader.CurrentTime
            Dim total As TimeSpan = audioFileReader.TotalTime
            lblTimer.Text = String.Format("{0:D2}:{1:D2} / {2:D2}:{3:D2}", elapsed.Minutes, elapsed.Seconds, total.Minutes, total.Seconds)
        Else
            lblTimer.Text = "00:00 / 00:00"
        End If
    End Sub

    Private Sub PlaybackTimer_Tick(sender As Object, e As EventArgs)
        If audioFileReader IsNot Nothing AndAlso waveOut IsNot Nothing AndAlso waveOut.PlaybackState = PlaybackState.Playing Then
            currentPosition = audioFileReader.CurrentTime.TotalSeconds
            UpdateTimerLabel()

            ' Ensure the form and controls are still valid
            If Me.IsHandleCreated Then
                Me.Invoke(Sub() DrawWaveform(Path.Combine(musicFolder, AudioFile), TBGain.Value / 10.0F))
            End If
        End If
    End Sub
    Private Sub picWav_MouseDown(sender As Object, e As MouseEventArgs) Handles picWav.MouseDown
        ' Ensure audio is loaded and playing
        If audioFileReader Is Nothing OrElse waveOut Is Nothing Then Exit Sub

        ' Calculate the new position based on the click
        Dim newPositionRatio As Double = e.X / picWav.Width
        newPositionRatio = Math.Max(0, Math.Min(newPositionRatio, 1)) ' Clamp between 0 and 1
        currentPosition = totalDuration * newPositionRatio

        ' Set the audio playback position
        audioFileReader.CurrentTime = TimeSpan.FromSeconds(currentPosition)

        ' Redraw the waveform with the new position
        If Not String.IsNullOrEmpty(AudioFile) Then
            DrawWaveform(Path.Combine(musicFolder, AudioFile), TBGain.Value / 10.0F)
        End If

        ' Ensure the playback timer continues
        playbackTimer.Start()

        ' Restart playback from the new position
        If waveOut.PlaybackState <> PlaybackState.Playing Then
            waveOut.Play()
        End If
    End Sub

    Private Sub btnStop_Click(sender As Object, e As EventArgs) Handles btnStop.Click
        If waveOut IsNot Nothing Then
            waveOut.Stop()
            audioFileReader.CurrentTime = TimeSpan.Zero
            currentPosition = 0
            playbackTimer.Stop()
            UpdateTimerLabel()
            DrawWaveform(Path.Combine(musicFolder, AudioFile), TBGain.Value / 10.0F)
            btnPlayPause.Image = Image.FromFile(Path.Combine(IconsFolder, "Play.png"))
            isPlaying = False
            btnPlayPause.BorderStyle = BorderStyle.FixedSingle

            ' Dispose and reinitialize waveOut and audioFileReader
            waveOut.Dispose()
            waveOut = Nothing
            audioFileReader.Dispose()
            audioFileReader = Nothing
        End If

        ' Reset back colors
        btnStop.BackColor = DefaultBackColor
        btnStop.BorderStyle = BorderStyle.Fixed3D
        If isRepeating Then
            btnRepeat.BackColor = DefaultBackColor
            btnRepeat.BorderStyle = BorderStyle.Fixed3D
        Else
            btnRepeat.BackColor = DefaultBackColor
            btnRepeat.BorderStyle = BorderStyle.None
        End If

        ' Start the timer to change the border style back to FixedSingle after a short delay
        borderStyleTimer.Interval = 100 ' 100 milliseconds (adjust as needed)
        borderStyleTimer.Start()
    End Sub

    Private Sub borderStyleTimer_Tick(sender As Object, e As EventArgs) Handles borderStyleTimer.Tick
        ' Change the border style back to FixedSingle
        btnStop.BorderStyle = BorderStyle.FixedSingle

        ' Stop the timer
        borderStyleTimer.Stop()
    End Sub

    Private Sub frmMusic_MouseMove(sender As Object, e As MouseEventArgs) Handles Me.MouseMove
        btnStop.BackColor = DefaultBackColor
        btnStop.BorderStyle = BorderStyle.None

        If isRepeating Then
            btnRepeat.BackColor = DefaultBackColor
            btnRepeat.BorderStyle = BorderStyle.Fixed3D
        Else
            btnRepeat.BackColor = DefaultBackColor
            btnRepeat.BorderStyle = BorderStyle.None
        End If

        If isPlaying Then
            btnPlayPause.BackColor = DefaultBackColor
            btnPlayPause.BorderStyle = BorderStyle.Fixed3D
        Else
            btnPlayPause.BackColor = DefaultBackColor
            btnPlayPause.BorderStyle = BorderStyle.None
        End If
    End Sub

    Private Sub btnRepeat_Click(sender As Object, e As EventArgs) Handles btnRepeat.Click
        btnRepeat.BackColor = Color.LightGray
        If isRepeating Then
            isRepeating = False
            btnRepeat.BorderStyle = BorderStyle.FixedSingle
        Else
            isRepeating = True
            btnRepeat.BorderStyle = BorderStyle.Fixed3D
        End If

        ' Update back colors for other buttons
        btnStop.BackColor = DefaultBackColor
        btnStop.BorderStyle = BorderStyle.None
        If isPlaying Then
            btnPlayPause.BorderStyle = BorderStyle.Fixed3D
        Else
            btnPlayPause.BorderStyle = BorderStyle.FixedSingle
        End If
    End Sub

    Private Sub btnPlayPause_Click(sender As Object, e As EventArgs) Handles btnPlayPause.Click
        If waveOut IsNot Nothing Then
            If waveOut.PlaybackState = PlaybackState.Playing Then
                waveOut.Pause()
                btnPlayPause.Image = Image.FromFile(Path.Combine(IconsFolder, "Play.png"))
                isPlaying = False
                btnPlayPause.BorderStyle = BorderStyle.FixedSingle
            ElseIf waveOut.PlaybackState = PlaybackState.Paused Then
                waveOut.Play()
                btnPlayPause.Image = Image.FromFile(Path.Combine(IconsFolder, "Pause.png"))
                isPlaying = True
                btnPlayPause.BorderStyle = BorderStyle.Fixed3D
            End If
        Else
            ' Start playing the selected track if audio is loaded for the first time
            Dim selectedRow As DataGridViewRow = dgSound.CurrentRow
            If selectedRow IsNot Nothing Then
                Dim fileNameWithoutExtension = selectedRow.Cells("Filename").Value.ToString()
                Dim fileType = selectedRow.Cells("Type").Value.ToString().ToLower()
                LoadAndPlayAudio(fileNameWithoutExtension, fileType)
                btnPlayPause.Image = Image.FromFile(Path.Combine(IconsFolder, "Pause.png"))
                isPlaying = True
                btnPlayPause.BorderStyle = BorderStyle.Fixed3D
            End If
        End If

        ' Update back colors for other buttons
        btnStop.BackColor = DefaultBackColor
        btnStop.BorderStyle = BorderStyle.None
        If isRepeating Then
            btnRepeat.BackColor = DefaultBackColor
            btnRepeat.BorderStyle = BorderStyle.Fixed3D
        Else
            btnRepeat.BackColor = DefaultBackColor
            btnRepeat.BorderStyle = BorderStyle.None
        End If
    End Sub

    Private Sub btnStop_MouseEnter(sender As Object, e As EventArgs) Handles btnStop.MouseEnter, btnPlayPause.MouseEnter, btnRepeat.MouseEnter
        Dim btn As PictureBox = CType(sender, PictureBox)
        btn.BackColor = Color.LightGray
        'If btn Is btnRepeat And isRepeating Then Exit Sub

        btn.BorderStyle = BorderStyle.FixedSingle
    End Sub

End Class


Look at frmMusic. I have set option for volume and gain. This is so the user can decide how loud to play the music. The gain works perfectly and doesn't effect any other sound, but adjusting the volume effects every other sound when the game is running. That shouldn't be the case. When the game is running, even if we set the volume to zero the sound effects shouldn't be effected and play normally.