Code/Internals/CallLocation.cs
#if DEBUG && SANDBOX
using System.IO;
#endif

namespace Sandbox.Reactivity.Internals;

/// <summary>
/// Used to avoid littering every method with optional caller info attributes.
/// </summary>
internal readonly ref struct CallLocation
{
#if DEBUG
	private readonly string? _location;
#endif

	/// <summary>
	/// Captures the current stack trace and uses it to determine the location of the current call.
	/// </summary>
	/// <param name="skipFrames">How many stack frames to skip when determining the call location.</param>
	public CallLocation(int skipFrames)
	{
#if DEBUG
		skipFrames += 2; // skip call to Environment.StackTrace and this method

		var span = Environment.StackTrace.AsSpan();
		var i = 0;

		foreach (var range in span.Split('\n'))
		{
			if (i++ < skipFrames)
			{
				continue;
			}

			var line = span[range];
			var locationIndex = line.IndexOf(") in ");

			if (locationIndex != -1)
			{
				var location = line[(locationIndex + 5)..];
				var separatorIndex = location.LastIndexOf(":line ");

				if (separatorIndex != -1)
				{
					var path = location[..separatorIndex];
					var lineNumber = location[(separatorIndex + 6)..].Trim();

					_location = $"{path}:{lineNumber}";
				}
				else
				{
					_location = location.ToString();
				}
			}
			else
			{
				_location = line.ToString();
			}

#if SANDBOX
			var codePath = Project.Current.GetCodePath();

			if (_location.StartsWith(codePath, StringComparison.InvariantCultureIgnoreCase))
			{
				_location = Path.GetRelativePath(codePath, _location);
			}
#endif
			break;
		}
#endif
	}

	/// <summary>
	/// Uses the given method as the location of the current call.
	/// </summary>
	/// <param name="type">The type to get the method from.</param>
	/// <param name="methodName">The name of the method in the given type to use as the call location.</param>
	public CallLocation(Type type, string methodName)
	{
#if DEBUG && SANDBOX
		if (TypeLibrary.GetType(type)?.GetMethod(methodName) is { } method)
		{
			_location = $"{method.SourceFile}:{method.SourceLine}";
		}
#endif
	}

	public static implicit operator string?(CallLocation capture)
	{
#if DEBUG
		return capture._location;
#else
		return null;
#endif
	}
}