import Vue from 'vue'
import { v4 as generateUuid } from 'uuid'
import { arraysEqual, assertSlot, setVisAttr, removeVisAttr } from './helpers'

/**
 * @typedef { import('./tags').Tag } Tag
 */

export const mutations = {
  /**
   * Tag objects in array
   * @param {Object} state
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {number[]} pl.arr array of object ids to tag
   * @param {number}   pl.uuid tag id to be assigned
   * @param {string}   pl.name name to be assigned
   * @param {string}   pl.color color to be assigned
   */
  TAG_NODES(state, pl) {
    assertSlot(state, pl.slotId)

    let tag = assignTags(
      state[pl.slotId].tags,
      state[pl.slotId].nodes,
      pl.arr,
      pl.uuid,
      pl.name,
      pl.color
    )

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

  /**
   * Switches tag's visibility
   * @param {Object} state
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {string}   pl.name tag name
   */
  SWITCH_TAG_VISIBILITY(state, pl) {
    assertSlot(state, pl.slotId)

    let tag = state[pl.slotId].tags.find(t => t.name === pl.name)
    if (!tag) throw Error(`Tag '${pl.name}' not found.`)
    tag.isVisualized = !tag.isVisualized
  },

  /**
   * @param {*} state
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {string}   pl.color tag color to be assigned
   */
  SET_TAG_COLOR(state, pl) {
    assertSlot(state, pl.slotId)

    state[pl.slotId].tagData.color = pl.color
  },

  /**
   * Changes tag's color and stroke
   * @param {*} state
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {string}   pl.name tag name
   * @param {string}   pl.color tag color
   */
  // FIXME: inadequate naming
  CHANGE_TAG_COLOR_OF_TAG(state, pl) {
    assertSlot(state, pl.slotId)

    let tag = state[pl.slotId].tags.find(t => t.name === pl.name)
    if (!tag) throw Error(`Tag '${pl.name}' not found.`)

    tag.properties.set('color', pl.properties.get('color'))
    if (tag.properties.get('color') === '') {
      tag.properties.set('color', 'none')
      tag.properties.set('stroke', '#000000')
    } else {
      tag.properties.set('stroke', '#000000')
    }
  },

  /**
   * @param {*} state
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {string}   pl.name tag name to be assigned
   */
  SET_TAG_NAME(state, pl) {
    assertSlot(state, pl.slotId)
    state[pl.slotId].tagData.name = pl.name
  },

  /**
   * @param {*} state
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   */
  SET_LAST_TAG_ID(state, pl) {
    assertSlot(state, pl.slotId)
    state[pl.slotId].lastTag = pl.uuid
  },

  /**
   * @param {*} state
   * @param {Object} pl payload
   * @param {number}   pl.slotId dataset slot
   * @param {number}   pl.uuid tag id of the newly created tag
   */
  SWITCH_TAG_IS_GROUPED(state, pl) {
    assertSlot(state, pl.slotId)
    let tag = findTagById(state[pl.slotId].tags, pl.uuid)
    tag.properties.set('isGrouped', !tag.properties.get('isGrouped'))
  },

  /**
   * @param {*} state
   * @param {Object} pl payload
   * @param {number}  pl.slotId
   * @param {number}  pl.tagId
   * @param {string}  pl.attr key to add
   * @param {*}       pl.val
   */
  SET_TAG_PROPERTY(state, pl) {
    assertSlot(state, pl.slotId)
    const tag = findTagById(state[pl.slotId].tags, pl.tagId)
    if (!tag) throw Error('Tag not found')
    // @todo FIX

    Vue.set(tag, pl.attr, pl.val)
  },

  /**
   * @param {*} state
   * @param {Object} pl payload
   * @param {number}  pl.slotId
   * @param {number}  pl.tagId
   * @param {string}  pl.attr to remove
   */
  REMOVE_TAG_ATTR(state, pl) {
    assertSlot(state, pl.slotId)
    const tag = findTagById(state[pl.slotId].tags, pl.tagId)
    if (!tag) throw Error('Tag not found')
    Vue.delete(tag, pl.attr)
  },

  //
  // MINIATURES OF NODES
  //
  /**
   * @param {*} state
   * @param {*} pl
   * @param {*}   pl.slotId
   * @param {*}   pl.nodeIds
   */
  MINIATURIZE_NODES(state, pl) {
    assertSlot(state, pl.slotId)
    for (const n of state[pl.slotId].nodes) {
      if (pl.nodeIds.includes(n.uuid)) setVisAttr(n, 'isMiniature', true)
    }
  },

  UNMINIATURIZE_NODES(state, pl) {
    assertSlot(state, pl.slotId)
    for (const n of state[pl.slotId].nodes) {
      if (pl.nodeIds.includes(n.uuid)) removeVisAttr(n, 'isMiniature')
    }
  }
}

export const actions = {
  tagNodes({ commit }, pl) {
    commit('TAG_NODES', pl)
  },

  switchTagVisibility({ commit }, pl) {
    commit('SWITCH_TAG_VISIBILITY', pl)
  },

  setTagColor({ commit }, pl) {
    commit('SET_TAG_COLOR', pl)
  },

  changeTagColorOfTag({ commit }, pl) {
    commit('CHANGE_TAG_COLOR_OF_TAG', pl)
  },

  setTagName({ commit }, pl) {
    commit('SET_TAG_NAME', pl)
  },

  switchTagIsGrouped({ commit }, pl) {
    commit('SWITCH_TAG_IS_GROUPED', pl)
  },

  setLastTag({ commit }, pl) {
    commit('SET_LAST_TAG_ID', pl)
  },

  // node miniatures
  /**
   * @param {*} _
   * @param {Object} pl payload
   * @param {number}  pl.slotId
   * @param {number}  pl.tagId
   */
  miniaturizeNodesByTag({ commit, getters }, pl) {
    const tag = getters.getTagByTagId(pl.slotId, pl.tagId)
    if (!tag) throw Error(`Tag id=${pl.tagId} not found`)
    pl.nodeIds = tag.assignedObjectsIds
    pl.nodeIds.push(tag.groupNodeId)
    commit('MINIATURIZE_NODES', pl)
    commit('SET_TAG_PROPERTY', { ...pl, property: 'isMiniaturized', val: true })
  },
  /**
   * @param {*} _
   * @param {Object} pl payload
   * @param {number}  pl.slotId
   * @param {number}  pl.tagId
   */
  unminiaturizeNodesByTag({ commit, getters }, pl) {
    const tag = getters.getTagByTagId(pl.slotId, pl.tagId)
    if (!tag) throw Error(`Tag id=${pl.tagId} not found`)
    pl.nodeIds = tag.assignedObjectsIds
    pl.nodeIds.push(tag.groupNodeId)
    commit('UNMINIATURIZE_NODES', pl)
    commit('SET_TAG_PROPERTY', { ...pl, property: 'isMiniaturized' })
  }
}

export const getters = {
  getTags: state => slotId => state[slotId].tags,
  getLastTagId: state => slotId => state[slotId].lastTag,
  getTagByTagId: state => (slotId, tagId) => findTagById(state[slotId].tags, tagId),
  // getTagByTagId: (state) => (slotId, tagId) =>
  //   state[slotId].tags.map((tagId) => state[slotId].tags[tagId]), // added, maybe add ids to the objects
  getTagsByNodeId: state => (slotId, nodeId) => filterTagsByNodeId(state[slotId].tags, nodeId),

  isTagMiniaturized: state => (slotId, tagId) => {
    const tag = findTagById(state[slotId].tags, tagId)
    return tag && tag.properties.get('isMiniaturized')
  }
}

/**
 * Return tags which are either assigned or created on the given node
 * @param {Tag[]} tags
 * @param {number} uuid of node
 */
export function filterTagsByNodeId(tags, uuid) {
  let filteredTags = []
  tags.forEach(tag => {
    if (tag.properties.get('groupNodeId') === uuid) filteredTags.push(tag)
    else if (tag.properties.get('assignedObjectsIds').includes(uuid)) filteredTags.push(tag)
    // else skip
  })
  return filteredTags
}

export function findTagById(tags, tagId) {
  return tags.find(t => t.uuid === tagId)
}

/**
 * Create new tag and push to tags array
 * @param {Tag[]}  tags from state
 * @property {number[]} assignedObjectsIds arr of node ids which have the tag
 * @param assignedObjectsIds
 * @param {number} tagId tag id to be assigned
 * @param {string}  name
 * @param {string}  color in hexa
 * @param {'group'|'alias'|'bundle'|'manual'|'link'} type if created automatically by aggregation or manually by user
 * @param {number} groupNodeId
 */
function createTag(tags, assignedObjectsIds, tagId, name, color, type, groupNodeId) {
  // if (type != 'bundle' && tags.find(t => t.name === name)) {
  //   throw Error('Tag with the same name already exists') // FIXME: not working (see tests for store/data)
  // }

  let stroke = 'none'

  if (color === '') {
    color = 'none'
    stroke = '#000000'
  }

  if (name === '') name = 'group ' + tagId

  let properties = new Map()
    .set('isVisualized', true)
    .set('color', color)
    .set('stroke', stroke)
    .set('groupNodeId', groupNodeId)
    .set('isGrouped', groupNodeId !== null)
    .set('assignedObjectsIds', assignedObjectsIds)

  /**
   * @type {Tag}
   */
  const tag = {
    uuid: tagId,
    name: name,
    type: type,
    properties
  }
  tags.push(tag)

  return tag
}

/**
 * Assigns tags to array of objects
 *
 * @param {Tag[]} tags from state
 * @param {Node[]|Link[]} objects from state
 * @param {number[]} arrIds array of object ids (Nodes, Links)
 * @param {number} tagId tag id to be assigned
 * @param {string} name tag name
 * @param {string} color tag color
 * @param {'group'|'alias'|'bundle'|'manual'} [type] aggregation type
 * @param {number} [groupNodeId] aggregation node Id
 */
function assignTags(
  tags,
  objects,
  arrIds,
  tagId,
  name,
  color,
  type = 'manual',
  groupNodeId = null
) {
  /*arrIds.forEach(id => {
    let obj = objects.find(o => o.id === id)
    //obj.vis.tags.push(tagId) // FIXME: dataset doesn't have vis.tags
  })*/

  return createTag(tags, arrIds, tagId, name, color, type, groupNodeId)
}

/**
 * If tag already exists on given objects, it updates it, else creates new tag
 *
 * @param {Tag[]}  tags from state
 * @param {Node[]|Link[]} objects from state
 * @param {number[]} arrIds array of object ids
 * @param {'group'|'alias'|'bundle'} action aggregation type
 * @param {Node} groupNode aggregation node (we get obj ref. from data/AGGREGATE_NODES)
 * @param {string}  color in hexa
 */
export function updateTags(tags, objects, arrIds, action, groupNode, color) {
  for (let tag of tags) {
    // find equivalent tag
    // a tag is assigned after aggregation, but after dissagregating, it stays there, in case
    // the user wants to aggregate again using the tag
    if (arraysEqual(tag.properties.get('assignedObjectsIds'), arrIds)) {
      if (action !== 'bundle' && tag.properties.get('groupNodeId') !== groupNode.uuid) {
        tag.properties.set('groupNodeId', groupNode.uuid)
        tag.name = groupNode.properties.get('label')
        tag.type = action // if the tag was manually created before, and we are aggregating using the tag, save aggr. type
      }
      return null
      // if adding additional nodes to bundle, only update the tag, do not create new one
    } else if (action === 'bundle' && tag.properties.get('groupNodeId') === groupNode.uuid) {
      arrIds.forEach(uuid => {
        if (!tag.properties.get('assignedObjectsIds').includes(uuid)) {
          //let obj = objects.find(o => o.uuid === id)
          //obj.vis.tags.push(id)  // FIXME: dataset doesn't have vis.tags
          tag.properties.get('assignedObjectsIds').push(uuid)
        }
      })
      tag.name = groupNode.properties.get('label')
      return null
    }
  }

  // for 'group' & 'alias': new group node and nodes within the group get new tag
  // for 'bundle': group node is not part of the bundle, it's only representative node
  // FIXME: since we deleted "bundleNodeId" attr, bundles also need to have groupNodeId -> does this break something else?
  let groupNodeId = groupNode.uuid //action != 'bundle' ? groupNode.uuid : null

  return assignTags(
    tags,
    objects,
    arrIds,
    generateUuid(),
    groupNode.properties.get('label'),
    color,
    action, //'aggregation',
    groupNodeId
  )
}
