const assert = require('assert')
const { once } = require('events')

module.exports = inject

function inject (bot, { version }) {
  const { entitiesByName } = bot.registry
  const Item = require('prismarine-item')(bot.registry)

  let selectTrade
  if (bot.supportFeature('useMCTrSel')) {
    bot._client.registerChannel('MC|TrSel', 'i32')
    selectTrade = (choice) => {
      bot._client.writeChannel('MC|TrSel', choice)
    }
  } else {
    selectTrade = (choice) => {
      bot._client.write('select_trade', { slot: choice })
    }
  }

  const tradeListSchema = [
    'container',
    [
      { type: 'i32', name: 'windowId' },
      {
        name: 'trades',
        type: [
          'array',
          {
            countType: 'i8',
            type: [
              'container',
              [
                { type: 'slot', name: 'inputItem1' },
                { type: 'slot', name: 'outputItem' },
                { type: 'bool', name: 'hasItem2' },
                {
                  name: 'inputItem2',
                  type: [
                    'switch',
                    {
                      compareTo: 'hasItem2',
                      fields: {
                        true: 'slot',
                        false: 'void'
                      }
                    }
                  ]
                },
                { type: 'bool', name: 'tradeDisabled' },
                { type: 'i32', name: 'nbTradeUses' },
                { type: 'i32', name: 'maximumNbTradeUses' }
              ]
            ]
          }]
      }
    ]
  ]

  let tradeListPacket
  if (bot.supportFeature('useMCTrList')) {
    tradeListPacket = 'MC|TrList'
    bot._client.registerChannel('MC|TrList', tradeListSchema)
  } else if (bot.supportFeature('usetraderlist')) {
    tradeListPacket = 'minecraft:trader_list'
    bot._client.registerChannel('minecraft:trader_list', tradeListSchema)
  } else {
    tradeListPacket = 'trade_list'
  }

  async function openVillager (villagerEntity) {
    const villagerType = entitiesByName.villager ? entitiesByName.villager.id : entitiesByName.Villager.id
    assert.strictEqual(villagerEntity.entityType, villagerType)
    let ready = false

    const villagerPromise = bot.openEntity(villagerEntity)
    bot._client.on(tradeListPacket, gotTrades)
    const villager = await villagerPromise
    if (villager.type !== 'minecraft:villager' && villager.type !== 'minecraft:merchant') {
      throw new Error('This is not a villager')
    }

    villager.trades = null
    villager.selectedTrade = null

    villager.once('close', () => {
      bot._client.removeListener(tradeListPacket, gotTrades)
    })

    villager.trade = async (index, count) => {
      await bot.trade(villager, index, count)
    }

    if (!ready) await once(villager, 'ready')
    return villager

    async function gotTrades (packet) {
      const villager = await villagerPromise
      if (packet.windowId !== villager.id) return
      assert.ok(packet.trades)
      villager.trades = packet.trades.map(trade => {
        trade.inputs = [trade.inputItem1 = Item.fromNotch(trade.inputItem1 || { blockId: -1 })]
        if (trade.inputItem2?.itemCount != null) {
          trade.inputs.push(trade.inputItem2 = Item.fromNotch(trade.inputItem2 || { blockId: -1 }))
        }

        trade.hasItem2 = !!(trade.inputItem2 && trade.inputItem2.type && trade.inputItem2.count)
        trade.outputs = [trade.outputItem = Item.fromNotch(trade.outputItem || { blockId: -1 })]

        if (trade.demand !== undefined && trade.specialPrice !== undefined) { // the price is affected by demand and reputation
          const demandDiff = Math.max(0, Math.floor(trade.inputItem1.count * trade.demand * trade.priceMultiplier))
          trade.realPrice = Math.min(Math.max((trade.inputItem1.count + trade.specialPrice + demandDiff), 1), trade.inputItem1.stackSize)
        } else {
          trade.realPrice = trade.inputItem1.count
        }
        return trade
      })
      if (!ready) {
        ready = true
        villager.emit('ready')
      }
    }
  }

  async function trade (villager, index, count) {
    const choice = parseInt(index, 10) // allow string argument
    assert.notStrictEqual(villager.trades, null)
    assert.notStrictEqual(villager.trades[choice], null)
    const Trade = villager.trades[choice]
    villager.selectedTrade = Trade
    count = count || Trade.maximumNbTradeUses - Trade.nbTradeUses
    assert.ok(Trade.maximumNbTradeUses - Trade.nbTradeUses > 0, 'trade blocked')
    assert.ok(Trade.maximumNbTradeUses - Trade.nbTradeUses >= count)

    const itemCount1 = villager.count(Trade.inputItem1.type, Trade.inputItem1.metadata)
    const hasEnoughItem1 = itemCount1 >= Trade.realPrice * count
    let hasEnoughItem2 = true
    let itemCount2 = 0
    if (Trade.hasItem2) {
      itemCount2 = villager.count(Trade.inputItem2.type, Trade.inputItem2.metadata)
      hasEnoughItem2 = itemCount2 >= Trade.inputItem2.count * count
    }
    if (!hasEnoughItem1 || !hasEnoughItem2) {
      throw new Error('Not enough items to trade')
    }

    selectTrade(choice)
    if (bot.supportFeature('selectingTradeMovesItems')) { // 1.14+ the server moves items around by itself after selecting a trade
      const proms = []
      proms.push(once(villager, 'updateSlot:0'))
      if (Trade.hasItem2) proms.push(once(villager, 'updateSlot:1'))
      if (bot.supportFeature('setSlotAsTransaction')) {
        proms.push(once(villager, 'updateSlot:2'))
        await new Promise((resolve, reject) => {
          let countOfItemOneLeftToTake = itemCount1 > 64 ? 64 : itemCount1
          let countOfItemTwoLeftToTake = 0
          if (Trade.hasItem2) {
            countOfItemTwoLeftToTake = itemCount2 > 64 ? 64 : itemCount2
          }
          const listener = (slot, oldItem, newItem) => {
            if (!(slot >= villager.inventoryStart && slot <= villager.inventoryEnd)) return
            if (newItem === null) {
              if (oldItem.type === Trade.inputItem1.type) countOfItemOneLeftToTake -= oldItem.count
              else if (Trade.hasItem2 && oldItem.type === Trade.inputItem2.type) countOfItemTwoLeftToTake -= oldItem.count
            }
            if (countOfItemOneLeftToTake === 0 && countOfItemTwoLeftToTake === 0) {
              villager.off('updateSlot', listener)
              resolve()
            }
          }
          villager.on('updateSlot', listener)
        })
      }
      await Promise.all(proms)
    }

    for (let i = 0; i < count; i++) {
      await putRequirements(villager, Trade, count)
      // ToDo: See if this does anything kappa
      Trade.nbTradeUses++
      if (Trade.maximumNbTradeUses - Trade.nbTradeUses === 0) {
        Trade.tradeDisabled = true
      }
      if (!bot.supportFeature('setSlotAsTransaction')) {
        villager.updateSlot(2, Object.assign({}, Trade.outputItem))

        const [slot1, slot2] = villager.slots
        if (slot1) {
          assert.strictEqual(slot1.type, Trade.inputItem1.type)
          const updatedCount1 = slot1.count - Trade.realPrice
          const updatedSlot1 = updatedCount1 <= 0
            ? null
            : { ...slot1, count: updatedCount1 }
          villager.updateSlot(0, updatedSlot1)
        }

        if (slot2) {
          assert.strictEqual(slot2.type, Trade.inputItem2.type)
          const updatedCount2 = slot2.count - Trade.inputItem2.count
          const updatedSlot2 = updatedCount2 <= 0
            ? null
            : { ...slot2, count: updatedCount2 }
          villager.updateSlot(1, updatedSlot2)
        }
      }

      await bot.putAway(2)
    }

    for (const i of [0, 1]) {
      if (villager.slots[i]) {
        await bot.putAway(i) // 1.14+ whole stacks of items will automatically be placed , so there might be some left over
      }
    }
  }

  async function putRequirements (window, Trade, count) {
    const [slot1, slot2] = window.slots
    const { stackSize: stackSize1, type: type1, metadata: metadata1 } = Trade.inputItem1
    const tradeCount1 = Trade.realPrice
    const neededCount1 = Math.min(stackSize1, tradeCount1 * count)

    const input1 = !slot1
      ? neededCount1
      : (slot1.count < tradeCount1 ? neededCount1 - slot1.count : 0)
    await deposit(window, type1, metadata1, input1, 0)
    if (Trade.hasItem2) {
      const { count: tradeCount2, stackSize: stackSize2, type: type2, metadata: metadata2 } = Trade.inputItem2
      const needCount2 = Math.min(stackSize2, tradeCount2 * count)

      const input2 = !slot2
        ? needCount2
        : (slot2.count < tradeCount2 ? needCount2 - slot2.count : 0)
      await deposit(window, type2, metadata2, input2, 1)
    }
  }

  async function deposit (window, itemType, metadata, count, slot) {
    const options = {
      window,
      itemType,
      metadata,
      count,
      sourceStart: window.inventoryStart,
      sourceEnd: window.inventoryEnd,
      destStart: slot,
      destEnd: slot + 1
    }
    await bot.transfer(options)
  }

  bot.openVillager = openVillager
  bot.trade = trade
}
