' test_procgen.jdb
' Demonstrates the new procedural generation functions in MLAB.

IMPORT MLAB

' ====================================================================
' ==  A* Pathfinding Algorithm
' ====================================================================
' Solves a maze from a start to an end point.
' maze: A 2D array where 0 is a path and 1 is a wall.
' start/end: A 1D array with two elements [x, y].
' Returns an array of points for the path, or an empty array if no path is found.

' --- Helper function to calculate Manhattan distance heuristic ---
FUNC Heuristic(p1, p2)
    RETURN ABS(p1[0] - p2[0]) + ABS(p1[1] - p2[1])
ENDFUNC

' ====================================================================
' ==  A* Pathfinding Algorithm (Fully Implemented)
' ====================================================================
' Solves a maze from a start to an end point.
' maze: A 2D array where 0 is a path and 1 is a wall.
' start/end: A 1D array with two elements [x, y].
' Returns a 2D array of points for the path, or an empty array if no path is found.
FUNC A_STAR_SOLVER(maze, start_pos, end_pos)
    ' --- Data Structures ---
    openList = []       ' An array of node Maps
    closedList = {}     ' A Map for fast lookup of visited nodes: closedList{"x,y"} = node_map

    ' --- Initialization ---
    start_node = {}
    start_node{"x"} = start_pos[0]
    start_node{"y"} = start_pos[1]
    start_node{"g"} = 0 ' Cost from start to current node
    start_node{"h"} = Heuristic(start_pos, end_pos) ' Heuristic cost to end
    start_node{"f"} = start_node{"g"} + start_node{"h"} ' Total estimated cost
    start_node{"parent_x"} = -1 ' No parent
    start_node{"parent_y"} = -1
    openList = APPEND(openList, start_node)

    ' --- Main Loop ---
    DO WHILE LEN(openList)[0] > 0
        ' Find the node with the lowest f_cost in the openList
        lowest_f = 999999
        current_idx = -1
        FOR i = 0 TO LEN(openList)[0] - 1
            IF openList[i]{"f"} < lowest_f THEN
                lowest_f = openList[i]{"f"}
                current_idx = i
            ENDIF
        NEXT i

        current_node = openList[current_idx]

        ' --- Manually remove current node from openList to avoid C++ crash ---
        newOpenList = []
        FOR j = 0 TO LEN(openList)[0] - 1
            IF j <> current_idx THEN
                newOpenList = APPEND(newOpenList, openList[j])
            ENDIF
        NEXT j
        openList = newOpenList
        
        ' Add current node to the closedList for efficient lookup
        closed_key$ = STR$(current_node{"x"}) + "," + STR$(current_node{"y"})
        closedList{closed_key$} = current_node

        ' --- Check if we reached the end ---
        IF current_node{"x"} = end_pos[0] ANDALSO current_node{"y"} = end_pos[1] THEN
            ' --- Path found! Reconstruct it by tracing parents. ---
            path_flat = []
            temp_node = current_node
            DO
                path_flat = APPEND(path_flat, [temp_node{"x"}, temp_node{"y"}])
                px = temp_node{"parent_x"}
                py = temp_node{"parent_y"}
                IF px = -1 THEN EXITDO
                
                parent_key$ = STR$(px) + "," + STR$(py)
                temp_node = closedList{parent_key$}
            LOOP
            ' The path is built backwards, so reverse it and reshape into coordinate pairs.
            path_flat = REVERSE(path_flat)
            RETURN RESHAPE(path_flat, [LEN(path_flat)[0] / 2, 2])
        ENDIF

        ' --- Check Neighbors ---
        neighbors_offset = [[0, -1], [0, 1], [-1, 0], [1, 0]] ' N, S, W, E
        FOR i = 0 TO 3
            offset = SLICE(neighbors_offset, 0, i)
            nx = current_node{"x"} + offset[0]
            ny = current_node{"y"} + offset[1]
            neighbor_key$ = STR$(nx) + "," + STR$(ny)

            ' Check if neighbor is valid (within bounds, not a wall, not already evaluated)
            is_valid = TRUE
            IF nx < 0 ORALSO nx >= LEN(maze)[1] ORALSO ny < 0 ORALSO ny >= LEN(maze)[0] THEN is_valid = FALSE
            IF is_valid ANDALSO maze[ny, nx] = 1 THEN is_valid = FALSE
            IF is_valid ANDALSO MAP.EXISTS(closedList, neighbor_key$) THEN is_valid = FALSE

            IF is_valid THEN
                g_cost = current_node{"g"} + 1
                h_cost = Heuristic([nx, ny], end_pos)
                f_cost = g_cost + h_cost

                ' Check if this neighbor is already in the open list
                in_open_list_idx = -1
                if LEN(openList)[0] - 1 > 0 THEN
                    FOR j = 0 TO LEN(openList)[0] - 1
                        IF openList[j]{"x"} = nx ANDALSO openList[j]{"y"} = ny THEN
                            in_open_list_idx = j
                            EXITFOR
                        ENDIF
                    NEXT j
                ENDIF

                IF in_open_list_idx = -1 THEN
                    ' If not in the open list, add it.
                    neighbor_node = {}
                    neighbor_node{"x"} = nx : neighbor_node{"y"} = ny
                    neighbor_node{"g"} = g_cost : neighbor_node{"h"} = h_cost : neighbor_node{"f"} = f_cost
                    neighbor_node{"parent_x"} = current_node{"x"}
                    neighbor_node{"parent_y"} = current_node{"y"}
                    openList = APPEND(openList, neighbor_node)
                ELSEIF g_cost < openList[in_open_list_idx]{"g"} THEN
                    ' If it IS in the open list but this new path is BETTER, update it.
                    ' Re-create the map to ensure the update persists correctly.
                    updated_node = openList[in_open_list_idx]
                    updated_node{"g"} = g_cost
                    updated_node{"f"} = f_cost
                    updated_node{"parent_x"} = current_node{"x"}
                    updated_node{"parent_y"} = current_node{"y"}
                    openList[in_open_list_idx] = updated_node
                ENDIF
            ENDIF
        NEXT i
    LOOP
    RETURN [] ' No path found
ENDFUNC

' --- 1. Generate Perlin Noise ---
PSIZE = 100
PRINT format$("Generating Perlin noise ({:3d}x{:3d})...", PSIZE,PSIZE)
' A larger scale gives smoother, more cloud-like noise.
NoiseMap = MLAB.PERLIN_NOISE(PSIZE, PSIZE, 32) 

' Convert the noise values [0, 1] to grayscale colors [0, 255]
ColorMap = NoiseMap * 255
' Pack into a single integer color matrix for PLOTRAW
' We set R, G, and B to the same value for grayscale.
FinalColorMap = ColorMap * 65536 + ColorMap * 256 + ColorMap

' --- 2. Generate a Maze ---
PRINT "Generating a 51x31 maze..."
' Width and height should ideally be odd numbers.
MazeMatrix = MLAB.CREATE_MAZE(51, 31)
' Convert the maze (0s and 1s) to black and white colors
MazeColor = MazeMatrix * 255 ' Invert so paths (0) become white (255)
FinalMazeMap = MazeColor * 65536 + MazeColor * 256 + MazeColor

' --- 2.5 Solve the Maze ---
PRINT "Solving maze with A*..."
start_point = [1, 1]
end_point = [49, 29]

path = A_STAR_SOLVER(MazeMatrix, start_point, end_point)

SUB Draw()
    ' Draw the noise on the left side
    PLOTRAW 45, 45, FinalColorMap, 5, 5
    ' Draw the maze on the right side, scaled up
    PLOTRAW X1, Y1, FinalMazeMap, Z, Z
ENDSUB

' --- 3. Draw the Results ---
PRINT "Drawing results..."
SCREEN 1920, 1080, "Procedural Generation Test"
CLS 20, 20, 40

L = LEN(path)[0] 
X1 = 50 : Y1 = 15 : Z = 15

IF L > 0 THEN
    PRINT "Drawing path..."
    Draw
    SCREENFLIP
    SLEEP 2000
    FOR J = 0 TO LEN(path)[0] - 1
        CLS 20, 20, 40
        Draw
        FOR i = 0 TO  J
            point = SLICE(path, 0, i) ' Get the i-th row (coordinate pair)
            px = X1*z + point[1] * Z
            py = Y1*z + point[0] * Z
            ' Draw a filled rectangle for each point in the path
            RECT px, py, Z, Z, 255, 0, 0, TRUE
        NEXT i
        SCREENFLIP
    NEXT J
ELSE
    PRINT "No path found."
    Draw
    SCREENFLIP
ENDIF

PRINT "Done. Press any key to exit."

WAITKEY$
