using Godot;
using System.Collections.Generic;
using static WFC;

public partial class Layer : Node3D
{
	private const int MaxGenerationAttempts = 1000;
	private static readonly Vector2I NoPosition = new Vector2I(-100, -100);

	private Node3D decorationRoot;
	public Tile[,] tiles;
	private int layerSize;
	public int level;
	private bool updateFailed = false;
	public bool hasContentGenerated = false;
	public Vector2I gateCoordinate;
	public List<string> currentResources;
	public bool isGateOpen = false;
	public List<Ingredient> gateIngredients = new();

	public override void _Ready()
	{
		currentResources = new List<string>();
		decorationRoot = new Node3D
		{
			Name = "Decorations"
		};
		AddChild(decorationRoot);
	}

	public void ClearDecorations()
	{
		foreach (Tile tile in tiles)
		{
			foreach (Node child in tile.ContentNode.GetChildren())
			{
				child.QueueFree();
			}
		}
	}

	public void SetupLayer(int layerSize, int level, Dictionary<string, MeshInstance3D> tileMeshes, Vector2I collapseOrigin)
	{
		this.layerSize = layerSize;
		this.level = level;
		tiles = new Tile[layerSize, layerSize];

		GenerateBaseStructure(tileMeshes);

		int safetyCounter = 0;
		while (true)
		{
			if (GenerateLayer(collapseOrigin)) break;
			ResetLayer(tileMeshes);
			safetyCounter++;
			if (safetyCounter > MaxGenerationAttempts) break;
		}
		CreateTileNodes();
	}

	private void GenerateBaseStructure(Dictionary<string, MeshInstance3D> tileMeshes)
	{
		Vector3 position;
		float offsetX;
		float offsetY = level * GameData.tileHeight * -1;
		float offsetZ;
		for (int x = 0; x < layerSize; x++)
		{
			offsetX = x * GameData.tileWidth;
			for (int y = 0; y < layerSize; y++)
			{
				offsetZ = y * GameData.tileWidth;
				position = new Vector3(offsetX, offsetY, offsetZ);
				Tile tile = new Tile();
				tile.SetMeshes(tileMeshes);
				tile.Position = position;
				tile.GridPosition = new Vector2I(x, y);
				tiles[x, y] = tile;
			}
		}
	}

	private void CreateTileNodes()
	{
		foreach (Tile tile in tiles)
		{
			Node3D node = new Node3D
			{
				Position = tile.Position,
				Visible = tile.collapsedMesh != null && tile.collapsedMesh == "gate"
			};
			decorationRoot.AddChild(node);

			tile.ContentNode = node;
		}
	}

	private void ResetLayer(Dictionary<string, MeshInstance3D> tileMeshes)
	{
		updateFailed = false;

		for (int x = 0; x < layerSize; x++)
		{
			for (int y = 0; y < layerSize; y++)
			{
				tiles[x, y].Reset(tileMeshes);
			}
		}
	}

	private void GenerateBorder()
	{
		for (int x = 0; x < layerSize; x++)
		{
			for (int z = 0; z < layerSize; z++)
			{
				if (x == 0 && z == 0 && level == 0) continue;
				if (!IsBorder(x, z))
					continue;

				Tile tile = tiles[x, z];
				List<string> possibilities = GetBorderPossibilities(x, z);

				if (possibilities.Count == 0)
					continue;

				tile.Collapse(possibilities[GameData.rand.Next(possibilities.Count)]);
				Propagate(new Vector2I(x, z));
			}
		}
	}

	private bool IsBorder(int x, int z)
	{
		return x == 0 || z == 0 || x == layerSize - 1 || z == layerSize - 1;
	}

	private void GenerateNecessaryTiles()
	{
		//Generate spawn only in the first layer
		if (level == 0)
		{
			tiles[0, 0].Collapse("spawn");
			Propagate(new Vector2I());
		}

		int posX, posY;
		while (true)
		{
			posX = GameData.rand.Next(layerSize);
			posY = GameData.rand.Next(layerSize);

			if (tiles[posX, posY].collapsedMesh != null) continue;
			if (tiles[posX, posY].tileMeshes.ContainsKey("gate"))
			{
				tiles[posX, posY].Collapse("gate");
				tiles[posX, posY].containsDecoration = true;
				gateCoordinate = new Vector2I(posX, posY);
				Propagate(gateCoordinate);
				break;
			}
		}
	}

	public bool GenerateLayer(Vector2I collapseOrigin)
	{
		int safetyCounter = 0;
		GenerateBorder();

		GenerateNecessaryTiles();
		Vector2I position = tiles[collapseOrigin.X, collapseOrigin.Y].collapsedMesh == null ? collapseOrigin : GetSmallestPossibilities();
		while (true)
		{
			string keyword = tiles[position.X, position.Y].Collapse("");
			if (keyword == "ERR") return false;
			if (keyword != "")
			{
				Propagate(position);
				if (updateFailed) break;
				position = GetSmallestPossibilities();
				if (position == NoPosition)
				{
					break;
				}
				continue;
			}
			safetyCounter++;
			if (safetyCounter == layerSize * layerSize) return false;
		}
		if (updateFailed) return false;
		if (!WFC.IsMapConnected(tiles, 0.8f)) return false;
		return true;
	}

	private void Propagate(Vector2I startPos)
	{
		Queue<Vector2I> queue = new Queue<Vector2I>();
		queue.Enqueue(startPos);

		while (queue.Count > 0)
		{
			Vector2I currentPos = queue.Dequeue();
			Tile currentTile = tiles[currentPos.X, currentPos.Y];

			HashSet<string> currentPossibilities = currentTile.collapsedMesh != null
				? new HashSet<string> { currentTile.collapsedMesh }
				: new HashSet<string>(currentTile.tileMeshes.Keys);

			for (int i = 0; i < offsets2D.Length; i++)
			{
				Vector2I newPos = currentPos + offsets2D[i];
				if (!InBounds(newPos, layerSize)) continue;

				Tile neighborTile = tiles[newPos.X, newPos.Y];

				HashSet<string> allowed = new HashSet<string>();

				foreach (string neighborOption in neighborTile.tileMeshes.Keys)
				{
					foreach (string item in currentPossibilities)
					{
						if (WFC.CanConnect(item, neighborOption, dirs[i], false))
						{
							allowed.Add(neighborOption);
							break;
						}
					}
				}

				int updateCount = neighborTile.Propagate(allowed);

				if (updateCount == int.MaxValue)
				{
					updateFailed = true;
					return;
				}

				if (updateCount > 0)
				{
					queue.Enqueue(newPos);
				}
			}
		}
	}


	private Vector2I GetSmallestPossibilities()
	{
		Vector2I result = NoPosition;
		int lowest = 100;
		int current;
		for (int x = 0; x < layerSize; x++)
		{
			for (int y = 0; y < layerSize; y++)
			{
				if (tiles[x, y].collapsedMesh != null) continue;
				current = tiles[x, y].tileMeshes.Count;
				if (current < lowest)
				{
					result = new Vector2I(x, y);
					lowest = current;
				}
			}
		}
		return result;
	}

	public string DisplayGateIngredients()
	{
		string result = "";
		foreach (Ingredient ingredient in gateIngredients)
		{
			result += $"{ItemData.GetReadableName(ingredient.Item)} ({ingredient.Amount})\r";
		}
		return result;
	}
}
