using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using gaemstone.Utility;
using static flecs_hub.flecs;

namespace gaemstone.ECS;

public unsafe class EntityRef
	: EntityBase<EntityRef>
	, IEquatable<EntityRef>
{
	public override World World { get; }
	public Entity Entity { get; }
	public uint Id => Entity.Id;

	public bool IsAlive => ecs_is_alive(World, this);
	public EntityType Type => new(World, ecs_get_type(World, this));

	public string? Name {
		get => ecs_get_name(World, this).FlecsToString()!;
		set { using var alloc = TempAllocator.Use();
		      ecs_set_name(World, this, alloc.AllocateCString(value)); }
	}
	public string? Symbol {
		get => ecs_get_symbol(World, this).FlecsToString()!;
		set { using var alloc = TempAllocator.Use();
		      ecs_set_symbol(World, this, alloc.AllocateCString(value)); }
	}


	private EntityRef(World world, Entity entity, bool throwOnInvalid)
	{
		if (throwOnInvalid && !ecs_is_valid(world, entity))
			throw new InvalidOperationException($"The entity {entity} is not valid");
		World  = world;
		Entity = entity;
	}

	public EntityRef(World world, Entity entity)
		: this(world, entity, true) {  }

	public static EntityRef? CreateOrNull(World world, Entity entity)
		=> ecs_is_valid(world, entity) ? new(world, entity) : null;


	public void Delete()
		=> ecs_delete(World, this);

	public EntityBuilder NewChild(EntityPath? path = null)
		=> World.New(this, EnsureRelativePath(path));
	public EntityRef? LookupChild(EntityPath path)
		=> World.LookupByPath(this, EnsureRelativePath(path)!);
	public EntityRef LookupChildOrThrow(EntityPath path)
		=> World.LookupByPathOrThrow(this, EnsureRelativePath(path)!);

	private static EntityPath? EnsureRelativePath(EntityPath? path)
	{
		if (path?.IsAbsolute == true) throw new ArgumentException(
			$"Path '{path}' must not be absolute", nameof(path));
		return path;
	}


	public EntityRef? Parent
		=> GetTargets(World.ChildOf).FirstOrDefault();

	// TODO: Change to property after all?
	public IEnumerable<EntityRef> GetChildren()
	{
		foreach (var iter in Iterator.FromTerm(World, new(World.ChildOf, this)))
			for (var i = 0; i < iter.Count; i++)
				yield return iter.Entity(i);
	}


	public override EntityRef Add(Id id) { ecs_add_id(World, this, id); return this; }
	public override EntityRef Remove(Id id) { ecs_remove_id(World, this, id); return this; }
	public override bool Has(Id id) => ecs_has_id(World, this, id);

	public override T? GetOrNull<T>(Id id)
	{
		var ptr = ecs_get_id(World, this, id);
		return (ptr != null) ? Unsafe.Read<T>(ptr) : null;
	}

	public override T? GetOrNull<T>(Id id, T _ = null!)
		where T : class
	{
		var ptr = ecs_get_id(World, this, id);
		return (T?)Unsafe.Read<ReferenceHandle>(ptr).Target;
	}

	public override T GetOrThrow<T>(Id id)
	{
		var ptr = ecs_get_id(World, this, id);
		if (ptr == null) throw new Exception($"Component {typeof(T)} not found on {this}");
		if (typeof(T).IsValueType) return Unsafe.Read<T>(ptr);
		else return (T)Unsafe.Read<ReferenceHandle>(ptr).Target!;
	}

	public override ref T GetRefOrNull<T>(Id id)
	{
		var @ref = ecs_ref_init_id(World, this, id);
		var ptr  = ecs_ref_get_id(World, &@ref, id);
		return ref (ptr != null) ? ref Unsafe.AsRef<T>(ptr)
		                         : ref Unsafe.NullRef<T>();
	}

	public override ref T GetRefOrThrow<T>(Id id)
	{
		ref var ptr = ref GetRefOrNull<T>(id);
		if (Unsafe.IsNullRef(ref ptr)) throw new Exception(
			$"Component {typeof(T)} not found on {this}");
		return ref ptr;
	}

	public override ref T GetMut<T>(Id id)
	{
		var ptr = ecs_get_mut_id(World, this, id);
		// NOTE: Value is added if it doesn't exist on the entity.
		return ref Unsafe.AsRef<T>(ptr);
	}

	public override void Modified<T>(Id id)
		=> ecs_modified_id(World, this, id);

	public override EntityRef Set<T>(Id id, in T value)
	{
		var size = (ulong)Unsafe.SizeOf<T>();
		fixed (T* ptr = &value)
			if (ecs_set_id(World, this, id, size, ptr).Data == 0)
				throw new InvalidOperationException();
		return this;
	}

	public override EntityRef Set<T>(Id id, T obj) where T : class
	{
		if (obj == null) throw new ArgumentNullException(nameof(obj));
		var size = (ulong)sizeof(ReferenceHandle);
		// Dispose this handle afterwards, since Flecs clones it.
		using var handle = ReferenceHandle.Alloc(obj);
		if (ecs_set_id(World, this, id, size, &handle).Data == 0)
			throw new InvalidOperationException();
		return this;
	}

	private EntityRef? GetTarget(Entity relation, int index)
		=> CreateOrNull(World, new(ecs_get_target(World, this, relation, index)));
	public IEnumerable<EntityRef> GetTargets(Entity relation)
		{ var index = 0; while (GetTarget(relation, index++) is EntityRef target) yield return target; }
	public IEnumerable<EntityRef> GetTargets(string symbol)
		=> GetTargets(World.LookupBySymbolOrThrow(symbol));

	public bool Equals(EntityRef? other) => (other is not null) && (World == other.World) && (Entity == other.Entity);
	public override bool Equals(object? obj) => Equals(obj as EntityRef);
	public override int GetHashCode() => HashCode.Combine(World, Entity);
	public override string? ToString() => ecs_entity_str(World, this).FlecsToStringAndFree()!;

	public static bool operator ==(EntityRef? left, EntityRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false);
	public static bool operator !=(EntityRef? left, EntityRef? right) => !(left == right);

	public static implicit operator Entity(EntityRef? e) => e?.Entity ?? default;
	public static implicit operator ecs_entity_t(EntityRef? e) => e?.Entity.Value ?? default;

	public static implicit operator Id(EntityRef? e) => new(e?.Entity.Value.Data ?? default);
	public static implicit operator ecs_id_t(EntityRef? e) => e?.Entity.Value.Data ?? default;
}