package com.aionemu.gameserver.model.stats.container;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang3.ArrayUtils;

import com.aionemu.gameserver.model.EmotionType;
import com.aionemu.gameserver.model.SkillElement;
import com.aionemu.gameserver.model.enchants.EnchantEffect;
import com.aionemu.gameserver.model.gameobjects.Creature;
import com.aionemu.gameserver.model.gameobjects.Item;
import com.aionemu.gameserver.model.items.ItemSlot;
import com.aionemu.gameserver.model.items.ManaStone;
import com.aionemu.gameserver.model.items.RandomBonusEffect;
import com.aionemu.gameserver.model.stats.calc.*;
import com.aionemu.gameserver.model.stats.calc.functions.IStatFunction;
import com.aionemu.gameserver.model.stats.calc.functions.StatFunctionProxy;
import com.aionemu.gameserver.model.templates.itemset.ItemSetTemplate;
import com.aionemu.gameserver.model.templates.stats.StatsTemplate;
import com.aionemu.gameserver.network.aion.serverpackets.SM_EMOTION;
import com.aionemu.gameserver.skillengine.model.Effect;
import com.aionemu.gameserver.utils.PacketSendUtility;
import com.aionemu.gameserver.utils.stats.CalculationType;

/**
 * @author xavier, Neon
 */
public abstract class CreatureGameStats<T extends Creature> {

	private static final int ATTACK_MAX_COUNTER = Integer.MAX_VALUE;

	protected final T owner;
	private final Map<StatEnum, List<IStatFunction>> stats = new ConcurrentHashMap<>();

	private int attackCounter = 0;
	private int cachedMaxHp, cachedMaxMp, cachedSpeed;

	protected CreatureGameStats(T owner) {
		this.owner = owner;
	}

	/**
	 * @return the atcount
	 */
	public int getAttackCounter() {
		return attackCounter;
	}

	/**
	 * @param attackCounter
	 *          the atcount to set
	 */
	protected void setAttackCounter(int attackCounter) {
		if (attackCounter <= 0) {
			this.attackCounter = 1;
		} else {
			this.attackCounter = attackCounter;
		}
	}

	public void increaseAttackCounter() {
		if (attackCounter == ATTACK_MAX_COUNTER) {
			this.attackCounter = 1;
		} else {
			this.attackCounter++;
		}
	}

	@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
	public final void addEffectOnly(StatOwner statOwner, List<? extends IStatFunction> functions) {
		for (IStatFunction function : functions) {
			IStatFunction functionToAdd = Objects.equals(statOwner, function.getOwner()) ? function : new StatFunctionProxy(statOwner, function);
			stats.compute(functionToAdd.getName(), (k, statFunctions) -> {
				if (statFunctions == null) {
					statFunctions = new ArrayList<>();
					statFunctions.add(functionToAdd);
				} else {
					synchronized (statFunctions) {
						statFunctions.add(functionToAdd);
						statFunctions.sort(null);
					}
				}
				return statFunctions;
			});
		}
	}

	public final void addEffect(StatOwner statOwner, List<? extends IStatFunction> functions) {
		addEffectOnly(statOwner, functions);
		onStatsChange(statOwner instanceof Effect effect ? effect : null);
	}

	@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
	public final void endEffect(StatOwner statOwner) {
		boolean statsChanged = false;
		for (List<IStatFunction> functions : stats.values()) {
			synchronized (functions) {
					statsChanged |= functions.removeIf(statFunction -> statOwner.equals(statFunction.getOwner()));
			}
		}
		if (statsChanged && !owner.isDead())
			onStatsChange(null);
	}

	public float getPositiveStat(StatEnum statEnum, float base) {
		Stat2 stat = getStat(statEnum, base);
		float value = stat.getCurrent();
		return value > 0 ? value : 0;
	}

	public int getPositiveReverseStat(StatEnum statEnum, int base) {
		Stat2 stat = getReverseStat(statEnum, base);
		int value = stat.getCurrent();
		return value > 0 ? value : 0;
	}

	public Stat2 getStat(StatEnum statEnum, float base, CalculationType... calculationTypes) {
		Stat2 stat = new AdditionStat(statEnum, base, owner);
		return getStat(statEnum, stat, calculationTypes);
	}

	public Stat2 getStat(StatEnum statEnum, float base, float bonusRate, CalculationType... calculationTypes) {
		Stat2 stat = new AdditionStat(statEnum, base, owner, bonusRate);
		return getStat(statEnum, stat, calculationTypes);
	}

	public Stat2 getReverseStat(StatEnum statEnum, float base) {
		Stat2 stat = new ReverseStat(statEnum, base, owner);
		return getStat(statEnum, stat);
	}

	public Stat2 getReverseStat(StatEnum statEnum, float base, float bonusRate) {
		Stat2 stat = new ReverseStat(statEnum, base, owner, bonusRate);
		return getStat(statEnum, stat);
	}

	public Stat2 getStat(StatEnum statEnum, Stat2 stat, CalculationType... calculationTypes) {
		List<IStatFunction> functions = getStatsSorted(statEnum);
		if (functions != null) {
			for (IStatFunction func : functions) {
				if (func.validate(stat)) {
					if ((statEnum == StatEnum.PHYSICAL_ATTACK || statEnum == StatEnum.MAGICAL_ATTACK) && func.getOwner() instanceof EnchantEffect ef) {
						if (ef.getItemSlot() == ItemSlot.MAIN_HAND && ArrayUtils.contains(calculationTypes, CalculationType.MAIN_HAND)
								|| ef.getItemSlot() == ItemSlot.SUB_HAND && ArrayUtils.contains(calculationTypes, CalculationType.OFF_HAND)) {
							func.apply(stat, calculationTypes);
						}
					} else {
						func.apply(stat, calculationTypes);
					}
				}
			}
			StatCapUtil.calculateBaseValue(stat, owner);
		}
		return stat;
	}

	public Stat2 getItemStatBoost(StatEnum statEnum, Stat2 stat) {
		List<IStatFunction> functions = getStatsSorted(statEnum);
		if (functions != null) {
			for (IStatFunction func : functions) {
				if (func.isBonus() && func.validate(stat) && (func.getOwner() instanceof Item || func.getOwner() instanceof ManaStone
					|| func.getOwner() instanceof ItemSetTemplate || func.getOwner() instanceof RandomBonusEffect)) {
					func.apply(stat);
				}
			}
		}
		return stat;
	}

	public abstract StatsTemplate getStatsTemplate();

	public Stat2 getPower() {
		return getStat(StatEnum.POWER, getStatsTemplate().getPower());
	}

	public Stat2 getHealth() {
		return getStat(StatEnum.HEALTH, getStatsTemplate().getHealth());
	}

	public Stat2 getAccuracy() {
		return getStat(StatEnum.ACCURACY, getStatsTemplate().getBaseAccuracy());
	}

	public Stat2 getAgility() {
		return getStat(StatEnum.AGILITY, getStatsTemplate().getAgility());
	}

	public Stat2 getKnowledge() {
		return getStat(StatEnum.KNOWLEDGE, getStatsTemplate().getKnowledge());
	}

	public Stat2 getWill() {
		return getStat(StatEnum.WILL, getStatsTemplate().getWill());
	}

	public Stat2 getMaxHp() {
		return getStat(StatEnum.MAXHP, getStatsTemplate().getMaxHp());
	}

	public Stat2 getMaxMp() {
		return getStat(StatEnum.MAXMP, getStatsTemplate().getMaxMp());
	}

	public Stat2 getPDef() {
		return getStat(StatEnum.PHYSICAL_DEFENSE, getStatsTemplate().getPdef());
	}

	public Stat2 getMDef() {
		return getStat(StatEnum.MAGICAL_DEFEND, getStatsTemplate().getMdef());
	}

	public Stat2 getEvasion() {
		return getStat(StatEnum.EVASION, getStatsTemplate().getEvasion());
	}

	public Stat2 getParry() {
		return getStat(StatEnum.PARRY, getStatsTemplate().getParry());
	}

	public Stat2 getBlock() {
		return getStat(StatEnum.BLOCK, getStatsTemplate().getBlock());
	}

	public Stat2 getMResist() {
		return getStat(StatEnum.MAGICAL_RESIST, getStatsTemplate().getMresist());
	}

	public Stat2 getPCR() {
		return getStat(StatEnum.PHYSICAL_CRITICAL_RESIST, getStatsTemplate().getStrikeResist());
	}

	public Stat2 getMCR() {
		return getStat(StatEnum.MAGICAL_CRITICAL_RESIST, getStatsTemplate().getSpellResist());
	}

	public Stat2 getMainHandPAttack(CalculationType... calculationTypes) {
		return getStat(StatEnum.PHYSICAL_ATTACK, getStatsTemplate().getAttack(), calculationTypes);
	}

	public Stat2 getMainHandPCritical() {
		return getStat(StatEnum.PHYSICAL_CRITICAL, getStatsTemplate().getPcrit());
	}

	public Stat2 getMainHandPAccuracy() {
		return getStat(StatEnum.PHYSICAL_ACCURACY, getStatsTemplate().getAccuracy());
	}

	public Stat2 getMainHandMAttack(CalculationType... calculationTypes) {
		return getStat(StatEnum.MAGICAL_ATTACK, getStatsTemplate().getMagicalAttack(), calculationTypes);
	}

	public Stat2 getMCritical() {
		return getStat(StatEnum.MAGICAL_CRITICAL, getStatsTemplate().getMcrit());
	}

	public Stat2 getMAccuracy() {
		return getStat(StatEnum.MAGICAL_ACCURACY, getStatsTemplate().getMacc());
	}

	public Stat2 getMBoost() {
		return getStat(StatEnum.BOOST_MAGICAL_SKILL, getStatsTemplate().getMagicBoost());
	}

	public Stat2 getMBResist() {
		return getStat(StatEnum.MAGIC_SKILL_BOOST_RESIST, getStatsTemplate().getMsup());
	}

	public Stat2 getAbnormalResistance() {
		return getStat(StatEnum.ABNORMAL_RESISTANCE_ALL, getStatsTemplate().getAbnormalResistance());
	}

	public abstract Stat2 getAttackSpeed();

	public abstract Stat2 getMovementSpeed();

	public abstract Stat2 getAttackRange();

	public abstract Stat2 getHpRegenRate();

	public abstract Stat2 getMpRegenRate();

	public int getMagicalDefenseFor(SkillElement element) {
		switch (element) {
			case EARTH:
				return getStat(StatEnum.EARTH_RESISTANCE, 0).getCurrent();
			case FIRE:
				return getStat(StatEnum.FIRE_RESISTANCE, 0).getCurrent();
			case WATER:
				return getStat(StatEnum.WATER_RESISTANCE, 0).getCurrent();
			case WIND:
				return getStat(StatEnum.WIND_RESISTANCE, 0).getCurrent();
			case LIGHT:
				return getStat(StatEnum.ELEMENTAL_RESISTANCE_LIGHT, 0).getCurrent();
			case DARK:
				return getStat(StatEnum.ELEMENTAL_RESISTANCE_DARK, 0).getCurrent();
			default:
				return 0;
		}
	}

	public float getMovementSpeedFloat() {
		return getMovementSpeed().getCurrent() / 1000f;
	}

	/**
	 * Send packet about stats info
	 */
	public void updateStatInfo() {
	}

	/**
	 * Send packet about speed info
	 */
	public void updateSpeedInfo() {
		PacketSendUtility.broadcastPacket(owner, new SM_EMOTION(owner, EmotionType.CHANGE_SPEED));
	}

	protected boolean checkSpeedStats() {
		int currentSpeed = getMovementSpeed().getCurrent();
		if (currentSpeed != cachedSpeed) {
			updateSpeedInfo();
			cachedSpeed = currentSpeed;
			return true;
		}
		return false;
	}

	/**
	 * @return All stat functions for the given stat sorted by priority
	 */
	public List<IStatFunction> getStatsSorted(StatEnum stat) {
		List<IStatFunction> statFunctions = stats.get(stat);
		if (statFunctions == null)
			return null;
		synchronized (statFunctions) {
				return new ArrayList<>(statFunctions);
		}
	}

	/**
	 * Perform additional calculations after effects added/removed<br>
	 * This method will be called outside of stats lock.
	 */
	protected void onStatsChange(Effect effect) {
		checkMaxHPChanged(effect);
		checkMaxMPChanged(effect);
	}

	private void checkMaxHPChanged(Effect effect) {
		synchronized (this) {
			int oldMaxHp = cachedMaxHp != 0 ? cachedMaxHp : getMaxHp().getBase();
			int currentMaxHp = cachedMaxHp = getMaxHp().getCurrent();
			if (oldMaxHp != currentMaxHp) {
				float percent = 1f * currentMaxHp / oldMaxHp;
				int newHp = Math.min(Math.round(owner.getLifeStats().getCurrentHp() * percent), currentMaxHp);
				Creature effector = effect == null ? owner : effect.getEffector();
				owner.getLifeStats().setCurrentHp(newHp, effector);
			}
		}
	}

	private void checkMaxMPChanged(Effect effect) {
		synchronized (this) {
			int oldMaxMp = cachedMaxMp != 0 ? cachedMaxMp : getMaxMp().getBase();
			int currentMaxMp = cachedMaxMp = getMaxMp().getCurrent();
			if (oldMaxMp != currentMaxMp) {
				float percent = 1f * currentMaxMp / oldMaxMp;
				owner.getLifeStats().setCurrentMp(Math.min(Math.round(owner.getLifeStats().getCurrentMp() * percent), currentMaxMp));
			}
		}
	}
}
