Source: node.js

/**
 * Accessible via `require('@buggyorg/graphtools').Node`
 * @module Node */

import curry from 'lodash/fp/curry'
import merge from 'lodash/fp/merge'
import find from 'lodash/fp/find'
import has from 'lodash/fp/has'
import every from 'lodash/fp/every'
import zip from 'lodash/fp/zip'
import * as Port from './port'
import cuid from 'cuid'
import {node as pathNode, isCompoundPath, equal as pathEqual, parent} from './compoundPath'

const newID = (process.env.NODE_IDS) ? (() => { var cnt = 0; return () => 'node_' + cnt++ })() : cuid

/**
 * Creates a normalized node object. It makes sure, that the node has all necessary information like an id
 * and normalized ports.
 * @param {Node} node A protypical node object.
 * @returns {Node} A complete node object
 */
export function create (node) {
  if (node.id) {
    throw new Error('You cannot explicitly assign an id for a node. Use the name field for node addressing')
  }
  var newNode = merge(node, {id: '#' + newID(), metaInformation: {}, settings: merge({}, node.settings), ports: (node.ports) ? node.ports.map(Port.normalize) : []})
  if (!isReference(newNode) && !isValid(newNode)) {
    throw new Error('Cannot create invalid node: ' + JSON.stringify(node))
  }
  return newNode
}

/**
 * Checks if the given object is an id.
 * @param obj The object to test
 * @returns {boolean} True if the object is an id, false otherwise.
 */
export function isID (str) {
  return typeof (str) === 'string' && (str[0] === '#')
}

/**
 * Returns the unique identifier of a node. The id is unique for the whole graph and cannot be assigned twice.
 * @params {Node} node The node
 * @returns {string} The unique identifier of the node
 * @throws {Error} If the node value is invalid.
 */
export function id (node) {
  if (typeof (node) === 'string') {
    return node
  } else if (node == null) {
    throw new Error('Cannot determine id of undefined node.')
  }
  return node.id
}

/**
 * Gets the name of a node. The name is a unique identifier in respect to the parent. Each graph
 * can have only one node with a specific name as its direct child. If a node has no name, the
 * id is the name of the node.
 * @params {Node} node The node
 * @returns {string} The name of the node.
 */
export function name (node) {
  if (typeof (node) === 'string') {
    return node
  } else if (node.name) {
    return node.name
  } else if (isCompoundPath(node)) {
    return pathNode(node)[0]
  } else {
    return node.id
  }
}

/**
 * Checks the node for a name
 * @param {Node} node The node
 * @returns {boolean} True if it has a name, false otherwise.
 */
export function hasName (node) {
  return !!node.name
}

/**
 * Checks the node for a path
 * @param {Node} node The node
 * @returns {boolean} True if it has a path, false otherwise.
 */
export function hasPath (node) {
  return !!node.path
}

/**
 * Checks the node for an id
 * @param {Node} node The node
 * @returns {boolean} True if it has an id, false otherwise.
 */
export function hasID (node) {
  return !!node.id
}

/**
 * Checks the node for children
 * @param {Node} node The node
 * @returns {boolean} True if it has children, false otherwise.
 */
export function hasChildren (node) {
  return !get('hideChildren', node) && Array.isArray(node.nodes)
}

/**
 * @function
 * @name equal
 * @description Tests whether two nodes are the same node. This tests only if their IDs are
 * the same not if both nodes contain the same information.
 * @param {Node} node1 One of the nodes to test.
 * @param {Node} node2 The other one.
 * @returns {boolean} True if they have the same id, false otherwise.
 */
export const equal = curry((node1, node2) => {
  if (((isValid(node1) && (hasID(node1) || !isReference(node1))) || isID(node1)) &&
      ((isValid(node2) && (hasID(node2) || !isReference(node2))) || isID(node2))) {
    return id(node1) && id(node2) && id(node1) === id(node2)
  } else if (Port.isPort(node1)) {
    return equal(Port.node(node1), node2)
  } else if (Port.isPort(node2)) {
    return equal(node1, Port.node(node2))
  } else if (hasPath(node1) && hasPath(node2)) {
    return pathEqual(parent(node1), parent(node2)) && name(node1) === name(node2)
  } else {
    return name(node1) === name(node2)
  }
})

export const isomorph = curry((node1, node2) => {
  if (!zip(ports(node1), ports(node2)).every(([p1, p2]) => Port.isomorph(p1, p2))) {
    return false
  }
  if (isAtomic(node1)) {
    return isAtomic(node2) && component(node1) === component(node2)
  } else {
    return !isAtomic(node2)
  }
})

/**
 * Gets all ports of the node.
 * @param {Node} node The node.
 * @returns {Port[]} A list of ports.
 */
export function ports (node) {
  return (node.ports) ? node.ports.map((n) => merge(n, {node: node.id})) : []
}

export function setPort (node, port, update) {
  return merge(node, {ports: node.ports.map((p, id) => {
    if (typeof (port) === 'number' && id === port) {
      return merge(p, update)
    } else if (typeof (port) === 'string' && Port.portName(p) === port) {
      return merge(p, update)
    } return p
  })})
}

/**
 * Gets all output ports of the node.
 * @param {Node} node The node.
 * @returns {Port[]} A possibly empty list of output ports.
 */
export function outputPorts (node, ignoreCompounds = true) {
  if (!ignoreCompounds && !node.atomic) {
    return ports(node)
  } else {
    return ports(node).filter(Port.isOutputPort)
  }
}

/**
 * Gets all input ports of the node.
 * @param {Node} node The node.
 * @returns {Port[]} A possibly empty list of input ports.
 */
export function inputPorts (node, ignoreCompounds = true) {
  if (!ignoreCompounds && !node.atomic) {
    return ports(node)
  } else {
    return ports(node).filter(Port.isInputPort)
  }
}

/**
 * @function
 * @name port
 * @description Returns the port data for a given node and port.
 * @param {String|Port} name The name of the port or a port object.
 * @param {Node} node The node which has the port.
 * @returns {Port} The port data.
 * @throws {Error} If no port with the given name exists in this node an error is thrown.
 */
export const port = curry((name, node) => {
  if (Port.isPort(name)) {
    return port(Port.portName(name), node)
  }
  var curPort = find((p) => Port.portName(p) === name, node.ports)
  if (!curPort) {
    throw new Error('Cannot find port with name ' + name + ' in node ' + JSON.stringify(node))
  }
  curPort.node = node.id
  return curPort
})

/**
 * @function
 * @name inputPort
 * @description Returns the input port data for a given node and port name / index.
 * @param {String|Port|Number} name The name of the input port, a port object or the index.
 * @param {Node} node The node which has the port.
 * @returns {Port} The input port data.
 * @throws {Error} If no port with the given name exists in this node an error is thrown.
 */
export const inputPort = curry((name, node) => {
  if (Port.isPort(name)) {
    return inputPort(Port.portName(name), node)
  }
  if (typeof (name) === 'number') return inputPorts(node)[name]
  var curPort = find((p) => Port.portName(p) === name, inputPorts(node))
  if (!curPort) {
    throw new Error('Cannot find port with name ' + name + ' in node ' + JSON.stringify(node))
  }
  return curPort
})

/**
 * @function
 * @name outputPort
 * @description Returns the output port data for a given node and port name / index.
 * @param {String|Port|Number} name The name of the output port, a port object or the index.
 * @param {Node} node The node which has the port.
 * @returns {Port} The output port data.
 * @throws {Error} If no port with the given name exists in this node an error is thrown.
 */
export const outputPort = curry((name, node) => {
  if (Port.isPort(name)) {
    return outputPort(Port.portName(name), node)
  }
  if (typeof (name) === 'number') return outputPorts(node)[name]
  var curPort = find((p) => Port.portName(p) === name, outputPorts(node))
  if (!curPort) {
    throw new Error('Cannot find port with name ' + name + ' in node ' + JSON.stringify(node))
  }
  return curPort
})

/**
 * Gets the path of a node
 * @param {Node} node The node
 * @returns {CompoundPath} The compound path of the node.
 */
export function path (node) {
  if (!node) return []
  return node.path
}

/**
 * @function
 * @name hasPort
 * @description Checks whether the node has the specific port.
 * @param {String|Port} name The name of the port or a port object.
 * @param {Node} node The node which has the port.
 * @returns {Port} True if the port has a port with the given name, false otherwise.
 */
export const hasPort = curry((name, node) => {
  if (Port.isPort(name)) {
    return hasPort(Port.portName(name), node)
  }
  return !!find((p) => Port.portName(p) === name, node.ports)
})

/**
 * @function
 * @name hasPort
 * @description Checks whether the node has the specific input port.
 * @param {String|Port} name The name of the port or a port object.
 * @param {Node} node The node which has the port.
 * @returns {Port} True if the port has an input port with the given name, false otherwise.
 */
export const hasInputPort = curry((name, node) => {
  if (Port.isPort(name)) {
    return hasInputPort(Port.portName(name), node)
  }
  return !!find((p) => Port.portName(p) === name, inputPorts(node))
})

/**
 * @function
 * @name hasPort
 * @description Checks whether the node has the specific output port.
 * @param {String|Port} name The name of the port or a port object.
 * @param {Node} node The node which has the port.
 * @returns {Port} True if the port has an output port with the given name, false otherwise.
 */
export const hasOutputPort = curry((name, node) => {
  if (Port.isPort(name)) {
    return hasOutputPort(Port.portName(name), node)
  }
  return !!find((p) => Port.portName(p) === name, outputPorts(node))
})


/**
 * Checks whether the node is a reference.
 * @param {Node} node The node.
 * @returns {boolean} True if the node is a reference, false otherwise.
 */
export function isReference (node) {
  return has('ref', node)
}

/**
 * Returns the componentId of the node.
 * @param {Node} node The node.
 * @returns {string} The componentId of the node.
 */
export function component (node) {
  return isReference(node) ? node.ref : node.componentId
}

/**
 * @function
 * @name set
 * @description Set properties for the node
 * @param {object} value An object with keys and values that should be set for a node.
 * @returns {Node} A new node that has the new properties applied.
 */
export const set = curry((value, node) => {
  return merge(node, {settings: merge(node.settings, value)})
})

/**
 * @function
 * @name get
 * @description Get a property for a node
 * @param {String} key The property key.
 * @returns The value of the property. If the property is not defined it will return undefined.
 */
export const get = curry((key, node) => (node.settings) ? node.settings[key] : node.settings)

/**
 * Checks whether a node is an atomic node.
 * @param {Node} node The node.
 * @returns {boolean} True if the node is an atomic node, false otherwise.
 */
export function isAtomic (node) {
  return !isReference(node) && node.atomic
}

/**
 * Checks whether a node is in a valid format, i.e. if it has an id field and at least one port.
 * @param {Node} node The node to test.
 * @returns {boolean} True if the node is valid, false otherwise.
 */
export function isValid (node) {
  return isReference(node) ||
    (typeof (node) === 'object' && typeof (node.id) === 'string' && node.id.length > 0 &&
    every(Port.isValid, ports(node)))
}

export function assertValid (node) {
  if (typeof (node) !== 'object') {
    throw new Error('Node object must be an object but got: ' + typeof (node))
  } else if (!node.id) {
    throw new Error('Node must have a valid ID in :(' + JSON.stringify(node) + ')')
  } else if (!node.id.length) {
    throw new Error('Node must have an ID with non zero length.')
  }
  ports(node).forEach(Port.assertValid)
}