import Core from './core.js'

const stringKeys = new Set([
  'website',
  'gametype',
  'gamemode',
  'player'
])

function normalizeEntry ([key, value]) {
  key = key.toLowerCase()
  const split = key.split('_')
  let keyType = key

  if (split.length === 2 && !isNaN(Number(split[1]))) {
    keyType = split[0]
  }

  if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope"
    if (value.toLowerCase() === 'true') {
      value = true
    } else if (value.toLowerCase() === 'false') {
      value = false
    } else if (value.length && !isNaN(Number(value))) {
      value = Number(value)
    }
  }

  return [key, value]
}

export default class gamespy1 extends Core {
  constructor () {
    super()
    this.encoding = 'latin1'
    this.byteorder = 'be'
  }

  async run (state) {
    const raw = await this.sendPacket('\\status\\xserverquery')
    // Convert all keys to lowercase and normalize value types
    const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry)))
    state.raw = data
    if ('hostname' in data) state.name = data.hostname
    if ('mapname' in data) state.map = data.mapname
    if (this.trueTest(data.password)) state.password = true
    if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers)
    if ('hostport' in data) state.gamePort = Number(data.hostport)

    const teamOffByOne = data.gamename === 'bfield1942'
    const playersById = {}
    const teamNamesById = {}
    for (const ident of Object.keys(data)) {
      const split = ident.split('_')
      if (split.length !== 2) continue
      let key = split[0].toLowerCase()
      const id = Number(split[1])
      if (isNaN(id)) continue
      let value = data[ident]

      delete data[ident]

      if (key !== 'team' && key.startsWith('team')) {
        // Info about a team
        if (key === 'teamname') {
          teamNamesById[id] = value
        } else {
          // other team info which we don't track
        }
      } else {
        // Info about a player
        if (!(id in playersById)) playersById[id] = {}

        if (key === 'playername' || key === 'player') {
          key = 'name'
        }
        if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed.
          key = 'teamId'
          value += teamOffByOne ? -1 : 0
        }

        playersById[id][key] = value
      }
    }
    state.raw.teams = teamNamesById

    const players = Object.values(playersById)

    for (const player of players) {
      // Convert player's team ID to team name if possible
      if (Object.prototype.hasOwnProperty.call(player, 'teamId')) {
        if (Object.keys(teamNamesById).length) {
          player.team = teamNamesById[player.teamId] || ''
        } else {
          player.team = player.teamId
          delete player.teamId
        }
      }

      state.players.push(player)
    }

    // Some servers (bf1942) send stale player entries when players leave the server (including duplicates)
    // Ignore any player list entries beyond the indicated number of players
    if (!isNaN(state.raw.numplayers) && state.players.length > state.raw.numplayers) {
      state.players = state.players.slice(0, state.raw.numplayers)
    }

    state.numplayers = state.players.length
    state.version = state.raw.gamever
  }

  async sendPacket (type) {
    let receivedQueryId
    const output = {}
    const parts = new Set()
    let maxPartNum = 0

    return await this.udpSend(type, buffer => {
      const reader = this.reader(buffer)
      const str = reader.string(buffer.length)
      const split = str.split('\\')
      split.shift()
      const data = {}
      while (split.length) {
        const key = split.shift()
        const value = split.shift() || ''
        data[key] = value
      }

      let queryId, partNum
      const partFinal = ('final' in data)
      if (data.queryid) {
        const split = data.queryid.split('.')
        if (split.length >= 2) {
          partNum = Number(split[1])
        }
        queryId = split[0]
      }
      delete data.final
      delete data.queryid
      this.logger.debug('Received part num=' + partNum + ' queryId=' + queryId + ' final=' + partFinal)

      if (queryId) {
        if (receivedQueryId && receivedQueryId !== queryId) {
          this.logger.debug('Rejected packet (Wrong query ID)')
          return
        } else if (!receivedQueryId) {
          receivedQueryId = queryId
        }
      }
      if (!partNum) {
        partNum = parts.size
        this.logger.debug('No part number received (assigned #' + partNum + ')')
      }
      if (parts.has(partNum)) {
        this.logger.debug('Rejected packet (Duplicate part)')
        return
      }
      parts.add(partNum)
      if (partFinal) {
        maxPartNum = partNum
      }

      this.logger.debug('Received part #' + partNum + ' of ' + (maxPartNum || '?'))
      for (const i of Object.keys(data)) {
        output[i] = data[i]
      }
      if (maxPartNum && parts.size === maxPartNum) {
        this.logger.debug('Received all parts')
        this.logger.debug(output)
        return output
      }
    })
  }
}
