using System;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using Random = UnityEngine.Random;
using System.Runtime.Serialization;

[Serializable]
public class Home
{
    public const int maxRoomCount = 8;
    public const int minRoomCount = 2;
    public static readonly Dictionary<RoomType, int> mustHaveRooms = new Dictionary<RoomType, int>()
    {
        {RoomType.Bedroom, 1},
        {RoomType.Bathroom, 1},
        {RoomType.LivingRoom, 0},
        {RoomType.Kitchen, 0}
    };

    public List<Room> rooms;
    [IgnoreDataMember]
    private List<Room> positionedRooms;
    private List<Sides> sides = new List<Sides>() {Sides.Right, Sides.Bottom, Sides.Left, Sides.Top};
    public Dictionary<int, Room> idToRoom;

    public int width, heigth, minX, maxX, minY, maxY;

    public Home()
    {
        rooms = new List<Room>();
        positionedRooms = new List<Room>();
        idToRoom = new Dictionary<int, Room>();
    }

    public void SetUp()
    {
        AddRooms();
        PositionRooms();
        CenterHome();
    }

    public void SetUp(Home homeModel)
    {
        width = homeModel.width;
        heigth = homeModel.heigth;
        minX = homeModel.minX;
        maxX = homeModel.maxX;
        minY = homeModel.minY;
        maxY = homeModel.maxY;

        var translationTable = new Dictionary<int, int>();

        // Copy rooms
        foreach (var room in homeModel.rooms)
        {
            var newRoom = new Room(room);
            translationTable.Add(room.id, newRoom.id);
            AddRoom(newRoom);
            positionedRooms.Add(newRoom);
        }

        // Set room links
        foreach (var room in homeModel.rooms)
        {
            var equivalentRoom = idToRoom[translationTable[room.id]];
            foreach (var linkedRoom in room.links)
            {
                var equivalentLinkedRoom = idToRoom[translationTable[linkedRoom.id]];
                equivalentRoom.AddLink(equivalentLinkedRoom);

                var linkedFurniture = equivalentRoom.linkToDoor[linkedRoom];
                equivalentRoom.linkToDoor.Remove(linkedRoom);
                equivalentRoom.linkToDoor.Add(equivalentLinkedRoom , linkedFurniture);
            }
        }
    }
    
    public void CenterHome()
    {
        minX = int.MaxValue;
        maxX = int.MinValue;
        minY = int.MaxValue;
        maxY = int.MinValue;

        foreach (var room in rooms)
        {
            var edges = room.GetEdges();

            minX = Mathf.Min(minX, edges[0]);
            maxX = Mathf.Max(maxX, edges[1]);
            minY = Mathf.Min(minY, edges[2]);
            maxY = Mathf.Max(maxY, edges[3]);
        }

        var deltaX = Mathf.FloorToInt((float)(minX + maxX + 1) / 2);
        var deltaY = Mathf.FloorToInt((float)(minY + maxY + 1) / 2);

        foreach (var room in rooms)
        {
            room.position[0] -= deltaX;
            room.position[1] -= deltaY;
        }
        
        // - - - - - - - - - - - - - - - - - - - - - - - - -
        
        minX = int.MaxValue;
        maxX = int.MinValue;
        minY = int.MaxValue;
        maxY = int.MinValue;
        
        foreach (var room in rooms)
        {
            var edges = room.GetEdges();

            minX = Mathf.Min(minX, edges[0]);
            maxX = Mathf.Max(maxX, edges[1]);
            minY = Mathf.Min(minY, edges[2]);
            maxY = Mathf.Max(maxY, edges[3]);
        }

        width = maxX - minX + 1;
        heigth = maxY - minY + 1;
    }
    
    private void AddRooms()
    {
        var roomCount = Random.Range(minRoomCount, maxRoomCount);

        foreach (var item in mustHaveRooms)
        {
            for(int i = 0; i < item.Value; i++)
                AddRoom(new Room(item.Key));
        }

        for (int i = rooms.Count; i < roomCount; i++)
        {
            AddRoom(new Room());
        }
    }

    public void AddRoom(Room newRoom)
    {
        rooms.Add(newRoom);
        idToRoom.Add(newRoom.id, newRoom);
    }

    public void RemoveRoom(Room room)
    {
        // Remove Links
        UnlinkRoom(room, false);

        // Remove room
        rooms.Remove(room);
        positionedRooms.Remove(room);

        // Check if shit is still connected
        ResetVisitedRooms();
        DFS(room.links[0]);

        var disconnectedRooms = new List<Room>();
        
        foreach (var disconnectedRoom in rooms)
        {
            if (!disconnectedRoom.visited)
            {
                disconnectedRooms.Add(disconnectedRoom);
                positionedRooms.Remove(disconnectedRoom);
                UnlinkRoom(disconnectedRoom, true);
            }
        }

        // Reconnect Fuckers
        foreach (var disconnectedRoom in disconnectedRooms)
            PositionRoom(disconnectedRoom);
    }

    public void UnlinkRoom(Room room, bool clear)
    {
        foreach (var otherRoom in room.links)
        {
            otherRoom.links.Remove(room);
    
            otherRoom.furnitures.Remove(otherRoom.linkToDoor[room]);
            otherRoom.linkToDoor.Remove(room);
        }

        if (clear)
        {
            foreach (var furniture in room.linkToDoor.Values)
                room.furnitures.Remove(furniture);
            
            room.linkToDoor.Clear();
            room.links.Clear();
        }
    }

    private void ResetVisitedRooms()
    {
        foreach (var room in rooms)
            room.visited = false;
    }
    
    private void DFS(Room room)
    {
        if (room.visited)
            return;
        
        room.visited = true;
        foreach (var otherRoom in room.links)
            DFS(otherRoom);
    }
    
    private void PositionRooms()
    {
        Utils.Shuffle(rooms);

        foreach (var room in rooms)
            PositionRoom(room);
    }

    public void PositionRoom(Room room)
    {
        if (positionedRooms.Count == 0)
        {
            room.SetPosition(new[]{0, 0});
        }

        else
        {
            while(true)
            {
                var index = Random.Range(0, positionedRooms.Count);
                var otherRoom = positionedRooms[index];

                if (TryLinkRooms(room, otherRoom))
                    break;
            }
        }
        
        positionedRooms.Add(room);
    }

    private bool TryLinkRooms(Room roomA, Room roomB)
    {
        Utils.Shuffle(sides);

        foreach (var linkSide in sides)
        {
            int[] position = {0, 0};
            int variablePos = 0;
            int startPos = 0;
            List<int> range = new List<int>();
            int doorDiff = 0;
            int doorPos = 0;
            Sides otherSide = Sides.Top;
            
            switch (linkSide)
            {
                case Sides.Top:
                    position = new[] {roomB.position[0], roomB.position[1] - roomA.size[1]};
                    variablePos = 0;
                    doorDiff = 1;
                    doorPos = roomB.position[1] - 1;
                    otherSide = Sides.Bottom;

                    startPos = roomB.position[0] - roomA.size[0] + 3;
                    
                    range = Enumerable.Range(
                        0,
                        roomB.position[0] + roomB.size[0] - 3 -  startPos
                    ).ToList();
                    break;
                case Sides.Bottom:
                    position = new[] {roomB.position[0], roomB.position[1] + roomB.size[1]};
                    variablePos = 0;
                    doorDiff = -1;
                    doorPos = roomB.position[1] + roomB.size[1];
                    otherSide = Sides.Top;
                    
                    startPos = roomB.position[0] - roomA.size[0] + 3;
                    
                    range = Enumerable.Range(
                        0,
                        roomB.position[0] + roomB.size[0] - 3 - startPos
                    ).ToList();
                    break;
                case Sides.Left:
                    position = new[] {roomB.position[0] - roomA.size[0], roomB.position[1]};
                    variablePos = 1;
                    doorDiff = 1;
                    doorPos = roomB.position[0] - 1;
                    otherSide = Sides.Right;
                    
                    startPos = roomB.position[1] - roomA.size[1] + 5;
                    
                    range = Enumerable.Range(
                        0,
                        roomB.position[1] + roomB.size[1] - 5 - startPos
                    ).ToList();
                    break;
                case Sides.Right:
                    position = new[] {roomB.position[0] + roomB.size[0], roomB.position[1]};
                    variablePos = 1;
                    doorDiff = -1;
                    doorPos = roomB.position[0] + roomB.size[0];
                    otherSide = Sides.Left;
                    
                    startPos = roomB.position[1] - roomA.size[1] + 5;
                    
                    range = Enumerable.Range(
                        0,
                        roomB.position[1] + roomB.size[1] - 5 - startPos
                    ).ToList();
                    break;
            }
            
            Utils.Shuffle(range);
    
            foreach (int num in range)
            {
                position[variablePos] = startPos + num;
                roomA.SetPosition(position);

                if (!CheckRoomCollision(roomA))
                {
                    var doorPositionElement = position[variablePos] > roomB.position[variablePos]
                        ? Random.Range(position[variablePos] + 1, roomB.position[variablePos] + roomB.size[variablePos] - 3)
                        : Random.Range(roomB.position[variablePos] + 1, position[variablePos] + roomA.size[variablePos] - 3);

                    var otherVariable = (variablePos + 1) % 2;
                    
                    // - - - - - - - - - - - - - - - - -

                    var doorAPosition = new int[2];
                    doorAPosition[variablePos] = doorPositionElement;
                    doorAPosition[otherVariable] = doorPos;
                    
                    roomA.AddDoor(doorAPosition, otherSide, roomB);
                    
                    // - - - - - - - - - - - - - - - - -
                    
                    var doorBPosition = new int[2];
                    doorBPosition[variablePos] = doorPositionElement;
                    doorBPosition[otherVariable] = doorPos + doorDiff;
                    
                    roomB.AddDoor(doorBPosition, linkSide, roomA);
                    
                    // - - - - - - - - - - - - - - - - -
                    
                    roomA.AddLink(roomB);
                    roomB.AddLink(roomA);
                    return true;
                }
            }
        }

        return false;
    }
 
    private bool CheckRoomCollision(Room room)
    {
        foreach (var otherRoom in positionedRooms)
        {
            if (room != otherRoom && room.Intersects(otherRoom))
                return true;
        }

        return false;
    }

    #region Utils

    public Dictionary<RoomType, int> GetRoomDiff(Home otherHome)
    {
        var diff = new Dictionary<RoomType, int>()
        {
            {RoomType.Bedroom, 0},
            {RoomType.Bathroom, 0},
            {RoomType.LivingRoom, 0},
            {RoomType.Kitchen, 0}
        };

        foreach (var room in rooms)
            diff[room.roomType]++;
        
        foreach (var room in otherHome.rooms)
            diff[room.roomType]--;

        return diff;
    }

    public Room GetRoomOfType(RoomType roomType)
    {
        Utils.Shuffle(rooms);

        foreach (var room in rooms)
        {
            if (room.roomType == roomType)
                return room;
        }

        throw new Exception();
    }

    public int GetRoomTypeCount(RoomType roomType)
    {
        int count = 0;

        foreach (var room in rooms)
        {
            if (room.roomType == roomType)
                count++;
        }

        return count;
    }

    #endregion
}