Wrappers/3D/Tools/Node3dTool.cs
using Nodebox.Nodes;
using Sandbox.Diagnostics;

namespace Nodebox;

public class Node3dTool : Component
{
    [RequireComponent] public Sandbox.WorldInput WorldInput { get; set; }
    [RequireComponent] public ScreenPanel ScreenPanel { get; set; }
    [RequireComponent] public Node3dSpawnMenu Node3dSpawnMenu { get; set; }
    [RequireComponent] public Node3dContextMenu Node3dContextMenu { get; set; }
    [RequireComponent] public Node3dPropertyMenu Node3dPropertyMenu { get; set; }
    
    [Property]
    [InputAction]
    public string ContextMenuAction { get; set; } = "Reload";

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

    public object ContextMenuTarget { get; set; }
    public GameObject PropertyMenuTarget { get; set; }
    public object PropertyMenuTargetComponent { get; set; }

    public Node3d HeldNode { get; private set; }
    public (Wire3d Wire3d, PinType TargetPinType, float Distance)? HeldWire { get; private set; }

    public List<Node3d> SelectedNode3ds { get; private set; }

    public Transform? previousTransform;

    private SoundFile SoundClick2 { get; set; } = SoundFile.Load("sounds/kenney/ui/click_002.vsnd_c");
    private SoundFile SoundError2 { get; set; } = SoundFile.Load("sounds/kenney/ui/error_002.vsnd_c");
    private SoundFile SoundSpawnNode { get; set; } = SoundFile.Load("sounds/kenney/ui/drop_002.vsnd_c");
    private static void PlaySound(SoundFile snd, float volume, float pitch) {
        var src = Sound.PlayFile(snd, volume, pitch);
        src.ListenLocal = true;
    }

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

    private TimeSince doubleLeftClickTimer;

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

    private (bool Down, bool Pressed, bool Released) ContextAction => (
        Input.Down(ContextMenuAction),
        Input.Pressed(ContextMenuAction),
        Input.Released(ContextMenuAction)
    );

	protected override void OnStart()
	{
        Node3dSpawnMenu.Enabled = false;
        Node3dContextMenu.Enabled = false;
        Node3dPropertyMenu.Enabled = false;
	}

    private void PositionNewGameObject(GameObject go) {
        go.WorldPosition = WorldPosition + WorldRotation.Forward * 50f;
        go.WorldRotation = Rotation.FromYaw(WorldRotation.Yaw() - 180f);
        PlaySound(SoundSpawnNode, 0.5f, 0.7f);
    }

    public GameObject OnNodeSpawnRequested(Library.Entry entry, bool autoAttach = true) {
        var go = entry.CreateNode3d();
        PositionNewGameObject(go);
        if (autoAttach)
            AttachToHeldWire(go);
        return go;
    }

    public void AttachToHeldWire(GameObject go) {
        if (HeldWire != null) {
            var (wire3d, targetPinType, _) = HeldWire.Value;
            var node3d = go.GetComponent<Node3d>();

            if (wire3d.TrySetAny(node3d, targetPinType)) {
                PlaySound(SoundClick2, 0.5f, 0.25f);
                HeldWire = null;
                CloseSpawnMenu();
            }
        }
    }

	protected override void OnUpdate()
    {
        HandleActions();

        MoveHeldNode();
        MoveHeldWire();
        previousTransform = GameObject.WorldTransform;

        PaintHud();
    }
    
    private void HandleActions() {
        if (Input.Pressed("menu")) {
            if (!Node3dSpawnMenu.Panel.IsValid()) {
                CloseContextMenu();
                OpenSpawnMenu();
            } else {
                CloseSpawnMenu();
            }
        }

        if (LeftAction.Pressed) {
            var target = WorldInput.Hovered;
            if (CheckNode3d(target, out var node3d)) {
                if (target.GetType() != typeof(PinButton)) {
                    StartHoldingNode(node3d);
                } else {
                    OnPinLeftClicked(node3d, (PinButton)target);
                }
            }
            else if (HeldWire != null) {
                if (doubleLeftClickTimer.Relative < 0.2f) {
                    TrySpawnConstantOrDisplay();
                } else {
                    doubleLeftClickTimer = 0;
                }
            }
        }

        if (LeftAction.Released) {
            StopHoldingNode();
        }

        if (RightAction.Pressed) {
            if (HeldWire.HasValue) {
                DestroyHeldWire();
            } else {
                var target = WorldInput.Hovered;
                if (!CheckNode3d(target, out var node3d)) return;
                if (target.GetType() == typeof(PinButton)) {
                    OnPinRightClicked(node3d, (PinButton)target);
                }
            }
        }

        if (ContextAction.Pressed) {
            StopHoldingNode();
            if (Node3dContextMenu.Panel != null && Node3dContextMenu.Panel.IsValid()) {
                CloseContextMenu();
                return;
            }
            
            var target = WorldInput.Hovered;
            if (target == null) {
                var trace = Scene.Trace.Ray(new Ray(WorldPosition, WorldTransform.Forward), ContextMenuRange)
                    .WithoutTags("player")
                    .Run();
                var go = trace.Component?.GameObject;
                if (go != null) {
                    OpenContextMenu(go);
                }
            } else {
                if (!CheckNode3d(target, out var node3d)) return;
                OpenContextMenu(node3d.GameObject);
            }
        }
    }

    public void StartHoldingNode(Node3d node3d) {
        if (HeldNode != null) {
            StopHoldingNode();
        }

        HeldNode = node3d;
    }

    public void StopHoldingNode() {
        if (HeldNode == null) return;
        HeldNode = null;
    }

    private void MoveHeldNode() {
        if (HeldNode == null) return;
        if (!previousTransform.HasValue) return;
        var previous = previousTransform.Value;
        var current = GameObject.WorldTransform;

        var previousOffset = previous.PointToLocal(HeldNode.WorldPosition);
        previousOffset = previousOffset.WithX(previousOffset.x + Input.MouseWheel.y * ScrollSpeed);
        var offset = current.PointToWorld(previousOffset);
        HeldNode.WorldPosition = offset;
        HeldNode.WorldRotation *= Rotation.FromYaw(current.Rotation.Yaw() - previous.Rotation.Yaw()) * (RightAction.Down ? -SpinSpeed : 1f);
    }

    private void OnPinLeftClicked(Node3d node3d, PinButton pinButton) {
        var index = pinButton.Index;
        var pinType = pinButton.IsOutput ? PinType.Output : PinType.Input;
        if (HeldWire.HasValue) {
            var (heldWire, targetPinType, _) = HeldWire.Value;
            if (pinType != targetPinType) return;

            if (pinButton.IsOutput) {
                if (node3d == heldWire.To) return;
                
                var e = heldWire.TrySetFrom(node3d, index);
                if (e != null) {
                    Log.Warning(e);
                    return;
                }
            } else {
                if (node3d == heldWire.From) return;
                
                // Disconnect existing input pins
                var pinInputWires = node3d.Node.GetPinWires(PinType.Input, index).ToList();
                if (pinInputWires.Count > 0) {
                    pinInputWires[0].GetMeta<Wire3d>().To = null;
                }

                var e = heldWire.TrySetTo(node3d, index);
                if (e == null) {

                    // Delete old disconnected wires
                    if (pinInputWires.Count > 0) {
                        pinInputWires[0].GetMeta<Wire3d>().GameObject.DestroyImmediate();
                    }
                } else {
                    // Reinstantiate old connections
                    if (pinInputWires.Count > 0) {
                        pinInputWires[0].GetMeta<Wire3d>().To = node3d;
                    }
                    
                    PlaySound(SoundError2, 0.5f, 0.7f);

                    Log.Warning(e);
                    return;
                }
            }

            HeldWire = null;
            PlaySound(SoundClick2, 0.5f, 0.4f);
            return;
        }

        var distance = node3d.GameObject.WorldPosition.Distance(GameObject.WorldPosition) - 10f;
        Wire3d wire;

        var inputWires = node3d.Node.GetPinWires(PinType.Input, index);
        if (pinType == PinType.Input && inputWires.Count() == 1) {
            wire = inputWires.First().GetMeta<Wire3d>();
            Assert.NotNull(wire, "wtf");
            wire.To = null;
            HeldWire = (wire, PinType.Input, distance);
            PlaySound(SoundClick2, 0.4f, 0.2f);
            return;
        } else {
            Wire3d.New(out wire);
        }
        
        if (pinType == PinType.Output) {
            wire.From = node3d;
            wire.FromIndex = index;
        } else {
            wire.To = node3d;
            wire.ToIndex = index;
        }

        HeldWire = (wire, pinType.Opposite(), distance);
        PlaySound(SoundClick2, 0.5f, 0.4f);
    }

    private void OnPinRightClicked(Node3d node3d, PinButton pinButton) {
        PlaySound(SoundClick2, 0.5f, 0.2f);
        node3d.Node.GetPinWires(pinButton.IsOutput ? PinType.Output : PinType.Input, pinButton.Index).ForEach(x => {
            x.GetMeta<Wire3d>()?.GameObject.Destroy();
        });
    }

    private void DestroyHeldWire() {
        HeldWire.Value.Wire3d.GameObject.Destroy();
        HeldWire = null;
    }

    private void MoveHeldWire() {
        if (!HeldWire.HasValue) return;
        var (wire, targetPinType, distance) = HeldWire.Value;
        var position = GameObject.WorldTransform.PointToWorld(new Vector3(distance, 0f, 0f));
        if (targetPinType == PinType.Input) {
            wire.End = position;
        } else {
            wire.Start = position;
        }
    }

    public void OpenSpawnMenu() {
        CloseAllMenus();
        Node3dSpawnMenu.Enabled = false;
        Node3dSpawnMenu.Enabled = true;
        Node3dSpawnMenu.Panel.UserData = this;
    }

    public void CloseSpawnMenu() {
        Node3dSpawnMenu.Panel?.Delete();
    }

    public void OpenContextMenu(object obj) {
        if (!(Node3dPropertyMenu.Panel == null || !Node3dPropertyMenu.Panel.IsValid())) {
            CloseAllMenus();
            return;
        }

        CloseAllMenus();
        ContextMenuTarget = obj;
        
        if (ContextMenuTarget.GetType() == typeof(GameObject)) {
            var targetGameObject = (GameObject)ContextMenuTarget;
            Node3dContextMenu.Entries = new() {
                new("Destroy", "delete", () => {
                    PlaySound(SoundFile.Load("sounds/kenney/ui/drop_003.vsnd_c"), 0.3f, 0.3f);
                    targetGameObject.Destroy();
                    CloseAllMenus();
                }),
                new("Duplicate", "content_copy", () => {
                    PlaySound(SoundFile.Load("sounds/kenney/ui/drop_003.vsnd_c"), 0.5f, 1.2f);
                    var go = targetGameObject.Clone(targetGameObject.WorldTransform.WithPosition(targetGameObject.WorldPosition + Vector3.Up * 20f));
                    var dstNode3d = go.GetComponent<Node3d>();
                    var srcNode3d = targetGameObject.GetComponent<Node3d>();
                    if (srcNode3d != null) {
                        dstNode3d.Node = srcNode3d.Node.Clone();
                    }

                    CloseAllMenus();
                }),
                new("Property List", "list_alt", () => {
                    CloseAllMenus();
                    
                    OpenPropertyMenu(targetGameObject);
                })
            };
        }
        else if (ContextMenuTarget.GetType() == typeof(PropertyDescription)) {
            var gameObject = PropertyMenuTarget;
            var propertyDescription = (PropertyDescription)ContextMenuTarget;
            var reference = TypeLibrary.GetType(typeof(Reference<>)).CreateGeneric<Reference>(
                [propertyDescription.PropertyType],
                [PropertyMenuTargetComponent, propertyDescription]
                );

            Node3dContextMenu.Entries = new() {
                new("Source", "output", () => {
                    var node3dGo = new GameObject();
                    PositionNewGameObject(node3dGo);
                    var node3d = node3dGo.GetOrAddComponent<Node3d>();
                    
                    var source = TypeLibrary.GetType(typeof(Source<>)).CreateGeneric<Node>([propertyDescription.PropertyType], [reference]);
                    node3d.Node = source;
                    AttachToHeldWire(node3dGo);
                    CloseContextMenu();
                }),
                new("Drive", "input", () => {
                    var node3dGo = new GameObject();
                    PositionNewGameObject(node3dGo);
                    var node3d = node3dGo.GetOrAddComponent<Node3d>();
                    
                    var drive = TypeLibrary.GetType(typeof(Drive<>)).CreateGeneric<Node>([propertyDescription.PropertyType], [reference]);
                    node3d.Node = drive;
                    AttachToHeldWire(node3dGo);
                    CloseContextMenu();
                }),
                new("Reference", "link", () => {
                    var node3dGo = new GameObject();
                    PositionNewGameObject(node3dGo);
                    var node3d = node3dGo.GetOrAddComponent<Node3d>();
                    
                    //var referenceType = TypeLibrary.GetType(typeof(Reference<>)).MakeGenericType([propertyDescription.PropertyType]);
                    var referenceType = reference.GetType();
                    node3d.Node = TypeLibrary.GetType(typeof(Constant<>)).CreateGeneric<Node>([referenceType], [reference]);
                    AttachToHeldWire(node3dGo);
                    CloseContextMenu();
                }),
            };
        }
        else {
            return;
        }

        Node3dContextMenu.Enabled = false;
        Node3dContextMenu.Enabled = true;
        Node3dContextMenu.Panel.UserData = this;
        PlaySound(SoundFile.Load("sounds/kenney/ui/maximize_006.vsnd_c"), 0.3f, 1.3f);
    }

    public void CloseContextMenu() {
        Node3dContextMenu.Panel?.Delete();
    }

    public void OpenPropertyMenu(GameObject go) {
        PlaySound(SoundFile.Load("sounds/kenney/ui/maximize_006.vsnd_c"), 0.3f, 1.1f);
        CloseAllMenus();
        PropertyMenuTarget = go;
        Node3dPropertyMenu.TargetGameObject = go;
        Node3dPropertyMenu.Enabled = false;
        Node3dPropertyMenu.Enabled = true;
        Node3dPropertyMenu.Panel.UserData = this;
    }

    public void ClosePropertyMenu() {
        if (Node3dPropertyMenu.Panel == null || !Node3dPropertyMenu.Panel.IsValid()) return;
        Node3dPropertyMenu.Panel.Delete();
        PlaySound(SoundFile.Load("sounds/kenney/ui/minimize_006.vsnd_c"), 0.2f, 1.1f);
    }

    public void CloseAllMenus() {
        CloseSpawnMenu();
        CloseContextMenu();
        ClosePropertyMenu();
    }

    public void TrySpawnConstantOrDisplay() {
        var pinType = HeldWire.Value.TargetPinType;
        if (pinType == PinType.Input) {
            var node3dGo = new GameObject();
            PositionNewGameObject(node3dGo);
            var node3d = node3dGo.GetOrAddComponent<Node3d>();
            var display = TypeLibrary.GetType<Display>().Create<Node>();
            node3d.Node = display;
            AttachToHeldWire(node3dGo);
            CloseContextMenu();
        } else {
            var wire3d = HeldWire.Value.Wire3d;
            var type = wire3d.To.Node.InputPins[wire3d.ToIndex].Type;
            var constantNodeGenericType = TypeLibrary.GetType(typeof(Constant<>)).MakeGenericType([type]);
            var constant = Library.Entries.Where(x => x.Type == constantNodeGenericType);
            if (!constant.Any()) {
                return;
            }

            OnNodeSpawnRequested(constant.First());
        }
    }

    private void PaintHud() {
        if ( Scene.Camera is null )
			return;

		var hud = Scene.Camera.Hud;
        var target = WorldInput.Hovered;
        if (target == null || !target.IsValid()) return;
        if (!CheckNode3d(target, out var node3d)) return;
        if (target.GetType() != typeof(PinButton)) return;
        var pinButton = (PinButton)target;
        var pinType = pinButton.IsOutput ? PinType.Output : PinType.Input;
        var index = pinButton.Index;
        var value = node3d.Node.GetPinValue(pinType, index);
        var pin = node3d.Node.GetPin(pinType, index);
        string text = $"{value?.ToString() ?? "null"} ({pin.Type.GetDisplayName()})";
        if (pin.Type == typeof(Polymorphic))
            text = $"{pin.Name} (Polymorphic)";
        hud.DrawText(new TextRendering.Scope(text, Color.White.WithAlphaMultiplied(0.8f), 16, "Poppins", 600), Screen.Size * 0.51f, TextFlag.LeftTop);
    }

    private static bool CheckNode3d(Panel target, out Node3d node3d) {
        node3d = null;
        if (target == null) return false;
        var root = target.FindRootPanel();
        if (root == null) return false;
        if (root.GetType() != typeof(Sandbox.UI.WorldPanel)) return false;
        var worldPanel = (Sandbox.UI.WorldPanel)root;
        var userData = worldPanel.UserData;
        if (userData.GetType() != typeof(Node3d)) return false;
        node3d = (Node3d)userData;
        return true;
    }
}