Source: graph/connections.js

import find from 'lodash/fp/find'
import map from 'lodash/fp/map'
import curry from 'lodash/fp/curry'
import merge from 'lodash/fp/merge'
import * as Edge from '../edge'
import { edgesDeep, edges } from './edge'
import { query } from '../location'
import { node, port, hasPort, parent } from '../graph/node'
import { kind } from '../port'
import { access, store } from './internal'

const nameNode = (op, node) => {
  if (typeof (node) === 'string') return op + '.' + node
  else if (node.id) return op + '.' + node.id
  else throw new Error('Cannot access cache for invalid node query: ' + node)
}

const namePort = (op, port) => {
  if (typeof (port) === 'string') return op + '.' + port
  else if (port.node && port.port) return op + '.' + port.port + '@' + port.node
  else throw new Error('Cannot access cache for invalid port query:' + port)
}

/**
 * @function
 * @name pointsTo
 * @description Checks whether an edge points to a given target.
 * @param {Location} target The target the edge should point to. This can either be a node or a port.
 * @param {PortGraph} graph The graph
 * @param {Edge} edge The edge to check
 * @returns {boolean} True if the edge points to the target, false otherwise.
 */
export const pointsTo = curry((target, graph, edge) => {
  try {
    if (edge.to.node) {
      if (target.id) return target.id === edge.to.node
      if (typeof (target) === 'string' && target[0] === '#') return edge.to.node === target
    }
    if (typeof (edge.to) === 'string') {
      if (target.id) return target.id === edge.to
      if (typeof (target) === 'string' && target[0] === '#') return edge.to === target
    }
    var q = query(target, graph)
    if (typeof (edge.to) === 'string') {
      return q(node(edge.to, graph))
    }
    return q(merge(edge.to, {additionalInfo: node(edge.to, graph)}))
  } catch (err) {
    return false
  }
})

/**
 * @function
 * @name isFrom
 * @description Checks whether an edge is from a given source.
 * @param {Location} target The source the edge should come from. This can either be a node or a port.
 * @param {PortGraph} graph The graph
 * @param {Edge} edge The edge to check
 * @returns {boolean} True if the edge comes from the source, false otherwise.
 */
export const isFrom = curry((source, graph, edge) => {
  try {
    if (edge.from.node) {
      if (source.id) return source.id === edge.from.node
      if (typeof (source) === 'string' && source[0] === '#') return edge.from.node === source
    }
    if (typeof (edge.from) === 'string') {
      if (source.id) return source.id === edge.from
      if (typeof (source) === 'string' && source[0] === '#') return edge.from === source
    }
    var q = query(source, graph)
    if (typeof (edge.from) === 'string') {
      return q(node(edge.from, graph))
    }
    return q(merge(edge.from, {additionalInfo: node(edge.from, graph)}))
  } catch (err) {
    return false
  }
})

/**
 * Checks whether the two nodes or ports are connected via an edge.
 * @param {Location} nodeFrom The starting point of our connection.
 * @param {Location} nodeTo The target of our connection.
 * @param {PortGraph} graph The graph in which we want to find the connection.
 * @returns {boolean} True if the graph has an edge going from "nodeFrom" to "nodeTo".
 */
export function areConnected (nodeFrom, nodeTo, graph) {
  return !!find((e) => isFrom(nodeFrom, graph, e) && pointsTo(nodeTo, graph, e), edgesDeep(graph))
}

/**
 * Returns a list of predecessors for a node or a port. Each node can only have exactly one
 * predecessor for every port but this function always returns a list.
 * @param {Location} target The target to which the predecessors point.
 * @param {PortGraph} graph The graph.
 * @param {Object} [config] Optional config object
 * @param {String[]} [config.layers=['dataflow']] Filter the edge based on the selected layers
 * @param {Boolean} [config.goIntoCompounds=false] Optional argument that activates looking for edges inside compounds.
 * If you specify a node as the location it will not go inside the node (if it is a compound) and look for
 * predecessors inside the compound.
 * @returns {Port[]} A list of ports with that are predecessors of `target`
 */
export function predecessors (target, graph, config = { layers: ['dataflow'], goIntoCompounds: false }) {
  return map('from')(inIncidents(target, graph, config))
}

/**
 * Returns the predecessors for a node or a port. Each node can only have exactly one
 * predecessor for every port.
 * @param {Location} target The target to which the predecessors points.
 * @param {PortGraph} graph The graph.
 * @param {Object} [config] Optional config object
 * @param {String[]} [config.layers=['dataflow']] Filter the edge based on the selected layers
 * @param {Boolean} [config.goIntoCompounds=false] Optional argument that activates looking for edges inside compounds.
 * If you specify a node as the location it will not go inside the node (if it is a compound) and look for
 * a predecessor inside the compound.
 * @returns {Port} The preceeding port
 */
export function predecessor (target, graph, config = { layers: ['dataflow'], goIntoCompounds: false }) {
  return predecessors(target, graph, config)[0]
}

/**
 * Get the dataflow predecessor of a node in the graph. This method works in O(1) when the cache is initialized.
 * @param {Port|String} source The target port as an ID or a node object.
 * @param {PortGraph} graph The graph
 * @returns {Port[]} The ports that preceedes the node.
 */
export function predecessorPort (targetPort, graph) {
  const name = namePort('predecessorPort', targetPort)
  return access(name, graph) || store(predecessor(targetPort, graph), name, graph)
}

/**
 * Get the dataflow predecessor of a node in the graph. This method works in O(1) when the cache is initialized.
 * @param {Node|String} source The target node as an ID or a node object.
 * @param {PortGraph} graph The graph
 * @returns {Port[]} The ports that preceedes the node.
 */
export function predecessorNode (targetNode, graph) {
  const name = nameNode('predecessorNode', targetNode)
  return access(name, graph) || store(predecessor(targetNode, graph), name, graph)
}

/**
 * Get the dataflow predecessors of a node in the graph. This method works in O(1) when the cache is initialized.
 * @param {Port|String} source The target port as an ID or a node object.
 * @param {PortGraph} graph The graph
 * @returns {Port[]} A list of ports that preceedes the node.
 */
export function predecessorsPort (targetPort, graph) {
  const name = namePort('predecessorsPort', targetPort)
  return access(name, graph) || store(predecessors(targetPort, graph), name, graph)
}

/**
 * Get the dataflow predecessors of a node in the graph. This method works in O(1) when the cache is initialized.
 * @param {Port|String} source The target port as an ID or a node object.
 * @param {PortGraph} graph The graph
 * @returns {Port[]} A list of ports that preceedes the node.
 */
export function predecessorsNodePort (target, graph) {
  if (target.id) return predecessorsNode(target, graph)
  if (target.port && target.node) return predecessorsPort(target, graph)
  if (typeof (target) === 'string') {
    if (target[0] === '#') return predecessorsNode(target, graph)
    else if (target.indexOf('@') !== -1) return predecessorsPort(target, graph)
    else return predecessors(target, graph)
  }
  throw new Error('PredecessorNodePort only works for nodes or ports. Given input: ' + JSON.stringify(target))
}

/**
 * Get the dataflow predecessors of a node in the graph. This method works in O(1) when the cache is initialized.
 * @param {Node|String} source The target node as an ID or a node object.
 * @param {PortGraph} graph The graph
 * @returns {Port[]} A list of ports that preceedes the node.
 */
export function predecessorsNode (targetNode, graph) {
  const name = nameNode('predecessorsNode', targetNode)
  return access(name, graph) || store(predecessors(targetNode, graph), name, graph)
}

/**
 * Gets all ingoing incident edges to a port
 * @param {Location} target The port to which the edges are incident. This is the target node or port of each edge.
 * @param {PortGraph} graph The graph
 * @param {Object} [config] Optional config object
 * @param {String[]} [config.layers=['dataflow']] Filter the edge based on the selected layers
 * @param {Boolean} [config.goIntoCompounds=false] Optional argument that activates looking for edges inside compounds.
 * If you specify a node as the location it will not go inside the node (if it is a compound) and look for
 * ingoing edges inside the compound.
 * @returns {Edge[]} An array of all ingoing (i.e. pointsTo(port)) incident edges.
 */
export function inIncidents (target, graph, { layers = ['dataflow'], goIntoCompounds = false } = {}) {
  const filterFn = (e) => pointsTo(target, graph, e) && layers.some(l => l === e.layer) &&
      (Edge.isBetweenPorts(e) || e.layer !== 'dataflow') &&
      (e.layer !== 'dataflow' || goIntoCompounds || hasPort(target, graph) || kind(port(e.to, graph)) === 'input')
  if (layers.length === 1 && layers[0] === 'dataflow' && !goIntoCompounds) {
    const n = node(target, graph)
    if (hasPort(target, graph)) {
      const p = port(target, graph)
      if (p.node === n.id && kind(p) === 'output') {
        return edges(n).filter(filterFn)
      }
    }
    return edges(parent(n, graph) || graph).filter(filterFn)
  }
  return edgesDeep(graph).filter(filterFn)
}

/**
 * Gets the ingoing incident edge to a port. Each port can only have one ingoing edge.
 * @param {Location} target The node or port to which the edge is incident. This is the target node or port of the edge.
 * @param {PortGraph} graph The graph
 * @param {Object} [config] Optional config object
 * @param {String[]} [config.layers=['dataflow']] Filter the edge based on the selected layers
 * @param {Boolean} [config.goIntoCompounds=false] Optional argument that activates looking for edges inside compounds.
 * If you specify a node as the location it will not go inside the node (if it is a compound) and look for
 * ingoing edges inside the compound.
 * @returns {Edge} The ingoing incident edge.
 */
export function inIncident (target, graph, config = { layers: ['dataflow'], goIntoCompounds: false }) {
  return inIncidents(target, graph, config)[0]
}

/**
 * Gets all outgoing incident edges to a port. The given port or node is the source of each edge.
 * @param {Location} source The port from which the edge comes. This is the source node or port of the edge.
 * @param {PortGraph} graph The graph
 * @param {Object} [config] Optional config object
 * @param {String[]} [config.layers=['dataflow']] Filter the edge based on the selected layers
 * @param {Boolean} [config.goIntoCompounds=false] Optional argument that activates looking for edges inside compounds.
 * If you specify a node as the location it will not go inside the node (if it is a compound) and look for
 * outgoing edges inside the compound.
 * @returns {Edge[]} An array of all outgoing (i.e. isFrom(port)) incident edges.
 */
export function outIncidents (source, graph, { layers = ['dataflow'], goIntoCompounds = false } = {}) {
  const filterFn = (e) => {
    return isFrom(source, graph, e) && layers.some(l => l === e.layer) && (Edge.isBetweenPorts(e) || e.layer !== 'dataflow') &&
      (e.layer !== 'dataflow' || goIntoCompounds || hasPort(source, graph) || kind(port(e.from, graph)) === 'output')
  }
  if (layers.length === 1 && layers[0] === 'dataflow' && !goIntoCompounds) {
    const n = node(source, graph)
    if (hasPort(source, graph)) {
      const p = port(source, graph)
      if (p.node === n.id && kind(p) === 'input') {
        return edges(n).filter(filterFn)
      }
    }
    const e = edges(parent(n, graph) || graph).filter(filterFn)
    return e
  }
  return edgesDeep(graph).filter(filterFn)
}

/**
 * Get the successors of one node in the graph, optionally for a specific port.
 * @param {Location} source The source from which to follow the edges.
 * @param {PortGraph} graph The graph.
 * @param {Object} [config] Optional config object
 * @param {String[]} [config.layers=['dataflow']] Filter the edge based on the selected layers
 * @param {Boolean} [config.goIntoCompounds=false] Optional argument that activates looking for edges inside compounds.
 * If you specify a node as the location it will not go inside the node (if it is a compound) and look for
 * successors inside the compound.
 * @returns {Port[]} A list of ports that succeed the node.
 */
export function successors (source, graph, config = { layers: ['dataflow'], goIntoCompounds: false }) {
  return map('to')(outIncidents(source, graph, config))
}

/**
 * Get the dataflow successor of a node in the graph. This method works in O(1) when the cache is initialized.
 * @param {Port|String} source The target port as an ID or a node object.
 * @param {PortGraph} graph The graph
 * @returns {Port[]} A list of ports that succeed the node.
 */
export function successorsPort (targetPort, graph) {
  const name = namePort('successorsPort', targetPort)
  return access(name, graph) || store(successors(targetPort, graph), name, graph)
}

function updateArray (name, value, graph) {
  var v = access(name, graph)
  if (v) {
    store(v.push(value), name, graph)
  } else {
    store([value], name, graph)
  }
}

export function cacheConnections (graph) {
  const edges = edgesDeep(graph)
  for (const e of edges) {
    if (e.layer !== 'dataflow') continue
    updateArray(nameNode('successorsNode', e.from.node), e.to, graph)
    updateArray(nameNode('predecessorsNode', e.to.node), e.from, graph)
  }
}

/**
 * Get the dataflow successor of a node in the graph. This method works in O(1) when the cache is initialized.
 * @param {Node|String} source The source node as an ID or a node object.
 * @param {PortGraph} graph The graph
 * @returns {Port[]} A list of ports that succeed the node.
 */
export function successorsNode (sourceNode, graph) {
  const name = nameNode('successorsNode', sourceNode)
  return access(name, graph) || store(successors(sourceNode, graph), name, graph)
}

function or (fn1, fn2) {
  return (v) => fn1(v) || fn2(v)
}

/**
 * @function
 * @name incidents
 * @description All incident edges to the given input.
 * @param {Location} loc The node or port.
 * @param {PortGraph} graph The graph
 * @returns {Edge[]} An array of all incident edges to the given location.
 */
export const incidents = curry((loc, graph) => {
  return edgesDeep(graph).filter(or(isFrom(loc, graph), pointsTo(loc, graph)))
})