Source: location.js

/**
 * A location is an object that defines a port or a node in the graph. A location can be one of the following:
 * - id,
 * - compound path
 * - node object
 * - port object.
 */

import curry from 'lodash/fp/curry'
import merge from 'lodash/fp/merge'
import {nodeByPath, idToPath, nodes} from './graph/internal'
import {isPort} from './port'
import {isValid as isNode, equal, id, isID, isReference} from './node'
import {rest, prefix} from './compoundPath'

/** A port notation can have every of the other notations for the node and as such
 * it is necessary to check first if it is a port notation. The symbol '@' is only used in
 * port notations.
 */
function isPortNotation (str) {
  return str.indexOf('@') !== -1
}

function isCompoundPathNotation (str) {
  return !isPortNotation(str) && str[0] === '»'
}

function isComponent (str) {
  return !isPortNotation(str) && str[0] === '/'
}

function isIndex (str) {
  return !isPortNotation(str) && str[0] === '#'
}

function isRoot (str) {
  return str === ''
}

/*
function isName (str) {
  return !isPortNotation(str) && !isIndex(str) && !isCompoundPathNotation(str)
}
*/

function parsePortNotation (port) {
  var split = port.split('@')
  if (split[1] === '') {
    throw new Error('Invalid port notation. Port notation does not contain a port. Parsed port: ' + port)
  }
  return merge(fromString(split[0], false), {type: 'location', locType: 'port', port: split[1]})
}

/**
 * Converts a compound path string into its array representation. The seperate parts must be divided by a '»'.
 * @param {String} compoundPathStr A string reperesenting the compound path divded by '»'.
 * @returns {String[]} An array of node IDs representing the compound path.
 */
export function parseCompoundPath (compoundPathStr) {
  if (compoundPathStr.indexOf('»') === -1) return [compoundPathStr]
  return compoundPathStr.split('»').slice(1)
}

/** Creates a location object from the string representation */
function fromString (str, allowsPorts = true) {
  if (isPortNotation(str)) {
    if (!allowsPorts) {
      throw new Error('Found unexpected port notation. Do you have multiple @\'s in your location string?')
    }
    return parsePortNotation(str)
  } else if (isCompoundPathNotation(str)) {
    return {type: 'location', locType: 'node', path: parseCompoundPath(str)}
  } else if (isComponent(str)) {
    return {type: 'query', queryType: 'component', query: str.slice(1)}
  } else if (isIndex(str)) {
    return {type: 'location', locType: 'node', index: str}
  } else if (isRoot(str)) {
    return {type: 'location', locType: 'node', path: []}
  } else {
    return {type: 'location', locType: 'node', path: [str]}
  }
}

function idify (path, graph) {
  if (path.length === 0) return path
  if (path.every((p) => isID(p))) return path
  var node = nodes(graph).filter(equal(path[0]))[0]
  if (!node) {
    if (equal(path[0], graph)) {
      return [id(graph)].concat(idify(rest(path), graph))
    }
    return
  }
  return [id(node)].concat(idify(rest(path), node))
}

function locPath (loc, graph) {
  var graphPath = graph.path
  if (loc.path) {
    let idPath = idify(loc.path, graph)
    return (idPath) ? prefix(idPath, graphPath) : undefined
  } else if (loc.name) {
    // best guess is that the node is at the root level...
    let idPath = idify([loc.name], graph)
    return (idPath) ? prefix(idPath, graphPath) : undefined
  } else if (loc.index) {
    var nodePath = idToPath(loc.index, graph)
    if (!nodePath) {
      throw new Error('Unable to locate node with id: ' + loc.index + '.')
    }
    return prefix(nodePath, graphPath)
  } else {
    throw new Error('Unable to process location. Not enough information to find node in the graph.')
  }
}

function fullLocation (loc, graph) {
  if (loc.type === 'query') return loc
  var path = locPath(loc, graph)
  var node = nodeByPath(path, graph) || {}
  return {
    type: 'location',
    locType: loc.locType,
    path,
    index: (loc.index) ? loc.index : node.id,
    name: (loc.name) ? loc.name : node.name,
    port: loc.port
  }
}

/**
 * Create a new location from a given object
 * @param loc Any processable form of location. TODO: list alle formats.
 * @param {PortGraph} graph The graph in which the location is valid.
 * @returns {Location} A location object
 */
export function location (loc, graph) {
  if (typeof (loc) === 'string') {
    return fullLocation(fromString(loc), graph)
  } else if (Array.isArray(loc)) {
    return fullLocation({type: 'location', locType: 'node', path: loc}, graph)
  } else if (typeof (loc) === 'object' && isPort(loc)) {
    var locObj = location(loc.node, graph)
    var merged = (locObj.type === 'query')
      ? merge({locType: 'port', port: loc.port}, location(loc.node, graph))
      : merge(location(loc.node, graph), {type: 'location', locType: 'port', port: loc.port})
    return fullLocation(merged, graph)
  } else if (typeof (loc) === 'object' && loc.id) {
    return fullLocation(fromString(loc.id), graph)
  } else if (typeof (loc) === 'object' && loc.name) {
    return fullLocation(fromString(loc.name), graph)
  } else if (typeof (loc) === 'object' && loc.path) {
    return fullLocation({type: 'location', locType: 'node', path: loc.path}, graph)
  } else {
    return {type: 'location', locType: 'invalid'}
//    throw new Error('Unknown location type: ' + JSON.stringify(loc))
  }
}

/**
 * Create a query function for a location.
 * @param {Location} loc A location identifier.
 * @param {PortGraph} graph The graph in which the location is valid.
 * @returns {function} A function that takes another location or location identifier
 * and compares it to the specified location `loc`. See `Location.identifies`.
 */
export function query (loc, graph) {
  return identifies(location(loc, graph))
}

function identifiesNode (loc, node) {
  return (isPort(node) &&
      ((isIndex(node.node) && loc.index === node.node) ||
      (!isIndex(node.node) && loc.name === node.node))) ||
    (!isPort(node) && loc.index === node.id)
}

function identifiesPort (loc, port) {
  return loc.locType === 'port' && loc.port === port.port && identifiesNode(loc, port)
}

function isRootNode (n) {
  return typeof (n) === 'object' && n.path && n.path.length === 0
}

/**
 * @function
 * @name identifies
 * @description Checks whether a location identifies the given object. This is true if
 * for example the location points to a node and the other object is the node.
 * Or if the other object is simply the ID of the node.
 * It also identifies a node if the location specifies the port. If you don't want
 * this behavior use equals (it is the strict version of identifies).
 * @params {Location} location A location object.
 * @params other Any type that can be a location.
 * @returns True if the location identifies the object stored in other.
 */
export const identifies = curry((loc, other) => {
  if (loc.type === 'query' && loc.queryType === 'component') {
    if (isReference(other)) {
      return other.ref === loc.query
    }
    if (isPort(other)) {
      return identifies(loc, other.additionalInfo)
    }
    return other.componentId === loc.query
  } else if (loc.locType === 'port' && isPort(other)) {
    return isPort(other) && identifiesPort(loc, other)
  } else if (loc.locType === 'node' && isID(other)) {
    return equal(loc, other)
  } else if (isNode(other) || isRootNode(other) || isPort(other)) {
    return identifiesNode(loc, other)
  } else {
    throw new Error('Unable to identify object type. Checking for: ' + JSON.stringify(loc) + ' object is: ' + JSON.stringify(other))
  }
})

export const toString = (loc) => {
  return JSON.stringify(loc)
}