Source: changeSet.js

/** @module ChangeSet
 * @overview
 * This methods are for internal usage. They do not check for bad inputs and can create broken graphs.
 * If you know what you are doing you can include them via `import * as ChangeSet from '@buggyorg/graphtools/changeSet'`.
 */

/**
 * A change set contains information about how to transform a graph, e.g. insert an edge or modify a node.
 * @type ChangeSet
 */

import _ from 'lodash'
import _f from 'lodash/fp'
import * as Node from './node'
import * as Edge from './edge'
import * as Component from './component'
import {relativeTo, isRoot} from './compoundPath'
import {access, store, forget, nodesDeep} from './graph/internal'

const hasChildren = Node.hasChildren

/**
 * Creates a change set to update a node with a given value
 * @param {string} node The identifier of the node.
 * @param {Object} mergeValue An object that contains parts of a node that should be set.
 * E.g. `{recursive: true}` will update the field `recursive` in the node and sets it to `true`.
 * @returns {ChangeSet} A change set containing the operation.
 */
export function updateNode (nodePath, mergeValue) {
  return {type: 'changeSet', operation: 'mergePath', query: nodePath, value: mergeValue}
}

/**
 * Creates a change set to replace a node with a given value
 * @param {string} node The identifier of the node.
 * @param {Object} mergeValue An object that contains a whole node that should be set.
 * E.g. `{recursive: true}` will update the field `recursive` in the node and sets it to `true`.
 * @returns {ChangeSet} A change set containing the operation.
 */
export function setNode (nodePath, setValue) {
  return {type: 'changeSet', operation: 'setPath', query: nodePath, value: setValue}
}

/**
 * Creates a change set that creates a new node.
 * @param {Object} value The new node.
 * @returns {ChangeSet} A change set containing the new node.
 */
export function insertNode (value, path = []) {
  return {type: 'changeSet', operation: 'insert', query: 'nodes', value, path}
}

export function removeNode (id) {
  return {type: 'changeSet', operation: 'remove', query: 'nodes', filter: (n) => Node.equal(n, id)}
}

/**
 * Creates a change set that creates a new component.
 * @param {Object} value The new component.
 * @returns {ChangeSet} A change set containing the new component.
 */
export function insertComponent (value) {
  return {type: 'changeSet', operation: 'insert', query: 'components', value}
}

/**
 * Creates a change set to update a component with a given value
 * @param {string} compId The componentId of the component.
 * @param {Object} mergeValue An object that contains parts of a component that should be set.
 * E.g. `{isType: true}` will update the field `isType` in the component and sets it to `true`.
 * @returns {ChangeSet} A change set containing the operation.
 */
export function updateComponent (compId, mergeValue) {
  return {type: 'changeSet', operation: 'mergeComponent', query: compId, value: mergeValue}
}

export function removeComponent (id) {
  return {type: 'changeSet', operation: 'remove', query: 'components', filter: (n) => Component.equal(n, id)}
}

export function addMetaInformation (key, value) {
  return {type: 'changeSet', operation: 'setKey', query: 'metaInformation', key, value}
}

export function setMetaInformation (meta) {
  return {type: 'changeSet', operation: 'set', query: 'metaInformation', value: meta}
}

export function removeMetaInformation (key) {
  return {type: 'changeSet', operation: 'removeKey', query: 'metaInformation', key}
}

export function empty () {
  return {type: 'changeSet', opertaion: 'none'}
}

/**
 * Creates a change set that inserts a new edge into the edge list
 * @param {Object} newEdge The edge that should be inserted.
 * @returns {ChangeSet} A change set containing the insertion operation.
 */
export function insertEdge (newEdge) {
  return {type: 'changeSet', operation: 'insert', query: 'edges', value: newEdge}
}

/**
 * Creates a change set that inserts a new edge into the edge list
 * @param {Object} newEdge The edge that should be inserted.
 * @returns {ChangeSet} A change set containing the insertion operation.
 */
export function updateEdge (edge, mergeEdge) {
  return {type: 'changeSet', operation: 'mergeEdge', query: edge, value: mergeEdge}
}

/**
 * Creates a change set that removes the edge `edge`.
 * @param {Object} edge The edge to remove.
 * @returns {ChangeSet} The change set containing the deletion operation.
 */
export function removeEdge (edge) {
  return {type: 'changeSet', operation: 'remove', query: 'edges', filter: _.partial(Edge.equal, edge)}
}

/**
 * Checks whether a value is a change set or not.
 * @param changeSet The value that should be checked.
 * @returns True if it is a changeSet, false otherwise.
 */
export function isChangeSet (changeSet) {
  return typeof (changeSet) === 'object' && changeSet.type === 'changeSet'
}

const applySetKey = (r, key, value) => {
  const v = _.get(r, key)
  if (typeof (v) === 'object' && typeof (value) === 'object') {
    return _f.set(key, _.merge(_.get(r, key), value), r)
  } else {
    return _f.set(key, value, r)
  }
}

const applyMergeByPath = (graph, path, value) => {
  var idx = _.findIndex(graph.nodes, Node.equal(path[0]))
  if (path.length === 1) {
    if (idx > -1) {
      return {
        ...graph,
        nodes: graph.nodes.map((i, itemIdx) => {
          if (idx === itemIdx) return _f.merge(i, value)
          return i
        })
      }
    }
  } else {
    if (idx > -1 && (hasChildren(graph.nodes[idx]))) {
      return {
        ...graph,
        nodes: graph.nodes.map((i, itemIdx) => {
          if (idx === itemIdx) return applyMergeByPath(i, path.slice(1), value)
          return i
        })
      }
    }
  }
}

const updateValue = (what, value, graph) => {
  var val = access(what, graph)
  if (val) {
    for (var idx in val) {
      if (val[idx].id === value.id) {
        val[idx] = value
        break
      }
    }
    store(val, what, graph)
  }
}

const setNodeByPath = (graph, path, value) => {
  var cur = graph
  for (var i = 0; i < path.length - 1; i++) {
    let idx = _.findIndex(cur.nodes, Node.equal(path[i]))
    cur = cur.nodes[idx]
  }
  let idx = _.findIndex(cur.nodes, Node.equal(path[path.length - 1]))
  cur.nodes[idx] = value
  updateValue('nodesDeep', value, graph)
  forget('edgesDeep', graph)
  forget('successorNode', graph)
  forget('successorPort', graph)
  forget('predecessorNode', graph)
  forget('predecessorPort', graph)
  updateValue('nodes', value, cur)
  forget('edges', cur)
  store(value, value.id, graph)
}

const getPath = (graph, path) => {
  if (path.length === 0) return graph
  var cur = graph
  for (var i = 0; i < path.length - 1; i++) {
    let idx = _.findIndex(cur.nodes, Node.equal(path[i]))
    cur = cur.nodes[idx]
  }
  let idx = _.findIndex(cur.nodes, Node.equal(path[path.length - 1]))
  return cur.nodes[idx]
}

const applySetByPath = (graph, path, value) => {
  if (graph.inplace) {
    setNodeByPath(graph, path, value)
    return graph
  }
  var idx = _.findIndex(graph.nodes, Node.equal(path[0]))
  if (path.length === 1) {
    if (idx > -1) {
      return {
        ...graph,
        nodes: graph.nodes.map((i, itemIdx) => {
          if (idx === itemIdx) return value
          return i
        })
      }
    }
  } else {
    if (idx > -1 && (hasChildren(graph.nodes[idx]))) {
      return {
        ...graph,
        nodes: graph.nodes.map((i, itemIdx) => {
          if (idx === itemIdx) return applySetByPath(i, path.slice(1), value)
          return i
        })
      }
    }
  }
}

const applyMergeByComponent = (graph, cId, value) => {
  var idx = _.findIndex(graph.components, (c) => Component.id(c) === cId)
  return {
    ...graph,
    components: graph.components.map((c, cIdx) => {
      if (idx === cIdx) return _f.merge(c, value)
      return c
    })
  }
}

const applyMergeByEdge = (graph, edge, value) => {
  var idx = _.findIndex(graph.edges, (e) => Edge.equal(edge, e))
  return {
    ...graph,
    edges: graph.edges.map((e, eIdx) => {
      if (idx === eIdx) return _f.merge(e, value)
      return e
    })
  }
}

const updatePush = (what, value, graph) => {
  var val = access(what, graph)
  if (val) {
    val.push(value)
    store(val, what, graph)
  }
}

const insertInGraph = (what, where, value, graph) => {
  if (graph.inplace && what === 'nodes') {
    var node = getPath(graph, where)
    node[what].push(value)
    const newNodes = nodesDeep(value)
    for (node of newNodes) {
      updatePush('nodesDeep', node, graph)
      updatePush('nodes', node, node)
      store(node, node.id, graph)
    }
    return graph
  }
  if (Array.isArray(where) && !isRoot(where) && relativeTo(where, graph.path).length !== 0) {
    throw new Error('Cannot insert deep without modifications. Path = ' + where)
  }
  return {
    ...graph,
    [what]: [ ...graph[what], value ]
  }
}

/**
 * Apply a changeSet on the given graph.
 * @param {Object} graph The graph in JSON format that should be changed.
 * @param {ChangeSet} changeSet The change set that should be applied.
 * @returns {Graphlib} A new graph with the applied change set graph.
 * @throws {Error} If the change set is no valid change set it throws an error.
 */
export function applyChangeSet (graph, changeSet) {
  // var newGraph = _.cloneDeep(graph)
  return applyChangeSetInplace(graph, changeSet)
}

/**
 * Apply an array of changeSets on the given graph. All changes are applied sequentially.
 * @param {PortGraph} graph The graph that should be changed.
 * @param {ChangeSet[]} changeSets The change sets that should be applied. The order might influence the resulting graph, they are processesed sequentially.
 * @returns {Graphlib} A new graph with the applied change set graph.
 * @throws {Error} If the change set is no valid change set it throws an error.
 */
export function applyChangeSets (graph, changeSets) {
  // var newGraph = _.cloneDeep(graph)
  return changeSets.reduce((g, c) => applyChangeSetInplace(g, c), graph)
}

/**
 * Apply a changeSet on the given graph inplace.
 * @param {PortGraph} graph The graph that should be changed.
 * @param {ChangeSet} changeSet The change set that should be applied.
 * @returns {Graphlib} The changed graph. Currently the changes are all made inplace so the return value is equal to the input graph.
 * @throws {Error} If the change set is no valid change set it throws an error.
 */
export function applyChangeSetInplace (graph, changeSet) {
  if (!isChangeSet(changeSet)) {
    throw new Error('Cannot apply non-ChangeSet ' + JSON.stringify(changeSet))
  } else if (changeSet.operation === 'mergePath') {
    return applyMergeByPath(graph, changeSet.query, changeSet.value)
  } else if (changeSet.operation === 'setPath') {
    return applySetByPath(graph, changeSet.query, changeSet.value)
  } else if (changeSet.operation === 'mergeComponent') {
    return applyMergeByComponent(graph, changeSet.query, changeSet.value)
  } else if (changeSet.operation === 'mergeEdge') {
    return applyMergeByEdge(graph, changeSet.query, changeSet.value)
  } else if (changeSet.operation === 'insert') {
    return insertInGraph(changeSet.query, changeSet.path, changeSet.value, graph)
  } else if (changeSet.operation === 'remove') {
    return {
      ...graph,
      [changeSet.query]: graph[changeSet.query].filter((n) => !changeSet.filter(n))
    }
  } else if (changeSet.operation === 'set') {
    return _f.set(changeSet.query, changeSet.value, graph)
  } else if (changeSet.operation === 'setKey') {
    return applySetKey(graph, changeSet.query + '.' + changeSet.key, changeSet.value)
  } else if (changeSet.operation === 'removeKey') {
    return _f.unset(changeSet.query + '.' + changeSet.key, graph)
    // return graph
  }
  return graph
}