text: const { Vec3 } = require('vec3')
  const assert = require('assert')
  const math = require('../math')
  const conv = require('../conversions')
  const { performance } = require('perf_hooks')
  const { callbackify, createDoneTask, createTask } = require('../promise_utils')
  
  const { Physics, PlayerState } = require('prismarine-physics')
  
  module.exports = inject
  
  const PI = Math.PI
  const PI_2 = Math.PI * 2
  const PHYSICS_INTERVAL_MS = 50
  const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000
  
  function inject (bot, { physicsEnabled }) {
      const mcData = require('minecraft-data')(bot.version)
  
      const world = { getBlock: (pos) => { return bot.blockAt(pos, false) } }
      const physics = Physics(mcData, world)
  
      bot.jumpQueued = false
      bot.jumpTicks = 0 // autojump cooldown
  
      const controlState = {
          forward: false,
          back: false,
          left: false,
          right: false,
          jump: false,
          sprint: false,
          sneak: false
      }
      bot.lastSentYaw = null
      bot.lastSentPitch = null
      bot.lastRollEvents = {
          yaw: null,
          pitch: null
      }
      let doPhysicsTimer = null
      let lastPhysicsFrameTime = null
      let shouldUsePhysics = false
      bot.physicsEnabled = physicsEnabled ?? true
  
      bot.lastSent = {
          x: 0,
          y: 0,
          z: 0,
          yaw: 0,
          pitch: 0,
          onGround: false,
          time: 0
      }
  
      // This function should be executed each tick (every 0.05 seconds)
      // How it works: https://gafferongames.com/post/fix_your_timestep/
      let timeAccumulator = 0
      function doPhysics () {
          const now = performance.now()
          const deltaSeconds = (now - lastPhysicsFrameTime) / 1000
          lastPhysicsFrameTime = now
  
          timeAccumulator += deltaSeconds
  
          while (timeAccumulator >= PHYSICS_TIMESTEP) {
              if (bot.physicsEnabled && shouldUsePhysics) {
                  physics.simulatePlayer(new PlayerState(bot, controlState), world).apply(bot)
                  bot.emit('physicsTick')
                  bot.emit('physicTick') // Deprecated, only exists to support old plugins. May be removed in the future
              }
              updatePosition(PHYSICS_TIMESTEP)
              timeAccumulator -= PHYSICS_TIMESTEP
          }
      }
  
      function cleanup () {
          clearInterval(doPhysicsTimer)
          doPhysicsTimer = null
      }
  
      function sendPacketPosition (position, onGround) {
          // sends data, no logic
          const oldPos = new Vec3(bot.lastSent.x, bot.lastSent.y, bot.lastSent.z)
          bot.lastSent.x = position.x
          bot.lastSent.y = position.y
          bot.lastSent.z = position.z
          bot.lastSent.onGround = onGround
          bot._client.write('position', bot.lastSent)
          bot.emit('move', oldPos)
      }
  
      function sendPacketLook (yaw, pitch, onGround) {
          // sends data, no logic
          const oldPos = new Vec3(bot.lastSent.x, bot.lastSent.y, bot.lastSent.z)
          bot.lastSent.yaw = yaw
          bot.lastSent.pitch = pitch
          bot.lastSent.onGround = onGround
          bot.lastRollEvents.yaw = bot.lastSentYaw
          bot.lastRollEvents.pitch = bot.lastSentPitch
          bot._client.write('look', bot.lastSent)
          bot.emit('move', oldPos)
          bot.emit('look', bot.lastRollEvents)
      }
  
      function sendPacketPositionAndLook (position, yaw, pitch, onGround) {
          // sends data, no logic
          const oldPos = new Vec3(bot.lastSent.x, bot.lastSent.y, bot.lastSent.z)
          bot.lastSent.x = position.x
          bot.lastSent.y = position.y
          bot.lastSent.z = position.z
          bot.lastSent.yaw = yaw
          bot.lastSent.pitch = pitch
          bot.lastSent.onGround = onGround
          bot.lastRollEvents.yaw = bot.lastSentYaw
          bot.lastRollEvents.pitch = bot.lastSentPitch
          bot._client.write('position_look', bot.lastSent)
          bot.emit('move', oldPos)
          bot.emit('look', bot.lastRollEvents)
      }
  
      function deltaYaw (yaw1, yaw2) {
          let dYaw = (yaw1 - yaw2) % PI_2
          if (dYaw < -PI) dYaw += PI_2
          else if (dYaw > PI) dYaw -= PI_2
  
          return dYaw
      }
  
      function updatePosition (dt) {
          // If you're dead, you're probably on the ground though ...
          if (!bot.isAlive) bot.entity.onGround = true
  
          // Increment the yaw in baby steps so that notchian clients (not the server) can keep up.
          const dYaw = deltaYaw(bot.entity.yaw, bot.lastSentYaw)
          const dPitch = bot.entity.pitch - (bot.lastSentPitch || 0)
  
          // Vanilla doesn't clamp yaw, so we don't want to do it either
          const maxDeltaYaw = dt * physics.yawSpeed
          const maxDeltaPitch = dt * physics.pitchSpeed
          bot.lastSentYaw += math.clamp(-maxDeltaYaw, dYaw, maxDeltaYaw)
          bot.lastSentPitch += math.clamp(-maxDeltaPitch, dPitch, maxDeltaPitch)
  
          const yaw = Math.fround(conv.toNotchianYaw(bot.lastSentYaw))
          const pitch = Math.fround(conv.toNotchianPitch(bot.lastSentPitch))
          const position = bot.entity.position
          const onGround = bot.entity.onGround
  
          // Only send a position update if necessary, select the appropriate packet
          const positionUpdated = bot.lastSent.x !== position.x || bot.lastSent.y !== position.y || bot.lastSent.z !== position.z
          const lookUpdated = bot.lastSent.yaw !== yaw || bot.lastSent.pitch !== pitch
  
          if (positionUpdated && lookUpdated && bot.isAlive) {
              sendPacketPositionAndLook(position, yaw, pitch, onGround)
          } else if (positionUpdated && bot.isAlive) {
              sendPacketPosition(position, onGround)
          } else if (lookUpdated && bot.isAlive) {
              sendPacketLook(yaw, pitch, onGround)
          } else if (performance.now() - bot.lastSent.time >= 1000) {
              // Send a position packet every second, even if no update was made
              sendPacketPosition(position, onGround)
              bot.lastSent.time = performance.now()
          } else if (bot.supportFeature('positionUpdateSentEveryTick') && bot.isAlive) {
              // For versions < 1.12, one player packet should be sent every tick
              // for the server to update health correctly
              bot._client.write('flying', { onGround: bot.entity.onGround })
          }
      }
  
      bot.physics = physics
  
      bot.setControlState = (control, state) => {
          assert.ok(control in controlState, `invalid control: ${control}`)
          assert.ok(typeof state === 'boolean', `invalid state: ${state}`)
          if (controlState[control] === state) return
          controlState[control] = state
          if (control === 'jump' && state) {
              bot.jumpQueued = true
          } else if (control === 'sprint') {
              bot._client.write('entity_action', {
                  entityId: bot.entity.id,
                  actionId: state ? 3 : 4,
                  jumpBoost: 0
              })
          } else if (control === 'sneak') {
              bot._client.write('entity_action', {
                  entityId: bot.entity.id,
                  actionId: state ? 0 : 1,
                  jumpBoost: 0
              })
          }
      }
  
      bot.getControlState = (control) => {
          assert.ok(control in controlState, `invalid control: ${control}`)
          return controlState[control]
      }
  
      bot.clearControlStates = () => {
          for (const control in controlState) {
              bot.setControlState(control, false)
          }
      }
  
      bot.controlState = {}
  
      for (const control of Object.keys(controlState)) {
          Object.defineProperty(bot.controlState, control, {
              get () {
                  return controlState[control]
              },
              set (state) {
                  bot.setControlState(control, state)
                  return state
              }
          })
      }
  
      let lookingTask = createDoneTask()
  
      bot.on('move', () => {
          if (!lookingTask.done && Math.abs(deltaYaw(bot.entity.yaw, bot.lastSentYaw)) < 0.001) {
              lookingTask.finish()
          }
      })
  
      bot._client.on('explosion', explosion => {
          // TODO: emit an explosion event with more info
          if (bot.physicsEnabled && bot.game.gameMode !== 'creative') {
              bot.entity.velocity.x += explosion.playerMotionX
              bot.entity.velocity.y += explosion.playerMotionY
              bot.entity.velocity.z += explosion.playerMotionZ
          }
      })
  
      bot.look = callbackify(async (yaw, pitch, force) => {
          if (!lookingTask.done) {
              lookingTask.finish() // finish the previous one
          }
          lookingTask = createTask()
  
          // this is done to bypass certain anticheat checks that detect the player's sensitivity
          // by calculating the gcd of how much they move the mouse each tick
          const sensitivity = conv.fromNotchianPitch(0.15) // this is equal to 100% sensitivity in vanilla
          const yawChange = Math.round((yaw - bot.entity.yaw) / sensitivity) * sensitivity
          const pitchChange = Math.round((pitch - bot.entity.pitch) / sensitivity) * sensitivity
  
          bot.entity.yaw += yawChange
          bot.entity.pitch += pitchChange
  
          if (force) {
              bot.lastSentYaw = yaw
              return
          }
  
          await lookingTask.promise
      })
  
      bot.lookAt = callbackify(async (point, force) => {
          const delta = point.minus(bot.entity.position.offset(0, bot.entity.height, 0))
          const yaw = Math.atan2(-delta.x, -delta.z)
          const groundDistance = Math.sqrt(delta.x * delta.x + delta.z * delta.z)
          const pitch = Math.atan2(delta.y, groundDistance)
          await bot.look(yaw, pitch, force)
      })
  
      // player position and look (clientbound)
      bot._client.on('position', (packet) => {
          bot.entity.height = 1.62
          bot.entity.velocity.set(0, 0, 0)
  
          // If flag is set, then the corresponding value is relative, else it is absolute
          const pos = bot.entity.position
          pos.set(
              packet.flags & 1 ? (pos.x + packet.x) : packet.x,
              packet.flags & 2 ? (pos.y + packet.y) : packet.y,
              packet.flags & 4 ? (pos.z + packet.z) : packet.z
          )
  
          const newYaw = (packet.flags & 8 ? conv.toNotchianYaw(bot.entity.yaw) : 0) + packet.yaw
          const newPitch = (packet.flags & 16 ? conv.toNotchianPitch(bot.entity.pitch) : 0) + packet.pitch
          bot.entity.yaw = conv.fromNotchianYaw(newYaw)
          bot.entity.pitch = conv.fromNotchianPitch(newPitch)
          bot.entity.onGround = false
  
          if (bot.supportFeature('teleportUsesOwnPacket')) {
              bot._client.write('teleport_confirm', { teleportId: packet.teleportId })
              // Force send an extra packet to be like vanilla client
              sendPacketPositionAndLook(pos, newYaw, newPitch, bot.entity.onGround)
          }
          sendPacketPositionAndLook(pos, newYaw, newPitch, bot.entity.onGround)
  
          shouldUsePhysics = true
          bot.entity.timeSinceOnGround = 0
          bot.lastSentYaw = bot.entity.yaw
  
          if (doPhysicsTimer === null) {
              lastPhysicsFrameTime = performance.now()
              doPhysicsTimer = setInterval(doPhysics, PHYSICS_INTERVAL_MS)
          }
          bot.emit('forcedMove')
      })
  
      bot.waitForTicks = async function (ticks) {
          if (ticks <= 0) return
          await new Promise(resolve => {
              const tickListener = () => {
                  ticks--
                  if (ticks === 0) {
                      bot.removeListener('physicsTick', tickListener)
                      resolve()
                  }
              }
  
              bot.on('physicsTick', tickListener)
          })
      }
  
      bot.on('mount', () => { shouldUsePhysics = false })
      bot.on('respawn', () => { shouldUsePhysics = false })
      bot.on('end', cleanup)
  }
  