/**
 * This helper method will create an array of slots, where every slot contains one of
 * the provided `items`. Every slot has a unique `id` and when the items change, the
 * slots are recycled to avoid re-render. Every item that persists will it's own
 * slot id.
 *
 * This is useful so virtual scroll. The `id` of the slot can be used to as
 * `:key="slot.id"` value in the template. Because the id will always match the item
 * id, it will never update or re-render the component. New items will get a
 * recycled slot id so that they don't have to be re-rendered.
 *
 * It is a requirement that every item has an `id` property to make sure the id of
 * the slot is properly recycled.
 *
 * Optionally a `getPosition` can be provided to set the `position` property in each
 * slot. This is typically used by virtual scrolling change the absolute position of
 * the dom element. This could be needed because the elements before are not rendered
 * and are not pushing it into the right position.
 *
 * Example:
 *
 * const slots = recycleSlots(
 *  [],
 *  [
 *    { id: 1, name: "Item 1" },
 *    { id: 2, name: "Item 2" }
 *  ]
 * ) == [
 *   { id: 0, position: {}, item: { id: 1, name: "Item 1" } },
 *   { id: 1, position: {}, item: { id: 2, name: "Item 2" } }
 * ]
 *
 * recycleSlots(
 *  slots,
 *  [
 *    { id: 2, name: "Item 2" },
 *    { id: 3, name: "Item 3" }
 *  ]
 * ) == [
 *   { id: 0, position: {}, item: { id: 3, name: "Item 3" } },
 *   { id: 1, position: {}, item: { id: 2, name: "Item 2" } }
 * ]
 */
export const recycleSlots = (slots, items, getPosition, min = items.length) => {
  // If there are more items than the minimum that must be rendered, we want to
  // increase the minimum to ensure all items are visible.
  if (min < items.length) {
    min = items.length
  }

  for (let i = slots.length; i < min; i++) {
    slots.push({
      id: i,
      position: {},
      item: undefined,
    })
  }

  // Remove slots that aren't needed anymore.
  if (slots.length > min) {
    slots.splice(items.length, min)
  }

  const emptySlots = []
  const itemSlotIndexMap = {}

  // Loop over the slots and clear the items that must not be rendered anymore.
  slots.forEach((slot, index) => {
    if (slot.item === undefined || slot.item === null) {
      emptySlots.push(index)
      return
    }

    const itemIndex = items.findIndex(
      (item) => item !== null && item.id === slot.item.id
    )

    if (itemIndex < 0) {
      // Slot item is not visible anymore.
      slot.item = undefined
      slot.position = {}
      emptySlots.push(index)
    } else {
      // Item is found in slot array.
      itemSlotIndexMap[items[itemIndex].id] = index
    }
  })

  // Loop over the items and assign them to a slot if they don't yet exist.
  items.forEach((item, position) => {
    let index = itemSlotIndexMap[item?.id]

    // If item isn't in a slot yet we use the first empty slot index.
    if (index === undefined) {
      index = emptySlots.shift()
    }

    // Only update the item and position if it has changed in the slot to avoid
    // re-renders.
    if (slots[index].item !== item) {
      slots[index].item = item
    }

    const slotPosition = getPosition(item, position)
    if (
      JSON.stringify(slotPosition) !== JSON.stringify(slots[index].position)
    ) {
      slots[index].position = slotPosition
    }
  })

  // The remaining empty slots must be cleared because they could contain old items.
  emptySlots.forEach((slotIndex) => {
    slots[slotIndex].item = undefined
    slots[slotIndex].position = {}
  })
}

/**
 * This function will order the slots based on the item position in items array. The
 * slots will be moved without recreating the array to prevent re-rendering.
 */
export const orderSlots = (slots, items) => {
  let i = 0
  while (i < items.length) {
    const item = items[i]

    // Items can be null until they're fetched from server.
    if (item === null) {
      i++
      continue
    }

    const existingIndex = slots.findIndex(
      (slot) =>
        slot.item !== null &&
        slot.item !== undefined &&
        slot.item.id === item.id
    )

    if (existingIndex > -1 && existingIndex !== i) {
      // If the item already exists in the slots array, but the position match yet,
      // we need to move it.
      if (existingIndex < i) {
        // In this case, the existing index is lower than the new index, so in order
        // avoid conflicts with already moved items we just swap them.
        slots.splice(i, 0, slots.splice(existingIndex, 1)[0])
        slots.splice(existingIndex, 0, slots.splice(i - 1, 1)[0])
        i++
      } else if (existingIndex > i) {
        // If the existing index is higher than the expected index, we need to move
        // it one by one to avoid conflicts with already moved items.
        slots.splice(existingIndex - 1, 0, slots.splice(existingIndex, 1)[0])
      }
    } else {
      i++
    }
  }
}