import Vue from 'vue'
import mockData from '@/datasets/data-small.js'
import { cloneDeep, merge, sortBy } from 'lodash'

/**
 * @typedef {Object} Node
 *
 * @property {number} id dataset id
 * @property {('regular'|'group'|'bundle'|'alias')} type
 * @property {string} [dataClass] like person, document, etc.
 * @property {string} [name]
 * @property {string} [label]
 *
 * @property {NodeVis} [vis] storage for visualization data
 *
 */

/**
 * @typedef {Object} NodeVis
 * @property {boolean}  [vis.isUserDefined]
 * @property {number}   [vis.replacedBy]
 * @property {boolean}  [vis.isRemoved]
 */

/**
 * @typedef {Object} Link
 *
 * @property {number} id dataset id
 * @property {number} source id of end node
 * @property {number} target id of end node
 *
 * @property {boolean|object} [direction] boolean for singlelink, object for multilink
 * @property {boolean}  [direction.nonDirectional] specific for multilinks
 * @property {boolean}  [direction.sourceToTarget] specific for multilinks
 * @property {boolean}  [direction.targetToSource] specific for multilinks
 *
 * @property {LinkVis}   [vis] storage for visualization data
 */

/**
 * @typedef {Object} LinkVis
 * @property {boolean}  [vis.isUserDefined]
 * @property {boolean}  [vis.isMultilink] specific for multilinks
 * @property {number}   [vis.inMultilink] id of multilink which it belongs to
 * @property {number}   [vis.newSource] specific for singlelinks:
 * @property {number}   [vis.newTarget]
 * @property {boolean}  [vis.isRemoved]
 * @property {boolean}  [vis.isArchived] specific for multilinks: when link is inactive, waiting to be recovered when ungrouping
 */

/**
 * @typedef {Object} Tag
 * @property {number}   number
 * @property {name}     string
 * @property {boolean}  isVisualized if tag is visible in graph near the nodes
 * @property {string}   color in hexa
 * @property {'aggregation'|'manual'|'link'} type if created automatically by aggregation or manually by user
 */

export const namespaced = true

export const state = {
  // Maps would be better, unfortunately Vuex does not support them.
  nodes: [],
  links: [],
  tags: []
}

const NODE_AGGREGATION_TYPES = ['group', 'alias', 'bundle']
Object.freeze(NODE_AGGREGATION_TYPES)

function filterVisibleLinks(links) {
  return links.filter(l => isLinkVisible(l))
}

export function filterVisibleNodes(nodes) {
  return nodes.filter(n => !n.vis || !n.vis.replacedBy)
}

export function isNodeAggregated(n) {
  return NODE_AGGREGATION_TYPES.includes(n.type)
}

export function findConnectedLinks(links, id) {
  let connected = []
  links.forEach(l => {
    if (l.target === id || l.source === id) connected.push(l)
  })
  return connected
}

/**
 * Check if visible: not in multilink, not an archived multilink, newTarget != newSource
 * @param {Link} l
 */
function isLinkVisible(l) {
  if (isInMultilink(l)) return false
  if (l.vis) {
    if (l.vis.isMultilink && l.vis.isArchived) return false
    if (l.vis.newSource !== undefined && l.vis.newSource === l.vis.newTarget) return false
  } else {
    if (l.source === l.target) return false
  }
  return true
}

/**
 * Find first available id by iterating over objects in array
 * @param {Object[]} arr of objects, each object has attribute id
 * @returns {number} id
 */
function arrGetNewId(arr) {
  if (arr === undefined || !Array.isArray(arr)) throw 'Calling arrGetNewId with undefined array'
  if (arr.length === 0) return 0

  return arr.reduce((prev, curr) => (prev.id > curr.id ? prev : curr)).id + 1
}

/**
 * Check if link has set vis.newSource and vis.newTarget
 * @param {Link} link 
 */
function isLinkTransferred(link) {
  return link.vis && link.vis.newSource !== undefined && link.vis.newTarget !== undefined
}

function isLinkSimilar(link, linkToMatch) {
  const source = isLinkTransferred(linkToMatch) ? linkToMatch.vis.newSource : linkToMatch.source
  const target = isLinkTransferred(linkToMatch) ? linkToMatch.vis.newTarget : linkToMatch.target

  if (link.vis && link.vis.newSource !== undefined && link.vis.newTarget !== undefined) {
    return (
      (source === link.vis.newSource && target === link.vis.newTarget) ||
      (target === link.vis.newSource && source === link.vis.newTarget)
    )
  }

  return (
    (source === link.source && target === link.target) ||
    (target === link.source && source === link.target)
  )
}

/**
 * Insert a link into array and check, if array contains a link with the same
 * source-target pair. In that case, also insert a multilink. Will set link.id
 *
 * Set multilink id to nodeId+1. Mark associated links as part of the multilink.
 * Update multilink direction.
 * @param {Object[]} arr where link or multilink will be created
 * @param {Link} link to insert
 * @param {Number} groupId so multilink has always source set to group node
 * @return {Object} multilink copy, if created; null otherwise
 */
function insertUpdatedLink(arr, link, groupId, newId) {
  const similarLink = arr.find(l => isLinkSimilar(l, link))
  if (!similarLink) {
    arr.push(cloneDeep(link))
    return null
  }

  // found link is the new multilink
  if (isMultilink(similarLink)) {
    // FIXME no coverage
    link.vis.inMultilink = similarLink.id
    arr.push(cloneDeep(link))
    multilinkUpdateDirection(arr, similarLink)

    // if found link is in multilink
  } else if (isInMultilink(similarLink)) {
    // it should be the newly created multilink in this arr
    const ml = arr.find(l => l.id === similarLink.vis.inMultilink)
    if (!ml) throw Error('This fucker is no multilink')
    link.vis.inMultilink = similarLink.vis.inMultilink
    arr.push(cloneDeep(link))
    multilinkUpdateDirection(arr, ml)

    // found link is not in multilink, ergo there is no multilink and we create new
  } else if (isMultilink(link)) {
    // FIXME no coverage
    return null
  } else {
    const isSourceGroup = groupId === getLinkSource(link)
    const ml = createMultilink(
      newId,
      isSourceGroup ? getLinkSource(link) : getLinkTarget(link),
      isSourceGroup ? getLinkTarget(link) : getLinkSource(link)
    )
    const merging = [similarLink, link]
    merging.forEach(l => {
      l.vis = l.vis || {}
      l.vis.inMultilink = ml.id
    })
    arr.push(cloneDeep(link))
    multilinkUpdateDirection(arr, ml)
    arr.push(ml)
    return ml
  }
  return null
}

/**
 * Return link.vis.newSource uif set, otherwise just link.source
 * @param {Link} link
 */
export function getLinkSource(link) {
  return link.vis && link.vis.newSource ? link.vis.newSource : link.source
}

/**
 * Return link.vis.newTarget uif set, otherwise just link.target
 * @param {Link} link
 */
export function getLinkTarget(link) {
  return link.vis && link.vis.newTarget ? link.vis.newTarget : link.target
}

/**
 * @summary Insert a link in given array, checking for links with similar endpoints, updating the array.
 * If similar link exists, assign the link to a multilink in the array and update the multilink direction.
 * Recover archived multilink in array if possible. Create new multilink if necessary.
 *
 * To enable recovery of archived multilinks, add them into the array before calling this.
 *
 * @param {Link} link to insert; NOT MUTATED
 * @param {Link[]} arr to be updated with the new link
 * @param {number} availableId for creating new multilink
 * @returns multilink, if new one was created during insertion; otherwise null
 */
function insertLinkWithMultilinkUpdate(link, arr, availableId) {
  if (isMultilink(link)) throw Error('Bad usage. Inserting a multilink is not allowed.')

  const source = getLinkSource(link)
  const target = getLinkTarget(link)
  const similarLink = arr.find(l => isLinkSimilar(l, link))
  if (!similarLink) {
    arr.push(cloneDeep(link))
    return null
  }

  if (isMultilink(similarLink)) {
    Vue.delete(similarLink.vis, 'isArchived')
    setVisAttr(link, 'inMultilink', similarLink.id)
    arr.push(cloneDeep(link))
    multilinkUpdateDirection(arr, similarLink)
  } else if (isInMultilink(similarLink)) {
    const ml = arr.find(l => l.id === similarLink.vis.inMultilink)
    if (!ml) throw Error('Unexpected.')
    setVisAttr(link, 'inMultilink', similarLink.vis.inMultilink)
    arr.push(cloneDeep(link))
    multilinkUpdateDirection(arr, ml)
  } else {
    const ml = createMultilink(availableId, source, target)
    const merging = [similarLink, link]
    merging.forEach(l => setVisAttr(l, 'inMultilink', ml.id))
    arr.push(cloneDeep(link))
    multilinkUpdateDirection(arr, ml)
    arr.push(ml)
    return ml
  }
  return null
}

/**
 * Update the aggregated direction of multilink from links in given array
 * @param {Link[]} arr of links with links belonging to given multilink
 * @param {Link} ml multilink to update
 */
function multilinkUpdateDirection(/* const */ arr, ml) {
  Vue.set(ml, 'direction', { nonDirectional: false, sourceToTarget: false, targetToSource: false })
  arr
    .filter(l => l.vis && l.vis.inMultilink === ml.id)
    .map(l => {
      if (!l.direction) {
        ml.direction.nonDirectional = true
      } else {
        if (l.vis.newSource === ml.source) ml.direction.sourceToTarget = true
        else ml.direction.targetToSource = true
      }
    })
}

/**
 * Check if element has replacedBy attribute, ie. is in aggregation
 * @param {Node} node
 * @param {Number} id to match replacedBy
 */
export function isReplacedBy(node, id) {
  if (!node.vis) return false
  return node.vis.replacedBy === id
}

/**
 * Check if link is in multilink
 * @param {Link} link
 * @return {Boolean}
 */
function isInMultilink(link) {
  if (!link.vis) return false
  return link.vis.inMultilink !== undefined
}

/**
 * Check for vis.isMultilink attribute
 * @param {Link} link
 */
function isMultilink(link) {
  if (!link.vis) return false
  return link.vis.isMultilink !== undefined
}

/**
 * Push new group node to state
 * @param {Object} state writes to state.nodes
 * @param {Node} node will be assigned type and new ID
 * @param {boolean} isUserDefined true to set this vis attr
 */
function pushNodeToState(state, node, isUserDefined) {
  node.id = arrGetNewId(state.nodes)
  if (isUserDefined) {
    node.vis = node.vis || {}
    node.vis.isUserDefined = true
  }
  state.nodes.push(cloneDeep(node))
  return node.id
}

/**
 * Set vis attribute to object
 * @param {Node|Link} el
 * @param {string} attr vis attribute to set
 * @param {*} val value to set
 */
function setVisAttr(el, attr, val) {
  el.vis = el.vis || {}
  Vue.set(el.vis, attr, cloneDeep(val))
}

/**
 * Set vis attributes to object, merging vis objects
 * @param {Node|Link} el
 * @param {LinkVis} attrObj vis object to merge
 */
function setVisAttrs(el, attrObj) {
  el.vis = el.vis || {}
  Vue.set(el, 'vis', merge(el.vis, attrObj))
}

/**
 * Merge visAttrs with each element given in arr
 * @param {Node[]|Link[]} arr
 * @param {LinkVis|NodeVis} visAttrs
 */
function arraySetVisAttrs(arr, visAttrs) {
  arr.forEach(el => {
    el.vis = el.vis || {}
    Vue.set(el, 'vis', merge(el.vis, visAttrs))
  })
}

/**
 * Get new multilink instance
 * @param {number} newId
 * @param {number} source
 * @param {number} target
 */
function createMultilink(newId, source, target) {
  // FIXME: when isUserDefined?
  return {
    id: newId,
    source: source,
    target: target,
    vis: { isMultilink: true }
  }
}

/**
 * Find last node which replaced given node
 * Nodes map is in form [{id: replacedById | undefined },..]
 * @param {Map} nodesMap created with makeNodesMap
 * @param {number} nodeId on which to start lookup
 * @returns {number} node id
 */
function findReplacementNode(nodesMap, nodeId) {
  if (!nodesMap.has(nodeId)) throw Error('Node not found in nodes map.')

  const replacedBy = nodesMap.get(nodeId)
  if (replacedBy === undefined) return nodeId
  else return findReplacementNode(nodesMap, replacedBy)
}

export const mutations = {
  /**
   * Add new node
   * @param {Object} state
   * @param {Node} node
   */
  ADD_NODE(state, node) {
    node.id = arrGetNewId(state.nodes)
    state.nodes.push(node)
  },

  /**
   * Batch replace node attributes
   * @param {Object} state
   * @param {Object[]} payload const;
   * every diff will have node id and attributes to replace
   * [...{ id: nodeId, ...updateAttr: newVal}]
   * @param {Number} payload[].id
   *
   * @throws {Error} if diff array is empty or undefined
   * @throws {Error} if node is not found in state. Beware that previous valid entries will be commited
   */
  UPDATE_NODES(state, payload) {
    const nodesDiff = payload
    if (!nodesDiff || nodesDiff.length < 1) throw Error('Update array undefined or empty')

    nodesDiff.forEach(ndiff => {
      if (!ndiff.id === undefined) throw Error('Entry has no id')
      const node = state.nodes.find(sn => sn.id === ndiff.id)
      if (!node) throw Error('Node not found')

      Vue.set(state.nodes, state.nodes.indexOf(node), merge(node, ndiff))
    })
  },

  /**
   * Remove multiple nodes and associated links
   * @param {Object}   state
   * @param {Object}   payload
   * @param {Number[]} payload.nodeIds to remove
   * @param {Number[]} [payload.removedNodeIds] OUT: will contain nodes to be removed from graph.
   * @param {Number[]} [payload.removedLinkIds] OUT: will contain links to be removed from graph.
   * Will not include original entries set to isRemoved
   *
   * @throws {Error} when given no nodeIds
   * @throws {Error} when nodes dont exist
   */
  REMOVE_NODES(state, payload) {
    const nodeIds = payload.nodeIds
    const removedNodeIds = (payload.removedNodeIds = [])
    const removedLinkIds = (payload.removedLinkIds = [])
    if (!Array.isArray(nodeIds) || nodeIds.length < 1) throw Error('Invalid input array')

    const filteredNodes = []
    let removedNo = 0
    for (const n of state.nodes) {
      // skip
      if (!nodeIds.includes(n.id)) {
        filteredNodes.push(n)
        continue
      }

      if (n.vis && n.vis.isUserDefined !== true) {
        // TODO: inform user via payload about removing original entries
        n.vis = n.vis || {}
        Vue.set(n.vis, 'isRemoved', true)
        //n.vis.isRemoved = true
        filteredNodes.push(n)
      } else {
        removedNodeIds.push(n.id)
      }
      removedNo++
    }
    if (removedNo !== nodeIds.length)
      throw Error('Some of the nodes marked for removal do not exist.')
    // TODO: what with group? nodes with inAggregation set?
    Vue.set(state, 'nodes', filteredNodes)

    const filteredLinks = []
    for (const l of state.links) {
      // skip irrelevant
      if (
        !nodeIds.includes(l.source) &&
        !nodeIds.includes(l.target) &&
        !isInAggregation(l, nodeIds)
      ) {
        filteredLinks.push(l)
        continue
      }

      if (!l.vis || l.vis.isUserDefined !== true) {
        l.vis = l.vis || {}
        l.vis.isRemoved = true
        filteredLinks.push(l)
        continue
      }

      if (l.vis && l.vis.inMultilink) continue
      removedLinkIds.push(l.id)
    }
    Vue.set(state, 'links', filteredLinks)
  },

  /**
   * Aggregate nodes in general group, alias or bundle
   * Push new group node/links to state, update nodes/links.
   *
   * Aggregation types
   * ALIAS
   * a regular node (represents one entity, not a group);
   * merge links, attributes, merge conflict should be solved by user
   *
   * BUNDLING
   * hides a set of nodes in a selected node;
   * nodes are hidden, links are transferred (meaning they connect to the bundle node instead)
   *
   * GENERAL GROUP
   * nodes are hidden, links are transferred
   *
   * @param {Object}   state
   * @param {Object}   payload
   * @param {Number[]} payload.nodeIds to aggregate
   * @param {Node}     [payload.targetGroupNode] ref. to the new group node without ID, which will be set
   * @param {Number[]} [payload.removedLinkIds] OUT: will contain links hidden during aggregation, ordered from smallest
   * @param {Object[]} [payload.newVisibleLinks] OUT: will contain new links, ordered by id from smallest
   *
   * @throws {Error} when nodeIds are not given or less than 2
   * @throws {Error} when targetGroupNode already has an ID or is just regular node
   */
  AGGREGATE_NODES(state, payload) {
    const nodesToGroupIds = payload.nodeIds,
      groupNode = payload.targetGroupNode,
      removedLinkIds = [],
      newVisibleLinks = []

    if (groupNode.type !== 'bundle' && (!nodesToGroupIds || nodesToGroupIds.length < 2))
      throw Error('Grouping requires less then 2 nodes')
    if (!groupNode)
      throw Error('Group node is undefined')
    if (groupNode.type === 'regular')
      throw Error('Node is regular.')
    if (groupNode.type !== 'bundle' && groupNode.id !== undefined)
      throw Error('Group node should not come with an ID')

    const groupId = groupNode.type === 'bundle' ? groupNode.id : pushNodeToState(state, groupNode, false)
    arraySetVisAttrs(state.nodes.filter(n => nodesToGroupIds.includes(n.id)), {
      replacedBy: groupId
    })

    const updatedLinks = []
    const newLinks = []
    let currentNewLinkId = arrGetNewId(state.links)
    for (const link of state.links) {
      const isGroupTarget = nodesToGroupIds.includes(link.target) || (groupNode.type === 'bundle' && groupId === link.target) ? true : false,
        isGroupSource = nodesToGroupIds.includes(link.source) || (groupNode.type === 'bundle' && groupId === link.source) ? true : false,
        isGroupNewTarget =
          link.vis &&
          link.vis.newTarget !== undefined &&
          (nodesToGroupIds.includes(link.vis.newTarget) || (groupNode.type === 'bundle' && groupId === link.vis.newTarget))
            ? true
            : false,
        isGroupNewSource =
          link.vis &&
          link.vis.newSource !== undefined &&
          (nodesToGroupIds.includes(link.vis.newSource) || (groupNode.type === 'bundle' && groupId === link.vis.newSource))
            ? true
            : false,
        isSourceAndTargetSame =
          link.vis &&
          link.vis.newTarget !== undefined &&
          link.vis.newSource !== undefined &&
          link.vis.newSource === link.vis.newTarget
            ? true
            : false

      let target = groupId,
        source = groupId
      // ignore links not connected to subgraph
      if (!isGroupTarget && !isGroupSource && (!isGroupNewTarget && !isGroupNewSource)) {
        continue
      }

      // remove all visible subgraph links from graph
      if (isLinkVisible(link)) removedLinkIds.push(link.id)

      // link was already transferred -> replace newSource, newTarget
      if (isGroupNewTarget || isGroupNewSource) {
        if (isSourceAndTargetSame) continue
        if (isGroupNewTarget && !isGroupNewSource) source = link.vis.newSource
        if (!isGroupNewTarget && isGroupNewSource) target = link.vis.newTarget
        // link is new -> set newSource, newTarget for the first time
      } else if (!isMultilink(link) && (isGroupTarget || isGroupSource)) {
        if (isGroupTarget && !isGroupSource) source = link.source
        if (!isGroupTarget && isGroupSource) target = link.target
      } else if (isMultilink(link)) {
        link.vis.isArchived = true
        continue
      }

      // update link connection
      if (link.source !== source || link.target !== target) {
        setVisAttrs(link, { newSource: source, newTarget: target })
      }
      if (link.vis && link.vis.inMultilink) Vue.delete(link.vis, 'inMultilink')
      // ignore if inside the group
      if (source === target) {
        continue
      }

      // check if updated links merge into multilinks
      const ml = insertUpdatedLink(updatedLinks, link, groupId, currentNewLinkId)
      if (ml) {
        newLinks.push(ml)
        currentNewLinkId += 1
      }
    }

    const updatedLinkIds = []
    updatedLinks.forEach(l => {
      updatedLinkIds.push(l.id)
    })
    state.links
      .filter(l => updatedLinkIds.includes(l.id))
      .forEach(l => {
        const newer = updatedLinks.find(ul => ul.id === l.id)
        if (!newer) throw Error('Motherfuck')
        if (isInMultilink(newer)) {
          setVisAttrs(l, { inMultilink: newer.vis.inMultilink })
        }
      })

    newLinks.forEach(l => state.links.push(cloneDeep(l)))

    // updated not-in-multilink links and new multilinks will be shown
    updatedLinks
      .filter(l => !isInMultilink(l))
      .forEach(l => {
        if (isMultilink(l)) {
          newVisibleLinks.push({ id: l.id, source: getLinkSource(l), target: getLinkTarget(l) })
        } else { 
          newVisibleLinks.push({ id: l.id, source: getLinkSource(l), target: getLinkTarget(l) })
        }
      })
    // set payload
    payload.removedLinkIds = sortBy(removedLinkIds, o => o)
    payload.newVisibleLinks = sortBy(newVisibleLinks, ['id'])
  },

  /**
   * Reverse node aggregation for group, alias or bundle
   *
   * New data for graph will be placed in payload object.
   *
   * @param {Object}   state
   * @param {Object}   payload
   * @param {Number}   payload.nodeId group node to ungroup
   * @param {boolean}  [payload.removeNode] OUT: will contain instruction whenever to remove the ungroped node
   * @param {Number[]} [payload.removedLinkIds] OUT: will contain removed links, ordered from smallest
   * @param {Object[]} [payload.newVisibleLinks] OUT: will contain replacement links for graph, ordered by ids from smallest
   * @param {Object[]} [payload.newVisibleNodes] OUT: will contain replacement nodes for graph, ordered by ids from smallest
   *
   * @throws {Error} when given node does not exist or is not a group
   * @throws {Error} when given node is not user created
   */
  DISAGGREGATE_NODE(state, payload) {
    const groupId = payload.nodeId,
      removedVisibleLinkIds = [],
      newVisibleLinks = [],
      newVisibleNodes = []

    if (typeof groupId !== 'number' || groupId < 0) throw Error('Invalid argument.')
    const groupNode = state.nodes.find(n => n.id === groupId)
    if (!groupNode) throw Error('Node is undefined.')
    if (!isNodeAggregated(groupNode)) throw Error('Node is not a group.')
    payload.removeNode = groupNode.type !== 'bundle'

    // reveal original nodes
    const revealedNodes = state.nodes.filter(n => isReplacedBy(n, groupId))
    const revealedNodeIds = []
    revealedNodes.forEach(n => {
      revealedNodeIds.push(n.id)
      Vue.delete(n.vis, 'replacedBy')
      if (Object.keys(n.vis).length === 0) Vue.delete(n, 'vis')
      newVisibleNodes.push(
        n.vis && n.vis.graph ? { id: n.id, vis: { graph: cloneDeep(n.vis.graph) } } : { id: n.id }
      )
    })
    // remove group node
    if (groupNode.type !== 'bundle')
      Vue.set(state, 'nodes', state.nodes.filter(n => n.id !== groupId))

    // convert state.nodes to hash map, because we will be doing many recursive
    // lookups to update the affected links
    const nodesMap = makeNodesMap(state)

    // divide links to two categories:
    //   - links untouched by ungrouping algorithm (for efficiency, as we do not need to iterate over them)
    //   - links that form the subgraph and do not originate from group
    // at the end, we will be merging them and replacing state.links
    // this is efficient, we are only creating vectors of references
    // however, we may want to deep copy these, if we will work with state.links
    const unaffectedLinks = []
    const affectedLinks = []
    const affectedMultilinks = []
    const removedLinkIds = [] // will be used to calculate id for new multilinks, as we have split arrays and cannot know the available id
    for (const l of state.links) {
      // remove links originating from group
      // these also account for current multilinks, as they cannot be transferred
      if (l.source === groupId || l.target === groupId) {
        removedLinkIds.push(l.id)
        if (isLinkVisible(l)) removedVisibleLinkIds.push(l.id)
        continue
      }

      // find links transferred to group
      if (l.vis) {
        // normal links from subgraph
        if (l.vis.newSource === groupId || l.vis.newTarget === groupId) {
          affectedLinks.push(l)
          continue
        }
        // multilinks from subgraph
        if (
          isMultilink(l) &&
          (revealedNodeIds.includes(l.source) || revealedNodeIds.includes(l.target))
        ) {
          affectedMultilinks.push(l)
          continue
        }
      }

      unaffectedLinks.push(l)
    }

    // iterate over subgraph links (those originating from group were already filtered out)
    //   - link is visible: remove from graph
    //   - is multilink, now what???
    //   - calculate newSource, newTarget
    //   - check against all these links, if we can create or recover a multilink
    const updatedAffectedLinks = []
    const linksForIdCalculation = state.links.filter(l => !removedLinkIds.includes(l.id))

    // firstly recover old multilinks, so we then know, if we can just assign the link to them or create new multilinks
    for (const l of affectedMultilinks) {
      if (!isMultilink(l)) throw Error('This fucker is no multilink')
      if (isLinkVisible(l)) removedVisibleLinkIds.push(l.id)
      updatedAffectedLinks.push(cloneDeep(l))
    }

    for (const l of affectedLinks) {
      // for simplicity, we remove all these links from graph and draw new after ungrouping, after multilinks formed
      if (isLinkVisible(l)) removedVisibleLinkIds.push(l.id)

      const updatedSource = findReplacementNode(nodesMap, l.source)
      const updatedTarget = findReplacementNode(nodesMap, l.target)

      Vue.delete(l.vis, 'inMultilink')

      // check, if we still transfer link or it is in original state
      if (l.source === updatedSource && l.target === updatedTarget) {
        Vue.delete(l.vis, 'newSource')
        Vue.delete(l.vis, 'newTarget')
        if (Object.keys(l.vis).length === 0) Vue.delete(l, 'vis')
      } else {
        l.vis.newSource = updatedSource
        l.vis.newTarget = updatedTarget
      }

      const ml = insertLinkWithMultilinkUpdate(
        l,
        updatedAffectedLinks,
        arrGetNewId(linksForIdCalculation)
      )
      if (ml) linksForIdCalculation.push(ml)
    }

    updatedAffectedLinks.forEach(l => {
      if (isLinkVisible(l))
        newVisibleLinks.push({
          id: l.id,
          source: l.vis && l.vis.newSource ? l.vis.newSource : l.source,
          target: l.vis && l.vis.newTarget ? l.vis.newTarget : l.target
        })
    })

    // now merge both link arrays and update state.links
    Vue.set(state, 'links', sortBy([...unaffectedLinks, ...updatedAffectedLinks], ['id']))
    // set payload
    payload.removedLinkIds = sortBy(removedVisibleLinkIds, o => o)
    payload.newVisibleLinks = sortBy(newVisibleLinks, ['id'])
    payload.newVisibleNodes = sortBy(newVisibleNodes, ['id'])
  },

  INIT(state, data) {
    state.nodes = cloneDeep(data.nodes)
    state.links = cloneDeep(data.links)
  }
}

/**
 * Create new link
 * @param {Object} newId to assign
 * @param {Number} source node id
 * @param {Number} target node id
 * @param {Object} [vis] vis attributes to set
 * @return created link instance
 */
export function createLink(newId, source, target, vis = null) {
  const l = { id: newId, target: target, source: source }
  if (vis !== null) {
    l.vis = l.vis || {}
    merge(l.vis, vis)
  }
  return l
}

/**
 * Create Map object from state.nodes. Use to speed up lookups
 * @param {Object} state
 */
function makeNodesMap(state) {
  return new Map(
    state.nodes.map(n => {
      const replacedBy = n.vis ? (n.vis.replacedBy ? n.vis.replacedBy : undefined) : undefined
      return [n.id, replacedBy]
    })
  )
}

export const actions = {
  fetchData({ commit }) {
    commit('INIT', mockData)
    // TODO: api call
  },

  import({ commit }, data) {
    commit('INIT', data)
    // TODO: validate
  },

  createNode({ commit }, node) {
    commit('ADD_NODE', node)
  },

  updateNodes({ commit }, nodesDiff) {
    commit('UPDATE_NODES', nodesDiff)
  },

  removeNode({ commit }, nodeId) {
    commit('REMOVE_NODES', { nodeIds: [nodeId] })
  },

  groupNodes({ commit }, nodeIdArr) {
    let groupNode = {
      name: 'Group of: ',
      type: 'group',
      label: 'Group'
    }

    commit('AGGREGATE_NODES', {
      nodeIds: nodeIdArr,
      targetGroupNode: groupNode
    })
  },

  aliasNodes({ commit }, nodeIdArr) {
    let groupNode = {
      name: 'Alias of: ',
      type: 'alias',
      label: 'Alias'
    }

    commit('AGGREGATE_NODES', {
      nodeIds: nodeIdArr,
      targetGroupNode: groupNode
    })
  },

  /**
   * @param {object} vuexContext
   * @param {object} payload
   * @param {number} payload.representativeNodeId the "bundle" node
   * @param {number[]} payload.nodeIds to bundle inside the representative node
   */
  bundleNodes({ commit, getters }, payload) {
    console.log('bundleNodes: ' + JSON.stringify(payload))
    commit('UPDATE_NODES', [{
        id: payload.representativeNodeId,
        type: 'bundle'
    }])

    commit('AGGREGATE_NODES', {
      nodeIds: payload.nodeIds,
      targetGroupNode: getters.getNodeById(payload.representativeNodeId)
    })
  },

  ungroupNode({ commit }, nodeId) {
    commit('DISAGGREGATE_NODE', {
      nodeId: nodeId
    })
  }
}

export const getters = {
  getState: state => state,
  getNodes: state => state.nodes,
  getLinks: state => state.links,

  getVisibleNodes: state => filterVisibleNodes(state.nodes),
  getVisibleLinks: state => filterVisibleLinks(state.links),

  getNodeById: state => id => state.nodes.find(n => n.id === id),
  getLinkById: state => id => state.links.find(n => n.id === id),

  replacedNodesCount: state => id => {
    return state.nodes.filter(n => isReplacedBy(n, id)).length
  },
  getLinkIdsConnectedToNode: state => id => findConnectedLinks(state.links, id)
}
