using Godot;
using System.Collections.Generic;
using System.Linq;
using static GameData;

public partial class World : Node3D
{
	public Dictionary<string, MeshInstance3D> tileMeshes;
	public Dictionary<string, MeshInstance3D> contentMeshes;
	public Dictionary<string, List<Placeholder>> tilePlaceholders;
	private PackedScene layerPrefab = ResourceLoader.LoadLayerPrefab();
	private Dictionary<string, MultiMeshInstance3D> multiMeshes = new Dictionary<string, MultiMeshInstance3D>();
	private Dictionary<string, Mesh> meshLibrary = new Dictionary<string, Mesh>();

	private MultiMeshHandler multiMeshHandler;

	public override void _Ready()
	{
		ResetRunState();
		WFC.FillAdjacencies();

		tileMeshes = ResourceLoader.LoadTiles();
		contentMeshes = ResourceLoader.LoadDecorations();

		tilePlaceholders = new Dictionary<string, List<Placeholder>>();

		foreach (KeyValuePair<string, MeshInstance3D> kvp in tileMeshes)
		{
			tilePlaceholders[kvp.Key] = new List<Placeholder>();

			foreach (Node3D child in kvp.Value.GetChildren())
			{
				tilePlaceholders[kvp.Key].Add(new Placeholder(child.Name, child.Transform));
			}
			meshLibrary[kvp.Key] = kvp.Value.Mesh;
			kvp.Value.QueueFree();
		}

		multiMeshes = CreateMultiMeshes(meshLibrary);
		multiMeshHandler = new MultiMeshHandler(multiMeshes);

		map = new Layer[ruinSize];
		GenerateWorld();

		Pathfinding.BuildAStarGraph();

		HandleRenderData(BuildRenderData(0));

		Robot robot = ResourceLoader.LoadRobotPrefab().Instantiate<Robot>();
		robot.Name = "Bob";
		robot.Position = map[0].tiles[0, 0].Position;
		AddChild(robot);
		robots.Add(robot);

		SetGateRequirements();
	}

	private Dictionary<string, MultiMeshInstance3D> CreateMultiMeshes(Dictionary<string, Mesh> meshLibrary)
	{
		Dictionary<string, MultiMeshInstance3D> result = new Dictionary<string, MultiMeshInstance3D>();

		foreach (KeyValuePair<string, Mesh> kvp in meshLibrary)
		{
			MultiMesh multiMesh = new MultiMesh
			{
				Mesh = kvp.Value,
				TransformFormat = MultiMesh.TransformFormatEnum.Transform3D
			};

			MultiMeshInstance3D instance = new MultiMeshInstance3D
			{
				Multimesh = multiMesh
			};

			AddChild(instance);

			result[kvp.Key] = instance;
		}

		return result;
	}

	public override void _Process(double delta)
	{
		survival.Update(delta);
		if (!canMove) return;

		if (Input.IsActionJustPressed("layer_up") && currentLayer > 0) currentLayer--;
		if (Input.IsActionJustPressed("layer_down") && currentLayer < ruinSize - 1 && map[currentLayer].isGateOpen) currentLayer++;

		if (currentLayer != visibleLayer)
		{
			ShowLayer(currentLayer);
		}
	}

	private void ResetRunState()
	{
		survival = new SurvivalState();
		robotStats = new RobotStats();
		inventory = new Inventory();
		availableResearch = ResourceLoader.LoadResearch();
		robots.Clear();
		currentLayer = 0;
		visibleLayer = 0;
		lowestLayer = 0;
		canMove = true;
	}

	private void GenerateWorld()
	{
		for (int layer = 0; layer < ruinSize; layer++)
		{
			Layer layerNode = layerPrefab.Instantiate<Layer>();
			AddChild(layerNode);

			if (layer == 0)
			{
				layerNode.SetupLayer(layerSize, layer, tileMeshes, new Vector2I());
			}
			else
			{
				layerNode.SetupLayer(layerSize, layer, tileMeshes, map[layer - 1].gateCoordinate);
			}

			map[layer] = layerNode;
		}
		SetupSpawn();
	}

	private void ShowLayer(int layer)
	{
		map[visibleLayer].ClearDecorations();
		HandleRenderData(BuildRenderData(layer));
		visibleLayer = layer;
	}

	private void SetupSpawn()
	{
		map[0].tiles[0, 0].wasVisited = true;
		map[0].tiles[0, 0].containsDecoration = true;
		map[0].tiles[0, 0].containsLight = true;
		map[0].tiles[0, 0].containsResource = false;
		map[0].tiles[0, 0].ContentNode.Visible = true;
	}

	private List<TileRenderData> BuildRenderData(int layerIndex)
	{
		List<TileRenderData> result = new List<TileRenderData>();
		Layer layer = map[layerIndex];
		layer.ClearDecorations();

		for (int x = 0; x < layerSize; x++)
		{
			for (int y = 0; y < layerSize; y++)
			{
				Tile tile = layer.tiles[x, y];
				result.Add(new TileRenderData
				{
					Tile = tile,
					MeshKey = tile.collapsedMesh,
					Transform = new Transform3D(Basis.Identity, tile.Position),
					Placeholders = tilePlaceholders[tile.collapsedMesh]
				});
			}
		}

		if (!layer.hasContentGenerated)
		{
			DistributeTileContent(layer);
			layer.hasContentGenerated = true;
		}
		return result;
	}

	private void DistributeTileContent(Layer layer)
	{
		int currentDecoration = 0;
		int currentLight = 0;
		int currentResource = 0;

		int posX, posY;

		while (currentLight < layerSize * 3)
		{
			posX = rand.Next(layerSize);
			posY = rand.Next(layerSize);
			if (CannotContainLight(layer.tiles[posX, posY])) continue;
			if (layer.tiles[posX, posY].containsLight) continue;

			layer.tiles[posX, posY].containsLight = true;
			currentLight++;
		}

		while (currentDecoration < layerSize)
		{
			posX = rand.Next(layerSize);
			posY = rand.Next(layerSize);
			if (layer.tiles[posX, posY].containsDecoration) continue;

			layer.tiles[posX, posY].containsDecoration = true;
			currentDecoration++;
		}

		while (currentResource < layerSize)
		{
			posX = rand.Next(layerSize);
			posY = rand.Next(layerSize);
			if (layer.tiles[posX, posY].containsResource) continue;

			layer.tiles[posX, posY].containsResource = true;
			layer.tiles[posX, posY].resource = new GameResource(ResourceDistributor.GetResource(layer.level));
			layer.currentResources.Add(layer.tiles[posX, posY].resource.name);
			currentResource++;
		}
	}

	private bool CannotContainLight(Tile tile)
	{
		return tile.collapsedMesh == "junction" || tile.collapsedMesh == "gate";
	}

	private void HandleRenderData(List<TileRenderData> renderData)
	{
		multiMeshHandler.Build(renderData);
		foreach (TileRenderData data in renderData)
		{
			data.Tile.SpawnContent(contentMeshes, data.Transform, data.Placeholders);
		}
	}

	private void SetGateRequirements()
	{
		List<string> availableResources = new List<string>();
		List<ItemData> possibleIngredients;
		bool canCraft;
		double highestCraftTime;
		double lowestCraftTime;
		foreach (Layer layer in map)
		{
			highestCraftTime = 0;
			lowestCraftTime = double.MaxValue;
			possibleIngredients = new List<ItemData>();
			//Step 1: Determine all possible resources for this and all previous layers combined
			foreach (string resource in layer.currentResources)
			{
				if (availableResources.Contains(resource)) continue;
				availableResources.Add(resource);
			}
			//Step 2: Check which items can be crafted with those items, repeat until no further items are added to the list
			bool addedNewItem;
			do
			{
				addedNewItem = false;

				foreach (ItemData item in availableItems.Values)
				{
					if (possibleIngredients.Any(existing => existing.Id == item.Id))
						continue;

					canCraft = item.Inputs.All(input => availableResources.Contains(input.Item));

					if (!canCraft)
						continue;

					possibleIngredients.Add(item);
					availableResources.Add(item.Id);

					lowestCraftTime = Mathf.Min(lowestCraftTime, item.CraftTime);
					highestCraftTime = Mathf.Max(highestCraftTime, item.CraftTime);

					addedNewItem = true;
				}

			} while (addedNewItem);
			//Step 3: Choose gate items needed based on crafting time and layer it is for (Lower layers -> More advanced items -> More crafting time)
			double goalCraftTime = Mathf.Lerp(lowestCraftTime, highestCraftTime, Mathf.Clamp(layer.level/(float)ruinSize, 0, 1));
			int ingredientAmount = rand.Next(1, 1 + ruinSize / 2);
			float craftTimeModifier = 0f;
			double craftTimeLower, craftTimeUpper;
			for (int i = 0; i < ingredientAmount; i++)
			{
				craftTimeLower = goalCraftTime - goalCraftTime * craftTimeModifier;
				craftTimeUpper = goalCraftTime + goalCraftTime * craftTimeModifier;

				List<ItemData> validIngredients = possibleIngredients
					.Where(item =>
						item.CraftTime >= craftTimeLower &&
						item.CraftTime <= craftTimeUpper &&
						!layer.gateIngredients.Any(ingredient => ingredient.Item == item.Id))
					.ToList();

				if (validIngredients.Count == 0)
				{
					i--;
					craftTimeModifier += 0.05f;
					continue;
				}

				ItemData item = validIngredients[rand.Next(validIngredients.Count)];

				layer.gateIngredients.Add(new Ingredient
				{
					Item = item.Id,
					Amount = rand.Next(5 + layer.level, 20 + layer.level)
				});
				craftTimeModifier = 0f;
			}
		}
	}
}
