import Vue from 'vue'
import * as _ from 'lodash'
import { sleep } from '@/mixins/helpers.js'
import { updateTags } from '@/store/modules/data/tags.js'
import { v4 as generateUuid } from 'uuid'

import {
  isMultilink,
  insertLinkWithMultilinkUpdate,
  getLinkSource,
  getLinkTarget,
  isNodeAggregated,
  isNodeBundle,
  isBundledIn,
  isReplacedBy,
  validateDataset,
  filterVisibleNodes,
  findConnectedLinks,
  findNodeByUuid,
  filterVisibleLinks,
  isInMultilinkOfId,
  pushNewArrElement,
  setVisAttr,
  arraySetVisAttrs,
  isLinkVisible,
  setVisAttrs,
  isInMultilink,
  removeVisAttr,
  makeNodesMap,
  findReplacementNode,
  getBundleSize,
  assertSlot,
  hasVisAttr,
  aggregateSinglelinksIfNeeded,
  aggregatePropertiesToMap
} from './data/helpers'

/* Tags feature */
import * as tags from './data/tags.js'
import * as vis from './data/vis.js'

/**
 * Type definitions for node and link data structures
 *
 * @typedef { import('./typedefs').Node } Node
 * @typedef { import('./typedefs').NodeVis } NodeVis
 * @typedef { import('./typedefs').Link } Link
 * @typedef { import('./typedefs').LinkVis } LinkVis
 * @typedef { import('./tags').Tag } Tag
 */

export const state = {
  // 0: { nodes: [], links: [], tags: [], lastTag: {}, vis: { selectedNodes: [] } },
  // 1: { ... }, ...
}

export const mutations = {
  ...tags.mutations,
  ...vis.mutations,

  /**
   * @brief Initialize dataset into a given slot.
   *
   * Aggregates singlelinks to multilinks if necessarry.
   * @param {Object} state
   * @param {Object} pl payload
   * @param {number}        pl.slotId dataset slot
   * @param {NetvisState[]} pl.state
   */
  INIT(state, pl) {
    const [nodes, tags] = pl.state.nodes.reduce(
      (current, node) => {
        current[node.type === 'regular' ? 0 : 1].push(node)
        return current
      },
      [[], []]
    )

    Vue.set(state, pl.slotId, {
      nodes: aggregatePropertiesToMap(nodes),
      links: aggregateSinglelinksIfNeeded(aggregatePropertiesToMap(_.cloneDeep(pl.state.links))),
      tags: aggregatePropertiesToMap(tags),
      vis: {
        selectedNodes: [],
        focusedNodes: hasVisAttr(pl.state, 'focusedNodes') ? pl.state.vis.focusedNodes : []
      },
      isImported: true
    })

    validateDataset(state[pl.slotId])
  },

  /**
   * Add new node
   * @param {Object} state
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {Node}     pl.node node to add
   */
  ADD_NODE(state, pl) {
    // assertSlot(pl.slotId)
    assertSlot(state, pl.slotId)
    pl.node.uuid = generateUuid()
    state[pl.slotId].nodes.push(pl.node)
  },

  /**
   * Batch replace node attributes
   * @param {Object} state
   * @param {Object} pl payload
   * @param {number}    pl.slotId dataset slot
   * @param {Object[]}  pl.diff[]
   * @param {Number}    pl.diff[].uuid
   * Every object of diff array needs node id and attributes to replace
   * [...{ uuid: nodeId, ...updateAttr: newVal}]
   *
   * @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, pl) {
    assertSlot(state, pl.slotId)

    const nodesDiff = pl.diff
    if (!Array.isArray(nodesDiff) || !nodesDiff.length) {
      throw Error('Update array undefined or empty')
    }

    nodesDiff.forEach(ndiff => {
      if (!ndiff.hasOwnProperty('uuid')) throw Error('Entry has no uuid')
      const node = state[pl.slotId].nodes.find(sn => sn.uuid === ndiff.uuid)
      if (!node) throw Error('Node not found')

      ndiff.properties = new Map([...node.properties, ...ndiff.properties])
      Vue.set(state[pl.slotId].nodes, state[pl.slotId].nodes.indexOf(node), _.merge(node, ndiff))
    })
  },

  /**
   * 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}   pl payload
   * @param {number}     pl.slotId dataset slot
   * @param {'group'|'alias'|'bundle'} pl.action aggregation type
   * @param {Number[]}   pl.nodeIds to aggregate
   * @param {boolean}    [pl.disableTags] disable tag functionality
   * @param {Node}       [pl.targetGroupNode] new group node without ID, which will be saved to state
   * @param {Number[]}   [pl.removedLinkIds] OUT: will contain links hidden during aggregation, ordered from smallest
   * @param {Object[]}   [pl.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, pl) {
    assertSlot(state, pl.slotId)

    const action = pl.action,
      nodesToGroupIds = pl.nodeIds,
      groupNodeToCreate = pl.targetGroupNode,
      removedLinkIds = [],
      newVisibleLinks = []

    if (action !== 'bundle' && (!nodesToGroupIds || nodesToGroupIds.length < 2))
      throw Error('Grouping requires at least 2 selected nodes')
    if (!groupNodeToCreate) throw Error('Group node is undefined')
    if ((action === 'group' || action === 'alias') && groupNodeToCreate.type === 'regular')
      throw Error('Node is regular.')
    if ((action === 'group' || action === 'alias') && groupNodeToCreate.uuid !== undefined)
      throw Error('Group node should not come with an UUID')

    // NODES
    const groupId =
      action === 'bundle'
        ? groupNodeToCreate.uuid
        : pushNewArrElement(state[pl.slotId].nodes, groupNodeToCreate)
    let groupNode = findNodeByUuid(state[pl.slotId].nodes, groupId)
    if (action === 'bundle') {
      if (hasVisAttr(groupNode, 'bundledNodes')) {
        const newBundledNodes = groupNode.properties.get('bundledNodes')
        setVisAttr(groupNode, 'bundledNodes', newBundledNodes.concat(nodesToGroupIds))
      } else {
        setVisAttr(groupNode, 'bundledNodes', nodesToGroupIds)
      }
      arraySetVisAttrs(
        state[pl.slotId].nodes.filter(n => nodesToGroupIds.includes(n.uuid)),
        {
          bundledIn: groupId
        }
      )
    } else {
      const nodesToGroup = state[pl.slotId].nodes.filter(n => nodesToGroupIds.includes(n.uuid))
      let hasUserDefinedNodes = false
      nodesToGroup.forEach(n => {
        if (hasVisAttr(n, 'isUserDefined')) hasUserDefinedNodes = true
      })
      if (hasUserDefinedNodes) setVisAttr(groupNode, 'aggregatesUserDefined')
      arraySetVisAttrs(nodesToGroup, { replacedBy: groupId })
    }

    // LINKS
    const updatedLinks = []
    const newLinks = []
    let currentNewLinkId = generateUuid()
    for (const link of state[pl.slotId].links) {
      const newTarget = link?.properties?.get('newTarget'),
        newSource = link?.properties?.get('newSource'),
        isGroupTarget =
          nodesToGroupIds.includes(link.target) || (action === 'bundle' && groupId === link.target),
        isGroupSource =
          nodesToGroupIds.includes(link.source) || (action === 'bundle' && groupId === link.source),
        isGroupNewTarget =
          newTarget &&
          (nodesToGroupIds.includes(newTarget) || (action === 'bundle' && groupId === newSource)),
        isGroupNewSource =
          newSource &&
          (nodesToGroupIds.includes(newSource) || (action === 'bundle' && groupId === newSource)),
        isSourceAndTargetSame = newSource === newTarget

      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.uuid)

      // link was already transferred -> replace newSource, newTarget
      if (isGroupNewTarget || isGroupNewSource) {
        if (isSourceAndTargetSame) continue
        if (isGroupNewTarget && !isGroupNewSource) source = link.properties.get('newSource')
        if (!isGroupNewTarget && isGroupNewSource) target = link.properties.get('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.properties.set('isArchived', true)
        continue
      }

      // update link connection
      removeVisAttr(link, 'isTransferred')
      if (link.source !== source || link.target !== target) {
        setVisAttrs(link, { newSource: source, newTarget: target })
        if (source !== target) setVisAttr(link, 'isTransferred', true)
      }
      if (isInMultilink(link)) {
        removeVisAttr(link, 'inMultilink')
      }
      // ignore if inside the group
      if (source === target) {
        continue
      }

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

    const updatedLinkIds = []
    updatedLinks.forEach(l => {
      updatedLinkIds.push(l.uuid)
    })
    state[pl.slotId].links
      .filter(l => updatedLinkIds.includes(l.uuid))
      .forEach(l => {
        const newer = updatedLinks.find(ul => ul.uuid === l.uuid)
        if (!newer) throw Error('Cannot find the new link')
        if (isInMultilink(newer)) {
          setVisAttrs(l, {
            inMultilink: newer.properties.get('inMultilink')
          })
        }
      })

    newLinks.forEach(l => state[pl.slotId].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({
            uuid: l.uuid,
            source: getLinkSource(l),
            target: getLinkTarget(l)
          })
        } else {
          newVisibleLinks.push({
            uuid: l.uuid,
            source: getLinkSource(l),
            target: getLinkTarget(l)
          })
        }
      })
    // prepare payload
    pl.removedLinkIds = _.sortBy(removedLinkIds, o => o)
    pl.newVisibleLinks = _.sortBy(newVisibleLinks, ['uuid'])

    // TAGS
    if (!pl.disableTags) {
      let tag = updateTags(
        state[pl.slotId].tags,
        state[pl.slotId].nodes,
        nodesToGroupIds,
        action,
        groupNode,
        state[pl.slotId].tagData ? state[pl.slotId].tagData.color : ''
      )

      if (tag) {
        Vue.set(state[pl.slotId], 'lastTag', tag.uuid)
      }
    }
  },

  /**
   * Reverse node aggregation for group, alias or bundle
   *
   * New data for graph will be placed in payload object.
   *
   * @param {Object}   state
   * @param {Object}   pl
   * @param {number}     pl.slotId dataset slot
   * @param {Number}     pl.nodeId group node to ungroup
   * @param {'group'|'bundle'} pl.revertAction
   * @param {boolean}    [pl.removeNode] OUT: will contain instruction whenever to remove the ungroped node
   * @param {Number[]}   [pl.removedLinkIds] OUT: will contain removed links, ordered from smallest
   * @param {Object[]}   [pl.newVisibleLinks] OUT: will contain replacement links for graph, ordered by ids from smallest
   * @param {Object[]}   [pl.newVisibleNodes] OUT: will contain replacement nodes for graph, ordered by ids from smallest
   *
   * @throws {Error} target node does not exist
   * @throws {Error} target is regular node or not a bundle (in case of reverting bundling)
   * @throws {Error} target is a group and also bundles nodes
   */
  DISAGGREGATE_NODE(state, pl) {
    assertSlot(state, pl.slotId)

    const revertAction = pl.revertAction,
      groupId = pl.nodeId,
      removedVisibleLinkIds = [],
      newVisibleLinks = [],
      newVisibleNodes = []

    if (revertAction === undefined) throw Error('Undefined dissagregation action.')
    if (typeof groupId !== 'string') throw Error('Invalid argument.')
    const groupNode = state[pl.slotId].nodes.find(n => n.uuid === groupId)
    if (!groupNode) throw Error('Target node was not found.')
    if (revertAction === 'group') {
      if (!isNodeAggregated(groupNode)) throw Error('Cannot revert grouping on regular node.')
      if (isNodeBundle(groupNode))
        throw Error('Cannot ungroup a bundled node. Revert bundle first.')
    } else if (revertAction === 'bundle' && !isNodeBundle(groupNode)) {
      throw Error('Cannot unbundle a node without bundled nodes.')
    }
    const removeTargetNode = revertAction !== 'bundle'

    // reveal original nodes
    const revealedNodeIds = []
    if (revertAction === 'bundle') {
      // all types of target node
      const revealedNodes = state[pl.slotId].nodes.filter(n => isBundledIn(n, groupId))
      revealedNodes.forEach(n => {
        revealedNodeIds.push(n.uuid)
        removeVisAttr(n, 'bundledIn')
        newVisibleNodes.push(
          n.properties
            ? {
                uuid: n.uuid,
                properties: new Map(n.properties)
              }
            : { uuid: n.uuid, properties: new Map() }
        )
      })
      removeVisAttr(groupNode, 'bundledNodes')
    } else {
      const revealedNodes = state[pl.slotId].nodes.filter(n => isReplacedBy(n, groupId))
      revealedNodes.forEach(n => {
        revealedNodeIds.push(n.uuid)
        removeVisAttr(n, 'replacedBy')
        newVisibleNodes.push(
          n.properties && n.properties.get('graph.x') && n.properties.get('graph.y')
            ? {
                uuid: n.uuid,
                properties: new Map(n.properties)
              }
            : { uuid: n.uuid, properties: new Map() }
        )
      })

      // remove the target node
      Vue.set(
        state[pl.slotId],
        'nodes',
        state[pl.slotId].nodes.filter(n => n.uuid !== groupId)
      )
    }

    // convert state.nodes to hash map, because we will be doing recursive
    // lookups to update the connections of affected links
    const nodesMap = makeNodesMap(state[pl.slotId].nodes)

    // 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[pl.slotId].links) {
      // remove links originating from target node, when target is being removed
      // these also account for current multilinks, as they cannot be transferred
      if ((removeTargetNode || isMultilink(l)) && (l.source === groupId || l.target === groupId)) {
        removedLinkIds.push(l.uuid)
        if (isLinkVisible(l)) removedVisibleLinkIds.push(l.uuid)
        continue
      }

      // find links transferred to group
      if (l.properties) {
        if (!isMultilink(l) && (getLinkSource(l) === groupId || getLinkTarget(l) === groupId)) {
          // normal links from subgraph
          affectedLinks.push(l)
          continue
        } else if (
          l.properties.get('newSource') === groupId ||
          l.properties.get('newTarget') === groupId
        ) {
          affectedLinks.push(l)
          continue
        } else if (
          isMultilink(l) &&
          (revealedNodeIds.includes(l.source) || revealedNodeIds.includes(l.target))
        ) {
          // multilinks from subgraph
          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[pl.slotId].links.filter(
      l => !removedLinkIds.includes(l.uuid)
    )

    // 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('Expected multilink')
      if (isLinkVisible(l)) removedVisibleLinkIds.push(l.uuid)
      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.uuid)

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

      removeVisAttr(l, 'inMultilink')
      if (revertAction === 'bundle') removeVisAttr(l, 'isTransferred')

      // check, if we still transfer link or it is in original state
      if (l.source === updatedSource && l.target === updatedTarget) {
        removeVisAttr(l, 'newSource')
        removeVisAttr(l, 'newTarget')
        removeVisAttr(l, 'isTransferred')
      } else {
        l.properties.set('newSource', updatedSource)
        l.properties.set('newTarget', updatedTarget)
        setVisAttr(l, 'isTransferred', true)
      }

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

    updatedAffectedLinks.forEach(l => {
      if (isLinkVisible(l))
        newVisibleLinks.push({
          uuid: l.uuid,
          source:
            l.properties && l.properties.get('newSource')
              ? l.properties.get('newSource')
              : l.source,
          target:
            l.properties && l.properties.get('newTarget') ? l.properties.get('newTarget') : l.target
        })
    })

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

export const actions = {
  ...tags.actions,
  ...vis.actions,

  /**
   * Initialize given slot with dataset
   * @param commit
   * @param {Object} pl payload
   * @param {number}        pl.slotId dataset slot
   * @param {NetvisState[]} pl.state
   */
  import({ commit }, pl) {
    if (pl.slotId === undefined) throw Error(`Invalid payload: ${JSON.stringify(pl)}`)

    // validateDataset(pl)
    commit('INIT', pl)
  },

  /**
   * @param state
   * @param commit
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {Node}     pl.node to add
   */
  createNode({ state, commit }, pl) {
    if (pl.slotId === undefined || !pl.node) throw Error(`Invalid payload: ${JSON.stringify(pl)}`)

    commit('ADD_NODE', pl)
    validateDataset(state[pl.slotId])
  },

  /**
   * @param commit
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {Object}   pl.diff nodes diff
   */
  updateNodes({ commit }, pl) {
    if (pl.slotId === undefined || !pl.diff) throw Error(`Invalid payload: ${JSON.stringify(pl)}`)

    commit('UPDATE_NODES', pl)
  },

  /**
   * Aggregate nodes into group or alias
   * @param state
   * @param commit
   * @param dispatch
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {'group'|'alias'} pl.action
   * @param {number[]} pl.nodeIds to group
   * @param {string}   pl.label to group
   */
  groupNodes({ state, commit, dispatch }, pl) {
    if (pl.slotId === undefined || !pl.nodeIds || !['group', 'alias'].includes(pl.action))
      throw Error(`Invalid payload: ${JSON.stringify(pl)}`)

    let groupNode = {
      name: pl.action === 'group' ? `Group` : `Alias` + ` of '${pl.label}'`,
      type: pl.action,
      properties: new Map([
        ['label', pl.label],
        ['icon', pl.action]
      ])
    }

    commit('AGGREGATE_NODES', {
      slotId: pl.slotId,
      action: pl.action,
      nodeIds: pl.nodeIds,
      targetGroupNode: groupNode
    })

    validateDataset(state[pl.slotId])
    dispatch('deselectNodes', pl)
  },

  /**
   * @param state
   * @param commit
   * @param getters
   * @param dispatch
   * @param {object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {number}   pl.representativeNodeId the "bundle" node
   * @param {number[]} pl.nodeIds to bundle inside the representative node
   */
  bundleNodes({ state, commit, getters, dispatch }, pl) {
    if (pl.slotId === undefined || pl.representativeNodeId === undefined || !pl.nodeIds)
      throw Error(`Invalid payload: ${JSON.stringify(pl)}`)

    commit('AGGREGATE_NODES', {
      slotId: pl.slotId,
      action: 'bundle',
      nodeIds: pl.nodeIds,
      targetGroupNode: getters.getNodeById(pl.slotId, pl.representativeNodeId)
    })
    validateDataset(state[pl.slotId])
    dispatch('deselectNodes', pl)
  },

  /**
   * @param state
   * @param commit
   * @param getters
   * @param dispatch
   * @param {object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {number}   pl.nodeId node to ungroup
   */
  ungroupNode({ state, commit, getters, dispatch }, pl) {
    if (pl.slotId === undefined || pl.nodeId === undefined)
      throw Error(`Invalid payload: ${JSON.stringify(pl)}`)

    // unbundle first if bundle
    if (getters.getNodeBundledNodeCount(pl.slotId, pl.nodeId) > 0) {
      commit('DISAGGREGATE_NODE', {
        slotId: pl.slotId,
        revertAction: 'bundle',
        nodeId: pl.nodeId
      })
    }

    commit('DISAGGREGATE_NODE', {
      slotId: pl.slotId,
      revertAction: 'group',
      nodeId: pl.nodeId
    })
    validateDataset(state[pl.slotId])
    dispatch('deselectNodes', pl)
  },

  /**
   * @param state
   * @param commit
   * @param {object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {number}   pl.nodeId node to ungroup
   */
  unbundleNode({ state, commit }, pl) {
    if (pl.slotId === undefined || pl.nodeId === undefined)
      throw Error(`Invalid payload: ${JSON.stringify(pl)}`)

    commit('DISAGGREGATE_NODE', {
      slotId: pl.slotId,
      revertAction: 'bundle',
      nodeId: pl.nodeId
    })
    validateDataset(state[pl.slotId])
  },

  simulateBatchGrouping({ state, commit }, pl) {
    assertSlot(state, pl.slotId)

    let nonGroupedNodeIds = []
    filterVisibleNodes(state[pl.slotId].nodes).forEach(n => nonGroupedNodeIds.push(n.uuid))
    const groupedNodesIds = []

    const updateGraph = async () => {
      // grouping
      while (nonGroupedNodeIds.length >= 2) {
        await sleep(300)
        let chooseCount = _.random(2, (state[pl.slotId].nodes.length / 6) >> 0)
        if (chooseCount > nonGroupedNodeIds.length) chooseCount = nonGroupedNodeIds.length
        if (chooseCount < 2) chooseCount = 2

        let chosenIds = []
        while (chosenIds.length < chooseCount) {
          const chosenId = nonGroupedNodeIds[_.random(0, nonGroupedNodeIds.length - 1)]
          chosenIds.push(chosenId)
          nonGroupedNodeIds = nonGroupedNodeIds.filter(n => n !== chosenId)
        }
        if (chosenIds.length < 2) throw Error('chosen less then 2 nodes for grouping')

        const chosenType = _.random(0, 100) < 80 ? 'group' : 'alias'
        const groupNode = {
          type: chosenType,
          label: chosenType === 'group' ? 'Group' : 'Alias'
        }
        commit('AGGREGATE_NODES', {
          slotId: pl.slotId,
          action: chosenType,
          nodeIds: chosenIds,
          targetGroupNode: groupNode
        })

        chosenIds.forEach(id => groupedNodesIds.push(id))
      }
    }
    updateGraph()
  }
}

export const getters = {
  ...tags.getters,
  ...vis.getters,

  getState: state => slotId => state[slotId],
  getNodes: state => slotId => state[slotId].nodes,
  getLinks: state => slotId => state[slotId].links,

  getIsImported: state => slotId => state[slotId].isImported,

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

  getNodeById: state => (slotId, uuid) => findNodeByUuid(state[slotId].nodes, uuid),
  getNodesByIds: state => (slotId, uuids) =>
    state[slotId].nodes.filter(n => uuids.includes(n.uuid)),
  getLinkById: state => (slotId, uuid) => state[slotId].links.find(l => l.uuid === uuid),

  getMultilinkLinks: state => (slotId, multilinkId) =>
    state[slotId].links.filter(l => isInMultilinkOfId(l, multilinkId)),

  getLinkIdsConnectedToNode: state => (slotId, nodeId) =>
    findConnectedLinks(state[slotId].links, nodeId),

  // FIXME: deprecated
  getNodeBundledNodeCount: state => (slotId, uuid) =>
    getBundleSize(
      state[slotId].nodes.find(n => n.uuid === uuid),
      uuid
    ),

  getNodesInAggregation: state => (slotId, parentId) =>
    state[slotId].nodes.filter(n => isReplacedBy(n, parentId)),

  getNodesInBundle: state => (slotId, bundleId) =>
    state[slotId].nodes.filter(n => isBundledIn(n, bundleId))
}
