Source: graph/node.js


import curry from 'lodash/fp/curry'
import merge from 'lodash/fp/merge'
import omit from 'lodash/fp/omit'
import {isRoot, rest as pathRest, base as pathBase, parent as pathParent, relativeTo, equal, join} from '../compoundPath'
import {normalize as normalizePort, portName} from '../port'
import * as Node from '../node'
import * as changeSet from '../changeSet'
import {assertGraph} from '../assert'
import {flow, flowCallback, Let, sequential} from './flow'
import {nodeBy, mergeNodes, rePath, addNodeInternal, unID, nodesDeep, access, store, forget} from './internal'
import {query, toString} from '../location'
import {incidents, isFrom, pointsTo} from './connections'
import {createNode} from '../component'
import {removeEdge, realizeEdgesForNode} from './edge'

/**
 * @function
 * @name nodes
 * @description Returns a list of nodes on the root level.
 * @param {PortGraph} graph The graph.
 * @returns {Nodes[]} A list of nodes.
 */
export const nodes = (graph) => {
  return graph.nodes || []
}

/**
 * @function
 * @name nodesBy
 * @description Returns a list of nodes on the root level selected by a given predicate.
 * @param {function|Location} predicate A function that filters nodes. Or alternatively you can use a location query.
 * @param {PortGraph} graph The graph.
 * @returns {Nodes[]} A list of nodes.
 * @example <caption>Select by function</caption>
 * // all nodes that have the name select
 * nodesBy((node) => node.name === 'select', graph)
 * @example <caption>Select by location query</caption>
 * // selects all if components in the current layer.
 * nodesBy('/if', graph)
 */
export const nodesBy = curry((predicate, graph) => {
  if (typeof (predicate) !== 'function') {
    return nodes(graph).filter(query(predicate, graph))
  }
  return nodes(graph).filter(predicate)
})

/**
 * Get all nodes at all depths. It will go into every compound node / lambda node and return their nodes
 * and the nodes of their compound nodes, etc.
 * @param {PortGraph} graph The graph to work on
 * @returns {Node[]} A list of nodes.
 */
export {nodesDeep}

/**
 * @function
 * @name nodesDeepBy
 * @description Get all nodes at all depths that fulfill the given predicate. It will go into every compound node
 * and return their nodes and the nodes of their compound nodes, etc.
 * @param {function|Location} predicate A function that filters nodes. Or alternatively you can use a location query.
 * @param {PortGraph} graph The graph to work on
 * @returns {Node[]} A list of nodes that fulfill the predicate.
 */
export const nodesDeepBy = curry((predicate, graph) => {
  if (typeof (predicate) !== 'function') {
    return nodesDeep(graph).filter(query(predicate, graph))
  }
  return nodesDeep(graph).filter(predicate)
})

/**
 * Returns a list of node names. [Performance O(|V|)]
 * @param {PortGraph} graph The graph.
 * @returns {string[]} A list of node names.
 */
export function nodeNames (graph) {
  return nodes(graph).map(Node.id)
}

/**
 * @function
 * @name node
 * @description Returns the node at the given location. [Performance O(|V|)]
 * @param {Location} loc A location identifying the node.
 * @param {PortGraph} graph The graph.
 * @returns {Node} The node in the graph
 * @throws {Error} If the queried node does not exist in the graph.
 */
export const node = (loc, graph) => {
  try {
    var node
    if (Array.isArray(loc) && loc.length === 0) return graph
    if (loc.id || loc.node || (Array.isArray(loc) && loc[loc.length - 1][0] === '#') || (typeof (loc) === 'string' && loc[0] === '#')) {
      const id = loc.id || loc.node || (Array.isArray(loc) ? loc[loc.length - 1] : loc)
      node = (access(id, graph)) || store(nodeBy(query(loc, graph), graph), id, graph)
    } else {
      node = nodeBy(query(loc, graph), graph)
    }
  } catch (err) {
    throw new Error(`Node: '${Node.id(loc) || JSON.stringify(loc)}' does not exist in the graph.`)
  }
  if (!node) {
    throw new Error(`Node: '${Node.id(loc) || JSON.stringify(loc)}' does not exist in the graph.`)
  }
  return node
}

/**
 * @function
 * @name port
 * @description Returns a port specified by the short notation or a port query object. [Performance O(|V|)]
 * @param {Port} port A port object or a short notation for a port.
 * @param {PortGraph} graph The graph.
 * @returns {Node} The actual port object with type information.
 * @throws {Error} If the queried port does not exist in the graph.
 */
export const port = (p, graph) => {
  var nodeObj = node(p, graph)
  return Node.port(normalizePort(p), nodeObj)
}

export const hasPort = (port, graph) => {
  if (!hasNode(port, graph)) return false
  var nodeObj = node(port, graph)
  return Node.hasPort(normalizePort(port), nodeObj)
}

/**
 * @function
 * @name hasNode
 * @description Checks whether the graph has a node. [Performance O(|V|)]
 * @param {Location} loc A location identifying the node
 * @param {PortGraph} graph The graph.
 * @returns {boolean} True if the graph has a node with the given id, false otherwise.
 */
export const hasNode = (loc, graph) => {
  return !!nodeBy(query(loc, graph), graph)
}

export function checkNode (graph, nodeToCheck) {
  const parent = node(pathParent(nodeToCheck.path), graph)
  if (hasNode(unID(nodeToCheck), graph) && !equal(node(unID(nodeToCheck), graph).path, graph.path) && !Node.equal(nodeToCheck, graph) && (equal(pathParent(node(unID(nodeToCheck), graph).path), parent.path))) {
    throw new Error('Cannot add already existing node: ' + Node.name(nodeToCheck))
  }
  if (!nodeToCheck) {
    throw new Error('Cannot add undefined node to graph.')
  } else if (!Node.isValid(nodeToCheck)) {
    throw new Error('Cannot add invalid node to graph. Are you missing the id or a port?\nNode: ' + JSON.stringify(nodeToCheck))
  } else {
    if (Node.hasName(nodeToCheck) && hasNode(Node.name(nodeToCheck), parent) && !Node.equal(unID(nodeToCheck), parent)) {
      throw new Error('Cannot add a node if the name is already used. Names must be unique in every compound. Tried to add node: ' + JSON.stringify(nodeToCheck))
    }
  }
}

/**
 * @function
 * @name addNodeByPath
 * @description Add a node at a specific path.
 * @param {CompoundPath} parentPath A compound path identifying the location in the compound graph.
 * @param {Node} node The node to add to the graph.
 * @param {PortGraph} graph The graph that is the root for the nodePath
 * @returns {PortGraph} A new graph that contains the node at the specific path.
 */
const addNodeByPath = curry((parentPath, nodeData, graph, ...cbs) => {
  const cb = flowCallback(cbs)
  var newNode
  var newGraph
  if (isRoot(parentPath) || graph.inplace) {
    newGraph = addNodeInternal(nodeData, graph, join(graph.path, parentPath), checkNode, (n, g) => { newNode = n; return g })
  } else {
    let parentGraph = node(parentPath, graph)
    newGraph = replaceNode(parentPath, addNodeInternal(nodeData, parentGraph, parentGraph.path, checkNode, (n, g) => { newNode = n; return g }), graph)
  }
  return cb(newNode, newGraph)
})

/**
 * @function
 * @name addNodeIn
 * @description Add a node in a given compound node.
 * @param {Location} parentLoc A location identifying the parent for the new node.
 * @param {Node} node The node to add to the graph.
 * @param {PortGraph} graph The graph
 * @param {Callback} contextCallback A context-callback that is called after the new node was inserted. It
 * has the signature `Node x Graph -> Graph`. It will get the newly inserted node and the graph (that
 * has this node). And it must return a graph (the graph will be the return value of this function).
 * @returns {PortGraph} A new graph that contains the node as child of `parentLoc`.
 * @example <caption>Printing the id of the newly inserted node</caption>
 * Graph.addNodeIn(parent, {...}, graph, (newNode, graph) => {
 *   console.log(Node.id(newNode))
 *   return graph
 * })
 */
export const addNodeIn = curry((parentLoc, nodeData, graph, ...cbs) => {
  if (Node.isAtomic(node(parentLoc, graph))) {
    throw new Error('Cannot add Node to atomic node at: ' + graph.path)
  }
  return addNodeByPath(Node.path(node(parentLoc, graph)), createNode({}, nodeData), graph, ...cbs)
})

/**
 * @function
 * @name addNode
 * @description Add a node to the graph (at the root level), returns a new graph. [Performance O(|V| + |E|)]
 * @param {Node} node The node object that should be added. If the node already exists in the graph it will be copied.
 *   The node object must contain at least one valid ports. This functions checks if the node has ports AND if
 *   every port is a valid port (i.e. has a name as `port` and the port type (output/input) as `kind`).
 * @param {PortGraph} graph The graph.
 * @param {Callback} contextCallback A context-callback that is called after the new node was inserted. It
 * has the signature `Node x Graph -> Graph`. It will get the newly inserted node and the graph (that
 * has this node). And it must return a graph (the graph will be the return value of this function).
 * @returns {PortGraph} A new graph that includes the node.
 * @example <caption>Inserting nodes and connecting them</caption>
 * Graph.Let(
 *   [
 *     Graph.addNode({ports: [{port: 'out', kind: 'output', type: 'Number'}]}),
 *     Graph.addNode({ports: [{port: 'in', kind: 'output', type: 'Number'}]})
 *   ],
 *   ([node1, node2], graph) =>
 *     Graph.addEdge({from: Node.port('out', node1), to: Node.port('in', node2)}, graph))
 * @example <caption>Printing the id of the newly inserted node</caption>
 * Graph.addNode({...}, graph, (newNode, graph) => {
 *   console.log(Node.id(newNode))
 *   return graph
 * })
 */
export const addNode = curry((node, graph, ...cbs) => {
  assertGraph(graph, 2, 'addNode')
  if (Node.isAtomic(graph)) {
    throw new Error('Cannot add Node to atomic node at: ' + graph.path)
  }
  return addNodeInternal(createNode({}, node), graph, graph.path, checkNode, ...cbs)
})

export const addNodeWithID = curry((node, graph, ...cbs) => {
  assertGraph(graph, 2, 'addNode')
  return sequential([
    addNode(node),
    mergeNodes({id: node.id})
  ])(graph, ...cbs)
})

/**
 * @function
 * @name set
 * @description Sets properties for node.
 * @example
 * var graph = ...
 * var newGraph = Graph.set({property: value}, '#nodeIDOrLocation', graph)
 * // you can get the value via get in the newGraph
 * var propertyValue = Graph.get('property', '#nodeIDOrLocation', graph)
 * @param {Object} value The properties to set, e.g. `{recursion: true, recursiveRoot: true}`
 * @param {Location} loc The location identifying the node in which the property should be changed.
 * @param {PortGraph} graph The graph
 * @returns {PortGraph} A graph in which the change is realized.
 */
export const set = curry((value, loc, graph) => {
  assertGraph(graph, 3, 'set')
  var nodeObj = node(loc, graph)
  return replaceNode(nodeObj, Node.set(value, nodeObj), graph)
})

/**
 * @function
 * @name addNodeTuple
 * @description Add a node an return an array of the graph and id.
 * @param {Node} node The node object that should be added. If the node already exists in the graph it will be copied.
 * @param {PortGraph} graph The graph.
 * @returns {PortGraph} A new graph that includes the node and the id as an array in [graph, id].
 */
export const addNodeTuple = curry((node, graph) => {
  assertGraph(graph, 2, 'addNodeTuple')
  var id
  var newGraph = flow(
    Let(addNode(node), (node, graph) => {
      id = Node.id(node)
      return graph
    })
  )(graph)
  return [newGraph, id]
})

/**
 * @function
 * @name get
 * @description Get a property of a node.
 * @example
 * * var graph = ...
 * var newGraph = Graph.set({property: value}, '#nodeIDOrLocation', graph)
 * // you can get the value via get in the newGraph
 * var propertyValue = Graph.get('property', '#nodeIDOrLocation', graph)
 * @param {String} key The key of the property like 'recursion'
 * @param {Location} loc The location identifying the node for which the property is queried.
 * @param {PortGraph} graph The graph.
 * @returns The value of the property or undefined if the property does not exist in the node.
 */
export const get = curry((key, nodeQuery, graph) => Node.get(key, node(nodeQuery, graph)))

const removeNodeInternal = curry((query, deleteEdges, graph, ...cbs) => {
  const cb = flowCallback(cbs)
  var remNode = node(query, graph)
  var path = relativeTo(remNode.path, graph.path)
  var basePath = pathBase(path)
  if (basePath.length === 0) {
    var remEdgesGraph = graph
    if (deleteEdges) {
      var inc = incidents(path, graph)
      remEdgesGraph = inc.reduce((curGraph, edge) => removeEdge(edge, curGraph), graph)
    }
    return cb(remNode, changeSet.applyChangeSet(remEdgesGraph, changeSet.removeNode(remNode.id)))
  }
  var parentGraph = node(basePath, graph)
  // remove node in its compound and replace the graphs on the path
  return Let(removeNodeInternal(pathRest(path), deleteEdges), (remNode, newSubGraph) =>
    cb(remNode, replaceNode(basePath, newSubGraph, graph)))(parentGraph)
})

function removeNodeInternalInplace (query, deleteEdges, graph, ...cbs) {
  const cb = flowCallback(cbs)
  var remNode = node(query, graph)
  var parentNode = parent(remNode, graph)
  var idx = parentNode.nodes.findIndex((v) => Node.equal(remNode, v))
  if (deleteEdges) {
    incidents(remNode, graph)
      .forEach((e) => {
        var remEdges = parentNode.edges
          .map((e, eIdx) => (isFrom(remNode, graph, e) || pointsTo(remNode, graph, e)) ? eIdx : -1)
          .filter((index) => index !== -1)
        for (var i = remEdges.length - 1; i >= 0; i--) {
          parentNode.edges.splice(remEdges[i], 1)
        }
      })
    forget('edges', parentNode)
    forget('edgesDeep', parentNode)
    forget('edgesDeep', graph)
  }
  parentNode.nodes.splice(idx, 1)
  forget('nodes', parent)
  forget('nodesDeep', parent)
  forget('nodesDeep', graph)
  forget(remNode.id, graph)
  return cb(remNode, graph)
}

/**
 * @function
 * @name removeNode
 * @description Removes a node from the graph. [Performance O(|V| + |E|)]
 * @param {Location} loc The location identifying the node to delete.
 * @param {PortGraph} graph The graph.
 * @returns {PortGraph} A new graph without the given node.
 */
export const removeNode = curry((loc, graph, ...cbs) => {
  assertGraph(graph, 2, 'removeNode')
  if (parent(loc, graph) && Node.isAtomic(parent(loc, graph))) {
    throw new Error('Cannot remove child nodes of an atomic node. Tried deleting : ' + loc)
  }
  if (graph.inplace) {
    return removeNodeInternalInplace(loc, true, graph, ...cbs)
  } else {
    return removeNodeInternal(loc, true, graph, ...cbs)
  }
})

function nodeParentPath (path, graph) {
  /* if (isCompoundPath(path)) {
    return pathParent(path)
  } else { */
  return relativeTo(pathParent(node(path, graph).path), graph.path)
  // }
}

/**
 * @function
 * @name replaceNode
 * @description Replace a node in the graph with another one. It tries to keep all edges.
 * @param {Location} loc A location specifying the node to replace
 * @param {Node} newNode The new node that replaces the old one.
 * @param {PortGraph} graph The graph
 * @returns {PortGraph} A new graph in which the old node was replaces by the new one.
 */
export const replaceNode = curry((loc, newNode, graph) => {
  assertGraph(graph, 3, 'replaceNode')
  var preNode = node(loc, graph)
  if (equal(preNode.path, graph.path)) return newNode
  if ((!newNode.id || newNode.id === preNode.id) && graph.inplace) {
    const g = flow(
      mergeNodes(preNode, Object.assign({id: preNode.id}, newNode)),
      rePath,
      (Node.isReference(preNode) && !Node.isReference(newNode)) ? realizeEdgesForNode(loc) : (graph) => graph,
      {name: '[replaceNode] For location ' + toString(loc)}
    )(graph)
    return g
  }
  delete graph.inplace
  const newGraph = flow(
    Let(
        [removeNodeInternal(loc, false), addNodeByPath(nodeParentPath(loc, graph), newNode)],
        ([removedNode, insertedNode], graph) => mergeNodes(removedNode, insertedNode, graph)),
    rePath,
    (Node.isReference(preNode) && !Node.isReference(newNode)) ? realizeEdgesForNode(loc) : (graph) => graph,
    {name: '[replaceNode] For location ' + toString(loc)}
  )(graph)
  return newGraph
})

/**
 * @function
 * @name setPortName
 * @description Updates a port of a node.
 * @param {Location} loc A location specifying the node to update.
 * @param {port} port The port name or its index.
 * @param {Port} portUpdate The new port object or parts of the new object (it will merge with the existing values).
 * @param {PortGraph} graph The graph
 * @returns {PortGraph} A new graph in which the port has been updated.
 * @throws {Error} If the location does not specify a node in the graph.
 */
export const setNodePort = curry((loc, port, portUpdate, graph) => {
  var nodeObj = node(loc, graph)
  return replaceNode(loc, Node.setPort(nodeObj, port, portUpdate), graph)
})

/**
 * @function
 * @name parent
 * @description Gets the parent of a node.
 * @param {Location} loc A location identifying the node whose parent is wanted.
 * @param {PortGraph} graph The graph.
 * @returns {Node} The node id of the parent node or undefined if the node has no parent.
 */
export const parent = curry((loc, graph) => {
  if (equal(node(loc, graph).path, graph.path)) {
    // parent points to a node not accessible from this graph (or loc is the root of the whole graph)
    return
  }
  return node(pathParent(relativeTo(node(loc, graph).path, graph.path)), graph)
})

/**
 * Replaces a port of a node in a graph
 * @param {Port} oldPort Port object to be replaced
 * @param {Port} newPort Port object to replace with. It will update the old port. Old attributes will not be overwritten.
 * @return {Portgraph} Updated graph with oldPort replaced by newPort
 */
export function replacePort (oldPort, newPort, graph) {
  const nodeObj = node(oldPort, graph)
  const newNode = merge(omit('ports', nodeObj), {ports: Node.ports(nodeObj)
    .map((port) =>
      (portName(port) === portName(oldPort))
      ? Object.assign({}, port, newPort)
      : port)
  })
  return replaceNode(nodeObj, newNode, graph)
}