// UTILS

function splitArray(arr, predicate) {
  let lastArr = [];
  const multiArr = [lastArr];
  arr.forEach(item => {
    if (predicate(item)) {
      if (lastArr.length > 0) {
        lastArr = [];
        multiArr.push(lastArr);
      }
    } else {
      lastArr.push(item);
    }
  });
  return multiArr;
}

function joinArrays(arrays, joiner) {
  const joinedArr = [];
  arrays.forEach((arr, i) => {
    if (i > 0 && arr.length > 0) {
      joinedArr.push(joiner);
    }
    joinedArr.push(...arr);
  });
  return joinedArr;
}

const pushOntoMultiMap = (map, key, value) => {
  if (!map.has(key)) {
    map.set(key, []);
  }
  map.get(key).push(value);
};

function indexOfGroupContainingCommand(groups, command, ignoreGroup) {
  return groups.findIndex(
    candiateGroup =>
      candiateGroup !== ignoreGroup &&
      candiateGroup.some(candidateItem => candidateItem.command === command)
  );
}

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

  function visit(id) {
    if (marked.has(id)) {
      // Either this node has already been placed, or we have encountered a
      // cycle and need to exit.
      return;
    }
    marked.add(id);
    const edges = edgesById.get(id);
    if (edges != null) {
      edges.forEach(visit);
    }
    sorted.push(id);
  }

  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 toCommands = [...(item.before || []), ...(item.after || [])];
      for (const command of toCommands) {
        const index = indexOfGroupContainingCommand(groups, command, group);
        if (index === -1) {
          // No valid edge for this command
          continue;
        }
        const mergeTarget = groups[index];
        // Merge with group containing `command`
        mergeTarget.push(...group);
        groups.splice(i, 1);
        return true;
      }
    }
  }
  return false;
}

// Merge groups based on before/after positions
// Mutates both the array of groups, and the individual group arrays.
function mergeGroups(groups) {
  let mergedAGroup = true;
  while (mergedAGroup) {
    mergedAGroup = attemptToMergeAGroup(groups);
  }
  return groups;
}

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

  group.forEach((item, i) => {
    if (item.before) {
      item.before.forEach(toCommand => {
        const to = commandToIndex.get(toCommand);
        if (to != null) {
          pushOntoMultiMap(edges, to, i);
        }
      });
    }
    if (item.after) {
      item.after.forEach(toCommand => {
        const to = commandToIndex.get(toCommand);
        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 command of item.beforeGroupContaining) {
        const to = indexOfGroupContainingCommand(groups, command, group);
        if (to !== -1) {
          pushOntoMultiMap(edges, to, i);
          return;
        }
      }
    }
    if (item.afterGroupContaining) {
      for (const command of item.afterGroupContaining) {
        const to = indexOfGroupContainingCommand(groups, command, 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 isSeparator(item) {
  return item.type === 'separator';
}

function sortMenuItems(menuItems) {
  // Split the items into their implicit groups based upon separators.
  const groups = splitArray(menuItems, isSeparator);
  // Merge groups that contain before/after references to eachother.
  const mergedGroups = mergeGroups(groups);
  // Sort each individual group internally.
  const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup);
  // Sort the groups based upon their beforeGroupContaining/afterGroupContaining
  // references.
  const sortedGroups = sortGroups(mergedGroupsWithSortedItems);
  // Join the groups back
  return joinArrays(sortedGroups, { type: 'separator' });
}

module.exports = { sortMenuItems };
