Editor/Inspector/SerializedReactionDependencyProperty.cs
#if DEBUG

using System.Reflection;
using System.Text;
using Sandbox.Internal;
using Sandbox.Reactivity.Internals;

namespace Sandbox.Reactivity.Editor.Inspector;

/// <summary>
/// A property that corresponds to an <see cref="IProducer" /> that an <see cref="IReaction" /> depends on. It uses the
/// name/location of the property the producer was accessed from (where possible).
/// </summary>
internal class SerializedReactionDependencyProperty : SerializedProperty
{
	private readonly IReaction _reaction;

	private readonly string? _sourceFile;

	private readonly int _sourceLine;

	private readonly PropertyInfo _valueProperty;

	public SerializedReactionDependencyProperty(IProducer producer, IReaction reaction)
	{
		if (producer.GetType() is not { GenericTypeArguments: [var valueType] } producerType)
		{
			throw new InvalidOperationException("Could not determine producer type");
		}

		if (producerType.GetProperty("Value", valueType) is not { } valueProperty)
		{
			throw new InvalidOperationException("Could not find producer value property");
		}

		_reaction = reaction;
		_valueProperty = valueProperty;
		Producer = producer;
		PropertyType = valueType;

		if (reaction is Effect effect)
		{
			void OnEffectDispose()
			{
				NoteChanged();

				effect.OnDisposed -= OnEffectDispose;
			}

			effect.OnDisposed += OnEffectDispose;
		}

		if (producer.Container is { SourceFile: { } propertyFile, SourceLine: var propertyLine })
		{
			_sourceFile = propertyFile;
			_sourceLine = propertyLine;
		}
		else if (producer.Location is { } location)
		{
			var separatorIndex = location.LastIndexOf(':');

			if (separatorIndex != -1)
			{
				_sourceFile = location[..separatorIndex];
				_sourceLine = int.TryParse(location[(separatorIndex + 1)..], out var line) ? line : 0;
			}
			else
			{
				_sourceFile = location;
			}
		}
	}

	// needed since we can't get a concrete generic type definition from a type library to call SetValue on that will
	// do this internally. conversion is required for things like enums because the control widget passes around an
	// int64 instead of the actual underlying type
	private static MethodInfo TryConvertMethod =>
		field ??= typeof(GlobalSystemNamespace).Assembly.GetType("Sandbox.Translation")
				?.GetMethod("TryConvert",
					BindingFlags.Static | BindingFlags.NonPublic,
					[typeof(object), typeof(Type), typeof(object).MakeByRefType()]) ??
			throw new MemberAccessException("Could not find Sandbox.Translation.TryConvert");

	public IProducer Producer { get; }

	public override Type PropertyType { get; }

	public override bool IsProperty => true;

	public override bool IsEditable => _reaction is not Effect { IsDisposed: true };

	public override bool IsValid => _reaction is not Effect { IsDisposed: true } && base.IsValid;

	public override string Name => Producer.Name ?? "Reactive Object";

	public override string DisplayName => Producer.Container?.Title ?? Producer.Name ?? "Reactive Object";

	public override string Description
	{
		get
		{
			var description = "";
			var containerProperty = "";
			var fileLocation = "";

			if (Producer.Container is { } property)
			{
				description = property.Description;
				containerProperty =
					$" {property.TypeDescription.TargetType.ToRichText()}.<span style='color: #9CDCFE; font-weight: 600;'>{property.Name}</span>";
			}

			if (_sourceFile != null)
			{
				var line = _sourceLine != 0 ? $":{_sourceLine}" : "";
				fileLocation =
					$"<br/><span style='color: {Theme.Primary.Desaturate(0.3f).Hex};'>{_sourceFile}{line}</span>";
			}

			StringBuilder result = new();

			if (!string.IsNullOrWhiteSpace(containerProperty) || !string.IsNullOrWhiteSpace(_sourceFile))
			{
				result.Append("Declared in:");
			}

			result.Append(containerProperty);
			result.Append(fileLocation);

			if (!string.IsNullOrWhiteSpace(description))
			{
				result.Append("<br/><br/>");
				result.Append(description);
			}

			return result.ToString();
		}
	}

	public override string SourceFile => _sourceFile!;

	public override int SourceLine => _sourceLine;

	public override void SetValue<T>(T value)
	{
		object?[] parameters = [value, _valueProperty.PropertyType, null];

		if (TryConvertMethod.Invoke(null, parameters) is false)
		{
			return;
		}

		NotePreChange();
		_valueProperty.SetValue(Producer, parameters[2]);
		NoteChanged();
	}

	public override T GetValue<T>(T defaultValue = default!)
	{
		return ValueToType(Producer.NonReactiveValue, defaultValue);
	}
}

#endif