Code/UI/Spatial/Wire.cs
namespace Nodebox.UI.Spatial;

[Title("Nodebox Spatial Wire")]
[Category("Nodebox")]
public class Wire : Component, IMeta, IGraphStyle {
    [RequireComponent] public LineRenderer LineRenderer { get; set; }

    public Dictionary<string, object> Meta => Inner.Meta;

    [Property]
    public GraphStyle GraphStyle { get; set; }

    private Nodebox.Wire _inner;

    [Property, ReadOnly]
    [JsonIgnore]
    public Nodebox.Wire Inner => _inner; // TODO: IReadOnlyWire..?

    [Property, Hide]
    [JsonIgnore]
    public Type SourceType => Inner?.SourceType;

    [Property, Hide]
    [JsonIgnore]
    public Type DestinationType => Inner?.DestinationType;

    private HashSet<Graph> _graphs = [];
    [JsonIgnore]
    public IReadOnlySet<Graph> Graphs => _graphs;

    [JsonIgnore]
    public Node Source => GameObject?.Parent?.GetComponent<Node>(false);

    private PinIndex _sourceIndex = PinIndex.Invalid;

    [Property]
    public PinIndex SourceIndex {
        get => _sourceIndex;
        set {
            if (_sourceIndex == value) { return; }
            _sourceIndex = value;
            Reconnect();
        }
    }

    private Node _destination = null;

    [Property]
    public Node Destination {
        get => _destination;
        set {
            if (_destination == value) { return; }
            _destination?.OnRecreated -= Reconnect;
            _destination?.OnComponentDisabled -= Disconnect;
            _destination = value;
            _destination?.OnRecreated += Reconnect;
            _destination?.OnComponentDisabled += Disconnect;
            Reconnect();
        }
    }

    private PinIndex _destinationIndex = PinIndex.Invalid;

    [Property]
    public PinIndex DestinationIndex {
        get => _destinationIndex;
        set {
            if (_destinationIndex == value) { return; }
            _destinationIndex = value;
            Reconnect();
        }
    }

    protected override void OnAwake() {
        base.OnAwake();

        GraphStyle = GraphStyle.DefaultForExecutionContext();
        GraphStyle.ConfigureLineRenderer(LineRenderer);

        LineRenderer.Enabled = false;
        LineRenderer.VectorPoints = [
            new(), new(), new(), new(),
        ];
    }


    protected override void OnEnabled() {
        base.OnEnabled();
        Reconnect();

        Source?.OnRecreated += Reconnect;
        Source?.OnComponentDisabled += Disconnect;
    }

    protected override void OnDisabled() {
        Source?.OnRecreated -= Reconnect;
        Source?.OnComponentDisabled -= Disconnect;

        Disconnect();
        LineRenderer.Enabled = false;
        base.OnDisabled();
    }

    protected override void OnRefresh() {
        base.OnRefresh();
        Reconnect();
    }

    internal void Connect() {
        Assert.IsNull(_inner);

        if (!Enabled) { return; }
        if (!IsValidConnection()) { return; }

        _inner = new(Source.Inner, SourceIndex, Destination.Inner, DestinationIndex) {
            Spatial = this
        };
        // _inner.OnDestroyed += OnInnerDestroyed;

        Assert.IsValid(_inner);
        _graphs = [.. Source.Graphs];
        RegisterInner();
    }

    internal void Disconnect() {
        using var _ = new SuppressDestruction(this);
        UnRegisterInner();
        _graphs = [];
        _inner?.Destroy();
        _inner = null;
    }

    public void Reconnect() {
        if (!Enabled) { return; }
        var suppressPoly = _inner?.Source?.Spatial == Source &&
            _inner?.SourceIndex == SourceIndex &&
            _inner?.Destination.Spatial == Destination &&
            _inner?.DestinationIndex == DestinationIndex;

        if (suppressPoly && _inner.Source.Spatial.IsValid() && _inner.Destination.Spatial.IsValid()) {
            using var _ = new Node.SuppressPolymorph(_inner.Source.Spatial);
            using var __ = new Node.SuppressPolymorph(_inner.Destination.Spatial);

            Disconnect();
            Connect();
            return;
        }

        Disconnect();
        Connect();
    }

    private void RegisterInner() {
        foreach (var graph in _graphs) {
            if (graph.IsValid() && graph.Inner.IsValid()) {
                if (_inner.IsValid()) {
                    graph.Inner.Add(_inner);
                    graph.Inner.OnDestroy += OnGraphDestroy;
                }
            }
        }
    }

    private void UnRegisterInner() {
        foreach (var graph in _graphs) {
            if (graph.IsValid() && graph.Inner.IsValid()) {
                if (_inner.IsValid()) {
                    graph.Inner.Remove(_inner);
                }
                graph.Inner.OnDestroy -= OnGraphDestroy;
            }
        }
        _graphs.Clear();
    }

    private void OnGraphDestroy(Nodebox.Graph innerGraph) {
        _graphs.Remove(innerGraph.Spatial);
    }

    private bool IsValidConnection() {
        if (!Source.IsValid() || !Source.Enabled || !Destination.IsValid() || !Destination.Enabled) {
            return false;
        }

        if (!Source.Inner.IsValid() || !Destination.Inner.IsValid()) {
            return false;
        }

        try {
            Nodebox.Wire.Check(Source.Inner, SourceIndex, Destination.Inner, DestinationIndex);
        }
        catch (Exception) {
            return false;
        }

        return true;
    }

    private void OnInnerDestroyed(GraphElement _) {
        // if (DestructionSuppressed > 0) { return; }
        // Task.MainThread().OnCompleted(() => {
        //     DestroyGameObject();
        // });
    }

    protected override void OnFixedUpdate() {
        base.OnFixedUpdate();
        if (!_inner.IsValid()) {
            DestroyGameObject();
        }
    }

    protected override void OnPreRender() {
        if (!Inner.IsValid()) {
            LineRenderer.Enabled = false;
            return;
        }

        if (Source?.PanelToWorldRelativeToPin(
            Flow.Output,
            SourceIndex,
            new Vector2(0.5f, 0.5f)
        ) is not Vector3 start) {
            LineRenderer.Enabled = false;
            return;
        }

        if (Destination?.PanelToWorldRelativeToPin(Flow.Input, DestinationIndex, new Vector2(0f, 0.5f)
        ) is not Vector3 end) {
            LineRenderer.Enabled = false;
            return;
        }

        var sourceColor = GraphStyle?.TypePalette?.Get(SourceType).Color ?? Color.White;
        var destinationColor = GraphStyle?.TypePalette?.Get(DestinationType).Color ?? Color.White;
        var gradient = new Gradient( // TODO: Cache
            new Gradient.ColorFrame(0.0f, sourceColor),
            new Gradient.ColorFrame(1.0f, destinationColor)
        );

        // DebugOverlay.Line(start, end, sourceColor);

        var startNormal = Source?.WorldTransform.NormalToWorld(new Vector3(0f, 1f, 0f)) ?? Vector3.Zero;
        var endNormal = Destination?.WorldTransform.NormalToWorld(new Vector3(0f, -1f, 0f)) ?? Vector3.Zero;

        LineRenderer.Enabled = true;
        LineRenderer.Color = gradient;
        LineRenderer.VectorPoints[0] = start; //- startNormal * 0f;
        LineRenderer.VectorPoints[1] = start + startNormal * 3f;
        LineRenderer.VectorPoints[2] = end + endNormal * 3f;
        LineRenderer.VectorPoints[3] = end + endNormal * 1f;
    }

    protected override void OnParentChanged(GameObject oldParent, GameObject newParent) {
        base.OnParentChanged(oldParent, newParent);
        Reconnect();

        var oldSource = oldParent?.GetComponent<Node>();
        if (oldSource.IsValid()) {
            oldSource?.OnRecreated -= Reconnect;
            oldSource?.OnComponentDisabled -= Reconnect;
        }
        Source?.OnRecreated += Reconnect;
        Source?.OnComponentDisabled += Disconnect;
    }


    private uint DestructionSuppressed { get; set; } = 0;
    public sealed class SuppressDestruction : IDisposable {
        private Wire Wire { get; set; }
        public SuppressDestruction(Wire wire) {
            Wire = wire;
            Wire.DestructionSuppressed += 1;
        }
        public void Dispose() {
            Wire.DestructionSuppressed -= 1;
        }
    }
}