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>();

        // Ceiling lights storage
        static HashSet<(int, int)> ceilingLights = new HashSet<(int, int)>();

        // 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 = new List<(int, int)>();

        /// <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)] = '#';
                    }
                }
            }

            // Initialize ceiling lights
            InitializeCeilingLights(baseX, baseY, openRadius, random);
        }

        /// <summary>
        /// Initializes ceiling lights in the open areas randomly.
        /// </summary>
        /// <param name="baseX">Base X position of the player.</param>
        /// <param name="baseY">Base Y position of the player.</param>
        /// <param name="openRadius">Radius of the open area.</param>
        /// <param name="random">Random number generator.</param>
        static void InitializeCeilingLights(int baseX, int baseY, int openRadius, Random random)
        {
            int numLights = Math.Max(10, openRadius / 50); // Adjust number based on openRadius
            for (int i = 0; i < numLights; i++)
            {
                int lightX = random.Next(baseX - openRadius + 1, baseX + openRadius);
                int lightY = random.Next(baseY - openRadius + 1, baseY + openRadius);

                // Ensure it's an open area and not already a light
                if (map.ContainsKey((lightX, lightY)) && map[(lightX, lightY)] == ' ' && !ceilingLights.Contains((lightX, lightY)))
                {
                    ceilingLights.Add((lightX, lightY));
                }
            }
        }

        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 with possible lights
                for (int y = 0; y < drawStart; y++)
                {
                    if (y >= 0 && y < screenHeight && x >= 0 && x < screenWidth)
                    {
                        // Determine the map coordinates corresponding to this ceiling pixel
                        // Approximate by reversing the raycasting logic
                        double distanceToCeiling = perpWallDist; // Using perpendicular distance as approximation
                        double worldX = posX + rayDirX * distanceToCeiling * 1.0;
                        double worldY = posY + rayDirY * distanceToCeiling * 1.0;

                        int ceilingMapX = (int)Math.Floor(worldX);
                        int ceilingMapY = (int)Math.Floor(worldY);

                        bool isLightHere = ceilingLights.Contains((ceilingMapX, ceilingMapY));

                        if (isLightHere)
                        {
                            // Render light as white square
                            colorBuffer[y, x] = "\u001b[38;2;255;255;255m"; // White
                        }
                        else
                        {
                            // Regular ceiling color
                            colorBuffer[y, x] = "\u001b[38;2;111;99;39m";
                        }
                        screenBuffer[y, x] = '█'; // White square
                    }
                }

                // 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"; // Dark Yellow
                        screenBuffer[y, x] = '\u2588'; // Full block character
                    }
                }
            }

            // Render Ceiling Lights (Project lights onto the screen)
            RenderCeilingLights(screenBuffer, colorBuffer);

            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>
        /// Renders ceiling lights onto the screen by projecting their positions based on the player's view.
        /// </summary>
        /// <param name="screenBuffer">Character buffer for the screen.</param>
        /// <param name="colorBuffer">Color buffer for the screen.</param>
        static void RenderCeilingLights(char[,] screenBuffer, string[,] colorBuffer)
        {
            foreach (var light in ceilingLights)
            {
                double lightX = light.Item1 + 0.5; // Center of the cell
                double lightY = light.Item2 + 0.5;

                // Translate world coordinates to camera space
                double relX = lightX - posX;
                double relY = lightY - posY;

                // Transform to camera space
                double invDet = 1.0 / (planeX * dirY - dirX * planeY); // Calculate the inverse determinant
                double transformX = invDet * (dirY * relX - dirX * relY);
                double transformY = invDet * (-planeY * relX + planeX * relY); // Depth in camera space

                if (transformY <= 0)
                    continue; // Light is behind the player

                // Screen X position
                int screenX = (int)((screenWidth / 2) * (1 + transformX / transformY));

                if (screenX < 0 || screenX >= screenWidth)
                    continue; // Light is outside the horizontal FOV

                // Ceiling Y position (fixed near the top)
                int ceilingY = (int)(transformY > 0 ? 2 : 1); // Adjust as needed for desired Y position

                if (ceilingY >= 0 && ceilingY < screenHeight)
                {
                    // Optional: Adjust brightness based on distance
                    double distanceFactor = Math.Clamp(1.0 / (transformY * 0.5), 0.3, 1.0);
                    int brightness = (int)(255 * distanceFactor);
                    brightness = Math.Min(brightness, 255);
                    string lightColor = $"\u001b[38;2;{brightness};{brightness};{brightness}m";

                    // Render as a white square
                    colorBuffer[ceilingY, screenX] = lightColor;
                    screenBuffer[ceilingY, screenX] = '■';

                    // Optionally, render adjacent pixels for bigger lights
                    for (int dx = -1; dx <= 1; dx++)
                    {
                        for (int dy = -1; dy <= 1; dy++)
                        {
                            int adjX = screenX + dx;
                            int adjY = ceilingY + dy;
                            if (adjX >= 0 && adjX < screenWidth && adjY >= 0 && adjY < screenHeight)
                            {
                                colorBuffer[adjY, adjX] = lightColor;
                                screenBuffer[adjY, adjX] = '■';
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Initializes ceiling lights in dynamically generated areas.
        /// </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) ? '#' : ' ';

                        // Randomly place ceiling lights in open spaces
                        if (map[(x, y)] == ' ' && autoplayRandom.NextDouble() < 0.02) // 2% chance
                        {
                            ceilingLights.Add((x, y));
                        }
                    }
                }
            }
        }

        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;
        }

        /// <summary>
        /// Renders the minimap, including ceiling lights, on the screen.
        /// </summary>
        /// <param name="screenBuffer">Character buffer for the screen.</param>
        /// <param name="colorBuffer">Color buffer for the screen.</param>
        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] = '█';
                            }
                        }

                        // Render ceiling lights on the minimap
                        if (ceilingLights.Contains((mapX, mapY)))
                        {
                            if (IsWithinScreen(startX + x, startY + y))
                            {
                                colorBuffer[startY + y, startX + x] = "\u001b[38;2;255;255;255m"; // White
                                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;
        }
    }
}

the lights should appear as distorted and skewed polygons in the center of that space on the ceiling depending on the perspective and distance, like rays being cast to the ceiling should render a light if it's there