MehCode/Wrappers/3D/Wire3d/Wire3d.cs
namespace Nodebox;

public class Wire3d : Component
{
	[RequireComponent] public LineRenderer LineRenderer { get; set; }

	public Vector3 Start { get => GameObject.WorldPosition; set => GameObject.WorldPosition = value; }
	public Vector3 End { get; set; } = Vector3.Zero;

	private Node3d _from;
	private int _fromIndex;
	private Node3d _to;
	private int _toIndex;

	[Property] public Node3d From { get => _from; set {
		if (value == _from) return;
		_from = value;
		UpdateWire();
	} }

	[Property] public int FromIndex { get => _fromIndex; set {
		if (value == _fromIndex) return;
		_fromIndex = value;
		UpdateWire();
	} }

	[Property] public Node3d To { get => _to; set {
		if (value == _to) return;
		_to = value;
		UpdateWire();
	} }

	[Property] public int ToIndex { get => _toIndex; set {
		if (value == _toIndex) return;
		_toIndex = value;
		UpdateWire();
	} }

	private Wire _wire;
	public Wire Wire { get => _wire; private set {
		// var old = _wire;
		_wire = value;
		_wire.SetMeta(this);
		GameObject.Name = value.ToString();
	} }

	public static GameObject New(out Wire3d wire) {
		var go = new GameObject();
		wire = go.AddComponent<Wire3d>();
		return go;
	}

	public static GameObject Connect(Node3d from, int fromIndex, Node3d to, int toIndex) {
		var go = New(out var wire);
		wire.From = from;
		wire.FromIndex = fromIndex;
		wire.To = to;
		wire.ToIndex = toIndex;
		return go;
	}

	public static GameObject Connect(GameObject from, int fromIndex, GameObject to, int toIndex) => Connect(from.GetComponent<Node3d>(), fromIndex, to.GetComponent<Node3d>(), toIndex);

	private void UpdateWire() {
		Wire?.Dispose();
		if (!Enabled) return;
		if (From == null) return;
		if (To == null) return;

		var exception = GetWireException();
		if (exception != null) {
			Log.Warning(exception);
			return;
		}

		var wire = new Wire(From.Node, FromIndex, To.Node, ToIndex, true);

		Wire = wire;
		Wire.Connect();
	}

	public bool IsValidWire() {
		return GetWireException() == null;
	}

	public Exception GetWireException() {
		try {
			Wire.Check(From.Node, FromIndex, To.Node, ToIndex);
		} catch (Exception e) {
			return e;
		}
		return null;
	}

	public Exception TrySet(Node3d node3d, PinType pinType, int? index = null) {
		var oldEnabled = Enabled;
		Enabled = false;

		var oldIndex = pinType == PinType.Output ? FromIndex : ToIndex;
		if (index != null) {
			if (pinType == PinType.Output) {
				FromIndex = index.Value;
			} else {
				ToIndex = index.Value;
			}
		}

		if (pinType == PinType.Output) {
			From = node3d;
		} else {
			To = node3d;
		}
		
		var exception = GetWireException();
		Enabled = oldEnabled;
		
		if (exception != null) {
			if (pinType == PinType.Output) {
				From = null;
				FromIndex = oldIndex;
			} else {
				To = null;
				ToIndex = oldIndex;
			}
			return exception;
		}

		return null;
	}

	public bool TrySetAny(Node3d node3d, PinType pinType) {
		var pins = node3d.Node.GetPins(pinType);
		foreach (var item in pins.Enumerate()) {
			if (TrySet(node3d, pinType, item.Index) == null) {
				return true;
			}
		}

		return false;
	}
	
	public Exception TrySetFrom(Node3d from, int? index = null) => TrySet(from, PinType.Output, index);
	public Exception TrySetTo(Node3d to, int? index = null) => TrySet(to, PinType.Input, index);

	protected override void OnStart() {
		UpdateWire();
		
		LineRenderer.Color = Color.White;
		LineRenderer.Width = 0.6f;
		LineRenderer.EndCap = SceneLineObject.CapStyle.Arrow;
		// LineRenderer.StartCap = SceneLineObject.CapStyle.Rounded;
		LineRenderer.Face = SceneLineObject.FaceMode.Camera;
		LineRenderer.CastShadows = false;
		LineRenderer.UseVectorPoints = true;
		LineRenderer.RenderOptions.Bloom = true;
		LineRenderer.Enabled = true;
		LineRenderer.VectorPoints = new List<Vector3> {
			new(), new(), new(), new(),
		};
	}

	protected override void OnDisabled() {
		Wire?.Dispose();
	}

	protected override void OnEnabled() {
		UpdateWire();
	}

	protected override void OnUpdate() {
		if (From != null) {
			var fromPos = From.GetWorldPinPosition(PinType.Output, FromIndex);
			Start = fromPos ?? Start;
		}
		
		if (To != null) {
			var toPos = To.GetWorldPinPosition(PinType.Input, ToIndex);
			End = toPos ?? End;
		}
		
		var startExtra = From?.WorldTransform.NormalToWorld(new Vector3(0f, 1f, 0f)) ?? Vector3.Zero;
		var endExtra = To?.WorldTransform.NormalToWorld(new Vector3(0f, -1f, 0f)) ?? Vector3.Zero;

		
		LineRenderer.VectorPoints[0] = Start + startExtra * 0.8f;
		LineRenderer.VectorPoints[1] = Start + startExtra * 2.8f;
		LineRenderer.VectorPoints[2] = End + endExtra * 5f;
		LineRenderer.VectorPoints[3] = End + endExtra * 3f;
		
		var colorStart = From?.Node.OutputPins[FromIndex].GetColor();
		var colorEnd = To?.Node.InputPins[ToIndex].GetColor();
		colorEnd ??= colorStart;
		colorStart ??= colorEnd;
		colorStart ??= Color.White;
		colorEnd ??= Color.White;

		LineRenderer.Color = new Gradient(
			new Gradient.ColorFrame(0.0f, SrgbToLinear(colorStart.Value)),
			new Gradient.ColorFrame(0.25f, SrgbToLinear(colorStart.Value)),
			new Gradient.ColorFrame(0.75f, SrgbToLinear(colorEnd.Value)),
			new Gradient.ColorFrame(1.0f, SrgbToLinear(colorEnd.Value))
		);
	}

	protected override void OnDestroy() {
		Wire?.Dispose();
	}

	private static Color SrgbToLinear(Color color) {
		static float srgbToLinear(float x) {
			if (x <= 0.04045f)
				return x / 12.92f;
			return MathF.Pow( (x + 0.055f) / 1.055f, 2.4f );
		}

		return new Color(
			srgbToLinear(color.r),
			srgbToLinear(color.g),
			srgbToLinear(color.b),
			color.a
		);
	}
}