function loader (registry, Item) {
  // TODO: anvil support for bedrock
  if (registry.type === 'bedrock') return undefined

  function combine (itemOne, itemTwo, creative, renamedName) {
    const rename = typeof renamedName === 'string'
    const data = {
      finalEnchs: [],
      fixedDurability: 0
    }
    let onlyRename = false // to tell if it's just a rename
    if (!combinePossible(itemOne, itemTwo) && itemTwo !== null) return { xpCost: 0, item: null }
    let cost = 0
    if (rename) {
      onlyRename = true
      const renameCost = getRenameCost(itemOne)
      if (renameCost === -1) return { xpCost: 0, item: null }
      cost += renameCost
    }
    if (itemOne.durabilityUsed !== 0) {
      onlyRename = false
      const { xpLevelCost: repairCost, fixedDurability, usedMats } = getRepairCost(itemOne, itemTwo)
      data.fixedDurability = fixedDurability
      data.usedMats = usedMats
      cost += repairCost
    }
    if (itemTwo && (itemTwo.name === itemOne.name || itemTwo.name === 'enchanted_book')) {
      onlyRename = false
      const { xpLevelCost: enchantCost, finalEnchs } = combineEnchants(itemOne, itemTwo, creative)
      data.finalEnchs = finalEnchs
      if (enchantCost === 0 && !rename && itemOne.metadata === 0) return { xpCost: 0, item: null } // no change
      cost += enchantCost
    }
    if (itemTwo === null && itemOne.customName === renamedName) return { xpCost: 0, item: null } // no change
    cost += itemOne.repairCost + (itemTwo?.repairCost ?? 0)

    if (cost > 39 && onlyRename) cost = 39
    else if (cost >= 40) return { xpCost: 0, item: null } // show too expensive message

    let finalItem = null
    if (itemOne) {
      finalItem = new Item(itemOne.type, itemOne.count, 0, JSON.parse(JSON.stringify(itemOne.nbt)), null, true)
      const resultDurability = itemOne.durabilityUsed - data.fixedDurability
      const repairCost = Math.max(itemOne.repairCost, (itemTwo?.repairCost ?? 0)) * 2 + 1
      if (data?.finalEnchs.length > 0) finalItem.enchants = data.finalEnchs
      if (rename) finalItem.customName = renamedName
      finalItem.repairCost = repairCost
      if (resultDurability && itemOne.name !== 'enchanted_book') finalItem.durabilityUsed = resultDurability
    }
    return { xpCost: cost, item: finalItem, usedMats: data.usedMats }
  }

  /**
   *
   * @param {Item} itemOne left hand item
   * @param {Item} itemTwo right hand item
   * @param {boolean} creative whether the bot is in creative mode
   * @returns {{finalEnchs: (*|[]|{lvl: *, name: *|null}[]|NormalizedEnchant[]), xpLevelCost: number}}
   * xpLevelCost is enchant data that is strictly from combining enchants
   * finalEnchs is the array of enchants on the final object
   */
  function combineEnchants (itemOne, itemTwo, creative) {
    const rightIsBook = itemTwo.name === 'enchanted_book'
    const finalEnchs = itemOne.enchants
    const finalEnchsByName = finalEnchs.map(x => x.name)
    const itemTwoEnch = itemTwo.enchants
    let xpLevelCost = 0
    for (const ench of itemTwoEnch) {
      const enchOnItemOne = finalEnchs.find(x => x.name === ench.name)
      let { exclude, maxLevel, category, weight } = registry.enchantmentsByName[ench.name]
      const multiplier = getMultipliers(weight, rightIsBook)
      if (!(itemOne.name === 'enchanted_book' && rightIsBook) && !registry.itemsByName[itemOne.name].enchantCategories.includes(category) && !creative) continue
      else if (enchOnItemOne === undefined) { // first item doesn't have this ench
        exclude = exclude.map(name => registry.enchantmentsByName[name].name)
        if (exclude.some(excludedEnch => finalEnchsByName.includes(excludedEnch))) { // has an excluded enchant
          xpLevelCost++
        } else {
          const finalLevel = ench.lvl
          xpLevelCost += finalLevel * multiplier
          finalEnchs.push({ name: ench.name, lvl: ench.lvl })
        }
      } else {
        let finalLevel = 0
        const itemOneLevel = enchOnItemOne.lvl
        const itemTwoLevel = ench.lvl
        if (itemOneLevel === itemTwoLevel) {
          finalLevel = Math.min(itemOneLevel + 1, maxLevel)
          enchOnItemOne.lvl = finalLevel
        } else if (itemTwoLevel > itemOneLevel) {
          finalLevel = itemTwoLevel
          enchOnItemOne.lvl = finalLevel
        } else if (itemOneLevel > itemTwoLevel) {
          finalLevel = itemOneLevel
        }
        xpLevelCost += finalLevel * multiplier
      }
    }
    return { xpLevelCost, finalEnchs }
  }

  // converts enchant weight to enchant cost multiplier
  function getMultipliers (weight, isBook) {
    const itemMultiplier = {
      10: 1,
      5: 2,
      2: 4,
      1: 8
    }[weight]
    return isBook ? Math.max(1, itemMultiplier / 2) : itemMultiplier
  }

  /**
   *
   * @param {Item} itemOne left hand item
   * @param {Item} itemTwo right hand item
   * @returns {{usedMats: number, fixedDurability: number, xpLevelCost: number}|number}
   * xpLevelCost is the number of xp levels used for repair (if any)
   * fixedDurability is duribility after using the anvil
   * usedMats is the number of materials used to fix the broken item (if many mats is used)
   */
  function getRepairCost (itemOne, itemTwo) {
    if (itemTwo === null) return { xpLevelCost: 0, fixedDurability: 0, usedMats: 0 } // air
    else if (itemTwo.name === 'enchanted_book') return { xpLevelCost: 0, fixedDurability: 0, usedMats: 0 }

    const maxDurability = registry.itemsByName[itemOne.name].maxDurability
    const durabilityLost = itemOne.durabilityUsed
    const fixMaterials = registry.itemsByName[itemOne.name].repairWith.concat([itemOne.name])
    if (!fixMaterials.includes(itemTwo.name) && itemOne.name !== itemTwo.name) {
      return 0 // Enchanted book can't fix
    }
    let results = {
      fixedDurability: 0,
      xpLevelCost: 0,
      usedMats: 0
    }
    if (itemTwo.name === itemOne.name) {
      const possibleFixedDura = Math.floor(0.12 * maxDurability) + itemTwo.metadata
      results = {
        fixedDurability: itemOne.metadata < possibleFixedDura ? itemOne.durabilityUsed : possibleFixedDura,
        xpLevelCost: 2,
        usedMats: 1
      }
    } else if (durabilityLost !== 0) {
      const durabilityFixedPerMat = Math.floor(maxDurability * 0.25)
      const matsToFullyRepair = Math.ceil(durabilityLost / durabilityFixedPerMat)
      if (itemTwo.count > matsToFullyRepair) { // takeall of itemTwo
        results = {
          fixedDurability: maxDurability - itemOne.durabilityUsed,
          xpLevelCost: matsToFullyRepair, // 1 exp lvl per mat used
          usedMats: matsToFullyRepair
        }
      } else if (itemOne && itemTwo) {
        results = {
          fixedDurability: Math.min(itemOne.durabilityUsed, durabilityFixedPerMat * itemTwo.count),
          xpLevelCost: itemTwo.count, // 1 exp lvl per mat used
          usedMats: itemTwo.count
        }
      }
    }
    return results
  }

  function getRenameCost (item) {
    if (item?.nbt?.value?.RepairCost?.value === 0x7fffffff) return -1
    return 1
  }

  function combinePossible (itemOne, itemTwo) {
    if (!itemOne?.name || !itemTwo?.name || (!itemOne?.name && !itemTwo?.name)) return false
    let fixMaterials = (registry.itemsByName[itemOne.name].repairWith ?? []).concat([itemOne.name])
    if (itemOne.name !== 'enchanted_book') fixMaterials = fixMaterials.concat(['enchanted_book'])
    return fixMaterials.includes(itemTwo.name)
  }

  return combine
}

module.exports = loader
