self = (module?.exports || window)

if module?.exports
  EventPokemon = require('./event_pokemon')
  {INT_TO_GENERATION} = require('./ladders')
  {_} = require('underscore')
else
  EventPokemon = window.EventPokemon ? {}
  INT_TO_GENERATION = window.INT_TO_GENERATION
  _ = window._

unportableGenerations = [ 1, 3 ]
unportableGenerations.sort((a, b) -> b - a)  # numeric, descending

switchableFormes = [
  "Deoxys"
  "Rotom"
  "Shaymin"
]

unsketchableMoves = [
  "Chatter"
  "Hyperspace Hole"
  "Light of Ruin"
  "Steam Eruption"
  "Struggle"
  "Thousand Arrows"
  "Thousand Waves"
]

mustLearnMove =
  "Mamoswine"  : "AncientPower"
  "Yanmega"    : "AncientPower"
  "Tangrowth"  : "AncientPower"
  "Mr. Mime"   : "Mimic"
  "Sudowoodo"  : "Mimic"  # Note: Only 6 egg moves exist; 4 can be learned.
  "Ambipom"    : "Double Hit"
  "Lickilicky" : "Rollout"

getMinimumGeneration = (generation) ->
  for unportableGen in unportableGenerations
    if unportableGen <= generation
      return unportableGen
  minGeneration = Math.min(unportableGenerations...)
  throw new Error("Gen. '#{generation}' must be greater than #{minGeneration}.")

getGenerationFromInt = (generationInteger) ->
  INT_TO_GENERATION[generationInteger].toUpperCase()

# A helper method to loop through the learnsets for this pokemon and all
# prevolutions, and then runs an iterator function (that you give) on those
# learnsets. The iterator function takes one parameter, `learnset`, the learnset
# for a given generation for the Pokemon's current forme.
#
# All generations that can be used are taken into consideration. If the Pokemon
# has a hidden ability, but the generation doesn't support it, the iterator
# skips over that gneration.
loopLearnsets = (Generations, pokemon, forGeneration, iterator) ->
  minimumGeneration = getMinimumGeneration(forGeneration)
  {species, forme, ability} = pokemon
  formeName = forme || "default"
  # Find pre-evolutions and formes
  thePokemon = []
  theFormes = [ formeName ]
  {SpeciesData, FormeData} = Generations[getGenerationFromInt(forGeneration)]
  finalForme = FormeData[species][formeName]
  while species
    thePokemon.push(species)
    if species in switchableFormes && species not in theFormes
      theFormes.push((forme  for forme of FormeData[species])...)
    species = SpeciesData[species].evolvedFrom

  # The Pokemon may not have a default ability (due to tests, etc)
  ability ?= finalForme.abilities[0]
  hasHiddenAbility = (ability == finalForme.hiddenAbility &&
                      ability not in finalForme.abilities)

  # Loop through pre-evolutions and formes
  for species in thePokemon
    for formeName in theFormes
      # Loop through all available generations
      for generation in [minimumGeneration..forGeneration]
        {FormeData} = Generations[getGenerationFromInt(generation)]
        # Skip if this pokemon has no data.
        continue  if !FormeData[species]?
        # Since we check pre-evos, the pre-evo may not have the forme that its
        # parent has. We check if no forme exists; if so, we revert to default.
        formeName = "default"  if formeName not of FormeData[species]
        # The current forme may not have a learnset (Zen mode, megas), so we
        # do another check to see if it has a learnset. If not, use default.
        forme = FormeData[species][formeName]
        formeName = "default"  if !forme.learnset
        learnset = forme.learnset
        # Skip if this Pokemon has no learnset for this generation.
        continue  if !learnset

        # Skip if this Pokemon's ability is hidden and this generation has no
        # hidden abilities for this forme.
        continue  if hasHiddenAbility && !forme.hiddenAbility?
        return true  if iterator(learnset, species, formeName, generation) == true
  return false

# Returns an array of moves that this Pokemon can learn for a given generation.
self.learnableMoves = (Generations, pokemon, forGeneration) ->
  learnable = []
  loopLearnsets Generations, pokemon, forGeneration, (learnset, pokemonSpecies, formeName) ->
    # Push event moves
    events = EventPokemon[pokemonSpecies] || []
    events = events.filter((event) -> event.forme == formeName)
    for event in events
      learnable.push(event.moves)

    # Push learnset moves
    for method, moves of learnset
      if method in [ 'level-up', 'tutor', 'machine', 'egg' ] ||
          ((pokemon.forme || "default") == formeName)  # e.g. Hydro Pump Rotom-w
        learnable.push((moveName  for moveName of moves))

    # If the learnset includes Sketch, then we include every move.
    if learnset["level-up"]?["Sketch"]
      for moveName of Generations[getGenerationFromInt(forGeneration)].MoveData
        if moveName not in unsketchableMoves
          learnable.push(moveName)

  _.chain(learnable).flatten().sort().unique().value()

# Checks the moveset of a given Pokemon for a given generation, with the given
# species and forme data for all Pokemon.
#
# Returns true if the moveset is valid, false otherwise.
self.checkMoveset = (Generations, pokemon, generation, moves) ->
  looper = loopLearnsets.bind(null, Generations, pokemon, generation)
  pokemonSpecies = pokemon.species
  pokemonForme = pokemon.forme || "default"
  pokemonLevel = (pokemon.level || 100)
  {FormeData} = Generations[getGenerationFromInt(generation)]
  forme = FormeData[pokemonSpecies][pokemonForme]

  # In gen 4, pokemon must know *all* moves inside the `form-change` learnset.
  if generation == 4
    rsForme = Generations.DP.FormeData[pokemonSpecies]?[pokemonForme] || {}
    learnset = rsForme.learnset?['form-change'] || {}
    for move, level of learnset
      return false  if move not in moves || pokemonLevel < level

  # Get a list of all moves that the Pokemon can't learn
  # through level-up, tutoring, machines, or Sketch.
  leftoverMoves = (m  for m in moves when !checkMove(looper, pokemon, m))

  # Continuing the `forme-change` learnset group.
  # Get rid of leftover moves if this pokemon can learn it in this generation.
  learnset = forme.learnset?['form-change'] || {}
  lsetLeftovers = leftoverMoves.filter((move) -> pokemonLevel >= learnset[move])
  return true  if lsetLeftovers.length == leftoverMoves.length

  # Check against event Pokemon
  # TODO: Event Pokemon require more stringent checks, e.g. gender/ability etc.
  checksOut = looper (learnset, pokemonSpecies, formeName) ->
    events = EventPokemon[pokemonSpecies] || []
    events = events.filter((event) -> event.forme == formeName)
    for event in events
      lsetLeftovers = leftoverMoves.filter (move) ->
        move in event.moves && pokemonLevel >= event.level
      return true  if lsetLeftovers.length == leftoverMoves.length
  return true  if checksOut

  # These learnset groups are non-standard but can be used. If a non-standard
  # group completely overlaps the leftover moves, the moveset is valid.
  nonstandardGroups = [ "light-ball-egg", "stadium-surfing-pikachu" ]
  checksOut = looper (learnset) ->
    for group in nonstandardGroups
      continue  if !learnset[group]
      total = (m  for m in leftoverMoves when m of learnset[group]).length
      return true  if total == leftoverMoves.length
  return true  if checksOut

  # If the remaining moves are all dream world moves, it's a valid moveset.
  checksOut = looper (learnset) ->
    return  if !learnset['dreamWorld']
    dreamWorldMoves = []
    for moveName of learnset['dreamWorld']
      continue  if moveName in dreamWorldMoves || moveName not in leftoverMoves
      dreamWorldMoves.push(moveName)
    return true  if leftoverMoves.length == dreamWorldMoves.length
  return true  if checksOut

  # If the remaining moves are all egg moves, the moveset is valid.
  # TODO: Check chain-breeding for gens 5 and under.
  eggMoves = []
  looper (learnset) ->
    return  if !learnset['egg']
    for moveName of learnset['egg']
      continue  if moveName in eggMoves || moveName not in leftoverMoves
      eggMoves.push(moveName)

  # If the Pokemon has to know a certain move to evolve, check the egg moves
  # since you cannot have a moveset made completely of pure egg moves.
  # A magic constant of 4 is used to imitate the game's maximum of 4 moves.
  return false  if eggMoves.length == 4 && mustLearnMove[pokemon.species]
  return true  if eggMoves.length == leftoverMoves.length

  # This Pokemon cannot learn all these moves. Sorry.
  return false

# Checks a single move to see if the Pokemon can learn it through level-up,
# tutors, machines, Sketch, or pre-evolutions.
checkMove = (looper, pokemon, move) ->
  {level} = pokemon
  level ||= 100

  checksOut = looper (learnset) ->
    # Check level-up, TM/HM, and tutors.
    return true  if learnset["level-up"]?[move] <= level ||
                    learnset["machine"]?[move]  <= level ||
                    learnset["tutor"]?[move]    <= level

    # If the Pokemon can learn Sketch, then by golly, it can learn anything!
    # ... Except Chatter and Struggle. NOTE: Bogus moves are considered valid,
    # so you must take care of them at a higher level.
    # Unreleased event moves are also unsketchable until release.
    return true  if learnset["level-up"]?["Sketch"] <= level &&
                    move not in unsketchableMoves
  return true  if checksOut

  # TODO: Skip unavailable Pokemon (due to being a generation later).
  # TODO: level-up moves can be bred.
  return false
