using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class Maze
{
	[SerializeField]
	private MazeRoom[] mazeRooms;

	[SerializeField]
	private MazeCoordinates start;

	[SerializeField]
	private MazeCoordinates end;

	[SerializeField]
	private MazeDimensions mazeDimensions = new MazeDimensions(5, 5);

	private List<MazeCoordinates> availableRooms = new List<MazeCoordinates>(4);
	private List<MazeCoordinates> currentBranch = new List<MazeCoordinates>();

	public Maze()
	{

	}

	public Maze(MazeDimensions newDimensions, MazeCoordinates newStart, MazeCoordinates newEnd)
	{
		mazeDimensions = newDimensions;
		start = newStart;
		end = newEnd;

		GenerateMaze();
	}

	public MazeRoom[] GetRooms()
	{
		return mazeRooms;
	}

	public MazeCoordinates GetStart()
	{
		return start;
	}

	public MazeCoordinates GetEnd()
	{
		return end;
	}

	public void GenerateMaze()
	{
		InitializeMaze();
		PopulateMaze();
	}

	private void InitializeMaze()
	{
		mazeRooms = new MazeRoom[mazeDimensions.width * mazeDimensions.height];
		for(int y = 0; y < mazeDimensions.height; y++)
		{
			for(int x = 0; x < mazeDimensions.width; x++)
			{
				SetRoom(x, y, new MazeRoom());
			}
		}
	}

	private void PopulateMaze()
	{
		MazeCoordinates currentRoom = start;
		GetRoom(start).MarkVisited();
		currentBranch.Add(start);

		int visitedRooms = 0;
		while(visitedRooms < GetTotalRoomCount() - 1)
		{
			MazeCoordinates adjacentRoom;

			if(!GetAdjacentRoom(currentRoom, out adjacentRoom))
			{
				if (!GetNewStartPoint(ref currentBranch, out currentRoom))
				{
					Debug.LogErrorFormat("Backtracking revealed no available rooms. Found {0} rooms out of {1}", visitedRooms, GetTotalRoomCount());
					break;
				}

				if(!GetAdjacentRoom(currentRoom, out adjacentRoom))
				{
					Debug.LogErrorFormat("Was able to backtrack, but not proceed. Found {0} rooms out of {1}", visitedRooms, GetTotalRoomCount());
					break;
				}
			}

			StepBetweenRooms(currentRoom, adjacentRoom);
			currentRoom = adjacentRoom;
			currentBranch.Add(currentRoom);
			visitedRooms++;
			continue;
		}
	}

	private bool GetAdjacentRoom(MazeCoordinates currentRoom, out MazeCoordinates adjacentRoom)
	{
		availableRooms.Clear();

		availableRooms.Add(new MazeCoordinates(currentRoom.x, currentRoom.y + 1));
		availableRooms.Add(new MazeCoordinates(currentRoom.x, currentRoom.y - 1));
		availableRooms.Add(new MazeCoordinates(currentRoom.x + 1, currentRoom.y));
		availableRooms.Add(new MazeCoordinates(currentRoom.x - 1, currentRoom.y));

		for(int i = 3; i > -1; i--)
		{
			if(!IsAvailableRoom(availableRooms[i]))
			{
				availableRooms.RemoveAt(i);
			}
		}

		if(availableRooms.Count > 0)
		{
			adjacentRoom = GetRandomRoom(availableRooms);
			return true;
		}

		adjacentRoom = currentRoom;
		return false;
	}

	private bool GetNewStartPoint(ref List<MazeCoordinates> branch, out MazeCoordinates previousRoom)
	{
		if (branch == null || branch.Count < 1)
		{
			Debug.LogError("Maze branch was not initialized properly, or contained no rooms.");
			previousRoom = new MazeCoordinates();
			return false;
		}

		previousRoom = branch[branch.Count - 1];

		for (int i = branch.Count - 1; i > -1; i--)
		{
			if (HasAvailableNeighbour(branch[i]))
			{
				previousRoom = branch[i];
				return true;
			}
			branch.RemoveAt(i);
		}

		return false;
	}

	private void StepBetweenRooms(MazeCoordinates startRoom, MazeCoordinates endRoom)
	{
		MazeWallEnum stepDirection = GetRelativeDirection(startRoom, endRoom);
		GetRoom(startRoom).DisableWall(stepDirection);
		GetRoom(endRoom).DisableWall(GetOppositeDirection(stepDirection));
		GetRoom(endRoom).MarkVisited();
	}

	private bool IsAvailableRoom(MazeCoordinates currentRoom)
	{
		if(currentRoom.x < 0 || currentRoom.x >= GetMazeWidth())
		{
			return false;
		}

		if (currentRoom.y < 0 || currentRoom.y >= GetMazeHeight())
		{
			return false;
		}

		return !GetRoom(currentRoom).IsVisited();
	}

	private bool HasAvailableNeighbour(MazeCoordinates currentRoom)
	{
		return IsAvailableRoom(new MazeCoordinates(currentRoom.x, currentRoom.y + 1)) ||
			   IsAvailableRoom(new MazeCoordinates(currentRoom.x, currentRoom.y - 1)) ||
			   IsAvailableRoom(new MazeCoordinates(currentRoom.x + 1, currentRoom.y)) ||
			   IsAvailableRoom(new MazeCoordinates(currentRoom.x - 1, currentRoom.y));
	}

	private MazeCoordinates GetRandomRoom(List<MazeCoordinates> possibleRooms)
	{
		return possibleRooms[Random.Range(0, possibleRooms.Count)];
	}

	#region Get / Set Rooms within array

	public MazeRoom GetRoom(int x, int y)
	{
		return mazeRooms[GetRoomIndex(x, y)];
	}

	public MazeRoom GetRoom(MazeCoordinates roomCoordinates)
	{
		return GetRoom(roomCoordinates.x, roomCoordinates.y);
	}

	public void SetRoom(int x, int y, MazeRoom newRoom)
	{
		mazeRooms[GetRoomIndex(x, y)] = newRoom;
	}

	public void SetRoom(MazeCoordinates roomCoordinates, MazeRoom newRoom)
	{
		SetRoom(roomCoordinates.x, roomCoordinates.y, newRoom);
	}

	public int GetRoomIndex(MazeCoordinates roomCoordinates)
	{
		return GetRoomIndex(roomCoordinates.x, roomCoordinates.y);
	}

	public int GetRoomIndex(int x, int y)
	{
		return GetMazeWidth() * y + x;
	}

	#endregion

	#region MazeWallEnum operations

	private MazeWallEnum GetRelativeDirection(MazeCoordinates directionStart, MazeCoordinates directionEnd)
	{
		MazeCoordinates coordinateDirection =
			new MazeCoordinates(directionEnd.x - directionStart.x, directionEnd.y - directionStart.y);

		if(Mathf.Abs(coordinateDirection.y) > 0)
		{
			return Mathf.Sign(coordinateDirection.y) > 0 ? MazeWallEnum.north : MazeWallEnum.south;
		}

		else if (Mathf.Abs(coordinateDirection.x) > 0)
		{
			return Mathf.Sign(coordinateDirection.x) > 0 ? MazeWallEnum.west : MazeWallEnum.east;
		}

		else
		{
			Debug.LogError("start and end coordinates are the same");
			return MazeWallEnum.north;
		}
	}

	private MazeWallEnum GetOppositeDirection(MazeWallEnum direction)
	{
		switch (direction)
		{
			case MazeWallEnum.north:
				return MazeWallEnum.south;
			default:
			case MazeWallEnum.south:
				return MazeWallEnum.north;
			case MazeWallEnum.east:
				return MazeWallEnum.west;
			case MazeWallEnum.west:
				return MazeWallEnum.east;
		}
	}

	#endregion

	#region Maze Properties

	public int GetMazeWidth()
	{
		return mazeDimensions.width;
	}

	public int GetMazeHeight()
	{
		return mazeDimensions.height;
	}

	private int GetTotalRoomCount()
	{
		return mazeRooms.Length;
	}

	#endregion
}
