{Attachment, Status} = require './attachment'
{Weather} = require '../../shared/weather'
{Protocol} = require '../../shared/protocol'
Query = require('./queries')
util = require './util'

# A single Move in the Pokemon engine. Move objects are constructed in
# data/VERSION/moves.coffee, with only one instance per move (for example,
# there is only one Flamethrower). These instances are retrieved by the battle
# engine.
class @Move
  criticalMultiplier: 2

  constructor: (@name, attributes = {}) ->
    @accuracy = attributes.accuracy || 0
    @priority = attributes.priority || 0
    @power = attributes.power
    @target = attributes.target
    @description = attributes.description || "No description set"
    @type = attributes.type || '???'
    @spectra = attributes.damage || '???'
    @chLevel = attributes.criticalHitLevel || 1
    @flags = attributes.flags || []
    @flinchChance = (attributes.flinchChance || 0)
    @ailmentChance = (attributes.ailmentChance || 0)
    @ailmentId = attributes.ailmentId
    @primaryBoostStats = attributes.primaryBoostStats
    @primaryBoostTarget = attributes.primaryBoostTarget
    @secondaryBoostChance = attributes.secondaryBoostChance || 0
    @secondaryBoostStats = attributes.secondaryBoostStats
    @secondaryBoostTarget = attributes.secondaryBoostTarget
    @pp = attributes.pp
    @recoil = attributes.recoil
    {@minHits, @maxHits} = attributes

  isPhysical: ->
    @spectra == 'physical'

  isSpecial: ->
    @spectra == 'special'

  isNonDamaging: ->
    @spectra == 'non-damaging'

  hasFlag: (flagName) ->
    flagName in @flags

  hasPrimaryEffect: ->
    @primaryBoostStats? || (@ailmentId != "none" && @ailmentChance == 0)

  # A secondary effect also includes flinching.
  hasSecondaryEffect: ->
    (@ailmentChance > 0 && @ailmentId != "none") ||
      @flinchChance > 0 || @secondaryBoostChance > 0

  # Executes this move on several targets.
  # Only override this method if the move does not need to be
  # recorded on the enemy pokemon.
  execute: (battle, user, targets) ->
    # If there are no targets, then the move should automatically fail.
    # For example, Helping Hand may not have a target.
    return @fail(battle, user)  if targets.length == 0
    # If there were targets, but they are all no longer alive, then the engine
    # outputs a stock message and quits move execution.
    targets = targets.filter((p) -> p.isAlive())
    if targets.length == 0
      battle.cannedText('NO_TARGET')
      return

    # The move is executing. Run a hook.
    @executing(battle, user, targets)

    targetsHit = []
    totalDamage = 0

    for target in targets
      continue  if @use(battle, user, target, hitNumber) == false
      if target.shouldBlockExecution(this, user) == true
        @afterFail(battle, user, target)
        continue
      targetsHit.push(target)

    if targetsHit.length > 0
      targetSlots = targetsHit.map (target) ->
          return [ battle.playerIds.indexOf(target.playerId),
                   target.team.indexOf(target) ]
      user.tell(Protocol.MOVE_SUCCESS, targetSlots, @name)

    for target in targetsHit
      numHits = @calculateNumberOfHits(battle, user, targets)
      wasSlept = user.has(Status.Sleep)
      for hitNumber in [1..numHits]
        isDirect = @isDirectHit(battle, user, target)
        damage = @hit(battle, user, target, hitNumber, isDirect) || 0
        @afterHit(battle, user, target, damage, isDirect)
        totalDamage += damage
        break  if target.isFainted() || user.isFainted() ||
                  (!wasSlept && user.has(Status.Sleep))
      if numHits > 1
        battle.message @numHitsMessage Math.min(hitNumber, numHits)

    # Target faints if it has 0 HP.
    for target in targets when target.isFainted()
      target.faint()

    # Recoil moves
    if totalDamage > 0 && @recoil < 0 && !user.hasAbility("Rock Head")
      recoil = Math.round(totalDamage * -@recoil / 100)
      if user.damage(recoil)
        battle.cannedText('RECOIL', user)

    # If the move hit 1+ times, query the user's afterAllHits event.
    # If the user is affected by Sheer Force, these are all ignored.
    if targetsHit.length > 0 &&
        (!user.hasAbility("Sheer Force") || !@hasSecondaryEffect())
      user.afterAllHits(this)
      @afterAllHits(battle, user)
      for target in targetsHit
        target.afterAllHitsTarget(this, user)

  # A hook with a default implementation of returning false on a type immunity.
  # If `use` returns false, the `hit` hook is never called.
  use: (battle, user, target, hitNumber) ->
    if target.isImmune(@getType(battle, user, target), user: user, move: this)
      battle.cannedText('IMMUNITY', target)
      @afterFail(battle, user, target)
      return false

    if @willMiss(battle, user, target)
      battle.cannedText('MOVE_MISS', target)
      @afterMiss(battle, user, target)
      return false

  # Calculates damage, deals damage, and returns the amount of damage dealt
  hit: (battle, user, target, hitNumber, isDirect) ->
    damage = @calculateDamage(battle, user, target, hitNumber, isDirect)
    if damage > 0
      previousHP = target.get(Attachment.Substitute)?.hp ? target.currentHP
      damage = target.damage(damage, direct: isDirect, source: "move")
      if damage != 0
        percent = Math.floor(100 * damage / target.stat('hp'))
        battle.cannedText('GOT_HIT', target, percent)
      else
        currentHP = target.get(Attachment.Substitute)?.hp ? target.currentHP
        damage = previousHP - Math.max(0, currentHP)
    return damage

  # `hit` may be overridden, but we still want to run these callbacks.
  afterHit: (battle, user, target, damage, isDirect) ->
    # Drain moves
    if damage > 0 && @recoil > 0
      amount = Math.round(damage * @recoil / 100)
      user.drain(amount, target)
      battle.cannedText('DRAIN', target)
      battle.cannedText('ABSORB', user)

    if @shouldTriggerSecondary(battle, user, target, damage, isDirect)
      @triggerSecondaryEffect(battle, user, target)
    user.afterSuccessfulHit(this, user, target, damage, isDirect)
    target.afterBeingHit(this, user, target, damage, isDirect)
    @afterSuccessfulHit(battle, user, target, damage, isDirect)
    user.update()
    target.update()

    # Miscellaneous
    target.recordHit(user, damage, this, battle.turn, isDirect)

  # A hook that runs when the move is finally executing.
  executing: (battle, user, targets) ->

  # A hook that executes after a pokemon has been successfully damaged by
  # a standard move. If execute is overriden, this will not execute.
  afterSuccessfulHit: (battle, user, target, damage) ->

  # A hook that executes after a pokemon misses an attack. If execute is
  # overriden, this will not execute.
  afterMiss: (battle, user, target) ->

  # A hook that executes after a pokemon fails while using a move (NOT a miss.)
  # Examples: Target is immune, target successfully uses Protect/Detect
  afterFail: (battle, user, target) ->

  # A hook that executes after all hits have completed.
  afterAllHits: (battle, user) ->

  # A hook that executes when asking if this move should do direct damage
  isDirectHit: (battle, user, target) ->
    return true   if @hasFlag('authentic')
    return !target.has(Attachment.Substitute)

  # A hook that executes once a move fails.
  fail: (battle, user) ->
    battle.cannedText('MOVE_FAIL')

  numHitsMessage: (hitNumber) ->
    return "Hit #{hitNumber} time(s)!"

  # A hook that is only used by special "specific-move" targets.
  getTargets: (battle, user) ->
    throw new Error("Move #{@name} has not implemented getTargets.")

  calculateDamage: (battle, user, target, hitNumber=1, isDirect) ->
    return 0  if @basePower(battle, user, target, hitNumber) == 0

    user.crit = @isCriticalHit(battle, user, target)
    damage = @baseDamage(battle, user, target, hitNumber)
    # TODO: Multi-target modifier.
    damage = @modify(damage, @weatherModifier(battle, user, target))
    damage = damage * @criticalMultiplier  if user.crit
    damage = Math.floor(((100 - battle.rng.randInt(0, 15, "damage roll")) * damage) / 100)
    damage = @modify(damage, @stabModifier(battle, user, target))
    effectiveness = @typeEffectiveness(battle, user, target)
    damage = Math.floor(effectiveness * damage)
    damage = Math.floor(@burnCalculation(user) * damage)
    damage = Math.max(damage, 1)
    damage = @modify(damage, @modifyDamage(battle, user, target, hitNumber))

    if effectiveness < 1
      battle.cannedText('NOT_VERY_EFFECTIVE')
    else if effectiveness > 1
      battle.cannedText('SUPER_EFFECTIVE')

    if user.crit
      battle.cannedText('CRITICAL_HIT')
      target.informCriticalHit()
    damage

  canMiss: (battle, user, target) ->
    return true

  # An accuracy of 0 means this move always hits. A negative accuracy means the
  # target is invulnerable, even for 0-accuracy moves. A move always hits, regardless
  # of accuracy and invulnerability, under any of the following conditions:
  #   * The user or target has No Guard
  #   * The user is locked onto the target (Lock-On, Mind Reader)
  #   * The move has a "never-miss" effect built in
  #     (Helping Hand, XY Toxic used by Poison types)
  willMiss: (battle, user, target) ->
    return false  if user.hasAbility("No Guard") || target.hasAbility("No Guard")
    return false  if user.get(Attachment.LockOn)?.target == target
    return false  if !@canMiss(battle, user, target)
    accuracy = @chanceToHit(battle, user, target)
    accuracy = 100  if accuracy == 0
    console.log(accuracy)
    battle.rng.randInt(1, 100, "miss") > accuracy

  chanceToHit: (battle, user, target) ->
    return 0  if user == target
    userBoosts = user.editBoosts(ignoreAccuracy: target.hasAbility("Unaware"))
    targetBoosts = target.editBoosts(ignoreEvasion: user.hasAbility("Unaware"))
    accuracy = @getAccuracy(battle, user, target)
    # TODO: Find out how accuracy/evasion is chained and do the rest properly
    accuracy = 50  if @isNonDamaging() && accuracy > 50 && target.hasAbility("Wonder Skin")
    if userBoosts.accuracy > 0
      accuracy = Math.floor(accuracy * (3 + userBoosts.accuracy) / 3)
    else if userBoosts.accuracy < 0
      accuracy = Math.floor(accuracy / (3 - userBoosts.accuracy) * 3)
    if targetBoosts.evasion > 0
      accuracy = Math.floor(accuracy / (3 + targetBoosts.evasion) * 3)
    else if targetBoosts.evasion < 0
      accuracy = Math.floor(accuracy * (3 - targetBoosts.evasion) / 3)
    accuracy = user.editAccuracy(accuracy, this, target)
    accuracy = target.editEvasion(accuracy, this, user)
    accuracy

  getAccuracy: (battle, user, target) ->
    @accuracy

  weatherModifier: (battle, user, target) ->
    # TODO: This is wrong.
    type = @getType(battle, user, target)
    if type == 'Fire' && battle.hasWeather(Weather.SUN)
      0x1800
    else if type == 'Fire' && battle.hasWeather(Weather.RAIN)
      0x800
    else if type == 'Water' && battle.hasWeather(Weather.RAIN)
      0x1800
    else if type == 'Water' && battle.hasWeather(Weather.SUN)
      0x800
    else if type == 'Dark' && battle.hasWeather(Weather.MOON)
      0x159A
    else if type == 'Fairy' && battle.hasWeather(Weather.MOON)
      0xC00
    else
      0x1000

  stabModifier: (battle, user, target) ->
    type = @getType(battle, user, target)
    if user.hasType(type)
      return 0x2000  if user.hasAbility("Adaptability")
      return 0x1800
    return 0x1000

  ignoresImmunities: ->
    @isNonDamaging()

  typeEffectiveness: (battle, user, target) ->
    type = @getType(battle, user, target)
    effect = target.effectivenessOf(type, user: user, move: this)
    if target.hasAbility("Ethereal Shroud")
      ghosteffect = util.typeEffectiveness(type, ["Ghost"])
      if ghosteffect < 1
        effect *= ghosteffect
    effect

  burnCalculation: (user) ->
    if @isPhysical() && !user.hasAbility("Guts") && user.has(Status.Burn)
      .5
    else
      1

  basePower: (battle, user, target, hitNumber) ->
    @power

  shouldTriggerSecondary: (battle, user, target, damage, isDirect) ->
    return false  if !@hasSecondaryEffect()
    return false  if !isDirect && @secondaryBoostTarget != 'self'
    return false  if user.hasAbility("Sheer Force")
    return false  if target.hasAbility("Shield Dust") && @secondaryBoostTarget != 'self'
    return true

  triggerSecondaryEffect: (battle, user, target) ->
    # Multiply chances by 2 if the user has Serene Grace.
    chanceMultiplier = (if user.hasAbility("Serene Grace") then 2 else 1)

    # Secondary effects
    if @ailmentChance > 0 && battle.rng.randInt(0, 99, "secondary effect") < @ailmentChance * chanceMultiplier
      target.attach(battle.getAilmentEffect(this), source: user)

    # Secondary boosts
    if @secondaryBoostChance > 0 && battle.rng.randInt(0, 99, "secondary boost") < @secondaryBoostChance * chanceMultiplier
      pokemon = (if @secondaryBoostTarget == 'self' then user else target)
      pokemon.boost(@secondaryBoostStats, user)

    # Flinching. In the game, flinching is treated subtly different than
    # secondary effects. One result is that the Fang moves can both inflict
    # a secondary effect as well as flinch.
    if @flinchChance > 0 && battle.rng.randInt(0, 99, "flinch") < @flinchChance * chanceMultiplier
      target.attach(Attachment.Flinch)

  isCriticalHit: (battle, attacker, defender) ->
    return false  if defender.team?.has(Attachment.LuckyChant)
    return false  if defender.ability?.preventsCriticalHits

    chLevel = @criticalHitLevel(battle, attacker, defender)
    rand = battle.rng.next("ch")
    @determineCriticalHitFromLevel(chLevel, rand)

  determineCriticalHitFromLevel: (level, rand) ->
    switch level
      when -1
        true
      when 1
        rand < 0.0625
      when 2
        rand < 0.125
      when 3
        rand < 0.25
      when 4
        rand < 1/3
      else
        rand < .5

  criticalHitLevel: (battle, attacker, defender) ->
    # -1 means always crits
    return @chLevel  if @chLevel == -1

    stage = @chLevel
    stage += 1  if attacker.hasAbility('Super Luck')
    stage += 2  if attacker.has(Attachment.FocusEnergy)
    stage += attacker.criticalModifier()
    stage

  modify: (number, modifier) ->
    Math.ceil((number * modifier) / 0x1000 - 0.5)

  baseDamage: (battle, user, target, hitNumber=1) ->
    floor = Math.floor
    uStat = @pickAttackStat(user, target)
    tStat = @pickDefenseStat(user, target)
    if battle.hasWeather(Weather.SAND) && target.hasType("Rock") && @isSpecial()
      tStat = @modify(tStat, 0x1800)
    damage = floor((2 * user.level) / 5 + 2)
    damage *= @basePower(battle, user, target, hitNumber)
    damage = @modify(damage, @modifyBasePower(battle, user, target))
    damage *= @modify(uStat, @modifyAttack(battle, user, target))
    damage = floor(damage / tStat)
    damage = floor(damage / 50)
    damage += 2
    damage

  calculateNumberOfHits: (battle, user, targets) ->
    numHits = user.calculateNumberOfHits(this, targets)
    if numHits
      numHits
    else if @minHits == @maxHits
      @maxHits
    else if @minHits == 2 && @maxHits == 5
      # hard coding moves like fury swipes to have 2-3 hits have a 1/3 chance, and 4-5 have 1/6th
      battle.rng.choice([2, 2, 3, 3, 4, 5], "num hits")
    else
      battle.rng.randInt(@minHits, @maxHits, "num hits")

  modifyBasePower: (battle, user, target) ->
    modify = Query.modifiers("modifyBasePower", user.attachments.all(), this, target)
    modify = @modify(modify, Query.modifiers("modifyBasePowerTarget", target.attachments.all(), this, user))

  modifyDamage: (battle, user, target, hitNumber) ->
    modify = Query.modifiers('modifyDamageTarget', target.team.attachments.all(), this, user, hitNumber)
    modify = @modify(modify, Query.modifiers('modifyDamage', user.attachments.all(), this, target, hitNumber))
    modify = @modify(modify, Query.modifiers('modifyDamageTarget', target.attachments.all(), this, user, hitNumber))

  modifyAttack: (battle, user, target) ->
    modify = Query.modifiers('modifyAttack', user.attachments.all(), this, target)
    modify = @modify(modify, Query.modifiers('modifyAttackTarget', target.attachments.all(), this, user))

  getType: (battle, user, target) ->
    type = user.editMoveType(@type, target)
    type

  pickAttackStat: (user, target) ->
    stat = (if @isPhysical() then 'attack' else 'specialAttack')
    stat = 'specialAttack' if user.hasAbility("Spectral Jaws") and @hasFlag("bite")
    user.stat(stat, ignoreNegativeBoosts: user.crit, ignoreOffense: target.hasAbility("Unaware"))

  pickDefenseStat: (user, target) ->
    stat = (if @isPhysical() then 'defense' else 'specialDefense')
    stat = 'specialDefense' if user.hasAbility("Spectral Jaws") and @hasFlag("bite")
    target.stat(stat, ignorePositiveBoosts: user.crit, ignoreDefense: user.hasAbility("Unaware"))

  toString: ->
    "[Move name:#{@name}]"
