Code/Core/Node.cs
namespace Nodebox;

public abstract partial class Node : GraphElement, Node.INode, IMeta {
    [JsonIgnore]
    public override Type DirectoryType => typeof(Node);
    public Dictionary<string, object> Meta { get; private set; } = [];

    [JsonIgnore]
    public string Name => DisplayInfo.Name;
    [JsonIgnore]
    public string NameWithGenerics => DisplayInfo.ForGenericType(GetType()).Name;
    [JsonIgnore]
    public string Description => DisplayInfo.Description;
    [JsonIgnore]
    public string Icon => DisplayInfo.Icon;
    [JsonIgnore]
    public string[] Tags => DisplayInfo.Tags;
    [JsonIgnore]
    public string[] Aliases => DisplayInfo.Alias;
    [JsonIgnore]
    public DisplayInfo DisplayInfo => DisplayInfo.ForType(GetType(), true);

    [JsonIgnore]
    public virtual bool AreValidGenerics => true;
    public static bool GetAreValidGenerics(Type nodeType) =>
        TypeLibrary.Create<Node>(nodeType, []).AreValidGenerics;

    public static bool TryMakeGenericType(Type genericType, Type[] typeArguments, out Type type) {
        type = null;
        try {
            type = TypeLibrary.GetType(genericType).MakeGenericType(typeArguments);
        }
        catch (Exception) {
            // Log.Warning($"{wire} - {wireType} - {e}");
        }

        if (type == null) { return false; }
        if (!GetAreValidGenerics(type)) { type = null; return false; }

        return true;
    }

    [JsonIgnore]
    public virtual Dictionary<string, object> InitialMeta => [];
    [JsonIgnore]
    public virtual bool NullInputs => false;
    [JsonIgnore]
    public virtual (List<Pin> In, List<Pin> Out) InitialPins => ([], []);
    public static (List<Pin> In, List<Pin> Out) GetInitialPins(Type nodeType) =>
        TypeLibrary.Create<Node>(nodeType, []).InitialPins;
    public static (List<Pin> In, List<Pin> Out) GetInitialPins<T>() where T : Node => GetInitialPins(typeof(T));

#pragma warning disable IDE1006
    private List<Pin> inputPins { get; set; } = [];
    private List<Pin> outputPins { get; set; } = [];
    private List<HashSet<Wire>> inputWires { get; set; } = [];
    private List<HashSet<Wire>> outputWires { get; set; } = [];

    [JsonInclude]
    internal List<object> inputValues = [];

    [JsonIgnore]
    private ulong generation = 0;
#pragma warning restore IDE1006
    public ulong Generation => generation;
    public void MakeDirty() => generation += 1;

    [JsonIgnore]
    public Action PinsChanged { get; set; }
    protected virtual void OnPinsChanged() { }

    [JsonIgnore]
    public Action InputsChanged { get; set; }
    protected virtual void OnInputsChanged() { }

    [JsonIgnore]
    public Action<bool> WiresChanged { get; set; }
    protected virtual void OnWiresChanged(bool added) { }


    [JsonIgnore]
    public IReadOnlyList<Pin> InputPins => inputPins;
    [JsonIgnore]
    public IReadOnlyList<Pin> OutputPins => outputPins;

    [JsonIgnore]
    public IEnumerable<PinRef> InputPinRefs {
        get {
            foreach (var index in Enumerable.Range(0, inputPins.Count)) {
                yield return new PinRef(this, Flow.Input, (PinIndex)index);
            }
        }
    }
    [JsonIgnore]
    public IEnumerable<PinRef> OutputPinRefs {
        get {
            foreach (var index in Enumerable.Range(0, outputPins.Count)) {
                yield return new PinRef(this, Flow.Output, (PinIndex)index);
            }
        }
    }
    [JsonIgnore]
    public IReadOnlyList<IReadOnlySet<Wire>> InputWires => inputWires;
    [JsonIgnore]
    public IReadOnlyList<IReadOnlySet<Wire>> OutputWires => outputWires;
    [JsonIgnore]
    public IEnumerable<Wire> AllWires => InputWires.SelectMany(x => x).Concat(OutputWires.SelectMany(x => x));
    [JsonIgnore]
    public IReadOnlyList<object> Inputs => inputValues;

    public Node() {
        Meta = InitialMeta;
        inputPins.AddRange(InitialPins.In);
        outputPins.AddRange(InitialPins.Out);
        Resize();
    }

    public IReadOnlyList<Pin> GetPins(Flow flow) => GetPinsInternal(flow);
    private List<Pin> GetPinsInternal(Flow flow) => flow == Flow.Input ? inputPins : outputPins;
    public IEnumerable<(Flow, PinIndex, Pin)> GetAllPins() {
        foreach (var (index, pin) in InputPins.Enumerate()) {
            yield return (Flow.Input, (PinIndex)index, pin);
        }
        foreach (var (index, pin) in OutputPins.Enumerate()) {
            yield return (Flow.Output, (PinIndex)index, pin);
        }
    }

    public IReadOnlyList<IReadOnlySet<Wire>> GetWires(Flow flow) => GetWiresInternal(flow);
    private List<HashSet<Wire>> GetWiresInternal(Flow flow) => flow == Flow.Input ? inputWires : outputWires;

    internal void AddInputWire(int index, Wire wire) {
        inputWires[index].Add(wire);
        WiresChanged?.Invoke(true);
        OnWiresChanged(true);
        MakeDirty();
    }

    internal void AddOutputWire(int index, Wire wire) {
        outputWires[index].Add(wire);
        WiresChanged?.Invoke(true);
        OnWiresChanged(true);
        MakeDirty();
    }

    internal bool RemoveInputWire(int index, Wire wire) {
        var success = inputWires[index]?.Remove(wire) ?? false;
        if (success && !_destroying) {
            WiresChanged?.Invoke(false);
            OnWiresChanged(false);
            MakeDirty();
        }
        return success;
    }

    internal bool RemoveOutputWire(int index, Wire wire) {
        var success = outputWires[index]?.Remove(wire) ?? false;
        if (success && !_destroying) {
            WiresChanged?.Invoke(false);
            OnWiresChanged(false);
            MakeDirty();
        }

        return success;
    }

    public bool HasPin(Flow flow, PinIndex index) => GetPins(flow).Count > index;
    public Pin GetPin(Flow flow, PinIndex index) => GetPins(flow)[index];
    public bool TryGetPin(Flow flow, PinIndex index, out Pin pin) {
        if (!HasPin(flow, index)) {
            pin = default;
            return false;
        }

        pin = GetPins(flow)[index];
        return true;
    }

    public void SetPins((IList<Pin> Input, IList<Pin> Output) pins) {
        inputPins = [.. pins.Input];
        outputPins = [.. pins.Output];
        Resize();
        PinsChanged?.Invoke();
        OnPinsChanged();
    }

    public void AddPin(Flow flow, Pin pin, PinIndex? index = null) {
        var pins = GetPinsInternal(flow);
        if (pins.Count >= PinIndex.Invalid - 1) {
            throw new InvalidOperationException($"can't create more than {PinIndex.Invalid} pins");
        }

        var wires = GetWiresInternal(flow);
        if (index is PinIndex i) {
            pins.Insert(i, pin);
        } else {
            pins.Add(pin);
        }

        Resize();
        PinsChanged?.Invoke();
        OnPinsChanged();
    }

    public void RemovePin(Flow flow, PinIndex? index = null) {
        var pins = GetPinsInternal(flow);
        if (pins.Count == 0) {
            throw new InvalidOperationException($"there are no {flow} pins left");
        }

        var wires = GetWiresInternal(flow);
        PinIndex i = index ?? (PinIndex)(pins.Count - 1);
        pins.RemoveAt(i);
        foreach (var wire in wires[i]) {
            wire.Destroy();
        }
        wires.RemoveAt(i);

        if (flow == Flow.Input) {
            inputValues.RemoveAt(i);
        }

        PinsChanged?.Invoke();
        OnPinsChanged();
        MakeDirty();
    }

    public void ReplacePin(Flow flow, PinIndex index, Pin pin) {
        var pins = GetPinsInternal(flow);
        var oldPin = pins[index];
        pins[index] = pin; // bruh x3

        if (!oldPin.Type.IsAssignableTo(pin.Type)) {
            inputValues[index] = pin.CreateDefaultValue();
        }
        PinsChanged?.Invoke();
        OnPinsChanged();
        MakeDirty();
    }

    public void SetPinType(Flow flow, PinIndex index, Type type) =>
        ReplacePin(flow, index, GetPin(flow, index).WithType(type));


    public object GetInputValueOrNull(PinIndex index) {
        if (index >= inputPins.Count) {
            return null;
        }

        return Inputs[index];
    }

    public object GetInputValue(PinIndex index) {
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, inputPins.Count, nameof(index));
        return Inputs[index];
    }

    public T GetInputValue<T>(PinIndex index) => (T)GetInputValue(index);

    public void SetInputValue<T>(PinIndex index, T value) {
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, inputPins.Count, nameof(index));
        if (value != null) {
            if (!value.GetType().IsAssignableTo(inputPins[index].Type)) {
                throw new InvalidCastException($"{value} is not assignable to {DisplayInfo.ForGenericType(inputPins[index].Type).Name}");
            }
        } else {
            if (TypeLibrary.GetType(inputPins[index].Type).IsValueType) {
                throw new InvalidCastException($"{value} is not assignable to {DisplayInfo.ForGenericType(inputPins[index].Type).Name}");
            }
        }

        inputValues[index] = value;
        MakeDirty();
        InputsChanged?.Invoke();
        OnInputsChanged();
    }

    public virtual void SetProperty(string name, object obj) { }

    public object this[PinIndex index] {
        get => GetInputValue(index);
        set => SetInputValue(index, value);
    }

    private void Resize() {
        static HashSet<Wire> WireSetGenerator(int _) => [];
        static void WireSetDestructor(int _, HashSet<Wire> wireSet) {
            foreach (var wire in wireSet) {
                wire.Destroy();
            }
        }

        inputWires.Resize(inputPins.Count, WireSetGenerator, WireSetDestructor);
        outputWires.Resize(outputPins.Count, WireSetGenerator);
        inputValues.Resize(inputPins.Count, (index) => {
            if (NullInputs) {
                return null;
            }
            return inputPins[index].CreateDefaultValue();
        });
        MakeDirty();
    }

    private bool _destroying = false;
    protected override void Term() {
        _destroying = true;
        WiresChanged = null;
        foreach (var set in inputWires) {
            foreach (var wire in set) {
                wire.Destroy();
            }
        }

        foreach (var set in outputWires) {
            foreach (var wire in set) {
                wire.Destroy();
            }
        }


        Meta.Clear();
        base.Term();
    }

    ~Node() {
        Destroy();
    }
}