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

[Title("Nodebox Spatial Node Manipulator")]
[Category("Nodebox")]
public class NodeManipulator : Component, Node.IEvent, IGraphStyle {
    [RequireComponent] public ScreenPanel ScreenPanel { get; set; }
    [RequireComponent] public NodePickRoot NodePickRoot { get; set; }
    [RequireComponent] public Sandbox.WorldInput WorldInput { get; set; }
    [RequireComponent] public LineRenderer LineRenderer { get; set; }

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


    [Property] public float SpinSpeed { get; set; } = 5f;
    [Property] public float ScrollSpeed { get; set; } = 5f;

    [Property]
    [ReadOnly]
    [JsonIgnore]
    public Node Hovered => WorldInput.Hovered?.FindRootPanel()?.GameObject?.GetComponent<Node>();
    public bool IsHoveringPin => WorldInput.Hovered is PinButton;

    public (bool Down, bool Pressed, bool Released) LeftAction => (
        Input.Down(WorldInput.LeftMouseAction),
        Input.Pressed(WorldInput.LeftMouseAction),
        Input.Released(WorldInput.LeftMouseAction)
    );

    public (bool Down, bool Pressed, bool Released) RightAction => (
        Input.Down(WorldInput.RightMouseAction),
        Input.Pressed(WorldInput.RightMouseAction),
        Input.Released(WorldInput.RightMouseAction)
    );

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

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

        // ScreenPanel.Enabled = false;
        ScreenPanel.AutoScreenScale = false;
        NodePickRoot.Enabled = false;
        NodePickRoot.GraphStyle = GraphStyle;
        NodePickRoot.Collection = Nodebox.Node.Collection.FromAllWithIntrinsics();
        NodePickRoot.NodeSelected += OnNodeSelected;

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

        Graph ??= GlobalGraph.Instance?.Graph;
    }


    [Property]
    [ReadOnly]
    [JsonIgnore]
    public Node Grabbed { get; protected set; }
    public Vector3 GrabbedLocalPosition { get; protected set; } = Vector3.Zero;
    protected void Grab() {
        if (!Hovered.IsValid()) { return; }
        if (Hovered.MousePosition is not Vector3 worldMousePosition) { return; }
        Grabbed = Hovered;
        GrabbedLocalPosition = Grabbed.WorldTransform.PointToLocal(worldMousePosition);
    }

    public void UnGrab() {
        Grabbed = null;
    }


    [Property]
    [ReadOnly]
    [JsonIgnore]
    public (Node Node, PinRef PinRef)? WirePoint { get; protected set; } = null;
    public (Node Node, PinRef PinRef)? WireSource => (WirePoint.HasValue && WirePoint.Value.PinRef.Flow == Flow.Output) ? WirePoint.Value : null;
    public (Node Node, PinRef PinRef)? WireDestination => (WirePoint.HasValue && WirePoint.Value.PinRef.Flow == Flow.Input) ? WirePoint.Value : null;

    protected float WireDistance { get; set; } = 0f;
    protected void BeginWire(Node node, PinRef pinRef) {
        if (node.PanelToWorldRelativeToPin(
            pinRef.Flow,
            pinRef.Index,
            new Vector2(0.5f, 0.5f)
        ) is not Vector3 worldPosition) {
            return;
        }

        WireDistance = worldPosition.Distance(WorldPosition);

        if (pinRef.Flow == Flow.Input && pinRef.Wires.Any()) {
            foreach (var oldWire in pinRef.Wires) {
                WirePoint = (oldWire.Source.Spatial, new PinRef(oldWire.Source, Flow.Output, oldWire.SourceIndex));
                oldWire.Spatial.DestroyGameObject();
            }

            return;
        }

        WirePoint = (node, pinRef);
    }

    public void DropWire() {
        WirePoint = null;
    }

    public void EndWire(Node node2, PinRef pinRef2) {
        if (!WirePoint.HasValue) { return; }
        (Node node1, PinRef pinRef1) = WirePoint.Value;

        if (pinRef1.Flow == pinRef2.Flow) { DropWire(); return; }
        var source = WireSource?.Node ?? node2;
        var sourcePinRef = WireSource?.PinRef ?? pinRef2;
        var destination = WireDestination?.Node ?? node2;
        var destinationPinRef = WireDestination?.PinRef ?? pinRef2;

        if (sourcePinRef.Type != typeof(object) && destinationPinRef.Type != typeof(object)) {
            if (sourcePinRef.Type != destinationPinRef.Type) { DropWire(); return; } // TODO: Create a Cast<TSource, TDestination> somehow
        }

        foreach (var oldWire in destinationPinRef.Wires) {
            oldWire.Spatial.DestroyGameObject();
            if (oldWire.Source == source.Inner) {
                DropWire();
                return;
            }
        }

        var wireGo = new GameObject(source.GameObject, name: $"{source.GameObject.Name} -> {destination.GameObject.Name}");
        var wire = wireGo.AddComponent<Wire>(false);
        wire.GraphStyle = GraphStyle;
        wire.SourceIndex = sourcePinRef.Index;
        wire.Destination = destination;
        wire.DestinationIndex = destinationPinRef.Index;
        wire.Enabled = true;

        DropWire();
    }

    void Node.IEvent.OnPinClicked(Node node, PinRef pinRef) {
        if (WirePoint == null) {
            BeginWire(node, pinRef);
            return;
        }

        EndWire(node, pinRef);
    }


    protected Transform? PreviousTransform { get; set; }
    protected void MoveGrabbed() {
        if (!Grabbed.IsValid()) { return; }

        if (!PreviousTransform.HasValue) { return; }
        var previous = PreviousTransform.Value;
        var current = GameObject.WorldTransform;

        var worldTransform = Grabbed.WorldTransform.ToWorld(new Transform(GrabbedLocalPosition));

        var localTransform = previous.ToLocal(worldTransform);
        localTransform.Position = localTransform.Position.Normal * (localTransform.Position.Length + Input.MouseWheel.y * 5f);
        worldTransform = current.ToWorld(localTransform);

        worldTransform.Rotation = Rotation.LookAt(worldTransform.Forward.SubtractDirection(Vector3.Up).Normal, Vector3.Up);
        Grabbed.WorldTransform = worldTransform with { Position = worldTransform.Position - worldTransform.Rotation * GrabbedLocalPosition };
    }

    protected void UpdateLineRenderer() {
        if (!WirePoint.HasValue) {
            LineRenderer.Enabled = false;
            return;
        }

        var wirePointInFront = WorldPosition + WorldTransform.Forward * WireDistance;
        if (WireSource?.Node?.PanelToWorldRelativeToPin(
            Flow.Output,
            WireSource.Value.PinRef.Index,
            new Vector2(0.5f, 0.5f)
        ) is not Vector3 start) {
            start = wirePointInFront;
        }

        if (WireDestination?.Node?.PanelToWorldRelativeToPin(
            Flow.Input,
            WireDestination.Value.PinRef.Index,
            new Vector2(0f, 0.5f)
        ) is not Vector3 end) {
            end = wirePointInFront;
        }


        var color = GraphStyle?.TypePalette?.Get(WirePoint.Value.PinRef.Type).Color ?? Color.White;

        var startNormal = WireSource?.Node.WorldTransform.NormalToWorld(new Vector3(0f, 1f, 0f)) ?? Vector3.Zero;
        var endNormal = WireDestination?.Node.WorldTransform.NormalToWorld(new Vector3(0f, -1f, 0f)) ?? Vector3.Zero;

        LineRenderer.Enabled = true;
        LineRenderer.Color = color;

        LineRenderer.VectorPoints[0] = start; //- startNormal * 0f;
        if (WireSource.HasValue) {
            LineRenderer.VectorPoints[1] = start + startNormal * 3f;
        } else {
            LineRenderer.VectorPoints[1] = end + endNormal * 3f;
        }
        LineRenderer.VectorPoints[2] = end + endNormal * 1f;
    }

    protected override void OnUpdate() {
        WorldInput.Enabled = Enabled;

        if (RightAction.Released) {
            if (WorldInput.Hovered == null) {
                NodePickRoot.Enabled = true;
            } else {
                var go = WorldInput.Hovered.GameObject;
                if (go.IsValid() && go.GetComponent<Node>() is Node node && node.IsValid()) {
                    go.Destroy();
                }
            }
        }

        if (WirePoint.HasValue && !WirePoint.Value.Node.IsValid()) {
            DropWire();
        }

        NodePickRoot.NodePick?.PinRef = WirePoint?.PinRef;

        if (LeftAction.Pressed && !IsHoveringPin) {
            Grab();
            if (!Grabbed.IsValid()) {
                DropWire();
            }
        }

        if (!LeftAction.Down && Grabbed != null) {
            UnGrab();
        }

        MoveGrabbed();
        PreviousTransform = GameObject.WorldTransform;
    }

    protected void OnNodeSelected(SerializableType type) {
        NodePickRoot.Enabled = false;

        var nodeGo = new GameObject(Graph.GameObject, name: $"{DisplayInfo.ForGenericType(type).Name}");
        var distance = 50f;
        if (WirePoint.HasValue) {
            distance = WireDistance;
        }
        nodeGo.WorldPosition = WorldTransform.PointToWorld(Vector3.Forward * distance);
        nodeGo.WorldRotation = Rotation.LookAt(-WorldTransform.Forward.SubtractDirection(Vector3.Up).Normal, Vector3.Up);

        var node = nodeGo.AddComponent<Node>(false);
        node.Type = type;
        node.Enabled = true;

        if (WirePoint.HasValue) {
            EndWire(node, new PinRef(node.Inner, WirePoint.Value.PinRef.Flow.Opposite, 0));
        }
    }

    protected override void OnPreRender() {
        base.OnPreRender();
        UpdateLineRenderer();
    }

    protected override void OnDisabled() {
        base.OnDisabled();
        UnGrab();
    }
}