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

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 = true;
        static bool endGame = false;
        public static void Start()
        {
            var tempCursor = Console.CursorVisible;
            Console.CursorVisible = false;
            UI.Wipe();
            UI.WriteCenterCenter("Loading...");
            // Set initial window and buffer size
            //try
            //{
            //    Console.SetWindowSize(120, 40);
            //    Console.SetBufferSize(120, 40);
            //}
            //catch
            //{
            //    // Ignore if setting window size fails due to system limitations
            //}
            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);

                // Render frame
                RenderFrame();

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

        /// <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;
                moveSpeed *= mult;
                rotSpeed *= mult;
                if (key == ConsoleKey.W || key == ConsoleKey.UpArrow)
                {
                    if (IsPositionEmpty(posX + dirX * moveSpeed, posY))
                        posX += dirX * moveSpeed;
                    if (IsPositionEmpty(posX, posY + dirY * moveSpeed))
                        posY += dirY * moveSpeed;

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

                    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 * moveSpeed, posY))
                        posX += perpDirX * moveSpeed;
                    if (IsPositionEmpty(posX, posY + perpDirY * moveSpeed))
                        posY += perpDirY * moveSpeed;

                    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 * moveSpeed, posY))
                        posX += perpDirX * moveSpeed;
                    if (IsPositionEmpty(posX, posY + perpDirY * moveSpeed))
                        posY += perpDirY * moveSpeed;

                    EnsureMapAroundPlayer();
                }
                if (key == ConsoleKey.LeftArrow)
                {
                    // Rotate left (Yaw)
                    Rotate(-rotSpeed);
                }
                if (key == ConsoleKey.RightArrow)
                {
                    // Rotate right (Yaw)
                    Rotate(rotSpeed);
                }
                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)
                {
                    AutoplayMaze();
                }
            }
        }

        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
                    {
                        // Generate new map area on-the-fly to simulate infinite map
                        //GenerateMapArea(mapX, mapY);
                        if (map.ContainsKey((mapX, mapY)) && map[(mapX, mapY)] == '#') hit = true;
                        else
                            hit = true; // Treat unknown areas as walls
                    }
                }

                // 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] = isAlternateColor ? '█' : '█'; // Different characters 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
            }
        }

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

            // Calculate top-left corner of minimap at bottom-right
            int startX = screenWidth - minimapWidth - 2; // 2 characters padding from right
            int startY = screenHeight - minimapHeight - 1; // 1 line padding from bottom

            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 'P'
                        if (IsWithinScreen(startX + x, startY + y))
                        {
                            colorBuffer[startY + y, startX + x] = "\u001b[38;2;0;255;0m"; // Bright Green
                            screenBuffer[startY + y, startX + x] = 'P';
                            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;255;255;255m"; // 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;200;200;200m"; // 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[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}°";

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

            for (int i = 0; i < rotInfo.Length && i < screenWidth; i++)
            {
                if (IsWithinScreen(i, screenHeight - 1))
                {
                    colorBuffer[screenHeight - 1, i] = "\u001b[38;2;255;255;0m"; // Yellow
                    screenBuffer[screenHeight - 1, i] = rotInfo[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)))
                    {
                        map[(x, y)] = ' '; // Keep central area open
                    }
                }
            }
        }
    }
}

make the AutoplayMaze method that does inputs automatically that automatically explores the maze (rotating the head etc) so that you can still see what's going on until you press space again. add some randomness to that exploration. non deterministic randomness. make sure the agent that plays the game will explore the map as much as it can until space is pressed again.