{_} = require 'underscore'
{User, MaskedUser} = require '../user'
{FakeRNG} = require './rng'
{Pokemon} = require './pokemon'
{Move} = require './move'
{Team} = require './team'
{Weather} = require '../../shared/weather'
{Attachment, Attachments, Status, BaseAttachment} = require './attachment'
{Protocol} = require '../../shared/protocol'
{CannedText} = require('../../shared/canned_text')
Query = require './queries'
{Room} = require '../rooms'
logger = require '../logger'

# Represents a single ongoing battle
class @Battle extends Room
  {Moves, MoveList, SpeciesData, FormeData} = require './data'
  Moves: Moves
  MoveList: MoveList
  SpeciesData: SpeciesData
  FormeData: FormeData

  generation: 'bw'

  # 1 hour
  ONGOING_BATTLE_TTL: 1 * 60 * 60 * 1000

  # 30 minutes
  ENDED_BATTLE_TTL: 30 * 60 * 1000

  actionMap:
    switch:
      priority: -> 10
      action: (action) ->
        @performSwitch(action.pokemon, action.to)
    move:
      priority: (action) ->
        {move} = action
        {priority} = move
        action.pokemon.editPriority(priority, move)
      action: (action) ->
        @performMove(action.pokemon, action.move)

  constructor: (@id, @players, attributes = {}) ->
    super(@id)
    # Number of pokemon on each side of the field
    @numActive = attributes.numActive || 1

    # Which battling format was selected
    @format = attributes.format

    # An array of conditions like clauses or team preview that this battle has.
    # TODO: Remove
    @conditions = (attributes.conditions && _.clone(attributes.conditions))
    @conditions ||= []

    # Stores the current turn of the battle
    @turn = 0

    # Stores the actions each player is about to make
    @pokemonActions = []

    # Stores the current and completed requests for action.
    # Keyed by player.id
    @requests = {}
    @completedRequests = {}

    # Creates a RNG for this battle.
    @rng = new FakeRNG()

    # Current battle weather.
    @weather = Weather.NONE

    # Current turn duration for the weather. -1 means infinity.
    @weatherDuration = -1

    # Stores last move used
    @lastMove = null

    # Stores current pokemon moving
    @currentPokemon = null

    # Stores the confusion recoil move as it may be different cross-generations
    @confusionMove = @getMove('Confusion Recoil')

    # Stores the Struggle move as it is different cross-generation
    @struggleMove = @getMove('Struggle')

    # Stores attachments on the battle itself.
    @attachments = new Attachments()

    # Stores an ongoing log of the battle
    @log = []

    # Teams for each player, keyed by player id.
    @teams = {}

    # Battle update information for each player, keyed by player id.
    @queues = {}

    # Holds all playerIds. The location in this array is the player's index.
    @playerIds = []

    # Holds all player names.
    @playerNames = []

    # Populates @playerIds and creates the teams for each player
    for player in @players
      @playerIds.push(player.id)
      @playerNames.push(player.name)
      # TODO: Get the actual player object and use player.name
      @teams[player.id] = new Team(this, player.id, player.name, player.team, @numActive)

    # Holds battle state information
    @replacing = false
    @finished = false

    @once 'end', (winnerId) ->
      @finished = true
      @resetExpiration()

    # Store when the battle was created
    @createdAt = Date.now()

    @resetExpiration()

    @logger = logger.withContext(battleId: @id)

  # Creates a new log messages with context
  debug: (message, context) ->
    # TODO: Add more context. Elements such as the turn.
    @logger.log(message, context)

  begin: ->
    @tell(Protocol.INITIALIZE, @getTeams().map((t) -> t.toJSON(hidden: true)))
    if @listeners('beforeStart').length > 0
      @emit('beforeStart')
    else
      for id in @playerIds
        team = @teams[id]
        allpokemon = team.all()
        for pokemon in allpokemon
          if pokemon.hasAbility('Illusion')
            alivemons = team.getAlivePokemon()
            lastalivemon = alivemons[alivemons.length-1]
            pokemon.attach(Attachment.Illusion, lastalivemon)
      @startBattle()

  startBattle: ->
    @emit('start')
    @tell(Protocol.START_BATTLE)
    # TODO: Merge this with performReplacements?
    for playerId in @playerIds
      for slot in [0...@numActive]
        pokemon = @getTeam(playerId).at(slot)
        continue  if !pokemon
        @performReplacement(pokemon, slot)
    # TODO: Switch-in events are ordered by speed
    for pokemon in @getActivePokemon()
      pokemon.team.switchIn(pokemon)
      pokemon.turnsActive = 1

    @beginTurn()

  getPlayerIndex: (playerId) ->
    index = @playerIds.indexOf(playerId)
    return (if index == -1 then null else index)

  getPlayerName: (playerId) ->
    index = @getPlayerIndex(playerId)
    return (if index? then @playerNames[index] else playerId)

  getPlayer: (playerId) ->
    _(@players).find((p) -> p.id == playerId)

  getTeam: (playerId) ->
    @teams[playerId]

  # Returns teams in order of index.
  getTeams: ->
    (@getTeam(playerId)  for playerId in @playerIds)

  # Returns non-fainted opposing pokemon of a given pokemon.
  getOpponents: (pokemon) ->
    opponents = @getAllOpponents(pokemon)
    opponents = opponents.filter((p) -> p.isAlive())
    opponents

  # Returns all opposing pokemon of a given pokemon.
  getAllOpponents: (pokemon) ->
    opponents = @getOpponentOwners(pokemon)
    teams = (@getTeam(playerId).slice(0, @numActive)  for playerId in opponents)
    opponents = _.flatten(teams)
    opponents

  # Returns all opponent players of a given pokemon. In a 1v1 it returns
  # an array with only one opponent.
  getOpponentOwners: (pokemon) ->
    id = @getOwner(pokemon)
    (playerId  for playerId in @playerIds when id != playerId)

  # Returns all active pokemon on the field belonging to both players.
  # Active pokemon include fainted pokemon that have not been switched out.
  getActivePokemon: ->
    pokemon = []
    for team in @getTeams()
      pokemon.push(team.getActivePokemon()...)
    pokemon

  getActiveAlivePokemon: ->
    pokemon = @getActivePokemon()
    pokemon.filter((p) -> p.isAlive())

  getActiveFaintedPokemon: ->
    pokemon = @getActivePokemon()
    pokemon.filter((p) -> p.isFainted())

  # Finds the Player attached to a certain Pokemon.
  getOwner: (pokemon) ->
    for playerId in @playerIds
      return playerId  if @getTeam(playerId).contains(pokemon)

  getSlotNumber: (pokemon) ->
    pokemon.team.indexOf(pokemon)

  # Forces the owner of a Pokemon to switch.
  forceSwitch: (pokemon) ->
    return false  if @isOver()
    playerId = @getOwner(pokemon)
    switches = pokemon.team.getAliveBenchedPokemon()
    slot = @getSlotNumber(pokemon)
    @cancelAction(pokemon)
    @requestActions(playerId, [ {switches, slot} ])

  # Returns true if the Pokemon has yet to move.
  willMove: (pokemon) ->
    action = @getAction(pokemon)
    action?.type == 'move'

  # Returns the move associated with a Pokemon.
  peekMove: (pokemon) ->
    @getAction(pokemon)?.move

  changeMove: (pokemon, move) ->
    action = @getAction(pokemon)
    if action?.type == 'move'
      action.move = move

  # Bumps a Pokemon to the front of a priority bracket.
  # If no bracket is provided, the Pokemon's current priority bracket is used.
  bump: (pokemon, bracket) ->
    if !bracket?
      action = @getAction(pokemon)
      return  if !action
      bracket = @actionPriority(action)

    # Find the priority segment associated with this pokemon
    index = @pokemonActions.map((o) -> o.pokemon).indexOf(pokemon)
    segment = @pokemonActions.splice(index, 1)[0]

    # Put segment in proper place in the queue
    for action, i in @pokemonActions
      break  if @actionPriority(action) <= bracket
    @pokemonActions.splice(i, 0, segment)

  # Delays a Pokemon to the end of a priority bracket.
  # If no bracket is provided, the Pokemon's current priority bracket is used.
  delay: (pokemon, bracket) ->
    if !bracket?
      action = @getAction(pokemon)
      return  if !action
      bracket = @actionPriority(action)

    # Find the priority segment associated with this pokemon
    index = @pokemonActions.map((o) -> o.pokemon).indexOf(pokemon)
    segment = @pokemonActions.splice(index, 1)[0]

    # Put segment in proper place in the queue
    for i in [(@pokemonActions.length - 1)..0] by -1
      action = @pokemonActions[i]
      break  if @actionPriority(action) >= bracket
    @pokemonActions.splice(i + 1, 0, segment)

  # Add `string` to a buffer that will be sent to each client.
  message: (string) ->
    @tell(Protocol.RAW_MESSAGE, string)

  # Tells every spectator something.
  tell: (args...) ->
    for id, user of @users
      @tellPlayer(user.name, args...)
    @log.push(args)
    true

  tellPlayer: (id, args...) ->
    @queues[id] ?= []
    @queues[id].push(args)

  # Sends a message to every spectator.
  send: ->
    for id, user of @users
      user.send.apply(user, arguments)

  # Passing -1 to turns makes the weather last forever.
  setWeather: (weatherName, turns=-1) ->
    cannedText = switch weatherName
      when Weather.SUN  then "SUN_START"
      when Weather.RAIN then "RAIN_START"
      when Weather.SAND then "SAND_START"
      when Weather.HAIL then "HAIL_START"
      when Weather.MOON then "MOON_START"
      else
        switch @weather
          when Weather.SUN  then "SUN_END"
          when Weather.RAIN then "RAIN_END"
          when Weather.SAND then "SAND_END"
          when Weather.HAIL then "HAIL_END"
          when Weather.MOON then "MOON_END"
    @cannedText(cannedText)  if cannedText
    @weather = weatherName
    @weatherDuration = turns
    pokemon.informWeather(@weather)  for pokemon in @getActiveAlivePokemon()
    @tell(Protocol.WEATHER_CHANGE, @weather)

  hasWeather: (weatherName) ->
    return @weather != Weather.NONE  if !weatherName
    weather = (if @hasWeatherCancelAbilityOnField() then Weather.NONE else @weather)
    weatherName == weather

  weatherCannedText: ->
    switch @weather
      when Weather.SAND then "SAND_CONTINUE"
      when Weather.HAIL then "HAIL_CONTINUE"
      when Weather.MOON then "MOON_CONTINUE" 

  weatherUpkeep: ->
    if @weatherDuration == 1
      @setWeather(Weather.NONE)
    else if @weatherDuration > 1
      @weatherDuration--

    cannedText = @weatherCannedText()
    @cannedText(cannedText)  if cannedText?

    activePokemon = @getActivePokemon().filter((p) -> !p.isFainted())
    for pokemon in activePokemon
      continue  if pokemon.isWeatherDamageImmune(@weather)
      damage = pokemon.stat('hp') >> 4
      if @hasWeather(Weather.HAIL)
        if pokemon.damage(damage)
          @cannedText('HAIL_HURT', pokemon)
      else if @hasWeather(Weather.SAND)
        if pokemon.damage(damage)
          @cannedText('SAND_HURT', pokemon)

  hasWeatherCancelAbilityOnField: ->
    _.any @getActivePokemon(), (pokemon) ->
      pokemon.ability?.preventsWeather

  # Begins the turn. Actions are requested from each player. If no pokemon can
  # move, then the battle engine progresses to continueTurn. Otherwise, the
  # battle waits for user responses.
  beginTurn: ->
    @turn++
    @tell(Protocol.START_TURN, @turn)
    pokemon.resetBlocks()  for pokemon in @getActivePokemon()
    @query('beginTurn')
    @emit('beginTurn')

    # Send appropriate requests to players
    for playerId in @playerIds
      actions = []
      for slot in [0...@numActive]
        team = @getTeam(playerId)
        pokemon = team.at(slot)
        continue  if !pokemon || @getAction(pokemon)
        moves = pokemon.validMoves()
        switches = team.getAliveBenchedPokemon()
        switches = []  if pokemon.isSwitchBlocked()
        # This guarantees the user always has a move to pick.
        moves.push(@struggleMove)  if moves.length == 0
        actions.push({moves, switches, slot})

        team.faintedLastTurn = team.faintedThisTurn
        team.faintedThisTurn = false
      @requestActions(playerId, actions)

  # A callback done after turn order is calculated for the first time.
  # Use this callback to edit the turn order after players have selected
  # their orders, but before the turn continues.
  afterTurnOrder: ->
    pokemon = @getActiveAlivePokemon()
    p.afterTurnOrder()  for p in pokemon

  # Continues the turn. This is called once all requests
  # have been submitted and the battle is ready to continue.
  continueTurn: ->
    # We're done processing requests, so cancelling shouldn't be possible anymore.
    # Clean the completed requests
    @completedRequests = {}

    @tell(Protocol.CONTINUE_TURN)
    @emit('continueTurn')

    @determineTurnOrder()
    for action in @pokemonActions
      action.move?.beforeTurn?(this, action.pokemon)

    while @hasActionsLeft()
      action = @pokemonActions.shift()
      {pokemon} = action
      continue  if pokemon.isFainted()

      @actionMap[action.type]["action"].call(this, action)
      @performFaints()

      # Update Pokemon itself.
      # TODO: Is this the right place?
      for active in @getActiveAlivePokemon()
        active.update()

      # If a move adds a request to the queue, the request must be resolved
      # before the battle can continue.
      break  unless @areAllRequestsCompleted()

  # Performs end turn effects.
  endTurn: ->
    @weatherUpkeep()
    @query("endTurn")
    for pokemon in @getActivePokemon()
      pokemon.turnsActive += 1
      pokemon.update()
    @checkForReplacements()

  attach: (klass, options = {}) ->
    options = _.clone(options)
    attachment = @attachments.push(klass, options, battle: this)
    if attachment then @tell(Protocol.BATTLE_ATTACH, attachment.name)
    attachment

  unattach: (klass) ->
    attachment = @attachments.unattach(klass)
    if attachment then @tell(Protocol.BATTLE_UNATTACH, attachment.name)
    attachment

  get: (attachment) ->
    @attachments.get(attachment)

  has: (attachment) ->
    @attachments.contains(attachment)

  endBattle: ->
    return  if @finished
    winnerId = @getWinner()
    winnerIndex = @getPlayerIndex(winnerId)
    @tell(Protocol.END_BATTLE, winnerIndex)
    @emit('end', winnerId)

  incrementFaintedCounter: ->
    @faintedCounter = 0  unless @faintedCounter
    @faintedCounter += 1
    @faintedCounter

  getWinner: ->
    winner     = null

    # If each player has the same number of pokemon alive, return the owner of
    # the last pokemon that fainted earliest.
    teamLength = @getTeam(@playerIds[0]).getAlivePokemon().length
    playerSize = @playerIds.length
    count      = 1
    for i in [1...playerSize] by 1
      count++  if teamLength == @getTeam(@playerIds[i]).getAlivePokemon().length
    if count == playerSize
      pokemon = _.flatten(@getTeams().map((p) -> p.pokemon))
      # Get the owner of the pokemon that fainted last
      pokemon = pokemon.sort((a, b) -> b.fainted - a.fainted)[0]
      return pokemon.playerId

    # Otherwise, return the player with the most pokemon alive.
    length = 0
    for playerId in @playerIds
      newLength = @getTeam(playerId).getAlivePokemon().length
      if newLength > length
        length = newLength
        winner = playerId
    return winner

  isOver: ->
    @finished || _(@playerIds).any((id) => @getTeam(id).getAlivePokemon().length == 0)

  hasStarted: ->
    @turn >= 1

  getAllAttachments: ->
    array = @attachments.all()
    array.push(@getTeams().map((t) -> t.attachments.all()))
    array.push(@getActivePokemon().map((p) -> p.attachments.all()))
    _.flatten(array)

  isPokemon: (maybePokemon) ->
    maybePokemon instanceof Pokemon

  query: (eventName) ->
    Query(eventName, @getAllAttachments())

  # Tells the player to execute a certain move by name. The move is added
  # to the list of player actions, which are executed once the turn continues.
  #
  # player - the player object that will execute the move
  # moveName - the name of the move to execute
  #
  recordMove: (playerId, move, forSlot = 0) ->
    pokemon = @getTeam(playerId).at(forSlot)
    action = @addAction(type: 'move', move: move, pokemon: pokemon)
    @removeRequest(playerId, action, forSlot)

  # Tells the player to switch with a certain pokemon specified by position.
  # The switch is added to the list of player actions, which are executed
  # once the turn continues.
  #
  # player - the player object that will execute the move
  # toPosition - the index of the pokemon to switch to
  #
  recordSwitch: (playerId, toPosition, forSlot = 0) ->
    pokemon = @getTeam(playerId).at(forSlot)
    action = @addAction(type: 'switch', to: toPosition, pokemon: pokemon)
    @removeRequest(playerId, action, forSlot)

  addAction: (action) ->
    unless @getAction(action.pokemon)
      @pokemonActions.push(action)
      @emit('addAction', action.pokemon.playerId, action)
    return action

  removeRequest: (playerId, action, forSlot) ->
    if arguments.length == 2
      [action, forSlot] = [null, forSlot]
    forSlot ?= 0
    playerRequests = @requests[playerId] || []
    for request, i in playerRequests
      if request.slot == forSlot
        if action
          completed = { request, action }
          @completedRequests[playerId] ?= []
          @completedRequests[playerId].push(completed)

        playerRequests.splice(i, 1)
        delete @requests[playerId]  if playerRequests.length == 0
        break

  # Cancels the most recent completed request made by a certain player
  # Returns true if the cancel succeeded, and false if it didn't.
  undoCompletedRequest: (playerId) ->
    return false  if @isOver()
    return false  if @areAllRequestsCompleted()
    return false  if not @completedRequests[playerId]
    return false  if @completedRequests[playerId].length == 0
    return false  if playerId not in @playerIds

    {request, action} = @completedRequests[playerId].pop()

    # Add the cancelled request to the beginning of @requests
    @requests[playerId] ?= []
    @requests[playerId].unshift(request)

    # Remove the related pokemon actions. There may be more than one.
    index = 0
    while index < @pokemonActions.length
      if @pokemonActions[index].pokemon.playerId == playerId
        @pokemonActions.splice(index, 1)
      else
        index += 1

    @sendRequestTo(playerId)

    @emit('undoCompletedRequest', playerId)
    return true

  requestFor: (pokemon) ->
    playerId = @getOwner(pokemon)
    forSlot  = @getSlotNumber(pokemon)
    actions  = @requests[playerId] || []
    for action in actions
      if action.slot == forSlot
        return action
    return null

  hasCompletedRequests: (playerId) ->
    @completedRequests[playerId]?.length > 0

  getAction: (pokemon) ->
    for action in @pokemonActions
      if action.pokemon == pokemon && action.type in [ 'move', 'switch' ]
        return action
    return null

  popAction: (pokemon) ->
    action = @getAction(pokemon)
    if action
      index = @pokemonActions.indexOf(action)
      @pokemonActions.splice(index, 1)
    action

  cancelAction: (pokemon) ->
    action = @popAction(pokemon)
    action = @popAction(pokemon)  while action?

  requestActions: (playerId, validActions) ->
    # Normalize actions for the client
    # TODO: Should not need to do this here.
    total = 0
    for action in validActions
      {switches, moves} = action
      if switches?
        action.switches = switches.map((p) => @getSlotNumber(p))
        total += action.switches.length
      if moves?
        action.moves = moves.map((m) -> m.name)
        total += action.moves.length

    return false  if total == 0

    @requests[playerId] = validActions
    @sendRequestTo(playerId)
    @emit('requestActions', playerId)
    return true

  sendRequestTo: (playerId) ->
    @tellPlayer(playerId, Protocol.REQUEST_ACTIONS, @requests[playerId])

  # Returns true if all requests have been completed. False otherwise.
  areAllRequestsCompleted: ->
    total = 0
    total += (request  for request of @requests).length
    total == 0

  checkForReplacements: ->
    @performFaints()
    if @isOver()
      @endBattle()
    else if @areReplacementsNeeded()
      @requestFaintedReplacements()
    else
      @beginTurn()

  # Returns true if any player's active Pokemon are fainted.
  areReplacementsNeeded: ->
    @getActiveFaintedPokemon().length > 0

  # Force people to replace fainted Pokemon.
  requestFaintedReplacements: ->
    @replacing = true
    for playerId in @playerIds
      team = @getTeam(playerId)
      fainted = team.getActiveFaintedPokemon()
      size = fainted.length
      if size > 0
        benched = team.getAliveBenchedPokemon()
        validActions = ({switches: benched, slot: x}  for x in [0...size])
        @requestActions(playerId, validActions)

  determineTurnOrder: ->
    @sortActions()
    @afterTurnOrder()
    @pokemonActions

  # Uses a Schwartzian transform to cut down on unnecessary calculations.
  # The game bitshifts priority and subpriority to the end and tacks on speed.
  # As a result, speed precision is 13 bits long; an overflow happens at 8191.
  # Trick Room replaces the Pokemon's speed with 0x2710 - speed.
  sortActions: ->
    trickRoomed = @has(Attachment.TrickRoom)
    array = @pokemonActions.map (action) =>
      {pokemon} = action
      priority = @actionPriority(action)
      speed = pokemon.stat('speed')
      speed = 0x2710 - speed  if trickRoomed
      speed &= 8191
      integer = (priority << 13) | speed
      [ action, integer ]

    array.sort (a, b) =>
      diff = b[1] - a[1]
      diff = (if @rng.next("turn order") < .5 then -1 else 1)  if diff == 0
      diff

    @pokemonActions = array.map (elem) => elem[0]

  actionPriority: (action) ->
    throw new Error("Could not find action!")  if !action
    @actionMap[action.type]["priority"].call(this, action)

  hasActionsLeft: ->
    @pokemonActions.length > 0

  # Executed by @continueTurn
  performSwitch: (pokemon, toPosition) ->
    pokemon.team.switch(pokemon, toPosition)

  performReplacement: (pokemon, toPosition) ->
    pokemon.team.replace(pokemon, toPosition)

  # Executed by @beginTurn
  performReplacements: ->
    @replacing = false
    switched = []
    while @hasActionsLeft()
      {pokemon, to} = @pokemonActions.shift()
      switched.push @performReplacement(pokemon, to)
    # TODO: Switch-in events are ordered by speed
    for pokemon in switched
      pokemon.team.switchIn(pokemon)
      pokemon.turnsActive = 1

    # Pokemon may have fainted upon switch-in; we need to check.
    @checkForReplacements()

  # Executed by @continueTurn
  performMove: (pokemon, move) ->
    targets = @getTargets(move, pokemon)

    @cannedText('NO_MOVES_LEFT', pokemon)  if move == @struggleMove

    if pokemon.pp(move) <= 0
      # TODO: Send move id instead
      pokemon.tell(Protocol.MAKE_MOVE, move.name)
      @cannedText('NO_PP_LEFT')
      # TODO: Is this the right place...?
      pokemon.resetRecords()
    else
      if pokemon.beforeMove(move, pokemon, targets) != false
        pokemon.reducePP(move)
        pressureTargets = targets.filter (t) ->
          t instanceof Pokemon && t.hasAbility("Pressure") && !t.team.contains(pokemon)
        for target in pressureTargets
          pokemon.reducePP(move)
        @executeMove(move, pokemon, targets)
      # After the move finishes (whether it executed properly or not, e.g. par)
      pokemon.afterMove(move, pokemon, targets)
      pokemon.tell(Protocol.END_MOVE)

  # TODO: Put in priority queue
  performFaints: ->
    # Execute afterFaint events
    # TODO: If a Pokemon faints in an afterFaint, should it be added to this?
    pokemon.faint()  for pokemon in @getActiveFaintedPokemon()

  executeMove: (move, pokemon, targets) ->
    @debug('Battle#executeMove', move: move.name, attacker: pokemon.species)

    @currentPokemon = pokemon
    # TODO: Send move id instead
    pokemon.tell(Protocol.MAKE_MOVE, move.name)
    move.execute(this, pokemon, targets)

    # Record last move.
    @lastMove = move
    # TODO: Only record if none exists yet for this turn.
    pokemon.recordMove(move)

    # TODO: Is this the right place...?
    pokemon.resetRecords()
    @currentPokemon = null

  getTargets: (move, user) ->
    {team} = user
    targets = switch move.target
      when 'user'
        [ user ]
      when 'user-or-ally'
        [ @rng.choice(team.getActivePokemon()) ]
      when 'ally'
        # TODO: Actually get selected Pokemon from client
        team.getAdjacent(user)
      when 'all-opponents'
        @getOpponents(user)
      when 'selected-pokemon'
        # TODO: Actually get selected Pokemon from client.
        pokemon = @getOpponents(user)
        [ @rng.choice(pokemon, "selected pokemon target") ]
      when 'all-other-pokemon'
        @getActivePokemon().filter((p) -> p != user)
      when 'entire-field'
        @getActivePokemon()
      when 'random-opponent'
        pokemon = @getOpponents(user)
        [ @rng.choice(pokemon) ]
      when 'users-field'
        team.pokemon
      when 'specific-move'
        move.getTargets(this, user)
      when 'opponents-field'
        @getOpponentOwners(user)
      else
        throw new Error("Unimplemented target: #{move.target}.")
    if move.target != 'opponents-field'
      targets = targets.filter((p) -> p)
    return targets

  getMove: (moveName) ->
    throw new Error("#{moveName} does not exist.")  if moveName not of @Moves
    @Moves[moveName]

  findMove: (condition) ->
    for move in @MoveList
      if condition(move) then return move
    return null

  getAttachment: (attachmentName) ->
    Attachment[attachmentName]

  getAilmentEffect: (move) ->
    switch move.ailmentId
      when "confusion"   then Attachment.Confusion
      when "paralysis"   then Status.Paralyze
      when "freeze"      then Status.Freeze
      when "burn"        then Status.Burn
      when "sleep"       then Status.Sleep
      when "poison"      then Status.Poison
      when "toxic"       then Status.Toxic
      when "yawn"        then Attachment.Yawn
      when "infatuation" then Attachment.Attract
      when "disable"     then Attachment.Disable
      when "ingrain"     then Attachment.Ingrain
      when "leech-seed"  then Attachment.LeechSeed
      when "torment"     then Attachment.Torment
      when "perish-song" then Attachment.PerishSong
      when "embargo"     then Attachment.Embargo
      when "telekinesis" then Attachment.Telekinesis
      when "nightmare"   then Attachment.Nightmare
      when "unknown"
        switch move.name
          when "Tri Attack"
            triAttackEffects = [ Status.Paralyze, Status.Burn, Status.Freeze ]
            @rng.choice(triAttackEffects, "tri attack effect")
          else throw new Error("Unrecognized unknown ailment for #{move.name}")
      else throw new Error("Unrecognized ailment: #{move.ailmentId} for #{move.name}")

  add: (spark) ->
    user = spark.user

    # If this is a player, mask the spectator in case this is an alt
    player = _(@players).find((p) -> p.id == user.name)

    # Find the user's index (if any)
    index = (if player then @getPlayerIndex(player.id) else null)

    # Start spectating battle
    spark.send('spectateBattle',
      @id, @format, @numActive, index, @playerNames, @log)

    # Add to internal user store
    super(spark)

    # If this is a player, send them their own team
    if player
      teamJSON = @getTeam(player.id).toJSON()
      @tellPlayer(player.id, Protocol.RECEIVE_TEAM, teamJSON)

    @emit('spectateBattle', user)

  transformName: (name) ->
    @getPlayerName(name)

  forfeit: (id) ->
    return  if @isOver()
    index = @getPlayerIndex(id)
    return  unless index?
    @tell(Protocol.FORFEIT_BATTLE, index)
    winnerId = @playerIds[1 - index]
    @emit('end', winnerId)

  expire: ->
    return  if @expired
    @expired = true
    @tell(Protocol.BATTLE_EXPIRED)
    @emit('end')  if !@finished
    @emit('expire')

  resetExpiration: ->
    clearTimeout(@expirationId)  if @expirationId
    @expirationId = setTimeout((=> @expire()  unless @expired), @makeTTL())

  makeTTL: ->
    if @isOver()
      @ENDED_BATTLE_TTL
    else
      @ONGOING_BATTLE_TTL

  # Sends battle updates to each spectator.
  sendUpdates: ->
    for id, user of @users
      userName = user.name
      queue = @queues[userName]
      continue  if !queue || queue.length == 0
      user.send('updateBattle', @id, queue)
      delete @queues[userName]

  cannedText: (type, args...) ->
    newArgs = []
    # Convert any Pokemon in the arguments to its respective player/slot.
    for arg in args
      if arg instanceof Pokemon
        if arg.ability == 'Illusion'
          newArgs.push(@getPlayerIndex(arg.playerId), arg.team.indexOf(arg.team.size - 1))
        else
          newArgs.push(@getPlayerIndex(arg.playerId), arg.team.indexOf(arg))
      else if arg instanceof Move
        newArgs.push(arg.name)
      else if _.isObject(arg) && arg.prototype instanceof BaseAttachment
        newArgs.push(arg.displayName)
      else
        newArgs.push(arg)
    @tell(Protocol.CANNED_TEXT, CannedText[type], newArgs...)

  toString: ->
    "[Battle id:#{@id} turn:#{@turn} weather:#{@weather}]"
