using NodeDev.Core.Connections; using NodeDev.Core.NodeDecorations; using NodeDev.Core.Types; using System.Linq.Expressions; namespace NodeDev.Core.Nodes { [System.Diagnostics.DebuggerDisplay("{Name}. Inputs: {Inputs.Count}. Outputs: {Outputs.Count}")] public abstract class Node { public Node(Graph graph, string? id = null) { Graph = graph; Id = id ?? (Guid.NewGuid().ToString()); } public string Id { get; } public virtual string Name { get; set; } = ""; public virtual bool AllowEditingName { get; } = false; public Graph Graph { get; } public abstract string TitleColor { get; } public List Inputs { get; } = []; public List Outputs { get; } = []; public IEnumerable InputsAndOutputs => Inputs.Concat(Outputs); public abstract bool IsFlowNode { get; } public TypeFactory TypeFactory => Project.TypeFactory; public Project Project => Graph.SelfClass.Project; public virtual bool FetchState => false; public virtual bool ReOrderExecInputsAndOutputs => true; public bool CanBeInlined => !InputsAndOutputs.Any(x => x.Type.IsExec); /// /// True to allow remerging exec connection together later in the graph. /// This is used by nodes that have multiple exec outputs such as Branch, Loop, etc. /// If the value is false, such as for Loop, each exec output has to be a separate path. /// Ex, for ForEach node the value is false since "Loop Exec" path cannot have shared path with the "ExecOut" path. /// public virtual bool AllowRemergingExecConnections => true; /// /// Global index of this node in the graph. Each node in a graph has a unique index. /// public int GraphIndex { get; set; } = -1; public virtual IEnumerable GetUndefinedGenericTypes() => InputsAndOutputs.SelectMany(x => x.Type.GetUndefinedGenericTypes()).Distinct(); /// /// Called before the generic type are set on every connections /// public virtual void OnBeforeGenericTypeDefined(IReadOnlyDictionary changedGenerics) { } public record class AlternateOverload(TypeBase ReturnType, List Parameters); public virtual IEnumerable AlternatesOverloads => []; /// /// Triggered when the generic type of the connection has been defined (found and applied already). /// /// The connection that was generic, it is not generic anymore /// Returns a list of changed connections, if any. public virtual List GenericConnectionTypeDefined(Connection connection) { return []; } public virtual void SelectOverload(AlternateOverload overload, out List newConnections, out List removedConnections) { throw new NotImplementedException(); } internal virtual Expression BuildExpression(Dictionary? subChunks, BuildExpressionInfo info) => throw new NotImplementedException(); internal virtual void BuildInlineExpression(BuildExpressionInfo info) => throw new NotImplementedException(); /// /// Create an Expression node that can be used in the graph. /// Ie, the "Add" node will have two local variables, one for each input, and one output local variable. /// In C#, that would look like doing : /// var input1 = ..., input2 = ...; // ... would be replaced by the output local variable of the previous node /// var output = input1 + input2; /// /// This will ignore the exec connections. /// internal virtual IEnumerable<(Connection Connection, ParameterExpression LocalVariable)> CreateOutputsLocalVariableExpressions(BuildExpressionInfo info) { foreach (var output in Outputs) { if (output.Type.IsExec) continue; var variable = Expression.Variable(output.Type.MakeRealType(), $"{Name}_{output.Name}" .Replace(" ", string.Empty) .Replace(".", string.Empty) .Replace("<", string.Empty) .Replace(">", string.Empty)); yield return (output, variable); } } #region Path merging / Crossing examinations public abstract string GetExecOutputPathId(string pathId, Connection execOutput); public abstract bool DoesOutputPathAllowDeadEnd(Connection execOutput); public abstract bool DoesOutputPathAllowMerge(Connection execOutput); /// /// Returns true if this node breaks a dead end. These are usually "Return", "Break", "Continue", etc. /// This will allow a dead end in places where it shouldn't be allowed, such as a "Branch" node. /// public virtual bool BreaksDeadEnd => false; public class InfiniteLoopException(Node node) : Exception { public Node Node { get; } = node; } /// /// Returns all the possible execution path from this node. /// I know, I know. All of this is slow. /// public NodePaths SearchAllExecPaths(HashSet nodesBefore) { if (nodesBefore.Contains(this)) throw new InfiniteLoopException(this); var execOutputs = Outputs.Where(x => x.Type.IsExec).ToList(); if (execOutputs.Count == 0) return new(); // Prevent any children path from looping back to us nodesBefore.Add(this); var allPaths = new NodePaths(); foreach (var execOutput in execOutputs) { // Create a copy of the nodes already used since each execOutput can possibly hit the same nodes without it being an issue var nodesBefore_local = new HashSet(nodesBefore); var subPaths = SearchAllExecPaths(execOutput, nodesBefore_local); if (subPaths.CountPossiblePaths != 0) // it led somewhere { // Add the execOutput to the beginning of each path subPaths.PrependPath(new([execOutput])); } else // it led nowhere, the path is just the execOutput and that's it subPaths.AddNewIndependantBranch(new NodePath([execOutput])); allPaths.AddNewIndependantBranch(subPaths); } return allPaths; } /// /// List all the possible path taken from the output exec connection. /// private static NodePaths SearchAllExecPaths(Connection outputExec, HashSet alreadySeenNodes) { if (outputExec.Connections.Count == 0) return new(); // we're connected to nothing // If we are going through NormalFlowNodes, we can simply accumulate the connections one after the other // Then, we multiple paths offers, we can search those paths and Prepend the straightConnections to them var straightConnections = new NodePath(); var inputExec = outputExec.Connections.FirstOrDefault(); while (inputExec != null) { var otherNode = inputExec.Parent; if (otherNode is NormalFlowNode) { alreadySeenNodes.Add(otherNode); // used to prevent any children path from looping back to us var otherExec = otherNode.Outputs.Single(x => x.Type.IsExec); straightConnections.AppendPath(otherExec); inputExec = otherExec.Connections.FirstOrDefault(); } else { // We've hit a possible separation in the paths we can follow // We can recursively start a search again. // There is no need to add the current node to the alreadySeenNodes since the SearchAllExecPaths will do it for us var allSubPaths = otherNode.SearchAllExecPaths(alreadySeenNodes); // Append each path returned to the straightConnections allSubPaths.PrependPath(straightConnections); return allSubPaths; } } // if we're here, it means we never hit any branching of paths, we can return a single path possible if (straightConnections.Length == 0) return new(); // we're connected to nothing else return new NodePaths(straightConnections); } #endregion #region Decorations public Dictionary Decorations { get; init; } = []; public void AddDecoration(T attribute) where T : INodeDecoration => Decorations[typeof(T)] = attribute; public T GetOrAddDecoration(Func creator) where T : INodeDecoration { if (Decorations.TryGetValue(typeof(T), out var decoration)) return (T)decoration; var v = creator(); Decorations[typeof(T)] = v; return v; } #endregion #region Serialization public record SerializedNode(string Type, string Id, string Name, List Inputs, List Outputs, Dictionary Decorations); internal SerializedNode Serialize() { var serializedNode = new SerializedNode(GetType().FullName!, Id, Name, Inputs.Select(x => x.Serialize()).ToList(), Outputs.Select(x => x.Serialize()).ToList(), Decorations.ToDictionary(x => x.Key.FullName!, x => x.Value.Serialize())); return serializedNode; } internal static Node Deserialize(Graph graph, SerializedNode serializedNodeObj) { var type = TypeFactory.GetTypeByFullName(serializedNodeObj.Type) ?? throw new Exception($"Unable to find type: {serializedNodeObj.Type}"); var node = (Node?)Activator.CreateInstance(type, graph, serializedNodeObj.Id) ?? throw new Exception($"Unable to create instance of type: {serializedNodeObj.Type}"); foreach (var decoration in serializedNodeObj.Decorations) { var decorationType = TypeFactory.GetTypeByFullName(decoration.Key) ?? throw new Exception($"Unable to find type: {decoration.Key}"); var method = decorationType.GetMethod(nameof(INodeDecoration.Deserialize), System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); if (method == null) throw new Exception($"Unable to find Deserialize method on type: {decoration.Key}"); if (method.Invoke(null, [graph.SelfClass.TypeFactory, decoration.Value]) is not INodeDecoration decorationObj) throw new Exception($"Unable to deserialize decoration: {decoration.Key}"); node.Decorations[decorationType] = decorationObj; } node.Deserialize(serializedNodeObj); return node; } protected virtual void Deserialize(SerializedNode serializedNodeObj) { Inputs.Clear(); Outputs.Clear(); Name = serializedNodeObj.Name; foreach (var input in serializedNodeObj.Inputs) { var connection = Connection.Deserialize(this, input, true); Inputs.Add(connection); } foreach (var output in serializedNodeObj.Outputs) { var connection = Connection.Deserialize(this, output, false); Outputs.Add(connection); } } #endregion } }