package system.item.skill;

import java.util.ArrayList;
import java.util.Random;

import br.ufsc.inf.leobr.cliente.Jogada;
import system.battle.Coordinate;
import system.battle.Tile;
import system.character.BaseCharacter;
import system.character.CharacterTraits;
import system.item.Effect;
import system.item.Item;

/**
 * Especialização da classe Item que permite que um jogador interaja com os elementos
 * presentes numa batalha, tal como inimigos e outros jogadores.<br>
 * Implementa Jogada da biblioteca NetGames para permitir a comunicação em um estado multiplayer.
 */
public class Skill extends Item implements Jogada {
	/**
	 * Serve pra não dar merda na serialização de objetos.
	 */
	private static final long serialVersionUID = 8159957921630726811L;
	
	/**
	 * Custo da skill em mp
	 */
	private int cost;
	/**
	 * Quantidade de vezes que ela acerta seus alvos.
	 */
	private int numberOfHits = 1;
	/**
	 * Valor adicionado a iniciativa ao utilizar a habilidade.
	 */
	private int cooldown;
	/**
	 * Valor do dano causado. Valores negativos indicam cura.
	 */
	private int value;
	/**
	 * Taxa de variação (em percentuais) do value. Padrão é 20%. 
	 */
	private int variability = 20;
	
	/**
	 * Precisão de 0% a 100%
	 */
	private double skillAccuracy;
	/**
	 * Modificadores percentuais nos parâmetros principais.<br>
	 * Exemplo: ataque * 200%
	 */
	private double skillModifier;
	
	/**
	 * Parâmetros do usuário.
	 */
	private BaseCharacter user;

	/**
	 * Indica se o personagem já conhece esta habilidade. É um pré-requisito para utilizá-la.
	 */
	private boolean learned;
	/**
	 * Indica se o personagem equipou esta habilidade para permitir seu uso em batalha.
	 */
	private boolean equipped;
	/**
	 * Define se a habilidade causa dano ou não.
	 */
	private boolean damaging;
	
	/**
	 * Nome do parâmetro a modificar o value.
	 */
	private String parameter;
	/**
	 * Tipo da expansão da Range.
	 */
	private String expansionType;
	/**
	 * Tipo de expansão do Radius.
	 */
	private String radiusType;
	
	/**
	 * Gerador de números aleatórios
	 */
	private Random random = new Random();
	
	/**
	 * Os efeitos desta habilidade.
	 */
	private Effect effect;
	
	/**
	 * Cria uma Skill lol.<br>
	 * Alcance eficaz é tudo entre minRange e maxRange.
	 * @param name o nome da skill
	 * @param cost o custo em Mp da skill
	 * @param value o valor de bônus/penalidade no dano/cura causada
	 * @param minRange distância mínima do ponto inicial
	 * @param maxRange alcance máximo a partir do ponto inicial
	 * @param radius raio da seleção de alvos
	 * @param cooldown o "tempo de recarga" da habilidade. valores maiores penalizam mais a iniciativa do usuário
	 * @param numberOfHits a quantia de golpes da habilidade
	 * @param variability a taxa de variação percentual do dano/cura (ex: 20 é igual a 80%~120%)
	 * @param accuracy a taxa de precisão da habilidade.
	 * @param skillModifier modificador que multiplica o parâmetro principal passado para a skill
	 * @param user o usuário da habilidade
	 * @param effect os efeitos da habilidade
	 * @param parameter valores válidos:<br>Item, CurrentHp, MaxHp, CurrentMp, MaxMp, Attack, Defense, Speed
	 * @param expansionType o tipo de expansão da Skill. Valores válidos:<br>Circular, Cardinal, Diagonal, CardinalDiagonal
	 * @param radiusType o tipo de expansão do Radius. Valores válidos:<br>Singular, Circular, Straight, StraightAll
	 * @param damaging define se a habilidade causa dano (true) ou cura (false)
	 */
	public Skill(int id, String name, int cost, int value, int minRange, int maxRange, int radius, int cooldown, int numberOfHits,
			int variability, double accuracy, double skillModifier, BaseCharacter user, Effect effect,
			String parameter, String expansionType, String radiusType, boolean damaging, String description) {
		this.id = id;
		this.name = name;
		this.cost = cost;
		this.value = value;
		this.minRange = minRange;
		this.maxRange = maxRange;
		this.radius = radius;
		this.cooldown = cooldown;
		this.numberOfHits = numberOfHits;
		this.variability = variability;
		this.skillAccuracy = accuracy;
		this.skillModifier = skillModifier;
		this.user = user;
		this.effect = effect;
		this.parameter = parameter;
		this.expansionType = expansionType;
		this.radiusType = radiusType;
		this.damaging = damaging;
		this.description = description;
	}
	
	/**
	 * Executa a skill em foe considerando gastos, iniciativa e o caralho a quatro.
	 * @param foe - os alvos atingidos.
	 */
	public int[][] executeSkill(BaseCharacter[] foe) {
		// Paga o custo da habilidade.
//		payCost();
		// Atualiza a iniciativa
		updateInitiative();
		
		int[][] asd = new int[foe.length][2];
		
		for (int i = 0; i < foe.length; i++) {
			// O hit vale para todos os ataques sobre um alvo
			int damage = 0;
			int successfulHits = numberOfHits;		// Era pra começar em 0, mas deu merda no meio do caminho
			int[] results = new int[2];

			int hit = calculateHit(foe[i].getTraits().totalSecondaryParameter(1)); 
			
			// Acerto crítico
			if (hit == 2) {
				damage += applyHits(getParameter(), foe[i], damaging, true);
//				successfulHits++;
			}
			// Acerto normal
			if (hit == 1) {
				damage += applyHits(getParameter(), foe[i], damaging, false);
//				successfulHits++;
			}

			results[0] = damage;
			results[1] = successfulHits;
			
			asd[i] = results;
		}
		
		return asd;
	}
	
	/**
	 * Checa a taxa de precisão da Skill conforme a evasão dos alvos
	 * @param evasion - a taxa de evasão
	 * @return skillAccuracy + userAccuracy - evasion
	 */
	private double getTotalAccuracy(double evasion) {
		return skillAccuracy + user.getTraits().totalSecondaryParameter(0) - evasion;
	}
		
	/**
	 * Retorna o parâmetro modificador desta habilidade conforme estipulado em seu construtor.
	 * @return o valor do parâmetro ou 1 se for um item.
	 */
	public int getParameter() {
		if (parameter.equals("Item")) {
			return 1;
		} else if (parameter.equals("CurrentHp")) {
			return user.getTotalHp();
		} else if (parameter.equals("MaxHp")) {
			return user.getTotalHpMax();
		} else if (parameter.equals("CurrentMp")) {
			return user.getTotalMp();
		} else if (parameter.equals("MaxMp")) {
			return user.getTotalMpMax();
		} else if (parameter.equals("Attack")) {
			return user.getTraits().totalMainParameter(4);
		} else if (parameter.equals("Defense")) {
			return user.getTraits().totalMainParameter(5);
		} else if (parameter.equals("Speed")) {
			return user.getTraits().totalMainParameter(6);
		}
		return 0;
	}
	
	/**
	 * Calcula o sucesso de um ataque com base na skill accuracy
	 * @param evasion - a evasão do alvo a ser atingido
	 * @return 2 se for acerto crítico<br>
	 * 1 se for acerto normal<br>
	 * -1 se errar.
	 */
	private int calculateHit(double evasion) {
		int hit = random.nextInt(100);
		
		if (effect.isCannotMiss()) {
			return 1;
		} else if (hit <= getTotalAccuracy(evasion)) {
			if (hit <= user.getTraits().totalSecondaryParameter(2) && effect.isCanCritical()) {
				return 2;
			} else {				
				return 1;
			}
		} else {
			return -1;
		}
	}
	
	/**
	 * Fórmula do dano causado pela habilidade
	 * @param parameter - parâmetro passado como modificador
	 * @return (int) ((skillModifier * parameter + this.value) * generated)/100
	 */
	public int calculateValue(double parameter) {
		int lower = 100 - variability;
		int upper = 2 * variability + 1;
		int generated = random.nextInt(upper) + lower;
		
		int value = (int) ((skillModifier * parameter + this.value) * generated)/100;
		
		return value;
	}
	
	/**
	 * Retorna o valor estático da habilidade (sem considerar a taxa de variabilidade)
	 * @param parameter parâmetro passado como modificador
	 * @return (int) ((skillModifier * parameter + this.value))/100
	 */
	public int getStaticValue() {
		System.out.println(skillModifier +" "+getParameter()+" "+this.value);
		int value = (int) ((skillModifier * getParameter() + this.value));
		
		return value;
	}

	/**
	 * Paga o custo da habilidade em Mp.
	 */
	public void payCost() {
		user.decrementMp(cost);
	}
	
	/**
	 * Calcula a nova iniciativa do usuário levando em consideração o cooldown da habilidade.
	 */
	public void updateInitiative() {
		user.calculateInitiative(cooldown);
	}
	
	/**
	 * Checa se o usuário pode pagar o custo da habilidade.
	 * @return verdadeiro se puder pagar o custo, falso caso contrário.
	 */
	public boolean isCostPayable() {
		if (user.getTotalMp() >= cost) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Aplica o value numberOfHits vezes no alvo
	 * @param parameter - parâmetro de dano/cura
	 * @param foe - o alvo que será atingido
	 */
	public int applyHits(double parameter, BaseCharacter foe, boolean damaging, boolean critical) {
		int totalDamage = 0;

		if (foe != null) {	
			for (int j = 0; j < numberOfHits; j++) {
				// Se afeta o Hp
				if (effect.isAffectHp()) {
					int damage = calculateDamage(parameter, foe, damaging, critical);
					totalDamage += damage;
					
					// Revive (apenas possível afetando o Hp)
					if (effect.isRevive()) {
						foe.revive();
					}
					
					// Aplica o dano calculado
					if (damaging) {
						foe.decrementHp(damage);
					} else {
						foe.incrementHp(damage);
					}
				}
				// Se afeta o Mp
				if (effect.isAffectMp()) {
					int damage = calculateDamage(parameter, foe, damaging, critical);
					totalDamage += damage;
					
					// Aplica o dano calculado
					if (damaging) {
						foe.decrementMp(damage);
					} else {
						foe.incrementMp(damage);
					}
				}				
			}
			
			// Caso haja status negativos, aplica-se
			if (effect.getAfflictions() != null) {
				for (int i = 0; i < effect.getAfflictions().length; i++) {
					int affliction = effect.getAfflictions()[i].getId();
					
					if (foe.getTraits().hasAffliction(affliction) == false) {
						foe.addAffliction(affliction);
					}
				}
			}
		}
		
		return totalDamage;
	}
	
	public void applyHits(BaseCharacter foe, boolean damaging, boolean critical, int damage) {
		if (foe != null) {
			for (int j = 0; j < numberOfHits; j++) {
				// Se afeta o Hp
				if (effect.isAffectHp()) {
					// Revive (apenas possível afetando o Hp)
					if (effect.isRevive()) {
						foe.revive();
					}
					
					// Aplica o dano calculado
					if (damaging) {
						foe.decrementHp(damage);
					} else {
						foe.incrementHp(damage);
					}
				}
				// Se afeta o Mp
				if (effect.isAffectMp()) {					
					// Aplica o dano calculado
					if (damaging) {
						foe.decrementMp(damage);
					} else {
						foe.incrementMp(damage);
					}
				}				
			}
			
			// Caso haja status negativos, aplica-se
			if (effect.getAfflictions() != null) {
				for (int i = 0; i < effect.getAfflictions().length; i++) {
					int affliction = effect.getAfflictions()[i].getId();
					
					if (foe.getTraits().hasAffliction(affliction) == false) {
						foe.addAffliction(affliction);
					}
				}
			}
		}
	}
	
	/**
	 * Calcula um dano em um alvo em conformidade com as propriedades da Skill
	 * @param parameter o valor do parâmetro passado como modificador do dano (Ataque por exemplo)
	 * @param foe alvo que será atingido pelo dano calculado
	 * @param damaging define se o Hp/Mp do alvo será incrementado (false) ou decrementado (true)
	 * @param critical define se o dano foi crítico (x1.25)
	 * @return o valor do dano a ser causado no alvo (mínimo 1)
	 */
	public int calculateDamage(double parameter, BaseCharacter foe, boolean damaging, boolean critical) {
		// Calcula o valor inicial do dano
		int damage = calculateValue(parameter);
		
		// Se o objetivo for causar dano
		if (damaging) {
			// Reduz pela defesa do alvo
			damage -= foe.getTraits().totalMainParameter(5);
			
			// Pro dano não "curar" limita-o em 1
			if (damage < 0) {
				damage = 1;
			}
		}
		
		// Se for um golpe crítico
		if (critical) {
			damage *= 1.25;
		}
		
		// Reduz/aumenta conforme as resistências elementais
		damage *= getTotalElementalResistance(foe);
		
		// Limita o dano para MAX_PARAMETER_VALUE
		if (damage > CharacterTraits.getMaxParameterValue()) {
			damage = CharacterTraits.getMaxParameterValue();
		}
		
		// Aplica dreno (TEM QUE causar dano)
		if (effect.isDrain() && damaging && damage > 0) {
			user.incrementHp(damage);
		}
		
		return damage;
	}
	
	/**
	 * Com base no effect, retorna o produto de todos as resistências elementais relevantes.
	 * @param foe - alvo em questão
	 * @return - total = totalResistance/elementQuantity
	 */
	public double getTotalElementalResistance(BaseCharacter foe) {
		double totalResistance = 0.0;
		int elementQuantity = 0;

		if (effect.isElementMelee()) {
			totalResistance += foe.getTraits().getElementMelee();
			elementQuantity++;
		}
		if (effect.isElementRanged()) {
			totalResistance += foe.getTraits().getElementRanged();
			elementQuantity++;
		}
		if (effect.isElementNonElemental()) {
			totalResistance += foe.getTraits().getElementNonElemental();
			elementQuantity++;
		}
		if (effect.isElementFire()) {
			totalResistance += foe.getTraits().getElementFire();
			elementQuantity++;
		}
		if (effect.isElementIce()) {
			totalResistance += foe.getTraits().getElementIce();
			elementQuantity++;
		}
		if (effect.isElementThunder()) {
			totalResistance += foe.getTraits().getElementThunder();
			elementQuantity++;
		}
		if (effect.isElementWind()) {
			totalResistance += foe.getTraits().getElementWind();
			elementQuantity++;
		}
		if (effect.isElementDragon()) {
			totalResistance += foe.getTraits().getElementDragon();
			elementQuantity++;
		}
		
		if (elementQuantity == 0) {
			return 1.0;
		} else {
			double total = totalResistance/elementQuantity;
			return total;
		}
	}

	/**
	 * Define a skill como equipada.
	 */
	public void equip() {
		if (equipped == false) {
			equipped = true;			
		}
	}
	
	/**
	 * Define a skill como não equipada.
	 */
	public void unequip() {
		if (equipped) {
			equipped = false;			
		}
	}
	
	/**
	 * Percorre o mapa para encontrar posições válidas conforme os seguintes critérios:<br>
	 * - O limite da expansão é maxRange<br>
	 * - Se a diferença dos height de tiles vizinhos for maior que <b>maxRange</b>, não será possível expandir para tais tiles<br>
	 * - Há uma expansão que vai até minRange que delimita as posições inválidas a partir da posição inicial.<br>
	 * - Unidades de qualquer tipo não bloqueiam a expansão.<br>
	 * - Caso a skill tenha damaging = true a posição do usuário é considerada inválida
	 * @param map o mapa da batalha atual
	 * @return a lista de posições válidas para serem atacadas.
	 */
	public ArrayList<Tile> showSkillRange(Tile[][] map) {
		// Lista com todas as posições válidas para serem atingidas conforme maxRange
		ArrayList<Tile> skillRange = new ArrayList<Tile>();
		
		// Define o alcance de ataque mínimo como os tiles da lista visitedMin
		ArrayList<Tile> skillMinRange = new ArrayList<Tile>();
		
		if (expansionType.equals("Circular")) {
			skillRange = circularExpansion(map, user.getCoordinate(), maxRange);
			skillMinRange = circularExpansion(map, user.getCoordinate(), minRange);
		}
		if (expansionType.equals("Cardinal")) {
			skillRange = cardinalExpansion(map, user.getCoordinate(), maxRange);
			skillMinRange = cardinalExpansion(map, user.getCoordinate(), minRange);
		}

		// Lista de tiles inválidos a serem removidos de skillRange
		ArrayList<Tile> invalidTiles = new ArrayList<Tile>();
		
		// Adiciona os tiles de skillMinRange para invalidTiles
		for (Tile tile : skillMinRange) {
			invalidTiles.add(tile);
		}	

		// Adiciona o tile que em que o usuário se encontra se a Skill for damaging a invalidTiles
		for (Tile tile : skillRange) {
			if (tile.getOccupant() == user && (damaging || minRange == 1)) {
				invalidTiles.add(tile);
			}
		}
		
		// Remove os tiles inválidos de skillRange
		for (Tile tile : invalidTiles) {
			skillRange.remove(tile);
		}
		
		return skillRange;
	}
	
	/**
	 * Mesma merda que o outro, mas é agora relativo a uma coordenada
	 * @param map
	 * @param position
	 * @return
	 */
	public ArrayList<Tile> showSkillRange(Tile[][] map, Coordinate position) {
		// Lista com todas as posições válidas para serem atingidas conforme maxRange
		ArrayList<Tile> skillRange = new ArrayList<Tile>();
		
		// Define o alcance de ataque mínimo como os tiles da lista visitedMin
		ArrayList<Tile> skillMinRange = new ArrayList<Tile>();
		
		if (expansionType.equals("Circular")) {
			skillRange = circularExpansion(map, position, maxRange);
			skillMinRange = circularExpansion(map, position, minRange);
		}
		if (expansionType.equals("Cardinal")) {
			skillRange = cardinalExpansion(map, position, maxRange);
			skillMinRange = cardinalExpansion(map, position, minRange);
		}

		// Lista de tiles inválidos a serem removidos de skillRange
		ArrayList<Tile> invalidTiles = new ArrayList<Tile>();
		
		// Adiciona os tiles de skillMinRange para invalidTiles
		for (Tile tile : skillMinRange) {
			invalidTiles.add(tile);
		}	

		// Adiciona o tile que em que o usuário se encontra se a Skill for damaging a invalidTiles
		for (Tile tile : skillRange) {
			if (tile.getOccupant() == user && (damaging || minRange == 1)) {
				invalidTiles.add(tile);
			}
		}
		
		// Remove os tiles inválidos de skillRange
		for (Tile tile : invalidTiles) {
			skillRange.remove(tile);
		}
		
		return skillRange;
	}

	/**
	 * Mostra o raio da area de ação da skill
	 * @param map o mapa em que a skill foi executada
	 * @param chosenPosition a posição escolhida pelo jogador
	 * @return a lista com as posições válidas para receber dano.
	 */
	public ArrayList<Tile> showRadius(Tile[][] map, Coordinate chosenPosition) {
		// Lista com todas as posições válidas para serem atingidas conforme maxRange
		ArrayList<Tile> radiusRange = new ArrayList<Tile>();
		
		// Seleciona APENAS o tile presente em startingPosition
		if (radiusType.equals("Singular")) {
			radiusRange.add(map[chosenPosition.getX()][chosenPosition.getY()]);
		}
		// Pega um raio circular a partir da posição inicial
		if (radiusType.equals("Circular")) {
			radiusRange = circularExpansion(map, chosenPosition, radius);
			
			if (radiusRange.contains(map[chosenPosition.getX()][chosenPosition.getY()]) == false) {
				radiusRange.add(map[chosenPosition.getX()][chosenPosition.getY()]);
			}
		}
		if (radiusType.equals("Straight")) {
			// Pega uma linha de Radius, apenas a posição mais próxima será atingida
			ArrayList<Tile> radiusAux = CheckNeighbourTiles(map, user.getCoordinate());
			ArrayList<Tile> radiusAux2 = new ArrayList<Tile>(); // Porque uma auxiliar não é o suficiente
			
			// A porra do checkNeighbour dá merda se não pegar os 4 vizinhos 2 vezes, logo tem que fazer isso
			for (Tile tile : radiusAux) {
				if (radiusAux2.contains(tile) == false) {
					radiusAux2.add(tile);
				}
			}
			
			// Tá errado, não tá otimizado, mas tá funcionando do jeito que eu quero
			ArrayList<Tile> radiusLine1 = new ArrayList<Tile>();
			ArrayList<Tile> radiusLine2 = new ArrayList<Tile>();
			ArrayList<Tile> radiusLine3 = new ArrayList<Tile>();
			ArrayList<Tile> radiusLine4 = new ArrayList<Tile>();
			
			// QUALIDADE
			if (radiusAux2.size() > 0) {
				radiusLine1 = linearExpansion(map, radiusAux2.get(0).getCoordinate(), maxRange);
				
				if (radiusAux2.size() > 1) {
					radiusLine2 = linearExpansion(map, radiusAux2.get(1).getCoordinate(), maxRange);
					
					if (radiusAux2.size() > 2) {
						radiusLine3 = linearExpansion(map, radiusAux2.get(2).getCoordinate(), maxRange);
						
						if (radiusAux2.size() > 3) {
							radiusLine4 = linearExpansion(map, radiusAux2.get(3).getCoordinate(), maxRange);
						}
					}
				}
			}
			
			if (radiusLine1 != null) {
				for (Tile tile : radiusLine1) {
//					System.out.println("RADIUS1 "+tile.getCoordinate().getX()+","+tile.getCoordinate().getY());
					if (tile.getCoordinate().getX() == chosenPosition.getX() &&
						tile.getCoordinate().getY() == chosenPosition.getY()) {
						radiusRange = radiusLine1;
						break;
					}
				}
			}
			System.out.println();
			if (radiusLine2 != null) {
				for (Tile tile : radiusLine2) {
//					System.out.println("RADIUS2 "+tile.getCoordinate().getX()+","+tile.getCoordinate().getY());
					if (tile.getCoordinate().getX() == chosenPosition.getX() &&
						tile.getCoordinate().getY() == chosenPosition.getY()) {
						radiusRange = radiusLine2;
						break;
					}
				}
			}
			System.out.println();
			if (radiusLine3 != null) {
				for (Tile tile : radiusLine3) {
//					System.out.println("RADIUS3 "+tile.getCoordinate().getX()+","+tile.getCoordinate().getY());
					if (tile.getCoordinate().getX() == chosenPosition.getX() &&
						tile.getCoordinate().getY() == chosenPosition.getY()) {
						radiusRange = radiusLine3;
						break;
					}
				}
			}
			System.out.println();
			if (radiusLine4 != null) {
				for (Tile tile : radiusLine4) {
//					System.out.println("RADIUS4 "+tile.getCoordinate().getX()+","+tile.getCoordinate().getY());
					if (tile.getCoordinate().getX() == chosenPosition.getX() &&
						tile.getCoordinate().getY() == chosenPosition.getY()) {
						radiusRange = radiusLine4;
						break;
					}
				}
			}
			
			// Pra fazer acertar a primeira posição tente pegar o valor de radius type e agir de acordo já que vai estar tudo ordenado.
		}
		if (radiusType.equals("StraightAll")) {
			// Pega todas as linhas alcançáveis, todas as posições são atingidas
			ArrayList<Tile> radiusAux = CheckNeighbourTiles(map, user.getCoordinate());
			ArrayList<Tile> radiusAux2 = new ArrayList<Tile>(); // Porque uma auxiliar não é o suficiente
			
			// A porra do checkNeighbour dá merda se não pegar os 4 vizinhos 2 vezes, logo tem que fazer isso
			for (Tile tile : radiusAux) {
				if (radiusAux2.contains(tile) == false) {
					radiusAux2.add(tile);
				}
			}
			
			// Tá errado, não tá otimizado, mas tá funcionando do jeito que eu quero
			ArrayList<Tile> radiusLine1 = new ArrayList<Tile>();
			ArrayList<Tile> radiusLine2 = new ArrayList<Tile>();
			ArrayList<Tile> radiusLine3 = new ArrayList<Tile>();
			ArrayList<Tile> radiusLine4 = new ArrayList<Tile>();
			
			// QUALIDADE
			if (radiusAux2.size() > 0) {
				radiusLine1 = linearExpansion(map, radiusAux2.get(0).getCoordinate(), maxRange);
				
				if (radiusAux2.size() > 1) {
					radiusLine2 = linearExpansion(map, radiusAux2.get(1).getCoordinate(), maxRange);
					
					if (radiusAux2.size() > 2) {
						radiusLine3 = linearExpansion(map, radiusAux2.get(2).getCoordinate(), maxRange);
						
						if (radiusAux2.size() > 3) {
							radiusLine4 = linearExpansion(map, radiusAux2.get(3).getCoordinate(), maxRange);
						}
					}
				}
			}
			
			if (radiusLine1 != null) {
				for (Tile tile : radiusLine1) {
//					System.out.println("RADIUS1 "+tile.getCoordinate().getX()+","+tile.getCoordinate().getY());
					if (tile.getCoordinate().getX() == chosenPosition.getX() &&
						tile.getCoordinate().getY() == chosenPosition.getY()) {
						radiusRange = radiusLine1;
						break;
					}
				}
			}
			System.out.println();
			if (radiusLine2 != null) {
				for (Tile tile : radiusLine2) {
//					System.out.println("RADIUS2 "+tile.getCoordinate().getX()+","+tile.getCoordinate().getY());
					if (tile.getCoordinate().getX() == chosenPosition.getX() &&
						tile.getCoordinate().getY() == chosenPosition.getY()) {
						radiusRange = radiusLine2;
						break;
					}
				}
			}
			System.out.println();
			if (radiusLine3 != null) {
				for (Tile tile : radiusLine3) {
//					System.out.println("RADIUS3 "+tile.getCoordinate().getX()+","+tile.getCoordinate().getY());
					if (tile.getCoordinate().getX() == chosenPosition.getX() &&
						tile.getCoordinate().getY() == chosenPosition.getY()) {
						radiusRange = radiusLine3;
						break;
					}
				}
			}
			System.out.println();
			if (radiusLine4 != null) {
				for (Tile tile : radiusLine4) {
//					System.out.println("RADIUS4 "+tile.getCoordinate().getX()+","+tile.getCoordinate().getY());
					if (tile.getCoordinate().getX() == chosenPosition.getX() &&
						tile.getCoordinate().getY() == chosenPosition.getY()) {
						radiusRange = radiusLine4;
						break;
					}
				}
			}
		}
		
		return radiusRange;
	}
	
	/**
	 * Percorre o mapa para encontrar posições válidas conforme os seguintes critérios:<br>
	 * - O limite da expansão é maxRange<br>
	 * - Se a diferença dos height de tiles vizinhos for maior que limit, não será possível expandir para tais tiles<br>
	 * - Unidades de qualquer tipo não bloqueiam a expansão.<br>
	 * - Tem que ficar circular porra.
	 * @param map o mapa da batalha atual
	 * @param limit valor do limite da expansão
	 * @return a lista de posições válidas para serem atacadas.
	 */
	private ArrayList<Tile> circularExpansion(Tile[][] map, Coordinate startingPosition, int limit) {
		// Lista de Tiles visitados
		ArrayList<Tile> visited = new ArrayList<Tile>();
		// Lista dos próximos tiles a serem visitados
		ArrayList<Tile> visitedNext = new ArrayList<Tile>(CheckNeighbourTiles(map, startingPosition));
		// Lista com todas as posições inválidas
		ArrayList<Tile> attackRange = new ArrayList<Tile>();
		
		// Preenche a lista attackRange com os elementos válidos
		for (int i = 0; i < limit; i++) {
			ArrayList<Tile> border = new ArrayList<Tile>();
			
			for (Tile tile : visitedNext) {
				ArrayList<Tile> neighbours = new ArrayList<Tile>(CheckNeighbourTiles(map, tile.getCoordinate()));
				 
				for (Tile neighbour : neighbours) {
					int heightDifference = tile.getHeight() - neighbour.getHeight();
					
					if (heightDifference < 0) {
						heightDifference *= -1;
					}
					
					// Se a borda não ter o dito vizinho, adiciona o vizinho a ela.
					if (border.contains(neighbour) == false && heightDifference <= limit) {
						border.add(neighbour);
					}
				}
				
				// Se a lista de posições visitadas não possuem o tile, ele é adicionado a ela.
				if (visited.contains(tile) == false) {
					visited.add(tile);
				}
			}
			
			// Adiciona à lista de posições visitadas os tiles em visitedMinNext de forma que não haja repetições de um mesmo tile
			for (Tile tile : visited) {
				ArrayList<Tile> neighbours = new ArrayList<Tile>(CheckNeighbourTiles(map, tile.getCoordinate()));
				
				// foreach que adiciona as posições vizinhas contanto que não haja iguais
				for (Tile neighbour : neighbours) {
					if (visitedNext.contains(neighbour) == false) {
						visitedNext.add(neighbour);
					}
				}
			}
		}
		
		// Define o alcance de ataque como os tiles da lista visited
		attackRange = visited;
		
		return attackRange;
	}
	
	/**
	 * Percorre o mapa para encontrar posições cardinais válidas
	 * @param map mapa da expansão
	 * @param startingPosition a posição inicial da expansão
	 * @param limit o valor do limite da expansão
	 * @return todas as posições cardinais válidas
	 */
	private ArrayList<Tile> cardinalExpansion(Tile[][] map, Coordinate startingPosition, int limit) {
		// Lista com todas as posições válidas
		ArrayList<Tile> attackRange = new ArrayList<Tile>();
		
		for (int i = -limit; i <= limit; i++) {
			for (int j = -limit; j <= limit; j++) {
				if (i != 0 && j != 0) {
					Coordinate auxCoordinate = new Coordinate(startingPosition.getX() + i, startingPosition.getY() + j);
					
					if ((auxCoordinate.getX() < 0 || auxCoordinate.getX() >= map.length) ||
						(auxCoordinate.getY() < 0 || auxCoordinate.getY() >= map[startingPosition.getY()].length)) {
						// break leaves a loop, continue jumps to the next iteration.
						// Fonte: https://stackoverflow.com/questions/462373/difference-between-break-and-continue-statement
						continue;
					}

					int heightDifference1 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[auxCoordinate.getX()][startingPosition.getY()].getHeight();
					int heightDifference2 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[startingPosition.getX()][auxCoordinate.getY()].getHeight();
					
					// Se as diferenças na altura resultarem em um valor negativo, torne-os positivos
					if (heightDifference1 < 0) {
						heightDifference1 *= -1;
					}
					
					if (heightDifference2 < 0) {
						heightDifference2 *= -1;
					}
					
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference1 <= maxRange) {
						if (attackRange.contains(map[auxCoordinate.getX()][startingPosition.getY()]) == false) {
							attackRange.add(map[auxCoordinate.getX()][startingPosition.getY()]);							
						}
					}
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference2 <= maxRange) {
						if (attackRange.contains(map[startingPosition.getX()][auxCoordinate.getY()]) == false) {
							attackRange.add(map[startingPosition.getX()][auxCoordinate.getY()]); 							
						}
					}
				}
			}
		}
		
		return attackRange;
	}
	
	/**
	 * Checa os tiles vizinhos e retorna uma ArrayList com as posições válidas.
	 * @param map - o mapa que o user se encontra
	 * @param coordinate - coordenada do mapa que será analizada
	 * @return uma ArrayList com os elementos vizinhos a coordinate
	 */
	private ArrayList<Tile> CheckNeighbourTiles(Tile[][] map, Coordinate coordinate) {
		// Lista de Tiles que será retornada no final das operações
		ArrayList<Tile> listOfAdjacentTiles = new ArrayList<>();
		
		for (int i = -1; i <= 1; i++) {
			for (int j = -1; j <= 1; j++) {
				if (i != 0 && j != 0) {
					Coordinate auxCoordinate = new Coordinate(coordinate.getX() + i, coordinate.getY() + j);
					
					if ((auxCoordinate.getX() < 0 || auxCoordinate.getX() >= map.length) ||
						(auxCoordinate.getY() < 0 || auxCoordinate.getY() >= map[coordinate.getY()].length)) {
						// break leaves a loop, continue jumps to the next iteration.
						// Fonte: https://stackoverflow.com/questions/462373/difference-between-break-and-continue-statement
						continue;
					}

					int heightDifference1 = map[coordinate.getX()][coordinate.getY()].getHeight() - map[auxCoordinate.getX()][coordinate.getY()].getHeight();
					int heightDifference2 = map[coordinate.getX()][coordinate.getY()].getHeight() - map[coordinate.getX()][auxCoordinate.getY()].getHeight();
					
					// Se as diferenças na altura resultarem em um valor negativo, torne-os positivos
					if (heightDifference1 < 0) {
						heightDifference1 *= -1;
					}
					
					if (heightDifference2 < 0) {
						heightDifference2 *= -1;
					}
					
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference1 <= maxRange) {
						listOfAdjacentTiles.add(map[auxCoordinate.getX()][coordinate.getY()]);
					}
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference2 <= maxRange) {
						listOfAdjacentTiles.add(map[coordinate.getX()][auxCoordinate.getY()]);
					}
				}
			}	
		}
		
		return listOfAdjacentTiles;
	}
	
	private ArrayList<Tile> linearExpansion(Tile[][] map, Coordinate startingPosition, int limit) {
		// Lista com todas as posições válidas
		ArrayList<Tile> attackRange = new ArrayList<Tile>();
		
		// Se o x da posição selecionada for igual ao do usuário, isso quer dizer que uma coluna foi selecionada
		if (startingPosition.getX() == user.getCoordinate().getX()) {
			if (startingPosition.getY() > user.getCoordinate().getY()) {
				for (int i = 0; i < limit; i++) {
					Coordinate auxCoordinate = new Coordinate(startingPosition.getX(), startingPosition.getY() + i);
					
					if ((auxCoordinate.getX() < 0 || auxCoordinate.getX() >= map.length) ||
						(auxCoordinate.getY() < 0 || auxCoordinate.getY() >= map[startingPosition.getY()].length)) {
						// break leaves a loop, continue jumps to the next iteration.
						// Fonte: https://stackoverflow.com/questions/462373/difference-between-break-and-continue-statement
						continue;
					}

					int heightDifference1 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[auxCoordinate.getX()][startingPosition.getY()].getHeight();
					int heightDifference2 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[startingPosition.getX()][auxCoordinate.getY()].getHeight();
					
					// Se as diferenças na altura resultarem em um valor negativo, torne-os positivos
					if (heightDifference1 < 0) {
						heightDifference1 *= -1;
					}
					
					if (heightDifference2 < 0) {
						heightDifference2 *= -1;
					}
					
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference1 <= maxRange) {
						if (attackRange.contains(map[auxCoordinate.getX()][startingPosition.getY()]) == false) {
							attackRange.add(map[auxCoordinate.getX()][startingPosition.getY()]);							
						}
					}
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference2 <= maxRange) {
						if (attackRange.contains(map[startingPosition.getX()][auxCoordinate.getY()]) == false) {
							attackRange.add(map[startingPosition.getX()][auxCoordinate.getY()]); 							
						}
					}
				}
			} else if (startingPosition.getY() < user.getCoordinate().getY()){
				for (int i = 0; i > -limit; i--) {
					Coordinate auxCoordinate = new Coordinate(startingPosition.getX(), startingPosition.getY() + i);
					
					if ((auxCoordinate.getX() < 0 || auxCoordinate.getX() >= map.length) ||
						(auxCoordinate.getY() < 0 || auxCoordinate.getY() >= map[startingPosition.getY()].length)) {
						// break leaves a loop, continue jumps to the next iteration.
						// Fonte: https://stackoverflow.com/questions/462373/difference-between-break-and-continue-statement
						continue;
					}

					int heightDifference1 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[auxCoordinate.getX()][startingPosition.getY()].getHeight();
					int heightDifference2 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[startingPosition.getX()][auxCoordinate.getY()].getHeight();
					
					// Se as diferenças na altura resultarem em um valor negativo, torne-os positivos
					if (heightDifference1 < 0) {
						heightDifference1 *= -1;
					}
					
					if (heightDifference2 < 0) {
						heightDifference2 *= -1;
					}
					
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference1 <= maxRange) {
						if (attackRange.contains(map[auxCoordinate.getX()][startingPosition.getY()]) == false) {
							attackRange.add(map[auxCoordinate.getX()][startingPosition.getY()]);							
						}
					}
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference2 <= maxRange) {
						if (attackRange.contains(map[startingPosition.getX()][auxCoordinate.getY()]) == false) {
							attackRange.add(map[startingPosition.getX()][auxCoordinate.getY()]); 							
						}
					}
				}
			}
		} else if (startingPosition.getY() == user.getCoordinate().getY()) {
			if (startingPosition.getX() > user.getCoordinate().getX()) {
				for (int i = 0; i < limit; i++) {
					Coordinate auxCoordinate = new Coordinate(startingPosition.getX() + i, startingPosition.getY());
					
					if ((auxCoordinate.getX() < 0 || auxCoordinate.getX() >= map.length) ||
						(auxCoordinate.getY() < 0 || auxCoordinate.getY() >= map[startingPosition.getY()].length)) {
						// break leaves a loop, continue jumps to the next iteration.
						// Fonte: https://stackoverflow.com/questions/462373/difference-between-break-and-continue-statement
						continue;
					}

					int heightDifference1 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[auxCoordinate.getX()][startingPosition.getY()].getHeight();
					int heightDifference2 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[startingPosition.getX()][auxCoordinate.getY()].getHeight();
					
					// Se as diferenças na altura resultarem em um valor negativo, torne-os positivos
					if (heightDifference1 < 0) {
						heightDifference1 *= -1;
					}
					
					if (heightDifference2 < 0) {
						heightDifference2 *= -1;
					}
					
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference1 <= maxRange) {
						if (attackRange.contains(map[auxCoordinate.getX()][startingPosition.getY()]) == false) {
							attackRange.add(map[auxCoordinate.getX()][startingPosition.getY()]);							
						}
					}
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference2 <= maxRange) {
						if (attackRange.contains(map[startingPosition.getX()][auxCoordinate.getY()]) == false) {
							attackRange.add(map[startingPosition.getX()][auxCoordinate.getY()]); 							
						}
					}
				}
			} else if (startingPosition.getX() < user.getCoordinate().getX()){
				for (int i = 0; i > -limit; i--) {
					Coordinate auxCoordinate = new Coordinate(startingPosition.getX() + i, startingPosition.getY());
					
					if ((auxCoordinate.getX() < 0 || auxCoordinate.getX() >= map.length) ||
						(auxCoordinate.getY() < 0 || auxCoordinate.getY() >= map[startingPosition.getY()].length)) {
						// break leaves a loop, continue jumps to the next iteration.
						// Fonte: https://stackoverflow.com/questions/462373/difference-between-break-and-continue-statement
						continue;
					}

					int heightDifference1 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[auxCoordinate.getX()][startingPosition.getY()].getHeight();
					int heightDifference2 = map[startingPosition.getX()][startingPosition.getY()].getHeight() - map[startingPosition.getX()][auxCoordinate.getY()].getHeight();
					
					// Se as diferenças na altura resultarem em um valor negativo, torne-os positivos
					if (heightDifference1 < 0) {
						heightDifference1 *= -1;
					}
					
					if (heightDifference2 < 0) {
						heightDifference2 *= -1;
					}
					
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference1 <= maxRange) {
						if (attackRange.contains(map[auxCoordinate.getX()][startingPosition.getY()]) == false) {
							attackRange.add(map[auxCoordinate.getX()][startingPosition.getY()]);							
						}
					}
					/*
					 * A posição é válida se:
					 * heightDifference <= maxRange
					 */
					if (heightDifference2 <= maxRange) {
						if (attackRange.contains(map[startingPosition.getX()][auxCoordinate.getY()]) == false) {
							attackRange.add(map[startingPosition.getX()][auxCoordinate.getY()]); 							
						}
					}
				}
			}
		}
		
		return attackRange;
	}
	
	// --------------------------------------------------------------------------------------------
	// Getters e setters
	// --------------------------------------------------------------------------------------------
	
	public int getCost() {
		return cost;
	}

	public int getNumberOfHits() {
		return numberOfHits;
	}

	public int getCooldown() {
		return cooldown;
	}

	public int getValue() {
		return value;
	}

	public double getSkillAccuracy() {
		return skillAccuracy;
	}

	public boolean isEquipped() {
		return equipped;
	}

	public String getName() {
		return name;
	}
	
	public boolean isLearned() {
		return learned;
	}

	public BaseCharacter getUser() {
		return user;
	}

	public void setUser(BaseCharacter user) {
		this.user = user;
	}

	public boolean isDamaging() {
		return damaging;
	}

	public String getRadiusType() {
		return radiusType;
	}
	
	public String getDescription() {
		return description;
	}

	public Effect getEffect() {
		return effect;
	}

	public String getExpansionType() {
		return expansionType;
	}

	public int getVariability() {
		return variability;
	}
}
