function splitArray (arr, predicate) {
  const result = arr.reduce((multi, item) => {
    const current = multi[multi.length - 1]
    if (predicate(item)) {
      if (current.length > 0) multi.push([])
    } else {
      current.push(item)
    }
    return multi
  }, [[]])

  if (result[result.length - 1].length === 0) {
    return result.slice(0, result.length - 1)
  }
  return result
}

function joinArrays (arrays, joiner) {
  return arrays.reduce((joined, arr, i) => {
    if (i > 0 && arr.length) {
      joined.push(joiner)
    }
    return joined.concat(arr)
  }, [])
}

function pushOntoMultiMap (map, key, value) {
  if (!map.has(key)) {
    map.set(key, [])
  }
  map.get(key).push(value)
}

function indexOfGroupContainingID (groups, id, ignoreGroup) {
  return groups.findIndex(
    candidateGroup =>
      candidateGroup !== ignoreGroup &&
      candidateGroup.some(
        candidateItem => candidateItem.id === id
      )
  )
}

// Sort nodes topologically using a depth-first approach. Encountered cycles
// are broken.
function sortTopologically (originalOrder, edgesById) {
  const sorted = []
  const marked = new Set()

  const visit = (mark) => {
    if (marked.has(mark)) return
    marked.add(mark)
    const edges = edgesById.get(mark)
    if (edges != null) {
      edges.forEach(visit)
    }
    sorted.push(mark)
  }

  originalOrder.forEach(visit)
  return sorted
}

function attemptToMergeAGroup (groups) {
  for (let i = 0; i < groups.length; i++) {
    const group = groups[i]
    for (const item of group) {
      const toIDs = [...(item.before || []), ...(item.after || [])]
      for (const id of toIDs) {
        const index = indexOfGroupContainingID(groups, id, group)
        if (index === -1) continue
        const mergeTarget = groups[index]

        mergeTarget.push(...group)
        groups.splice(i, 1)
        return true
      }
    }
  }
  return false
}

function mergeGroups (groups) {
  let merged = true
  while (merged) {
    merged = attemptToMergeAGroup(groups)
  }
  return groups
}

function sortItemsInGroup (group) {
  const originalOrder = group.map((node, i) => i)
  const edges = new Map()
  const idToIndex = new Map(group.map((item, i) => [item.id, i]))

  group.forEach((item, i) => {
    if (item.before) {
      item.before.forEach(toID => {
        const to = idToIndex.get(toID)
        if (to != null) {
          pushOntoMultiMap(edges, to, i)
        }
      })
    }
    if (item.after) {
      item.after.forEach(toID => {
        const to = idToIndex.get(toID)
        if (to != null) {
          pushOntoMultiMap(edges, i, to)
        }
      })
    }
  })

  const sortedNodes = sortTopologically(originalOrder, edges)
  return sortedNodes.map(i => group[i])
}

function findEdgesInGroup (groups, i, edges) {
  const group = groups[i]
  for (const item of group) {
    if (item.beforeGroupContaining) {
      for (const id of item.beforeGroupContaining) {
        const to = indexOfGroupContainingID(groups, id, group)
        if (to !== -1) {
          pushOntoMultiMap(edges, to, i)
          return
        }
      }
    }
    if (item.afterGroupContaining) {
      for (const id of item.afterGroupContaining) {
        const to = indexOfGroupContainingID(groups, id, group)
        if (to !== -1) {
          pushOntoMultiMap(edges, i, to)
          return
        }
      }
    }
  }
}

function sortGroups (groups) {
  const originalOrder = groups.map((item, i) => i)
  const edges = new Map()

  for (let i = 0; i < groups.length; i++) {
    findEdgesInGroup(groups, i, edges)
  }

  const sortedGroupIndexes = sortTopologically(originalOrder, edges)
  return sortedGroupIndexes.map(i => groups[i])
}

function sortMenuItems (menuItems) {
  const isSeparator = (item) => item.type === 'separator'

  // Split the items into their implicit groups based upon separators.
  const groups = splitArray(menuItems, isSeparator)
  const mergedGroups = mergeGroups(groups)
  const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup)
  const sortedGroups = sortGroups(mergedGroupsWithSortedItems)

  const joined = joinArrays(sortedGroups, { type: 'separator' })
  return joined
}

module.exports = {sortMenuItems}
