//@ts-check

import { Vec3 } from 'vec3'
import { proxy, subscribe } from 'valtio'

import { ControMax } from 'contro-max/build/controMax'
import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types'
import { stringStartsWith } from 'contro-max/build/stringUtils'
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState } from './globalState'
import { goFullscreen, pointerLock, reloadChunks } from './utils'
import { options } from './optionsStorage'
import { openPlayerInventory } from './inventoryWindows'
import { chatInputValueGlobal } from './react/Chat'
import { fsState } from './loadSave'
import { showOptionsModal } from './react/SelectOption'
import widgets from './react/widgets'
import { getItemFromBlock } from './botUtils'

// todo move this to shared file with component
export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}'))
subscribe(customKeymaps, () => {
  localStorage.keymap = JSON.parse(customKeymaps)
})

const controlOptions = {
  preventDefault: true
}

export const contro = new ControMax({
  commands: {
    general: {
      jump: ['Space', 'A'],
      inventory: ['KeyE', 'X'],
      drop: ['KeyQ', 'B'],
      sneak: ['ShiftLeft'],
      toggleSneakOrDown: [null, 'Right Stick'],
      sprint: ['ControlLeft', 'Left Stick'],
      nextHotbarSlot: [null, 'Left Bumper'],
      prevHotbarSlot: [null, 'Right Bumper'],
      attackDestroy: [null, 'Right Trigger'],
      interactPlace: [null, 'Left Trigger'],
      chat: [['KeyT', 'Enter']],
      command: ['Slash'],
      selectItem: ['KeyH'] // default will be removed
    },
    ui: {
      back: [null/* 'Escape' */, 'B'],
      click: [null, 'A'],
    },
    advanced: {
      lockUrl: ['KeyY'],
    }
    // waila: {
    //   showLookingBlockRecipe: ['Numpad3'],
    //   showLookingBlockUsages: ['Numpad4']
    // }
  } satisfies Record<string, Record<string, SchemaCommandInput>>,
  movementKeymap: 'WASD',
  movementVector: '2d',
  groupedCommands: {
    general: {
      switchSlot: ['Digits', []]
    }
  },
}, {
  defaultControlOptions: controlOptions,
  target: document,
  captureEvents () {
    return bot && isGameActive(false)
  },
  storeProvider: {
    load: () => customKeymaps,
    save () { },
  },
  gamepadPollingInterval: 10
})
window.controMax = contro
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']

export const setDoPreventDefault = (state: boolean) => {
  controlOptions.preventDefault = state
}

const setSprinting = (state: boolean) => {
  bot.setControlState('sprint', state)
  gameAdditionalState.isSprinting = state
}

contro.on('movementUpdate', ({ vector, gamepadIndex }) => {
  miscUiState.usingGamepadInput = gamepadIndex !== undefined
  // gamepadIndex will be used for splitscreen in future
  const coordToAction = [
    ['z', -1, 'forward'],
    ['z', 1, 'back'],
    ['x', -1, 'left'],
    ['x', 1, 'right'],
  ] as const

  const newState: Partial<typeof bot.controlState> = {}
  for (const [coord, v] of Object.entries(vector)) {
    if (v === undefined || Math.abs(v) < 0.3) continue
    // todo use raw values eg for slow movement
    const mappedValue = v < 0 ? -1 : 1
    // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
    const foundAction = coordToAction.find(([c, mapV]) => c === coord && mapV === mappedValue)?.[2]!
    newState[foundAction] = true
  }

  for (const key of ['forward', 'back', 'left', 'right'] as const) {
    if (newState[key] === bot.controlState[key]) continue
    const action = !!newState[key]
    if (action && !isGameActive(true)) continue
    bot.setControlState(key, action)

    if (key === 'forward') {
      // todo workaround: need to refactor
      if (action) {
        void contro.emit('trigger', { command: 'general.forward' } as any)
      } else {
        setSprinting(false)
      }
    }
  }
})

let lastCommandTrigger = null as { command: string, time: number } | null

const secondActionActivationTimeout = 300
const secondActionCommands = {
  'general.jump' () {
    toggleFly()
  },
  'general.forward' () {
    setSprinting(true)
  }
}

// detect pause open, as ANY keyup event is not fired when you exit pointer lock (esc)
subscribe(activeModalStack, () => {
  if (activeModalStack.length) {
    // iterate over pressedKeys
    for (const key of contro.pressedKeys) {
      contro.pressedKeyOrButtonChanged({ code: key }, false)
    }
  }
})

const uiCommand = (command: Command) => {
  if (command === 'ui.back') {
    hideCurrentModal()
  } else if (command === 'ui.click') {
    // todo cursor
  }
}

const setSneaking = (state: boolean) => {
  gameAdditionalState.isSneaking = state
  bot.setControlState('sneak', state)
}

const onTriggerOrReleased = (command: Command, pressed: boolean) => {
  // always allow release!
  if (pressed && !isGameActive(true)) {
    uiCommand(command)
    return
  }
  if (stringStartsWith(command, 'general')) {
    // handle general commands
    // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
    switch (command) {
      case 'general.jump':
        bot.setControlState('jump', pressed)
        break
      case 'general.sneak':
        setSneaking(pressed)
        break
      case 'general.sprint':
        // todo add setting to change behavior
        if (pressed) {
          setSprinting(pressed)
        }
        break
      case 'general.toggleSneakOrDown':
        if (gameAdditionalState.isFlying) {
          setSneaking(pressed)
        } else if (pressed) {
          setSneaking(!gameAdditionalState.isSneaking)
        }

        break
      case 'general.attackDestroy':
        document.dispatchEvent(new MouseEvent(pressed ? 'mousedown' : 'mouseup', { button: 0 }))
        break
      case 'general.interactPlace':
        document.dispatchEvent(new MouseEvent(pressed ? 'mousedown' : 'mouseup', { button: 2 }))
        break
    }
  }
}

// im still not sure, maybe need to refactor to handle in inventory instead
const alwaysHandledCommand = (command: Command) => {
  if (command === 'general.inventory') {
    if (activeModalStack.at(-1)?.reactType?.startsWith?.('player_win:')) { // todo?
      hideCurrentModal()
    }
  }
}

contro.on('trigger', ({ command }) => {
  const willContinue = !isGameActive(true)
  alwaysHandledCommand(command)
  if (willContinue) return

  const secondActionCommand = secondActionCommands[command]
  if (secondActionCommand) {
    if (command === lastCommandTrigger?.command && Date.now() - lastCommandTrigger.time < secondActionActivationTimeout) {
      const commandToTrigger = secondActionCommands[lastCommandTrigger.command]
      commandToTrigger()
      lastCommandTrigger = null
    } else {
      lastCommandTrigger = {
        command,
        time: Date.now(),
      }
    }
  }

  onTriggerOrReleased(command, true)

  if (stringStartsWith(command, 'general')) {
    // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
    switch (command) {
      case 'general.inventory':
        document.exitPointerLock?.()
        openPlayerInventory()
        break
      case 'general.drop':
        // if (bot.heldItem/* && ctrl */) bot.tossStack(bot.heldItem)
        bot._client.write('block_dig', {
          'status': 4,
          'location': {
            'x': 0,
            'z': 0,
            'y': 0
          },
          'face': 0,
          sequence: 0
        })
        break
      case 'general.chat':
        showModal({ reactType: 'chat' })
        break
      case 'general.command':
        chatInputValueGlobal.value = '/'
        showModal({ reactType: 'chat' })
        break
      case 'general.selectItem':
        void selectItem()
        break
    }
  }
  if (command === 'advanced.lockUrl') {
    let newQs = ''
    if (fsState.saveLoaded) {
      const save = localServer!.options.worldFolder.split('/').at(-1)
      newQs = `loadSave=${save}`
    } else if (process.env.NODE_ENV === 'development') {
      newQs = `reconnect=1`
    } else {
      const qs = new URLSearchParams()
      const { server, version } = localStorage
      qs.set('server', server)
      if (version) qs.set('version', version)
      newQs = String(qs.toString())
    }

    window.history.replaceState({}, '', `${window.location.pathname}?${newQs}`)
    // return
  }
})

contro.on('release', ({ command }) => {
  onTriggerOrReleased(command, false)
})

// hard-coded keybindings

export const f3Keybinds = [
  {
    key: 'KeyA',
    action () {
      //@ts-expect-error
      const loadedChunks = Object.entries(worldView.loadedChunks).filter(([, v]) => v).map(([key]) => key.split(',').map(Number))
      for (const [x, z] of loadedChunks) {
        worldView!.unloadChunk({ x, z })
      }
      for (const child of viewer.scene.children) {
        if (child.name === 'chunk') { // should not happen
          viewer.scene.remove(child)
          console.warn('forcefully removed chunk from scene')
        }
      }
      if (localServer) {
        //@ts-expect-error not sure why it is private... maybe revisit api?
        localServer.players[0].world.columns = {}
      }
      void reloadChunks()
    },
    mobileTitle: 'Reload chunks',
  },
  {
    key: 'KeyG',
    action () {
      options.showChunkBorders = !options.showChunkBorders
      viewer.world.updateShowChunksBorder(options.showChunkBorders)
    },
    mobileTitle: 'Toggle chunk borders',
  },
  {
    key: 'KeyT',
    async action () {
      // waypoints
      const widgetNames = widgets.map(widget => widget.name)
      const widget = await showOptionsModal('Open Widget', widgetNames)
      if (!widget) return
      showModal({ reactType: `widget-${widget}` })
    },
    mobileTitle: 'Open Widget'
  }
]

const hardcodedPressedKeys = new Set<string>()
document.addEventListener('keydown', (e) => {
  if (!isGameActive(false)) return
  if (hardcodedPressedKeys.has('F3')) {
    const keybind = f3Keybinds.find((v) => v.key === e.code)
    if (keybind) {
      keybind.action()
      e.stopPropagation()
    }
    return
  }

  hardcodedPressedKeys.add(e.code)
}, {
  capture: true,
})
document.addEventListener('keyup', (e) => {
  hardcodedPressedKeys.delete(e.code)
})
document.addEventListener('visibilitychange', (e) => {
  if (document.visibilityState === 'hidden') {
    hardcodedPressedKeys.clear()
  }
})

// #region creative fly
// these controls are more like for gamemode 3

const makeInterval = (fn, interval) => {
  const intervalId = setInterval(fn, interval)

  const cleanup = () => {
    clearInterval(intervalId)
    cleanup.active = false
  }
  cleanup.active = true
  return cleanup
}

const isFlying = () => bot.physics.gravity === 0
let endFlyLoop: ReturnType<typeof makeInterval> | undefined

const currentFlyVector = new Vec3(0, 0, 0)
window.currentFlyVector = currentFlyVector

// todo cleanup
const flyingPressedKeys = {
  down: false,
  up: false
}

const startFlyLoop = () => {
  if (!isFlying()) return
  endFlyLoop?.()

  endFlyLoop = makeInterval(() => {
    if (!bot) {
      endFlyLoop?.()
      return
    }

    bot.entity.position.add(currentFlyVector.clone().multiply(new Vec3(0, 0.5, 0)))
  }, 50)
}

// todo we will get rid of patching it when refactor controls
let originalSetControlState
const patchedSetControlState = (action, state) => {
  if (!isFlying()) {
    return originalSetControlState(action, state)
  }

  const actionPerFlyVector = {
    jump: new Vec3(0, 1, 0),
    sneak: new Vec3(0, -1, 0),
  }

  const changeVec = actionPerFlyVector[action]
  if (!changeVec) {
    return originalSetControlState(action, state)
  }
  if (flyingPressedKeys[state === 'jump' ? 'up' : 'down'] === state) return
  const toAddVec = changeVec.scaled(state ? 1 : -1)
  for (const coord of ['x', 'y', 'z']) {
    if (toAddVec[coord] === 0) continue
    if (currentFlyVector[coord] === toAddVec[coord]) return
  }
  currentFlyVector.add(toAddVec)
  flyingPressedKeys[state === 'jump' ? 'up' : 'down'] = state
}

const startFlying = (sendAbilities = true) => {
  if (sendAbilities) {
    bot._client.write('abilities', {
      flags: 2,
    })
  }
  // window.flyingSpeed will be removed
  bot.physics['airborneAcceleration'] = window.flyingSpeed ?? 0.1 // todo use abilities
  bot.entity.velocity = new Vec3(0, 0, 0)
  bot.creative.startFlying()
  startFlyLoop()
}

const endFlying = (sendAbilities = true) => {
  if (bot.physics.gravity !== 0) return
  if (sendAbilities) {
    bot._client.write('abilities', {
      flags: 0,
    })
  }
  Object.assign(flyingPressedKeys, {
    up: false,
    down: false
  })
  currentFlyVector.set(0, 0, 0)
  bot.physics['airborneAcceleration'] = standardAirborneAcceleration
  bot.creative.stopFlying()
  endFlyLoop?.()
}

let allowFlying = false

export const onBotCreate = () => {
  bot._client.on('abilities', ({ flags }) => {
    if (flags & 2) { // flying
      toggleFly(true, false)
    } else {
      toggleFly(false, false)
    }
    allowFlying = !!(flags & 4)
  })
}

const standardAirborneAcceleration = 0.02
const toggleFly = (newState = !isFlying(), sendAbilities?: boolean) => {
  // if (bot.game.gameMode !== 'creative' && bot.game.gameMode !== 'spectator') return
  if (!allowFlying) return
  if (bot.setControlState !== patchedSetControlState) {
    originalSetControlState = bot.setControlState
    bot.setControlState = patchedSetControlState
  }

  if (newState) {
    startFlying(sendAbilities)
  } else {
    endFlying(sendAbilities)
  }
  gameAdditionalState.isFlying = isFlying()
}
// #endregion

const selectItem = async () => {
  const block = bot.blockAtCursor(5)
  if (!block) return
  const itemId = getItemFromBlock(block)?.id
  if (!itemId) return
  const Item = require('prismarine-item')(bot.version)
  const item = new Item(itemId, 1, 0)
  await bot.creative.setInventorySlot(bot.inventory.hotbarStart + bot.quickBarSlot, item)
  bot.updateHeldItem()
}

addEventListener('mousedown', async (e) => {
  if ((e.target as HTMLElement).matches?.('#VRButton')) return
  void pointerLock.requestPointerLock()
  if (!bot) return
  // wheel click
  // todo support ctrl+wheel (+nbt)
  if (e.button === 1) {
    await selectItem()
  }
})

window.addEventListener('keydown', (e) => {
  if (e.code !== 'Escape') return
  if (activeModalStack.length) {
    hideCurrentModal(undefined, () => {
      if (!activeModalStack.length) {
        pointerLock.justHitEscape = true
      }
    })
  } else if (pointerLock.hasPointerLock) {
    document.exitPointerLock?.()
    if (options.autoExitFullscreen) {
      void document.exitFullscreen()
    }
  } else {
    document.dispatchEvent(new Event('pointerlockchange'))
  }
})

// #region experimental debug things
window.addEventListener('keydown', (e) => {
  if (e.code === 'F11') {
    e.preventDefault()
    void goFullscreen(true)
  }
  if (e.code === 'KeyL' && e.altKey) {
    console.clear()
  }
})
// #endregion
