using System;
using System.Collections.Generic;
using System.Linq;

using Gamecraft.ColourPalette;
using Gamecraft.TimeRunning;
using Gamecraft.Wires;
using RobocraftX.Blocks;
using RobocraftX.Common;
using RobocraftX.Physics;
using RobocraftX.Rendering;
using Svelto.ECS.EntityStructs;

using Svelto.DataStructures;
using Svelto.ECS;
using Svelto.ECS.Hybrid;
using Unity.Mathematics;

using GamecraftModdingAPI.Engines;

namespace GamecraftModdingAPI.Blocks
{
	/// <summary>
	/// Engine for executing general block actions
	/// </summary>
	public partial class BlockEngine : IApiEngine
	{
		public string Name { get; } = "GamecraftModdingAPIBlockGameEngine";

		public EntitiesDB entitiesDB { set; private get; }

		public bool isRemovable => false;

		public void Dispose()
		{
		}

		public void Ready()
		{
		}

		public Block[] GetConnectedBlocks(EGID blockID)
		{
			if (!BlockExists(blockID)) return new Block[0];
			Stack<EGID> cubeStack = new Stack<EGID>();
			FasterList<EGID> cubes = new FasterList<EGID>(10);
			var coll = entitiesDB.QueryEntities<GridConnectionsEntityStruct>();
			foreach (var (ecoll, _) in coll)
			{
				var ecollB = ecoll.ToBuffer();
				for(int i = 0; i < ecoll.count; i++)
				{
					ref var conn = ref ecollB.buffer[i];
					conn.isProcessed = false;
				}
			}

			ConnectedCubesUtility.TreeTraversal.GetConnectedCubes(entitiesDB, blockID, cubeStack, cubes,
				(in GridConnectionsEntityStruct g) => { return false; });

			var ret = new Block[cubes.count];
			for (int i = 0; i < cubes.count; i++)
				ret[i] = new Block(cubes[i]);
			return ret;
		}

		public float4 ConvertBlockColor(byte index) => index == byte.MaxValue
			? new float4(-1f, -1f, -1f, -1f)
			: entitiesDB.QueryEntity<PaletteEntryEntityStruct>(index,
				CommonExclusiveGroups.COLOUR_PALETTE_GROUP).Colour;

		public ref T GetBlockInfo<T>(EGID blockID) where T : unmanaged, IEntityComponent
		{
			if (entitiesDB.Exists<T>(blockID))
				return ref entitiesDB.QueryEntity<T>(blockID);
			T[] structHolder = new T[1]; //Create something that can be referenced
			return ref structHolder[0]; //Gets a default value automatically
		}
		
		public ref T GetBlockInfoViewStruct<T>(EGID blockID) where T : struct, INeedEGID, IEntityViewComponent
		{
			if (entitiesDB.Exists<T>(blockID))
			{
				// TODO: optimize by using EntitiesDB internal calls instead of iterating over everything
				BT<MB<T>> entities = entitiesDB.QueryEntities<T>(blockID.groupID).ToBuffer();
				for (int i = 0; i < entities.count; i++)
				{
					if (entities.buffer[i].ID == blockID)
					{
						return ref entities.buffer[i];
					}
				}
			}
			T[] structHolder = new T[1]; //Create something that can be referenced
			return ref structHolder[0]; //Gets a default value automatically
		}

		public U GetBlockInfo<T, U>(Block block, Func<T, U> getter,
			U def = default) where T : unmanaged, IEntityComponent
		{
			if (entitiesDB.Exists<T>(block.Id))
				return getter(entitiesDB.QueryEntity<T>(block.Id));
			return GetBlockInitInfo(block, getter, def);
		}
		
		public U GetBlockInfoViewStruct<T, U>(Block block, Func<T, U> getter,
			U def = default) where T : struct, IEntityViewComponent
		{
			if (entitiesDB.Exists<T>(block.Id))
				return getter(entitiesDB.QueryEntity<T>(block.Id));
			return GetBlockInitInfo(block, getter, def);
		}

		private U GetBlockInitInfo<T, U>(Block block, Func<T, U> getter, U def) where T : struct, IEntityComponent
		{
			if (block.InitData.Group == null) return def;
			var initializer = new EntityInitializer(block.Id, block.InitData.Group);
			if (initializer.Has<T>())
				return getter(initializer.Get<T>());
			return def;
		}

		public delegate void Setter<T, U>(ref T component, U value) where T : struct, IEntityComponent;

		public void SetBlockInfoViewStruct<T, U>(Block block, Setter<T, U> setter, U value) where T : struct, IEntityViewComponent
		{
			if (entitiesDB.Exists<T>(block.Id))
				setter(ref entitiesDB.QueryEntity<T>(block.Id), value);
			else
				SetBlockInitInfo(block, setter, value);
		}
		
		public void SetBlockInfo<T, U>(Block block, Setter<T, U> setter, U value) where T : unmanaged, IEntityComponent
		{
			if (entitiesDB.Exists<T>(block.Id))
				setter(ref entitiesDB.QueryEntity<T>(block.Id), value);
			else
				SetBlockInitInfo(block, setter, value);
		}

		private void SetBlockInitInfo<T, U>(Block block, Setter<T, U> setter, U value)
			where T : struct, IEntityComponent
		{
			if (block.InitData.Group != null)
			{
				var initializer = new EntityInitializer(block.Id, block.InitData.Group);
				T component = initializer.Has<T>() ? initializer.Get<T>() : default;
				ref T structRef = ref component;
				setter(ref structRef, value);
				initializer.Init(structRef);
			}
		}

		public void UpdateDisplayedBlock(EGID id)
		{
			if (!BlockExists(id)) return;
			var pos = entitiesDB.QueryEntity<PositionEntityStruct>(id);
			var rot = entitiesDB.QueryEntity<RotationEntityStruct>(id);
			var scale = entitiesDB.QueryEntity<ScalingEntityStruct>(id);
			entitiesDB.QueryEntity<RenderingDataStruct>(id).matrix = float4x4.TRS(pos.position, rot.rotation, scale.scale);
		}

		public bool BlockExists(EGID blockID)
		{
			return entitiesDB.Exists<DBEntityStruct>(blockID);
		}

		public bool GetBlockInfoExists<T>(Block block) where T : struct, IEntityComponent
		{
			if (entitiesDB.Exists<T>(block.Id))
				return true;
			if (block.InitData.Group == null)
				return false;
			var init = new EntityInitializer(block.Id, block.InitData.Group);
			return init.Has<T>();
		}

		public SimBody[] GetSimBodiesFromID(byte id)
		{
			var ret = new FasterList<SimBody>(4);
			var oide = entitiesDB.QueryEntities<ObjectIdEntityStruct>();
			EGIDMapper<GridConnectionsEntityStruct>? connections = null;
			foreach (var ((oids, count), _) in oide)
			{
				for (int i = 0; i < count; i++)
				{	
					ref ObjectIdEntityStruct oid = ref oids[i];
					if (oid.objectId != id) continue;
					if (!connections.HasValue) //Would need reflection to get the group from the build group otherwise
						connections = entitiesDB.QueryMappedEntities<GridConnectionsEntityStruct>(oid.ID.groupID);
					var rid = connections.Value.Entity(oid.ID.entityID).machineRigidBodyId;
					foreach (var rb in ret)
					{
						if (rb.Id.entityID == rid)
							goto DUPLICATE; //Multiple Object Identifiers on one rigid body
					}

					ret.Add(new SimBody(rid));
					DUPLICATE: ;
				}
			}

			return ret.ToArray();
		}

		public ObjectIdentifier[] GetObjectIDsFromID(byte id, bool sim)
		{
			var ret = new FasterList<ObjectIdentifier>(4);
			var oide = entitiesDB.QueryEntities<ObjectIdEntityStruct>();
			foreach (var ((oids, count), _) in oide)
			{
				for (int i = 0; i < count; i++)
				{
					ref ObjectIdEntityStruct oid = ref oids[i];
					if (sim ? oid.simObjectId == id : oid.objectId == id)
						ret.Add(new ObjectIdentifier(oid.ID));
				}
			}

			return ret.ToArray();
		}

		public SimBody[] GetConnectedSimBodies(uint id)
		{
			var joints = entitiesDB.QueryEntities<JointEntityStruct>(MachineSimulationGroups.JOINTS_GROUP).ToBuffer();
			var list = new FasterList<SimBody>(4);
			for (int i = 0; i < joints.count; i++)
			{
				ref var joint = ref joints.buffer[i];
				if (joint.isBroken) continue;
				if (joint.connectedEntityA == id) list.Add(new SimBody(joint.connectedEntityB));
				else if (joint.connectedEntityB == id) list.Add(new SimBody(joint.connectedEntityA));
			}

			return list.ToArray();
		}

		public SimBody[] GetClusterBodies(uint cid)
		{
			var groups = entitiesDB.QueryEntities<GridConnectionsEntityStruct>();
			var bodies = new HashSet<uint>();
			foreach (var (coll, _) in groups)
			{
				var array = coll.ToBuffer().buffer;
				for (var index = 0; index < array.capacity; index++)
				{
					var conn = array[index];
					if (conn.clusterId == cid)
						bodies.Add(conn.machineRigidBodyId);
				}
			}

			return bodies.Select(id => new SimBody(id, cid)).ToArray();
		}

		public EGID? FindBlockEGID(uint id)
		{
			var groups = entitiesDB.FindGroups<DBEntityStruct>();
			foreach (ExclusiveGroupStruct group in groups)
			{
				if (entitiesDB.Exists<DBEntityStruct>(id, group))
					return new EGID(id, group);
			}

			return null;
		}

		public Cluster GetCluster(uint sbid)
		{
			var groups = entitiesDB.QueryEntities<GridConnectionsEntityStruct>();
			foreach (var (coll, _) in groups)
			{
				var array = coll.ToBuffer().buffer;
				for (var index = 0; index < array.capacity; index++)
				{
					var conn = array[index];
					//Static blocks don't have a cluster ID but the cluster destruction manager should have one
					if (conn.machineRigidBodyId == sbid && conn.clusterId != uint.MaxValue)
						return new Cluster(conn.clusterId);
				}
			}

			return null;
		}

		public Block[] GetBodyBlocks(uint sbid)
		{
			var groups = entitiesDB.QueryEntities<GridConnectionsEntityStruct>();
			var set = new HashSet<Block>();
			foreach (var (coll, _) in groups)
			{
				var array = coll.ToBuffer().buffer;
				for (var index = 0; index < array.capacity; index++)
				{
					var conn = array[index];
					if (conn.machineRigidBodyId == sbid)
						set.Add(new Block(conn.ID));
				}
			}

			return set.ToArray();
		}

#if DEBUG
		public EntitiesDB GetEntitiesDB()
		{
			return entitiesDB;
		}
#endif
	}
}