Persistence/PersistentItem.cs
using System;
using System.Collections;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Clover.Carriable;
using Clover.Data;
using Clover.Items;

namespace Clover.Persistence;

[JsonDerivedType( typeof(Persistence.PersistentItem), "base" )]
public class PersistentItem
{
	[Property] public string ItemId { get; set; }

	[Property] public string PackageIdent { get; set; }

	[JsonIgnore] public bool IsPackage => !string.IsNullOrEmpty( PackageIdent );

	/// <summary>
	///  The backbone of the persistence system. This is where you can store any data you want about an item.
	///  Don't access this directly, use <see cref="GetSaveData{T}"/> and <see cref="SetSaveData"/> instead.
	/// </summary>
	[Property]
	public Dictionary<string, object> ArbitraryData { get; set; } = new();

	[JsonIgnore]
	public ItemData ItemData
	{
		get => ItemData.Get( ItemId );
	}

	[JsonIgnore] public virtual bool IsStackable => ItemData.IsStackable;
	[JsonIgnore] public virtual int StackSize => ItemData.StackSize;

	public void Initialize()
	{
		ItemData?.OnPersistentItemInitialize( this );
	}

	public object GetSaveData( Type type, string key )
	{
		// XLog.Info( this, "Keys: " + string.Join( ", ", ArbitraryData.Keys ) );
		if ( ArbitraryData.TryGetValue( key, out var obj ) )
		{
			if ( obj == null )
			{
				return null;
			}

			// i don't even know why this started happening but apparently it sometimes doesn't need to deserialize
			if ( obj.GetType() == type || obj.GetType().IsSubclassOf( type ) )
			{
				return obj;
			}

			/*if ( obj is float single )
			{
				return Convert.ChangeType( single, type );
			}

			if ( obj is double @double )
			{
				return Convert.ChangeType( @double, type );
			}

			if ( obj is int integer )
			{
				return Convert.ChangeType( integer, type );
			}*/

			if ( obj is not JsonElement jsonElement )
			{
				Log.Error( $"Arbitrary data '{key}' on '{this}' is not a JsonElement: {obj} (type: {obj.GetType()})" );
				return null;
			}

			object data;

			try
			{
				data = JsonSerializer.Deserialize( jsonElement.GetRawText(), type, GameManager.JsonOptions );
			}
			catch ( Exception e )
			{
				Log.Error( $"GetSaveData - Failed to deserialize '{key}' on '{this}' to type {type}: {e.Message}" );
				Log.Error( e );
				return null;
			}

			if ( data is JsonElement jsonElement2 )
			{
				Log.Error( $"Deserialized {key} on {this} as {data} ({data?.GetType()})" );
				return null;
			}

			return data;
		}

		return null;
	}

	/// <summary>
	///  Get arbitrary data from this item. If the key doesn't exist, it will return the default value.
	///  Use <see cref="SetSaveData"/> to store arbitrary data.
	/// </summary>
	/// <param name="key">Key to get the value from</param>
	/// <param name="defaultValue">Value to return if the key doesn't exist</param>
	/// <typeparam name="T">The same type as you saved with <see cref="SetSaveData"/></typeparam>
	/// <returns></returns>
	public T GetSaveData<T>( string key, T defaultValue = default )
	{
		return TryGetSaveData<T>( key, out var value ) ? value : defaultValue;
	}

	/// <summary>
	/// Same as <see cref="GetSaveData{T}"/> but returns false if the key doesn't exist.
	/// </summary>
	/// <param name="key">Key to get the value from</param>
	/// <param name="value">Value of the key</param>
	/// <typeparam name="T">The same type as you saved with <see cref="SetSaveData"/></typeparam>
	/// <returns></returns>
	public bool TryGetSaveData<T>( string key, out T value )
	{
		if ( ArbitraryData.TryGetValue( key, out var obj ) )
		{
			if ( obj == null )
			{
				value = default;
				return false;
			}

			// i don't even know why this started happening but apparently it sometimes doesn't need to deserialize
			if ( obj is T t )
			{
				value = t;
				return true;
			} // Check if obj can be cast to T (handles base/derived class relationships)
			else if ( (typeof(T).IsAssignableFrom( obj.GetType() ) ||
			           obj.GetType().IsAssignableFrom( typeof(T) )) )
			{
				try
				{
					value = (T)Convert.ChangeType( obj, typeof(T) );
					return true;
				}
				catch
				{
					// Fallback to normal deserialization if conversion fails
					Log.Warning( $"Type conversion failed for {key}, falling back to deserialization" );
				}
			}

			if ( obj is not JsonElement jsonElement )
			{
				Log.Error( $"Arbitrary data {key} on {this} is not a JsonElement: {obj} ({obj.GetType()})" );
				value = default;
				return false;
			}

			value = JsonSerializer.Deserialize<T>( jsonElement.GetRawText(), GameManager.JsonOptions );

			return true;
		}

		value = default;
		return false;
	}

	/// <summary>
	///  Set arbitrary data on this item. Arbitrary in this case means that you can store any type of data that can be serialized.
	///  Even complex types like classes and lists should work.
	/// </summary>
	/// <param name="key">The key to store the data under</param>
	/// <param name="value">Any serializable object</param>
	[Icon( "description" )]
	public void SetSaveData( string key, object value, Type type = null )
	{
		if ( !ValidateKey( this, key ) )
		{
			return;
		}

		if ( !ValidateValue( this, value, key ) )
		{
			return;
		}

		ArbitraryData[key] = value;
	}

	public void SetSaveData<T>( string key, T value )
	{
		if ( !ValidateKey( this, key ) )
		{
			return;
		}

		if ( !ValidateValue( this, value, key ) )
		{
			return;
		}

		ArbitraryData[key] = value;
	}

	public static bool ValidateKey( PersistentItem itemCheck, string key )
	{
		if ( string.IsNullOrEmpty( key ) )
		{
			Log.Error( $"SetSaveData - Key cannot be null or empty on {itemCheck}" );
			return false;
		}

		return true;
	}

	public static bool ValidateValue<T>( PersistentItem itemCheck, T value, string context )
	{
		if ( value is Vector3 vector3 )
		{
			if ( vector3.IsNaN || vector3.IsInfinity )
			{
				Log.Error(
					$"SetSaveData - Value for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {vector3}" );
				return false;
			}
		}

		if ( value is float single )
		{
			if ( float.IsNaN( single ) || float.IsInfinity( single ) )
			{
				Log.Error(
					$"SetSaveData - Value for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {single}" );
				return false;
			}
		}

		if ( value is double @double )
		{
			if ( double.IsNaN( @double ) || double.IsInfinity( @double ) )
			{
				Log.Error(
					$"SetSaveData - Value for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {@double}" );
				return false;
			}
		}

		// lists and arrays
		if ( value is IList list )
		{
			foreach ( var item in list )
			{
				if ( item is float f )
				{
					if ( float.IsNaN( f ) || float.IsInfinity( f ) )
					{
						Log.Error(
							$"SetSaveData - List item for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {f}" );
						return false;
					}
				}

				if ( item is double d )
				{
					if ( double.IsNaN( d ) || double.IsInfinity( d ) )
					{
						Log.Error(
							$"SetSaveData - List item for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {d}" );
						return false;
					}
				}

				if ( item is Vector3 v3 )
				{
					if ( v3.IsNaN || v3.IsInfinity )
					{
						Log.Error(
							$"SetSaveData - List item for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {v3}" );
						return false;
					}
				}
			}
		}

		// dictionaries
		if ( value is IDictionary dict )
		{
			// check keys
			foreach ( var key in dict.Keys )
			{
				var item = dict[key];
				if ( item is float f )
				{
					if ( float.IsNaN( f ) || float.IsInfinity( f ) )
					{
						Log.Error(
							$"SetSaveData - Dictionary item for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {f}" );
						return false;
					}
				}

				if ( item is double d )
				{
					if ( double.IsNaN( d ) || double.IsInfinity( d ) )
					{
						Log.Error(
							$"SetSaveData - Dictionary item for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {d}" );
						return false;
					}
				}

				if ( item is Vector3 v3 )
				{
					if ( v3.IsNaN || v3.IsInfinity )
					{
						Log.Error(
							$"SetSaveData - Dictionary item for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {v3}" );
						return false;
					}
				}
			}

			// check values
			foreach ( var item in dict.Values )
			{
				if ( item is float f )
				{
					if ( float.IsNaN( f ) || float.IsInfinity( f ) )
					{
						Log.Error(
							$"SetSaveData - Dictionary item for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {f}" );
						return false;
					}
				}

				if ( item is double d )
				{
					if ( double.IsNaN( d ) || double.IsInfinity( d ) )
					{
						Log.Error(
							$"SetSaveData - Dictionary item for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {d}" );
						return false;
					}
				}

				if ( item is Vector3 v3 )
				{
					if ( v3.IsNaN || v3.IsInfinity )
					{
						Log.Error(
							$"SetSaveData - Dictionary item for '{context}' on '{itemCheck}' cannot be NaN or Infinity: {v3}" );
						return false;
					}
				}
			}
		}
		/*var type = value.GetType();

		// Check if the type is serializable by System.Text.Json
		try
		{
			JsonSerializer.Serialize( value, type, BraxnetGame.JsonOptions );
		}
		catch ( Exception ex )
		{
			XLog.Warning( this, $"SetSaveData - Value of type {type} is not serializable: {ex.Message}" );
			return false;
		}*/

		return true;
	}


	public virtual string GetName()
	{
		return ItemData?.Name;
	}

	public virtual string GetDescription()
	{
		return ItemData?.Description;
	}

	public virtual string GetIcon()
	{
		return ItemData?.GetIcon();
	}

	public virtual Texture GetIconTexture()
	{
		return ItemData?.GetIconTexture();
	}

	/// <summary>
	///		 Returns true if this item can be merged with the other item. Throws an exception if it can't.
	/// </summary>
	/// <param name="other"></param>
	/// <returns></returns>
	/// <exception cref="Exception"></exception>
	public virtual bool CanMergeWith( PersistentItem other )
	{
		return true;
	}

	public virtual void MergeWith( PersistentItem other )
	{
		return;
	}

	public PersistentItem Clone()
	{
		// TODO: DON'T DO THIS KIDS, PLEASE FIND A BETTER WAY
		return JsonSerializer.Deserialize<PersistentItem>( JsonSerializer.Serialize( this, GameManager.JsonOptions ),
			GameManager.JsonOptions );
	}

	/*public GameObject Create()
	{
		var gameObject = new GameObject();

		if ( gameObject.Components.TryGet<WorldItem>( out var worldItem ) )
		{
			worldItem.NodeLink.Persistence = this;
		}

		if ( gameObject.Components.TryGet<Persistent>( out var persistent ) )
		{
			persistent.OnItemLoad( this );
		}

		return gameObject;
	}*/

	// TODO: maybe less hardcoded and repeated code
	public static PersistentItem Create( GameObject gameObject )
	{
		/*if ( !gameObject.IsValid() ) throw new Exception( "Item is null" );

		var persistentItem = new PersistentItem();

		if ( gameObject.Components.TryGet<WorldItem>( out var worldItem ) )
		{
			persistentItem.ItemId = worldItem.ItemData.GetIdentifier();
		}

		if ( gameObject.Components.TryGet<BaseCarriable>( out var carriable ) )
		{
			persistentItem.ItemId ??= carriable.ItemData.GetIdentifier();
		}

		if ( gameObject.Components.TryGet<WorldObject>( out var worldObject ) )
		{
			worldObject.OnObjectSaveAction?.Invoke( persistentItem );
		}

		if ( gameObject.Components.TryGet<Persistent>( out var persistent ) )
		{
			persistent.OnItemSave( persistentItem );
		}

		if ( gameObject.Components.TryGet<IPersistent>( out var persistent2 ) )
		{
			persistent2.OnSave( persistentItem );
		}

		/*var nodeLink = WorldManager.Instance.GetWorldNodeLink( gameObject );
		if ( nodeLink != null )
		{
			nodeLink.OnNodeSave();
			persistentItem = nodeLink.GetPersistence();
		}#1#*/

		if ( !gameObject.IsValid() )
		{
			throw new Exception( "PersistentItem Create: gameObject is invalid" );
		}

		var persistentItem = new PersistentItem();

		if ( gameObject.Components.TryGet<WorldItem>( out var worldItem ) )
		{
			var type = worldItem.PersistentItemType ?? worldItem.ItemData?.PersistentType;

			// XLog.Debug( module: "PersistentItem",
			// 	$"Creating persistent item for {gameObject} with type {type} (has WorldObject, WorldObjectType: {worldObject.PersistentItemType}, ItemDataType: {worldObject.ItemData?.PersistentType})" );

			if ( type != null )
			{
				persistentItem = TypeLibrary.Create<PersistentItem>( type );
			}
			else
			{
				Log.Warning(
					$"PersistentItemType is null for {gameObject} (ItemDataType: {worldItem.ItemData?.PersistentType}, WorldObjectType: {worldItem.PersistentItemType})" );
			}

			// persistentItem.Tags = worldObject.PersistentItemTags;

			// XLog.Debug( module: "PersistentItem",
			// 	$"Created persistent item of type {type} for {gameObject}" );

			if ( worldItem.ItemData != null )
			{
				persistentItem.ItemId = worldItem.ItemData.Id;
			}
			else
			{
				Log.Error( $"ItemData is null for {gameObject}" );
			}

			worldItem.SavePersistence( persistentItem );

			// XLog.Debug( module: "PersistentItem", $"Saved persistence for {gameObject}" );
		}

		// XLog.Debug( module: "PersistentItem", $"Calling OnSave for {gameObject} IPersistent" );
		// foreach ( var persistent in gameObject.Components.GetAll<IPersistent>() )
		// {
		// 	XLog.Debug( module: "PersistentItem", $"Calling OnSave for {gameObject} {persistent}" );
		// 	persistent.OnSave( persistentItem );
		// }

		return persistentItem;
	}

	public static PersistentItem Create( ItemData itemData, bool initialize = false )
	{
		var item = new PersistentItem { ItemId = itemData.GetIdentifier() };

		if ( initialize ) item.Initialize();

		return item;
	}

	public static PersistentItem Create( string itemId, bool initialize = false )
	{
		return Create( ItemData.Get( itemId ), initialize );
	}

	/// <summary>
	///  Might sound stupid but don't use this unless you're spawning a carriable.
	/// </summary>
	/// <returns></returns>
	/// <exception cref="Exception"></exception>
	public BaseCarriable SpawnCarriable()
	{
		if ( ItemData is not ToolData toolData ) throw new Exception( $"ItemData is not a ToolData for {ItemId}" );

		var carriable = toolData.SpawnCarriable();

		if ( carriable == null ) throw new Exception( $"Carriable is null for {ItemId}" );

		carriable.Durability = GetSaveData<int>( "Durability" );

		if ( carriable.GameObject.Components.TryGet<Persistent>( out var persistent ) )
		{
			persistent.OnItemLoad( this );
		}

		if ( carriable.GameObject.Components.TryGet<IPersistent>( out var persistent2 ) )
		{
			persistent2.OnLoad( this );
		}

		return carriable;
	}

	public async Task<Package> GetPackage()
	{
		return await Package.Fetch( PackageIdent, false );
	}
}