using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Unity.VisualScripting;
using UnityEngine;
using Random = System.Random;

namespace DungeonGenerator
{
    public class DungeonGenerator
    {
        private const int SIDE_LENGTH_OF_MONSTER = 4;
        private const int SIDE_LENGTH_OF_NORMAL  = 2;
        private const int SIDE_LENGTH_OF_ENTRANCE = 2;
        private const int WIDTH_OF_BOSS = 10;
        private const int HEIGHT_OF_BOSS = 6;
        
        private int _xLength = 40;
        private int _yLength = 28;
        
        public DungeonMap GenerateDungeon(int length, float monsterRoomRatio)
        {
            _xLength = 40;
            _yLength = 28;

            Random random = new Random();
            DungeonMap dungeonMap = new DungeonMap(_xLength, _yLength);
            
            dungeonMap.AddRooms(GenerateEntranceRooms(_xLength, _yLength, random.Next(1,4)));
            dungeonMap.AddRoom(GenerateOnlyBossRoom(_xLength, _yLength, WIDTH_OF_BOSS, HEIGHT_OF_BOSS));
            
            EvenDisperser disperser = new EvenDisperser(_xLength, _yLength, dungeonMap.GetUnoccupiedPoints()); //TODO calculate L and W from length 
            int numberOfMonsterRooms = 7; // TODO: Calculate from ratio
            
            for (var i = 0; i < numberOfMonsterRooms; i ++)
            {
                dungeonMap.AddRoom(disperser.GenerateAndPlaceRoom(
                    SIDE_LENGTH_OF_MONSTER, 
                    SIDE_LENGTH_OF_MONSTER,
                    RoomType.Monster));
            }
            
            AddNormalRoomsAroundMonsterRooms(dungeonMap);

            return dungeonMap;
        }

        private void AddNormalRoomsAroundMonsterRooms(DungeonMap dungeon)
        {
            HashSet<Point> unoccupiedPoints = dungeon.GetUnoccupiedPoints();
            Random random = new Random();
            foreach (Room monsterRoom in dungeon.GetMonsterRooms())
            {
                var roomPoints = monsterRoom.GetPointsInRoom();
                int minX = roomPoints.Min(p => p.X);
                int maxX = roomPoints.Max(p => p.X);
                int minY = roomPoints.Min(p => p.Y);
                int maxY = roomPoints.Max(p => p.Y);
                
                List<Point> topUnoccupiedPoints = new List<Point>();
                List<Point> bottomUnoccupiedPoints = new List<Point>();
                List<Point> leftUnoccupiedPoints = new List<Point>();
                List<Point> rightUnoccupiedPoints = new List<Point>();

                for (int x = minX - SIDE_LENGTH_OF_NORMAL; x <= maxX + SIDE_LENGTH_OF_NORMAL; x++)
                {
                    Point point = new Point(x, minY - SIDE_LENGTH_OF_NORMAL);
                    if (dungeon.GetUnoccupiedPoints().Contains(point))
                    {
                        topUnoccupiedPoints.Add(point);
                    }

                    point = new Point(x, maxY + SIDE_LENGTH_OF_NORMAL);
                    if (dungeon.GetUnoccupiedPoints().Contains(point))
                    {
                        bottomUnoccupiedPoints.Add(point);
                    }
                }

                for (int y = minY - SIDE_LENGTH_OF_NORMAL; y <= maxY + SIDE_LENGTH_OF_NORMAL; y++)
                {
                    Point point = new Point(minX - SIDE_LENGTH_OF_NORMAL, y);
                    if (dungeon.GetUnoccupiedPoints().Contains(point))
                    {
                        leftUnoccupiedPoints.Add(point);
                    }

                    point = new Point(maxX + SIDE_LENGTH_OF_NORMAL, y);
                    if (dungeon.GetUnoccupiedPoints().Contains(point))
                    {
                        rightUnoccupiedPoints.Add(point);
                    }
                }

                int minNecessaryAvailablePoints = (SIDE_LENGTH_OF_MONSTER + SIDE_LENGTH_OF_NORMAL) / 2;
                
                List<RoomSide> availableSides = new List<RoomSide>();
                if (topUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Top);}
                if (bottomUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Bottom);}
                if (leftUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Left);}
                if (rightUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Right);}
                
                // Ensure between 3 and 4 rooms are added
                int numRoomsToAdd = Math.Min(availableSides.Count, random.Next(3, 5));
        
                // Randomly shuffle and take the required number of sides
                foreach (RoomSide side in availableSides.OrderBy(_ => random.Next()).Take(numRoomsToAdd))
                {
                    // Select appropriate unoccupied points based on the side
                    List<Point> unoccupiedPointsOnSide = side switch
                    {
                        RoomSide.Top => topUnoccupiedPoints,
                        RoomSide.Bottom => bottomUnoccupiedPoints,
                        RoomSide.Left => leftUnoccupiedPoints,
                        RoomSide.Right => rightUnoccupiedPoints,
                        _ => new List<Point>()
                    };

                    // Create room and add it if valid
                    Room newRoom = CreateAdjacentRoom(RoomType.Normal, unoccupiedPointsOnSide, side, dungeon.GetOccupiedPoints());
                    if (newRoom != null)
                    {
                        monsterRoom.AddAdjacentRoom(newRoom, side);
                        dungeon.AddRoom(newRoom);
                    }
                }
            }
        }

        private Room CreateAdjacentRoom(RoomType type, List<Point> unoccupiedPointsOnSide, RoomSide side, HashSet<Point> occupiedPoints)
        {
            int sizeOfNewRoom = GetRoomSizeByType(type);
            Random random = new Random();
            
            // Sort points by their coordinate based on the side of placement
            var orderedPoints = side == RoomSide.Left || side == RoomSide.Right
                ? unoccupiedPointsOnSide.OrderBy(p => p.Y).ToList()  // Sort by Y for vertical placement
                : unoccupiedPointsOnSide.OrderBy(p => p.X).ToList();

            // List to store possible valid room placements (top-left points of the room)
            List<Point> validPlacements = new List<Point>();

            // Iterate over the ordered points to find potential placements
            for (int i = 0; i < orderedPoints.Count - sizeOfNewRoom + 1; i++)
            {
                // Get a sequence of SIZE_OF_ROOM points starting from index i
                var potentialPoints = orderedPoints.Skip(i).Take(sizeOfNewRoom).ToList();

                // Check if all points are consecutive and fit within the range
                if (IsRoomWideEnough(potentialPoints, side))
                {
                    Point firstPoint = potentialPoints.First();
                    
                    // Adjust the first point for different sides
                    Point adjustedPoint = side switch
                    {
                        RoomSide.Top => new Point(firstPoint.X, firstPoint.Y),  // Move up for top side
                        RoomSide.Bottom => new Point(firstPoint.X, firstPoint.Y - 1),  // Move down for bottom side
                        RoomSide.Left => new Point(firstPoint.X, firstPoint.Y),  // Move left for left side
                        RoomSide.Right => new Point(firstPoint.X - 1, firstPoint.Y),  // Move right for right side
                        _ => firstPoint  // Default case (shouldn't happen)
                    };

                    if (!potentialPoints.Any(occupiedPoints.Contains))
                    {
                        validPlacements.Add(adjustedPoint); // First point is the top-left point of the room
                    }
                }
            }

            // If there are valid placements, select a random one and create the room
            if (validPlacements.Any())
            {
                Point randomPlacement = validPlacements[random.Next(validPlacements.Count)];
                Room newRoom = new Room(type, sizeOfNewRoom, sizeOfNewRoom, randomPlacement);

                return newRoom;
            }

            // If no valid placements are found, return null or handle it as necessary
            return null;
        }
        
        // Helper method to check if a sequence of points is wide enough (consecutive points)
        private bool IsRoomWideEnough(List<Point> points, RoomSide side)
        {
            for (int i = 1; i < points.Count; i++)
            {
                if (side == RoomSide.Left || side == RoomSide.Right)
                {
                    // Check if points are consecutive along the Y-axis for left/right side
                    if (points[i].Y != points[i - 1].Y + 1)
                    {
                        return false;
                    }
                }
                else
                {
                    // Check if points are consecutive along the X-axis for top/bottom side
                    if (points[i].X != points[i - 1].X + 1)
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        private int GetRoomSizeByType(RoomType type)
        {
            switch (type)
            {
                case RoomType.Entrance:
                    return SIDE_LENGTH_OF_ENTRANCE;
                case RoomType.Monster:
                    return 
                        SIDE_LENGTH_OF_MONSTER;
                case RoomType.Normal:
                    return SIDE_LENGTH_OF_NORMAL;
                default:
                    return SIDE_LENGTH_OF_NORMAL;
            }
        }
        
        private Room GenerateOnlyBossRoom(int xLengthOfDungeon, int yLengthOfDungeon, int width, int heigth)
        {
            // Place monster room in the middle third
            int middleAreaX = xLengthOfDungeon / 3;
            int middleAreaY = yLengthOfDungeon / 3;
            Random random = new Random();
            int bossX = middleAreaX + random.Next(0, middleAreaX - width);
            int bossY = middleAreaY + random.Next(0, middleAreaY - heigth);
            return new Room(RoomType.Boss, width, heigth, new Point(bossX, bossY));
        }
        
        private enum Side
        {
            Top,
            Right,
            Bottom,
            Left
        }
        
        private List<Room> GenerateEntranceRooms(int xLengthOfDungeon, int yLengthOfDungeon, int numberOfEntranceLines)
        {
            if (numberOfEntranceLines > 4)
            {
                throw new Exception("Number of entrance lines cannot be greater than 4");
            }
            
            const int numEntranceRooms = 12;
            List<Room> entranceRooms = new List<Room>();
            Random random = new Random();
            Dictionary<Side, int> sidesToEnterOn = new()
            {
                { Side.Top, 0},
                { Side.Right, 0},
                { Side.Bottom, 0},
                { Side.Left, 0}
            };
            
            // Randomly assign starting lines to sides of dungeon
            for (var i = 0; i < numberOfEntranceLines; i++)
            {
                while (true)
                {
                    Side side = (Side)random.Next(0, 4);
                    if (sidesToEnterOn[side] < 2)
                    {
                        sidesToEnterOn[side] += 1;
                        break;
                    }
                }
            }

            int roomsPerLine = numEntranceRooms / numberOfEntranceLines;
            int bottom = yLengthOfDungeon - SIDE_LENGTH_OF_NORMAL;
            int oneRoomFromBottom = bottom - SIDE_LENGTH_OF_NORMAL;
            int right = xLengthOfDungeon - SIDE_LENGTH_OF_NORMAL;
            // Generate entrance lines on each side
            switch (sidesToEnterOn[Side.Top])
            {
                case 2:
                    // Add half of points for this entrance line to the top left side
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(i,0)));
                    }
                    
                    // Add the rest of the points for this entrance line to the top right side.
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right - i,0)));
                    }
                    break;
                case 1:
                    int startOfLine = GetStartOfCenteredLine(xLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL);
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL,new Point(startOfLine + i, 0)));
                    }
                    break;
            }

            switch (sidesToEnterOn[Side.Right])
            {
                case 2:
                    // Add points for this entrance line to the top right side
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right, i + SIDE_LENGTH_OF_NORMAL)));
                    }
                    
                    // Add the rest of the points for this entrance line to the bottom right side.
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right, oneRoomFromBottom - i)));
                    }
                    break;
                case 1:
                    int startOfLine = GetStartOfCenteredLine(yLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL);
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right, startOfLine + i)));
                    }
                    break;
            }

            switch (sidesToEnterOn[Side.Bottom])
            {
                case 2:
                    // Add half of points for this entrance line to the bottom left side
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(i, bottom)));
                    }
                    
                    // Add the rest of the points for this entrance line to the bottom right side.
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point((xLengthOfDungeon - 1) - i, bottom)));
                    }
                    break;
                case 1:
                    int startOfLine = GetStartOfCenteredLine(xLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL);
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(startOfLine + i, bottom)));
                    }
                    break;
            }

            switch (sidesToEnterOn[Side.Left])
            {
                case 2:
                    // Add half of points for this entrance line to the top left side
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(0, i + SIDE_LENGTH_OF_NORMAL)));
                    }
                    
                    // Add the rest of the points for this entrance line to the bottom left side.
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(0, oneRoomFromBottom - i)));
                    }
                    break;
                case 1:
                    int startOfLine = GetStartOfCenteredLine(yLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL);
                    for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
                    {
                        entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(0, startOfLine + i)));
                    }
                    break;
            }

            return entranceRooms;
        }

        private int GetStartOfCenteredLine(int length, int numberOfRooms, int sizeOfRoom)
        {
            int midpoint = length / 2;
            return midpoint - (numberOfRooms * sizeOfRoom / 2);
        }
    }
}