using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace Tumble
{
    public static class Backrooms3D
    {
        // Screen dimensions
        static int screenWidth;
        static int screenHeight;

        // Map storage using a dictionary to support an infinite map
        static Dictionary<(int, int), char> map = new Dictionary<(int, int), char>();

        // Player variables
        static double posX = 0.0, posY = 0.0; // Player position starts at (0,0)
        static double dirX = 1.0, dirY = 0.0;    // Player direction (facing right initially)
        static double planeX = 0, planeY = 0.66; // Camera plane

        // Camera rotation angles
        static double pitch = 0.0; // Not used in rendering but tracked for potential future use
        static double yaw = 0.0;   // Yaw is equivalent to dirX and dirY rotation

        // Timing
        static double time = 0; // Time of current frame
        static double oldTime = 0; // Time of previous frame

        // ANSI Escape Codes
        const string resetColor = "\u001b[0m";

        // Minimap dimensions (restored to original aspect ratio)
        const int minimapSize = 20;

        // Debug information toggle
        static bool showUI = false;
        static bool wallTexture = false;
        static bool endGame = false;

        // Autoplay variables
        static bool isAutoplay = false;
        static readonly Random autoplayRandom = new Random();
        static string currentAutoplayAction = "idle";
        const double wallThreshold = 1.9; // Distance threshold to start avoiding walls
        const int wallRotations = 4; // Number of rotations when facing a wall
        const int noWallSwitchThreshold = 15; // Number of frames not facing a wall to switch primary turn direction
        static bool fullscreenMinimap = false;
        public static void Start()
        {
            var tempCursor = Console.CursorVisible;
            Console.CursorVisible = false;
            UI.Wipe();
            UI.WriteCenterCenter("Loading...");
            screenWidth = Console.WindowWidth;
            screenHeight = Console.WindowHeight;

            // Initialize player's starting position to ensure adequate open space
            posX = 0.0;
            posY = 0.0;
            InitializePlayerStartingArea();

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            while (true)
            {
                // Handle window resizing
                if (screenWidth != Console.WindowWidth || screenHeight != Console.WindowHeight)
                {
                    screenWidth = Console.WindowWidth;
                    screenHeight = Console.WindowHeight;
                    try
                    {
                        Console.SetBufferSize(screenWidth, screenHeight);
                        UI.Wipe();
                    }
                    catch
                    {
                        // Ignore if setting buffer size fails
                    }
                }

                // Timing
                oldTime = time;
                time = stopwatch.Elapsed.TotalSeconds;
                double frameTime = time - oldTime;

                // Input
                ProcessInput(frameTime);

                // Autoplay
                if (isAutoplay)
                {
                    //PreventAutoplayLoop();
                    AutoplayMaze(frameTime);
                }

                // Render frame
                RenderFrame();

                // Sleep to reduce CPU usage
                Thread.Sleep(1);
                if (endGame)
                {
                    endGame = false;
                    UI.Wipe();
                    Console.CursorVisible = tempCursor;
                    break;
                }
            }
        }

        private static void PreventAutoplayLoop()
        {
            // check if player was in the same 3x3 area for the last 150 frames
            // if yes, remove a radius 3 area of obstacles around the player
            int maxFramesInArea = 100;
            int removeRadius = 3;
            lastPositions.Add(((int)posX, (int)posY));
            // complete here
            if (lastPositions.Count >= maxFramesInArea)
            {
                var uniquePositions = lastPositions.Distinct().ToList();
                if (uniquePositions.Count <= 9) // Player has been in a 3x3 area for the last 150 frames
                {
                    int centerX = (int)posX;
                    int centerY = (int)posY;

                    for (int y = centerY - removeRadius; y <= centerY + removeRadius; y++)
                    {
                        for (int x = centerX - removeRadius; x <= centerX + removeRadius; x++)
                        {
                            try
                            {
                                map[(x, y)] = ' '; // Remove obstacles in a radius 3 area
                            }
                            catch (Exception)
                            {

                                throw;
                            }

                        }
                    }
                    lastPositions.Clear(); // Reset the position history
                }
            }
        }

        static List<(int, int)> lastPositions = [];

        /// <summary>
        /// Initializes a larger open area around the player's starting position
        /// to prevent immediate trapping.
        /// </summary>
        static void InitializePlayerStartingArea()
        {
            int baseX = (int)Math.Floor(posX);
            int baseY = (int)Math.Floor(posY);
            Random random = new Random(1);

            // Define the radius of the open area
            int openRadius = 500;

            // Clear the base area
            for (int x = baseX - openRadius; x <= baseX + openRadius; x++)
            {
                for (int y = baseY - openRadius; y <= baseY + openRadius; y++)
                {
                    map[(x, y)] = ' ';
                }
            }

            // Add random walls
            for (int x = baseX - openRadius + 1; x < baseX + openRadius; x++)
            {
                for (int y = baseY - openRadius + 1; y < baseY + openRadius; y++)
                {
                    if (random.Next(100) < 30) // 30% chance of placing a wall
                    {
                        map[(x, y)] = '#';
                    }
                }
            }

            // Add random 3x3 rooms
            int numRooms = random.Next(3, 6); // 3 to 5 rooms
            for (int i = 0; i < numRooms; i++)
            {
                int roomX = random.Next(baseX - openRadius + 2, baseX + openRadius - 2);
                int roomY = random.Next(baseY - openRadius + 2, baseY + openRadius - 2);

                for (int x = roomX - 1; x <= roomX + 1; x++)
                {
                    for (int y = roomY - 1; y <= roomY + 1; y++)
                    {
                        map[(x, y)] = ' '; // Clear the room area
                    }
                }
            }

            // Ensure the player's starting position is clear
            map[(baseX, baseY)] = ' ';

            // Add walls around the perimeter
            for (int x = baseX - openRadius - 1; x <= baseX + openRadius + 1; x++)
            {
                for (int y = baseY - openRadius - 1; y <= baseY + openRadius + 1; y++)
                {
                    if (Math.Abs(x - baseX) == openRadius + 1 || Math.Abs(y - baseY) == openRadius + 1)
                    {
                        map[(x, y)] = '#';
                    }
                }
            }
        }

        static void ProcessInput(double frameTime)
        {
            double moveSpeed = frameTime * 3.75; // squares per second
            double rotSpeed = frameTime * 1.5;  // radians per second

            while (Console.KeyAvailable)
            {
                ConsoleKeyInfo ki = Console.ReadKey(true);
                var key = ki.Key;
                bool shift = ki.Modifiers == ConsoleModifiers.Shift;
                var mult = shift ? 0.2 : 1;
                double adjustedMoveSpeed = moveSpeed * (shift ? 0.2 : 1);
                double adjustedRotSpeed = rotSpeed * (shift ? 0.2 : 1);

                if (key == ConsoleKey.W || key == ConsoleKey.UpArrow)
                {
                    if (IsPositionEmpty(posX + dirX * adjustedMoveSpeed, posY))
                        posX += dirX * adjustedMoveSpeed;
                    if (IsPositionEmpty(posX, posY + dirY * adjustedMoveSpeed))
                        posY += dirY * adjustedMoveSpeed;

                    EnsureMapAroundPlayer();
                }
                if (key == ConsoleKey.S || key == ConsoleKey.DownArrow)
                {
                    if (IsPositionEmpty(posX - dirX * adjustedMoveSpeed, posY))
                        posX -= dirX * adjustedMoveSpeed;
                    if (IsPositionEmpty(posX, posY - dirY * adjustedMoveSpeed))
                        posY -= dirY * adjustedMoveSpeed;

                    EnsureMapAroundPlayer();
                }
                if (key == ConsoleKey.A)
                {
                    // Move left (strafe) relative to yaw
                    double perpDirX = Math.Cos(yaw - Math.PI / 2);
                    double perpDirY = Math.Sin(yaw - Math.PI / 2);
                    if (IsPositionEmpty(posX + perpDirX * adjustedMoveSpeed, posY))
                        posX += perpDirX * adjustedMoveSpeed;
                    if (IsPositionEmpty(posX, posY + perpDirY * adjustedMoveSpeed))
                        posY += perpDirY * adjustedMoveSpeed;

                    EnsureMapAroundPlayer();
                }

                if (key == ConsoleKey.D)
                {
                    // Move right (strafe) relative to yaw
                    double perpDirX = Math.Cos(yaw + Math.PI / 2);
                    double perpDirY = Math.Sin(yaw + Math.PI / 2);
                    if (IsPositionEmpty(posX + perpDirX * adjustedMoveSpeed, posY))
                        posX += perpDirX * adjustedMoveSpeed;
                    if (IsPositionEmpty(posX, posY + perpDirY * adjustedMoveSpeed))
                        posY += perpDirY * adjustedMoveSpeed;

                    EnsureMapAroundPlayer();
                }
                if (key == ConsoleKey.LeftArrow)
                {
                    // Rotate left (Yaw)
                    Rotate(-adjustedRotSpeed);
                }
                if (key == ConsoleKey.RightArrow)
                {
                    // Rotate right (Yaw)
                    Rotate(adjustedRotSpeed);
                }
                if (key == ConsoleKey.F1)
                {
                    // Toggle debug information
                    showUI = !showUI;
                }
                if (key == ConsoleKey.Escape)
                {
                    endGame = true;
                }
                if (key == ConsoleKey.G)
                {
                    wallTexture = !wallTexture;
                }
                if (key == ConsoleKey.Spacebar)
                {
                    // Toggle autoplay
                    isAutoplay = !isAutoplay;
                    currentAutoplayAction = "idle";
                }
                if (key == ConsoleKey.M)
                {
                    fullscreenMinimap = !fullscreenMinimap;
                }
            }
        }

        static bool IsPositionEmpty(double x, double y)
        {
            int mapX = (int)Math.Floor(x);
            int mapY = (int)Math.Floor(y);
            if (map.ContainsKey((mapX, mapY)))
                return map[(mapX, mapY)] == ' ';
            else
                return false; // Treat unknown areas as walls
        }

        static void Rotate(double angle)
        {
            double oldDirX = dirX;
            dirX = dirX * Math.Cos(angle) - dirY * Math.Sin(angle);
            dirY = oldDirX * Math.Sin(angle) + dirY * Math.Cos(angle);

            // Update camera plane based on new direction
            double oldPlaneX = planeX;
            planeX = planeX * Math.Cos(angle) - planeY * Math.Sin(angle);
            planeY = oldPlaneX * Math.Sin(angle) + planeY * Math.Cos(angle);

            yaw += angle;
            // Keep yaw within 0 to 2*PI
            yaw %= 2 * Math.PI;
            if (yaw < 0) yaw += 2 * Math.PI;
        }

        static void RenderFrame()
        {
            StringBuilder sb = new StringBuilder();

            // Initialize a 2D character array for the screen
            char[,] screenBuffer = new char[screenHeight, screenWidth];
            string[,] colorBuffer = new string[screenHeight, screenWidth];

            // Fill screen buffer with default characters and colors
            for (int y = 0; y < screenHeight; y++)
                for (int x = 0; x < screenWidth; x++)
                {
                    screenBuffer[y, x] = ' ';
                    colorBuffer[y, x] = "\u001b[0m"; // Reset color
                }

            // Raycasting
            for (int x = 0; x < screenWidth; x++)
            {
                // Calculate ray position and direction
                double cameraX = 2 * x / (double)screenWidth - 1; // x-coordinate in camera space
                double rayDirX = dirX + planeX * cameraX;
                double rayDirY = dirY + planeY * cameraX;

                // Which box of the map we're in
                int mapX = (int)Math.Floor(posX);
                int mapY = (int)Math.Floor(posY);

                // Length of ray from current position to next x or y-side
                double sideDistX;
                double sideDistY;

                // Length of ray from one x or y-side to next x or y-side
                double deltaDistX = (rayDirX == 0) ? 1e30 : Math.Abs(1 / rayDirX);
                double deltaDistY = (rayDirY == 0) ? 1e30 : Math.Abs(1 / rayDirY);
                double perpWallDist;

                // What direction to step in x or y-direction (either +1 or -1)
                int stepX, stepY;

                bool hit = false; // Was a wall hit?
                int side = 0; // Was the wall vertical or horizontal?

                // Calculate step and initial sideDist
                if (rayDirX < 0)
                {
                    stepX = -1;
                    sideDistX = (posX - mapX) * deltaDistX;
                }
                else
                {
                    stepX = 1;
                    sideDistX = (mapX + 1.0 - posX) * deltaDistX;
                }
                if (rayDirY < 0)
                {
                    stepY = -1;
                    sideDistY = (posY - mapY) * deltaDistY;
                }
                else
                {
                    stepY = 1;
                    sideDistY = (mapY + 1.0 - posY) * deltaDistY;
                }

                // Perform DDA
                while (!hit)
                {
                    // Jump to next map square, either in x-direction or y-direction
                    if (sideDistX < sideDistY)
                    {
                        sideDistX += deltaDistX;
                        mapX += stepX;
                        side = 0;
                    }
                    else
                    {
                        sideDistY += deltaDistY;
                        mapY += stepY;
                        side = 1;
                    }

                    // Check if ray has hit a wall
                    if (map.ContainsKey((mapX, mapY)))
                    {
                        if (map[(mapX, mapY)] == '#') hit = true;
                    }
                    else
                    {
                        // Treat unknown areas as walls
                        hit = true;
                    }
                }

                // Calculate distance to the point of impact
                if (side == 0)
                    perpWallDist = (sideDistX - deltaDistX);
                else
                    perpWallDist = (sideDistY - deltaDistY);

                // Calculate height of line to draw on screen
                int lineHeight = (int)(screenHeight / perpWallDist);

                // Calculate lowest and highest pixel to fill in current stripe
                int drawStart = Math.Max(-lineHeight / 2 + screenHeight / 2, 0);
                int drawEnd = Math.Min(lineHeight / 2 + screenHeight / 2, screenHeight - 1);

                if (wallTexture)
                {
                    // Base color components for #9e9a56
                    int baseR = 158; // 0x9e
                    int baseG = 154; // 0x9a
                    int baseB = 86;  // 0x56

                    // Adjust color intensity based on distance
                    float distanceFactor = (float)Math.Clamp(1.0 / (perpWallDist * 1.5), 0.0, 1.0);
                    int colorR = (int)(baseR * distanceFactor);
                    int colorG = (int)(baseG * distanceFactor);
                    int colorB = (int)(baseB * distanceFactor);

                    string baseWallColor = $"\u001b[38;2;{colorR};{colorG};{colorB}m";
                    string darkWallColor = $"\u001b[38;2;{Math.Max(colorR - 30, 0)};{Math.Max(colorG - 30, 0)};{Math.Max(colorB - 30, 0)}m";

                    // Darken colors for y-sides to add depth
                    if (side == 1)
                    {
                        colorR = Math.Max(colorR - 30, 0);
                        colorG = Math.Max(colorG - 30, 0);
                        colorB = Math.Max(colorB - 30, 0);
                        baseWallColor = $"\u001b[38;2;{colorR};{colorG};{colorB}m";
                        darkWallColor = $"\u001b[38;2;{Math.Max(colorR - 20, 0)};{Math.Max(colorG - 20, 0)};{Math.Max(colorB - 20, 0)}m";
                    }

                    for (int y = drawStart; y <= drawEnd; y++)
                    {
                        if (y >= 0 && y < screenHeight && x >= 0 && x < screenWidth)
                        {
                            // Create a simple pattern
                            bool isAlternateColor = ((y + x) % 2 == 0);

                            // Set wall color and pattern
                            colorBuffer[y, x] = isAlternateColor ? darkWallColor : baseWallColor;
                            screenBuffer[y, x] = '█'; // Full block for texture
                        }
                    }
                }
                else
                {
                    // Base color components for #9e9a56
                    int baseR = 158; // 0x9e
                    int baseG = 154; // 0x9a
                    int baseB = 86;  // 0x56

                    // Adjust color intensity based on distance
                    float distanceFactor = (float)Math.Clamp(1.0 / (perpWallDist * 1.5), 0.0, 1.0);
                    int colorR = (int)(baseR * distanceFactor);
                    int colorG = (int)(baseG * distanceFactor);
                    int colorB = (int)(baseB * distanceFactor);

                    string wallColor = $"\u001b[38;2;{colorR};{colorG};{colorB}m";

                    // Darken colors for y-sides to add depth
                    if (side == 1)
                    {
                        colorR = Math.Max(colorR - 30, 0);
                        colorG = Math.Max(colorG - 30, 0);
                        colorB = Math.Max(colorB - 30, 0);
                        wallColor = $"\u001b[38;2;{colorR};{colorG};{colorB}m";
                    }

                    for (int y = drawStart; y <= drawEnd; y++)
                    {
                        if (y >= 0 && y < screenHeight && x >= 0 && x < screenWidth)
                        {
                            // Set wall color
                            colorBuffer[y, x] = wallColor;
                            screenBuffer[y, x] = '\u2588'; // Full block character
                        }
                    }
                }

                // Ceiling: Bright yellow
                for (int y = 0; y < drawStart; y++)
                {
                    if (y >= 0 && y < screenHeight && x >= 0 && x < screenWidth)
                    {
                        colorBuffer[y, x] = "\u001b[38;2;111;99;39m";
                        screenBuffer[y, x] = '\u2588'; // Full block character
                    }
                }

                // Floor: Dark yellow
                for (int y = drawEnd + 1; y < screenHeight; y++)
                {
                    if (y >= 0 && y < screenHeight && x >= 0 && x < screenWidth)
                    {
                        //65541e
                        colorBuffer[y, x] = "\u001b[38;2;101;84;30m";
                        screenBuffer[y, x] = '\u2588'; // Full block character
                    }
                }
            }

            if (showUI)
            {
                // Overlay Minimap at bottom-right corner
                RenderMinimap(screenBuffer, colorBuffer);
                // Overlay Debug Information at bottom-left corner
                RenderDebugInfo(screenBuffer, colorBuffer);
            }

            // Build the final string with colors
            for (int y = 0; y < screenHeight; y++)
            {
                for (int x = 0; x < screenWidth; x++)
                {
                    sb.Append($"{colorBuffer[y, x]}{screenBuffer[y, x]}");
                }
                sb.Append(resetColor); // Reset color at end of line
                if (y != screenHeight - 1)
                {
                    sb.Append('\n');
                }
            }

            // Output to console
            try
            {
                Console.SetCursorPosition(0, 0);
                Console.Write(sb.ToString());
            }
            catch (ArgumentOutOfRangeException)
            {
                // Handle potential exceptions when writing outside the buffer
            }
        }
        const int minimapEdgeAmount = 5;

        /// <summary>
        /// Restores the minimap to its original aspect ratio without horizontal stretching.
        /// </summary>
        static void RenderMinimap(char[,] screenBuffer, string[,] colorBuffer)
        {
            int minimapWidth, minimapHeight, startX, startY;

            if (fullscreenMinimap)
            {
                minimapWidth = screenWidth - 2 * minimapEdgeAmount;
                minimapHeight = screenHeight - 2 * minimapEdgeAmount;
                startX = minimapEdgeAmount;
                startY = minimapEdgeAmount;
            }
            else
            {
                minimapWidth = minimapSize;
                minimapHeight = minimapSize;
                startX = screenWidth - minimapWidth - 2;
                startY = screenHeight - minimapHeight - 1;
            }

            bool playerAdded = false;

            for (int y = 0; y < minimapHeight; y++)
            {
                for (int x = 0; x < minimapWidth; x++)
                {
                    int mapX = (int)(posX - minimapWidth / 2 + x);
                    int mapY = (int)(posY - minimapHeight / 2 + y);

                    if (mapX == (int)posX && mapY == (int)posY && !playerAdded)
                    {
                        // Player position on minimap: Bright Green '█'
                        if (IsWithinScreen(startX + x, startY + y))
                        {
                            colorBuffer[startY + y, startX + x] = "\u001b[38;2;0;255;0m"; // Bright Green
                            screenBuffer[startY + y, startX + x] = '█';
                            playerAdded = true;
                        }
                    }
                    else if (map.ContainsKey((mapX, mapY)))
                    {
                        if (map[(mapX, mapY)] == '#')
                        {
                            // Wall on minimap: White '#'
                            if (IsWithinScreen(startX + x, startY + y))
                            {
                                colorBuffer[startY + y, startX + x] = "\u001b[38;2;164;156;81m"; // White
                                screenBuffer[startY + y, startX + x] = '█';
                            }
                        }
                        else
                        {
                            // Empty space on minimap: Light Gray '.'
                            if (IsWithinScreen(startX + x, startY + y))
                            {
                                colorBuffer[startY + y, startX + x] = "\u001b[38;2;194;169;116m"; // Light Gray
                                screenBuffer[startY + y, startX + x] = '█';
                            }
                        }
                    }
                    else
                    {
                        // Outside map bounds: Render space
                        if (IsWithinScreen(startX + x, startY + y))
                        {
                            colorBuffer[startY + y, startX + x] = "\u001b[38;2;0;0;0m"; // Reset color
                            screenBuffer[startY + y, startX + x] = '█';
                        }
                    }
                }
            }
        }

        static void RenderDebugInfo(char[,] screenBuffer, string[,] colorBuffer)
        {
            string posInfo = $"Pos: X={posX:F2}, Y={posY:F2}";
            string rotInfo = $"Yaw: {yaw * (180 / Math.PI):F2}° Pitch: {pitch * (180 / Math.PI):F2}°";
            string autoplayStatus = $"Autoplay: {(isAutoplay ? "ON" : "OFF")}";

            for (int i = 0; i < posInfo.Length && i < screenWidth; i++)
            {
                if (IsWithinScreen(i, screenHeight - 3))
                {
                    colorBuffer[screenHeight - 3, i] = "\u001b[38;2;255;255;0m"; // Yellow
                    screenBuffer[screenHeight - 3, i] = posInfo[i];
                }
            }

            for (int i = 0; i < rotInfo.Length && i < screenWidth; i++)
            {
                if (IsWithinScreen(i, screenHeight - 2))
                {
                    colorBuffer[screenHeight - 2, i] = "\u001b[38;2;255;255;0m"; // Yellow
                    screenBuffer[screenHeight - 2, i] = rotInfo[i];
                }
            }

            for (int i = 0; i < autoplayStatus.Length && i < screenWidth; i++)
            {
                if (IsWithinScreen(i, screenHeight - 1))
                {
                    colorBuffer[screenHeight - 1, i] = isAutoplay ? "\u001b[38;2;0;255;0m" : "\u001b[38;2;255;0;0m"; // Green if on, Red if off
                    screenBuffer[screenHeight - 1, i] = autoplayStatus[i];
                }
            }
        }

        static bool IsWithinScreen(int x, int y)
        {
            return x >= 0 && x < screenWidth && y >= 0 && y < screenHeight;
        }

        /// <summary>
        /// Ensures that the map is generated around the player's current position.
        /// Expands the map dynamically as the player moves.
        /// </summary>
        static void EnsureMapAroundPlayer()
        {
            int baseX = (int)Math.Floor(posX);
            int baseY = (int)Math.Floor(posY);

            // Define the radius around the player to generate the map
            int generationRadius = 10;

            for (int x = baseX - generationRadius; x <= baseX + generationRadius; x++)
            {
                for (int y = baseY - generationRadius; y <= baseY + generationRadius; y++)
                {
                    if (!map.ContainsKey((x, y)))
                    {
                        // Simple procedural generation: walls are less likely to be placed near the center
                        double distance = Math.Sqrt(Math.Pow(x - baseX, 2) + Math.Pow(y - baseY, 2));
                        double wallProbability = Math.Clamp(0.3 - (distance / generationRadius) * 0.2, 0.1, 0.3);
                        map[(x, y)] = (autoplayRandom.NextDouble() < wallProbability) ? '#' : ' ';
                    }
                }
            }
        }
        
        static bool turningRight = true;
        static int noWallCounter = 0;
        /// <summary>
        /// AutoplayMaze method that automatically explores the maze with intelligent wall avoidance.
        /// </summary>
        static void AutoplayMaze(double frameTime)
        {
            // Execute the chosen action
            double moveSpeed = frameTime * 3.75; // Consistent with manual movement
            double rotSpeed = frameTime * 1.5;   // Radians per frame
            double distanceAhead = GetDistanceToWall(dirX, dirY);
            if (distanceAhead < wallThreshold)
            {
                if (noWallCounter > noWallSwitchThreshold)
                {
                    turningRight = !turningRight;
                }
                noWallCounter = 0;
                currentAutoplayAction = "move_forward_small";
                for (int i = 0; i < wallRotations; i++)
                {
                    DoAction(turningRight ? "rotate_right" : "rotate_left", moveSpeed, rotSpeed);
                }
            }
            else
            {
                noWallCounter++;
                var choice = autoplayRandom.Next(0, 100);
                if (choice < 70)
                {
                    currentAutoplayAction = "move_forward";
                }
                else if (choice < 95)
                {
                    currentAutoplayAction = turningRight ? "rotate_left" : "rotate_right";
                }
                else
                {
                    currentAutoplayAction = turningRight ? "rotate_right" : "rotate_left";
                }
            }

            DoAction(currentAutoplayAction, moveSpeed, rotSpeed);
        }

        private static void DoAction(string action, double moveSpeed, double rotSpeed)
        {
            var smallStepFactor = 3;
            switch (action)
            {
                case "move_forward_small":
                    if (IsPositionEmpty(posX + dirX * moveSpeed / smallStepFactor, posY))
                        posX += dirX * moveSpeed / smallStepFactor;
                    if (IsPositionEmpty(posX, posY + dirY * moveSpeed / smallStepFactor))
                        posY += dirY * moveSpeed / smallStepFactor;

                    EnsureMapAroundPlayer();
                    break;
                case "move_forward":
                    if (IsPositionEmpty(posX + dirX * moveSpeed, posY))
                        posX += dirX * moveSpeed;
                    if (IsPositionEmpty(posX, posY + dirY * moveSpeed))
                        posY += dirY * moveSpeed;

                    EnsureMapAroundPlayer();
                    break;
                case "move_back":
                    if (IsPositionEmpty(posX - dirX * moveSpeed / 3, posY))
                        posX -= dirX * moveSpeed / 3;
                    if (IsPositionEmpty(posX, posY - dirY * moveSpeed / 3))
                        posY -= dirY * moveSpeed / 3;

                    EnsureMapAroundPlayer();
                    break;

                case "rotate_left":
                    Rotate(-rotSpeed);
                    break;

                case "rotate_right":
                    Rotate(rotSpeed);
                    break;

                default:
                    // Idle or unknown action
                    break;
            }
        }

        /// <summary>
        /// Calculates the distance from the player's current position in the facing direction to the nearest wall.
        /// </summary>
        /// <param name="dirX">The X component of the player's direction vector.</param>
        /// <param name="dirY">The Y component of the player's direction vector.</param>
        /// <returns>The perpendicular distance to the nearest wall.</returns>
        static double GetDistanceToWall(double dirX, double dirY)
        {
            // Initialize ray position and direction
            double rayPosX = posX;
            double rayPosY = posY;
            double rayDirX = dirX;
            double rayDirY = dirY;

            // Map position
            int mapX = (int)Math.Floor(rayPosX);
            int mapY = (int)Math.Floor(rayPosY);

            // Length of ray from one x or y-side to next x or y-side
            double deltaDistX = (rayDirX == 0) ? 1e30 : Math.Abs(1 / rayDirX);
            double deltaDistY = (rayDirY == 0) ? 1e30 : Math.Abs(1 / rayDirY);

            double sideDistX;
            double sideDistY;

            int stepX, stepY;

            bool hit = false; // Was a wall hit?
            int side = 0; // Was the wall hit a vertical side?

            // Calculate step and initial sideDist
            if (rayDirX < 0)
            {
                stepX = -1;
                sideDistX = (rayPosX - mapX) * deltaDistX;
            }
            else
            {
                stepX = 1;
                sideDistX = (mapX + 1.0 - rayPosX) * deltaDistX;
            }
            if (rayDirY < 0)
            {
                stepY = -1;
                sideDistY = (rayPosY - mapY) * deltaDistY;
            }
            else
            {
                stepY = 1;
                sideDistY = (mapY + 1.0 - rayPosY) * deltaDistY;
            }

            // Perform DDA
            while (!hit)
            {
                // Jump to next map square, either in x-direction or y-direction
                if (sideDistX < sideDistY)
                {
                    sideDistX += deltaDistX;
                    mapX += stepX;
                    side = 0;
                }
                else
                {
                    sideDistY += deltaDistY;
                    mapY += stepY;
                    side = 1;
                }

                // Check if ray has hit a wall
                if (map.ContainsKey((mapX, mapY)))
                {
                    if (map[(mapX, mapY)] == '#') hit = true;
                }
                else
                {
                    // Treat unknown areas as walls
                    hit = true;
                }
            }

            // Calculate distance to the point of impact
            double perpWallDist;
            if (side == 0)
                perpWallDist = (sideDistX - deltaDistX);
            else
                perpWallDist = (sideDistY - deltaDistY);

            return perpWallDist;
        }
    }
}

randomly put square lights on the ceiling, which will be represented by L characters on the character map. then if it contains a light source, it will appear on the ceiling in the rendered frame as a light source that glows light down. it will be a white square on the ceiling that also transforms along with the perspective and view. the rest of the space in the area where the light is is like an empty space. i will also be able to adjust the frequency of spawning of these lights on the map.