package com.aionemu.gameserver.controllers.effect;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import com.aionemu.gameserver.dataholders.DataManager;
import com.aionemu.gameserver.model.EmotionType;
import com.aionemu.gameserver.model.PlayerClass;
import com.aionemu.gameserver.model.gameobjects.Creature;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.gameobjects.state.CreatureState;
import com.aionemu.gameserver.network.aion.serverpackets.SM_ABNORMAL_EFFECT;
import com.aionemu.gameserver.network.aion.serverpackets.SM_EMOTION;
import com.aionemu.gameserver.network.aion.serverpackets.SM_SYSTEM_MESSAGE;
import com.aionemu.gameserver.skillengine.effect.*;
import com.aionemu.gameserver.skillengine.model.*;
import com.aionemu.gameserver.utils.PacketSendUtility;

/**
 * @author ATracer, Wakizashi, Sippolo, Cheatkiller, Neon
 */
public class EffectController {

	private final Creature owner;
	private final Map<String, Effect> passiveEffectMap = new LinkedHashMap<>();
	private final Map<String, Effect> noshowEffects = new LinkedHashMap<>();
	private final Map<String, Effect> abnormalEffectMap = new LinkedHashMap<>();
	private long abnormalMapUpdate;

	protected final ReadWriteLock lock = new ReentrantReadWriteLock();

	protected int abnormals;

	private boolean isUnderNormalShield = false;
	private boolean keepBuffsOnDie;

	public EffectController(Creature owner) {
		this.owner = owner;
	}

	public Creature getOwner() {
		return owner;
	}

	public boolean isUnderNormalShield() {
		return isUnderNormalShield;
	}

	public void setUnderNormalShield(boolean isUnderNormalShield) {
		this.isUnderNormalShield = isUnderNormalShield;
	}

	public void addEffect(Effect nextEffect) {
		Map<String, Effect> mapToUpdate = getMapForEffect(nextEffect);

		boolean useEffectId = true;
		if (nextEffect.isPassive()) {
			Effect existingEffect;
			lock.readLock().lock();
			try {
				existingEffect = mapToUpdate.get(nextEffect.getStack());
			} finally {
				lock.readLock().unlock();
			}
			if (existingEffect != null && existingEffect.isPassive()) {
				// check stack level
				if (existingEffect.getSkillStackLvl() > nextEffect.getSkillStackLvl())
					return;

				// check skill level (when stack level same)
				if (existingEffect.getSkillStackLvl() == nextEffect.getSkillStackLvl() && existingEffect.getSkillLevel() > nextEffect.getSkillLevel())
					return;

				existingEffect.endEffect();
				useEffectId = false;
			}
		}

		if (useEffectId) {
			// idea here is that effects with same effectId shouldn't stack, effect with higher basic lvl takes priority
			if (searchConflict(mapToUpdate, nextEffect))
				return;
		}
		endConflictedEffect(mapToUpdate, nextEffect);
		checkEffectCooldownId(nextEffect);

		// max 3 aura effects or 1 toggle skill in noshoweffects
		if (nextEffect.isToggle() && nextEffect.getTargetSlot() == SkillTargetSlot.NOSHOW) {
			int mts = nextEffect.getSkillSubType() == SkillSubType.CHANT ? 3 : 1;
			// Rangers & Riders are allowed to use 2 Toggle skills. There might be a pattern.
			if (nextEffect.getEffector() instanceof Player && (((Player) nextEffect.getEffector()).getPlayerClass() == PlayerClass.RANGER
				|| ((Player) nextEffect.getEffector()).getPlayerClass() == PlayerClass.RIDER)) {
				mts = 2;
			}
			List<Effect> filteredMap = nextEffect.getSkillSubType() == SkillSubType.CHANT ? getAuraEffects() : getNoShowToggleEffectsExceptAuras();
			if (filteredMap.size() >= mts)
				filteredMap.getFirst().endEffect();
		}

		// max 4 chants
		if (nextEffect.isChant()) {
			List<Effect> chants = filterEffects(abnormalEffectMap, Effect::isChant);
			if (chants.size() >= 4)
				chants.getFirst().endEffect();
		}
		lock.writeLock().lock();
		try {
			if (nextEffect.getSkill() != null && System.currentTimeMillis() - abnormalMapUpdate < nextEffect.getSkill().getHitTime()) {
				addBeforeLastElement(mapToUpdate, nextEffect);
			} else {
				abnormalMapUpdate = System.currentTimeMillis();
				mapToUpdate.put(nextEffect.getStack(), nextEffect);
			}
		} finally {
			lock.writeLock().unlock();
		}

		nextEffect.startEffect();

		if (!nextEffect.isPassive())
			broadCastEffects(nextEffect);
	}

	private void endConflictedEffect(Map<String, Effect> effectMap, Effect newEffect) {
		int conflictId = newEffect.getSkillTemplate().getConflictId();
		if (conflictId == 0)
			return;
		Effect effectToEnd = findFirstEffect(effectMap, effect -> effect.getSkillTemplate().getConflictId() == conflictId);
		if (effectToEnd != null)
			effectToEnd.endEffect();
	}

	private boolean searchConflict(Map<String, Effect> mapToUpdate, Effect nextEffect) {
		if (checkExtraEffect(mapToUpdate, nextEffect))
			return false;
		Effect effectToEnd = null;
		lock.readLock().lock();
		try {
			mainLoop:
			for (Effect effect : mapToUpdate.values()) {
				if (effect.getSkillSubType() == nextEffect.getSkillSubType() || effect.getTargetSlot() == nextEffect.getTargetSlot()) {
					for (EffectTemplate et : effect.getEffectTemplates()) {
						if (et.getEffectId() == 0)
							continue;
						for (EffectTemplate et2 : nextEffect.getEffectTemplates()) {
							if (et2.getEffectId() == 0)
								continue;
							if ((et.getEffectId() == et2.getEffectId()) || (et instanceof SilenceEffect && et2 instanceof SilenceEffect)) {
								if (et.getBasicLvl() > et2.getBasicLvl()) {
									if (!nextEffect.isPassive() && nextEffect.getTargetSlot() != SkillTargetSlot.DEBUFF)
										nextEffect.setEffectResult(EffectResult.CONFLICT);
									return true;
								} else {
									effectToEnd = effect;
									break mainLoop;
								}
							}
						}
					}
				}
			}
		} finally {
			lock.readLock().unlock();
		}
		if (effectToEnd != null)
			effectToEnd.endEffect(false);
		return false;
	}

	/**
	 * Checks whether {@code newEffectTemplate} is in conflict
	 * with any existing effects without removing any effects. <br>
	 * {@code newEffect}'s EffectResult is set to {@code EffectResult.CONFLICT} if a conflict is found. <br>
	 * Note: EffectResult is not changed in case of passive effects or effects with {@code SkillTargetSlot.DEBUFF}.
	 * 
	 * @param newEffect
	 *          The effect {@code newEffectTemplate} belongs to.
	 * @param newEffectTemplate
	 *          The {@code EffectTemplate} to check for conflicts.
	 * @return True if {@code newEffectTemplate} is in conflict with another existing effect.
	 */
	public boolean isConflicting(Effect newEffect, EffectTemplate newEffectTemplate) {
		if (newEffectTemplate.getEffectId() == 0)
			return false;
		Map<String, Effect> mapToUpdate = getMapForEffect(newEffect);
		lock.readLock().lock();
		try {
			mainLoop:
			for (Effect currentEffect : mapToUpdate.values()) {
				if (currentEffect.getSkillSubType() == newEffect.getSkillSubType() || currentEffect.getTargetSlot() == newEffect.getTargetSlot()) {
					for (EffectTemplate currentEffectTemplate : currentEffect.getEffectTemplates()) {
						if (currentEffectTemplate.getEffectId() == 0)
							continue;
						if ((currentEffectTemplate.getEffectId() == newEffectTemplate.getEffectId())
							|| (currentEffectTemplate instanceof SilenceEffect && newEffectTemplate instanceof SilenceEffect)) {
							if (currentEffectTemplate.getBasicLvl() > newEffectTemplate.getBasicLvl() && !(currentEffectTemplate instanceof HideEffect)) {
								return true;
							} else {
								break mainLoop;
							}
						}
					}
				}
			}
		} finally {
			lock.readLock().unlock();
		}
		return false;
	}

	private boolean checkExtraEffect(Map<String, Effect> effectMap, Effect nextEffect) {
		if (nextEffect.isPassive() || nextEffect.getDispelCategory() != DispelCategoryType.EXTRA)
			return false;
		Effect extraEffect = findFirstEffect(effectMap, effect -> effect.getDispelCategory() == DispelCategoryType.EXTRA
			&& !effect.getSkillTemplate().getStack().startsWith("IDSEAL_BOSS_VRITRA_BUFF"));
		if (extraEffect != null) {
			extraEffect.endEffect();
			return true;
		}
		return false;
	}

	private List<Effect> getAuraEffects() {
		return filterEffects(noshowEffects, effect -> effect.getSkillSubType() == SkillSubType.CHANT);
	}

	private List<Effect> getNoShowToggleEffectsExceptAuras() {
		return filterEffects(noshowEffects, effect -> effect.getSkillSubType() != SkillSubType.CHANT);
	}

	private void checkEffectCooldownId(Effect effect) {
		if (effect.isPassive() || effect.getTargetSlot() == SkillTargetSlot.NOSHOW)
			return;
		int cdId = effect.getSkillTemplate().getCooldownId();
		if (cdId == 1)
			return;
		int size = 1;
		switch (cdId) {
			case 273: // Erosion & Flamecage
			case 353: // Lockdown & Dazing Severe Blow
				size = 2;
				break;
			case 6:
				size = 10;
				break;
		}
		List<Effect> effects = filterEffects(abnormalEffectMap,
			e -> e.getTargetSlot() == effect.getTargetSlot() && e.getSkillTemplate().getCooldownId() == cdId);
		if (effects.size() >= size)
			effects.getFirst().endEffect();
		// archer buffs
		if (cdId >= 2020 && cdId <= 2030) {
			int count = 0;
			Effect toRemove = null;
			for (Effect eff : getAbnormalEffectsToShow()) {
				switch (eff.getSkillTemplate().getCooldownId()) {
					case 2020: // Dodging
					case 2022: // Focused Shots
					case 2024: // Aiming
					case 2026: // Bestial Fury
					case 2028: // Hunter's Eye
					case 2030: // Strong Shots
						if (toRemove == null)
							toRemove = eff;
						if (++count >= 2) {
							toRemove.endEffect();
							return;
						}
						break;
				}
			}
		}
	}

	/**
	 * DON'T call this method anywhere else than addEffect() since it's not no synchronized
	 */
	private void addBeforeLastElement(Map<String, Effect> mapToUpdate, Effect newEffect) {
		Effect lastEffect = null;
		for (Effect effect : mapToUpdate.values()) {
			lastEffect = effect;
		}

		if (lastEffect != null && !(lastEffect.getEffector() instanceof Player))
			mapToUpdate.remove(lastEffect.getStack());
		mapToUpdate.put(newEffect.getStack(), newEffect);
		if (lastEffect != null && !(lastEffect.getEffector() instanceof Player))
			mapToUpdate.put(lastEffect.getStack(), lastEffect);
	}

	protected Map<String, Effect> getMapForEffect(Effect effect) {
		return getMapForEffect(effect.getSkillTemplate());
	}

	private Map<String, Effect> getMapForEffect(SkillTemplate template) {
		if (template.isPassive())
			return passiveEffectMap;

		if (template.getActivationAttribute() == ActivationAttribute.TOGGLE && template.getTargetSlot() == SkillTargetSlot.NOSHOW)
			return noshowEffects;

		return abnormalEffectMap;
	}

	public Effect getAbnormalEffect(String stack) {
		lock.readLock().lock();
		try {
			return abnormalEffectMap.get(stack);
		} finally {
			lock.readLock().unlock();
		}
	}

	public boolean hasAbnormalEffect(Predicate<Effect> predicate) {
		return findFirstEffect(abnormalEffectMap, predicate) != null;
	}

	public boolean hasAbnormalEffect(int skillId) {
		return hasAbnormalEffect(effect -> effect.getSkillId() == skillId);
	}

	public void broadCastEffects(Effect effect) {
		int slot = effect != null ? effect.getTargetSlot().getId() : SkillTargetSlot.FULLSLOTS;
		List<Effect> effects = getAbnormalEffects();
		PacketSendUtility.broadcastPacket(getOwner(), new SM_ABNORMAL_EFFECT(getOwner(), abnormals, effects, slot));
	}

	public void clearEffect(Effect effect, boolean broadCastEffects) {
		Map<String, Effect> effectMap = getMapForEffect(effect);
		lock.writeLock().lock();
		try {
			Effect oldEffect = effectMap.get(effect.getStack());
			if (oldEffect != null) {
				if (!oldEffect.equals(effect))
					return; // effect in map was already replaced by a newer one (e.g. when toggling many auras), so there's no need to re-broadcast
				effectMap.remove(effect.getStack());
			}
		} finally {
			lock.writeLock().unlock();
		}
		if (broadCastEffects)
			broadCastEffects(effect);
	}

	public Effect findBySkillId(int skillId) {
		SkillTemplate skillTemplate = DataManager.SKILL_DATA.getSkillTemplate(skillId);
		if (skillTemplate == null)
			throw new NullPointerException("Skill with ID " + skillId + " does not exist");
		return findFirstEffect(getMapForEffect(skillTemplate), effect -> effect.getSkillId() == skillId);
	}

	public void removeEffect(int skillId) {
		Effect effect = findBySkillId(skillId);
		if (effect != null)
			effect.endEffect();
	}

	public void removeHideEffects() {
		removeEffects(abnormalEffectMap, Effect::isHideEffect);
	}

	public void removePetOrderUnSummonEffects() {
		removeEffects(abnormalEffectMap, Effect::isPetOrderUnSummonEffect);
	}

	public void removeParalyzeEffects() {
		removeEffects(abnormalEffectMap, Effect::isParalyzeEffect);
	}

	public void removeStunEffects() {
		removeEffects(abnormalEffectMap, Effect::isStunEffect);
	}

	/**
	 * Removes Transform effects from owner. For more info see {@link TransformEffect} it doesn't remove Avatar transforms (TODO find out on retail)
	 */
	public void removeTransformEffects() {
		removeEffects(abnormalEffectMap, effect -> {
			for (EffectTemplate et : effect.getEffectTemplates()) {
				if (et instanceof TransformEffect && ((TransformEffect) et).getTransformType() != TransformType.AVATAR)
					return true;
			}
			return false;
		});
	}

	public void removeInstanceEffects() {
		removeEffects(abnormalEffectMap, effect -> {
			if (effect.getDispelCategory() == DispelCategoryType.NPC_BUFF)
				return true;
			for (EffectTemplate et : effect.getEffectTemplates()) {
				if (et instanceof TransformEffect te && te.getTransformType() == TransformType.FORM1)
					return true;
			}
			return false;
		});
	}

	private void removeEffects(Map<String, Effect> mapForEffect, Predicate<Effect> predicate) {
		for (Effect effect : filterEffects(mapForEffect, predicate))
			effect.endEffect();
	}

	private Effect findFirstEffect(Map<String, Effect> effectMap, Predicate<Effect> filter) {
		lock.readLock().lock();
		try {
			for (Effect effect : effectMap.values()) {
				if (filter.test(effect))
					return effect;
			}
		} finally {
			lock.readLock().unlock();
		}
		return null;
	}

	public List<Effect> getAllEffects() {
		List<Effect> effects = new ArrayList<>();
		lock.readLock().lock();
		try {
			effects.addAll(abnormalEffectMap.values());
			effects.addAll(noshowEffects.values());
			effects.addAll(passiveEffectMap.values());
		} finally {
			lock.readLock().unlock();
		}
		return effects;
	}

	private List<Effect> filterEffects(Map<String, Effect> effectMap, Predicate<Effect> filter) {
		List<Effect> effects = new ArrayList<>();
		lock.readLock().lock();
		try {
			for (Effect effect : effectMap.values()) {
				if (filter.test(effect))
					effects.add(effect);
			}
		} finally {
			lock.readLock().unlock();
		}
		return effects;
	}

	/**
	 * Removes all effects by given dispel slot type
	 */
	public void removeByDispelSlotType(DispelSlotType dispelSlotType) {
		removeByDispelEffect(null, dispelSlotType, 255, 100, 100);
	}

	public boolean removeByEffectId(int effectId, int dispelLevel, int power) {
		if (removeByEffectId(abnormalEffectMap, effectId, dispelLevel, power))
			return true;
		return removeByEffectId(noshowEffects, effectId, dispelLevel, power);
	}

	private boolean removeByEffectId(Map<String, Effect> effectMap, int effectId, int dispelLevel, int power) {
		Effect effectToEnd = findFirstEffect(effectMap,
			effect -> effect.getReqDispelLevel() <= dispelLevel && effect.containsEffectId(effectId) && removePower(effect, power));
		if (effectToEnd != null) {
			effectToEnd.endEffect();
			return true;
		} else {
			return false;
		}
	}

	public void removeByDispelEffect(EffectType effectType, DispelSlotType dispelSlotType, int count, int dispelLevel, int power) {
		count = removeByDispelEffect(abnormalEffectMap, effectType, dispelSlotType, count, dispelLevel, power);
		if (count > 0)
			removeByDispelEffect(noshowEffects, effectType, dispelSlotType, count, dispelLevel, power);
	}

	private int removeByDispelEffect(Map<String, Effect> effectMap, EffectType effectType, DispelSlotType dispelSlotType, int count, int dispelLevel,
		int power) {
		List<Effect> effectsToEnd = new ArrayList<>();
		lock.readLock().lock();
		try {
			for (Effect effect : effectMap.values()) {
				// check count
				if (count == 0)
					break;

				if (effectType != null) {
					if (!effect.getSkillTemplate().hasAnyEffect(effectType))
						continue;
					if (effectType.equals(EffectType.STUN) && effect.getSkillId() == 11904) { // avatar skill irremovable by Remove Shock TODO: logic
						continue;
					}
				}
				if (dispelSlotType != null) {
					if (effect.getTargetSlot() != SkillTargetSlot.of(dispelSlotType))
						continue;
				}
				// check dispel level
				if (effect.getReqDispelLevel() > dispelLevel)
					continue;

				if (removePower(effect, power))
					effectsToEnd.add(effect);
				// decrease count
				count--;
			}
		} finally {
			lock.readLock().unlock();
		}
		for (Effect effect : effectsToEnd)
			effect.endEffect();
		return count;
	}

	/**
	 * Calculates effects that will be removed and reduces their power. Used only in DispelBuffCounterAtkEffect
	 * 
	 * @return number of effects that will be removed
	 */
	public int calculateBuffsOrEffectorDebuffsToRemove(Effect effect, int count, int dispelLevel, int power) {
		int dispelledEffectCount = 0;
		lock.readLock().lock();
		try {
			for (Effect ef : abnormalEffectMap.values()) {
				if (count == 0)
					break;

				DispelCategoryType dispelCat = ef.getDispelCategory();
				SkillTargetSlot targetSlot = ef.getSkillTemplate().getTargetSlot();

				// skip effects that are about to be removed by another dispel effect
				if (ef.getDesignatedDispelEffect() != null)
					continue;

				// effects with duration 86400000 cant be dispelled
				// TODO recheck
				if (ef.getDuration() >= 86400000 && !isRemovableEffect(ef))
					continue;

				if (ef.isSanctuaryEffect())
					continue;

				// check for targetslot, effects with target slot higher or equal to 2 cant be removed (ex. skillId: 11885)
				if (targetSlot != SkillTargetSlot.BUFF && (targetSlot != SkillTargetSlot.DEBUFF && dispelCat != DispelCategoryType.ALL)
					|| ef.getTargetSlotLevel() >= 2)
					continue;

				// remove only debuffs of the effector
				if (targetSlot == SkillTargetSlot.DEBUFF && !effect.getEffector().equals(ef.getEffector()))
					continue;

				switch (dispelCat) {
					case ALL:
					case BUFF:// DispelBuffCounterAtkEffect
						if (ef.getReqDispelLevel() <= dispelLevel) {
							if (removePower(ef, power)) {
								ef.setDesignatedDispelEffect(effect);
								dispelledEffectCount++;
							}
							count--;
						}
						break;
				}
			}
		} finally {
			lock.readLock().unlock();
		}
		return dispelledEffectCount;
	}

	public void removeEffectByDispelCat(DispelCategoryType dispelCat, SkillTargetSlot targetSlot, int count, int dispelLevel, int power) {
		List<Effect> effectsToEnd = new ArrayList<>();
		boolean insufficientDispelPower = false;
		boolean insufficientDispelLevel = false;
		lock.readLock().lock();
		try {
			for (Effect effect : abnormalEffectMap.values()) {
				if (count == 0)
					break;
				// effects with duration 86400000 cant be dispelled
				// TODO recheck
				if (effect.getDuration() >= 86400000 && !isRemovableEffect(effect)) {
					continue;
				}

				if (effect.isSanctuaryEffect())
					continue;

				// skip effects that are about to be removed by another dispel effect
				if (effect.getDesignatedDispelEffect() != null)
					continue;

				// check for targetslot, effects with target slot level higher or equal to 2 cant be removed (ex. skillId: 11885)
				if (effect.getTargetSlot() != targetSlot || effect.getTargetSlotLevel() >= 2)
					continue;

				boolean remove = false;
				switch (dispelCat) {
					case ALL:// DispelDebuffEffect
						if ((effect.getDispelCategory() == DispelCategoryType.ALL || effect.getDispelCategory() == DispelCategoryType.DEBUFF_MENTAL
							|| effect.getDispelCategory() == DispelCategoryType.DEBUFF_PHYSICAL) && effect.getReqDispelLevel() <= dispelLevel)
							remove = true;
						break;
					case DEBUFF_MENTAL:// DispelDebuffMentalEffect
						if ((effect.getDispelCategory() == DispelCategoryType.ALL || effect.getDispelCategory() == DispelCategoryType.DEBUFF_MENTAL)
							&& effect.getReqDispelLevel() <= dispelLevel)
							remove = true;
						break;
					case DEBUFF_PHYSICAL:// DispelDebuffPhysicalEffect
						if ((effect.getDispelCategory() == DispelCategoryType.ALL || effect.getDispelCategory() == DispelCategoryType.DEBUFF_PHYSICAL)
							&& effect.getReqDispelLevel() <= dispelLevel)
							remove = true;
						break;
					case BUFF:// DispelBuffEffect or DispelBuffCounterAtkEffect
						if (effect.getDispelCategory() == DispelCategoryType.BUFF && effect.getReqDispelLevel() <= dispelLevel)
							remove = true;
						break;
					case STUN:
						if (effect.getDispelCategory() == DispelCategoryType.STUN)
							remove = true;
						break;
					case NPC_BUFF:// DispelNpcBuff
						if (effect.getDispelCategory() == DispelCategoryType.NPC_BUFF)
							remove = true;
						break;
					case NPC_DEBUFF_PHYSICAL:// DispelNpcDebuff
						if (effect.getDispelCategory() == DispelCategoryType.NPC_DEBUFF_PHYSICAL)
							remove = true;
						break;
				}

				if (remove) {
					if (removePower(effect, power)) {
						effectsToEnd.add(effect);
					} else if (owner instanceof Player) {
						insufficientDispelPower = true;
					}
					count--;
				} else
					insufficientDispelLevel = true;
			}
		} finally {
			lock.readLock().unlock();
		}
		for (Effect effect : effectsToEnd) // end outside lock so broadcasting effects can't cause deadlocks
			effect.endEffect();
		if (owner instanceof Player) {
			if (insufficientDispelPower)
				PacketSendUtility.sendPacket((Player) owner, SM_SYSTEM_MESSAGE.STR_MSG_NOT_ENOUGH_DISPELCOUNT());
			if (insufficientDispelLevel)
				PacketSendUtility.sendPacket((Player) owner, SM_SYSTEM_MESSAGE.STR_MSG_NOT_ENOUGH_DISPELLEVEL());
		}
	}

	public void dispelBuffCounterAtkEffect(Effect effect) {
		List<Effect> effectsToEnd = new ArrayList<>();
		lock.readLock().lock();
		try {
			for (Effect ef : abnormalEffectMap.values()) {
				if (effect.equals(ef.getDesignatedDispelEffect())) {
					effectsToEnd.add(ef);
				}
			}
		} finally {
			lock.readLock().unlock();
		}
		for (Effect ef : effectsToEnd) {
			ef.endEffect();
		}
	}

	// TODO this should be removed
	private boolean isRemovableEffect(Effect effect) {
		int skillId = effect.getSkillId();
		switch (skillId) {
			case 20941:
			case 20942:
			case 19370:
			case 19371:
			case 19372:
			case 20530:
			case 20531:
			case 19345:
			case 19346:
			case 21438:
			case 21121:
			case 21124:
				// TODO
				return true;
			default:
				return false;
		}
	}

	private boolean removePower(Effect effect, int power) {
		return effect.removePower(power) <= 0;
	}

	/**
	 * Removes all effects from controllers and ends them appropriately Passive effect will not be removed
	 */
	public void removeAllEffects() {
		removeAllEffects(false);
	}

	public void removeAllEffects(boolean logout) {
		List<Effect> effects;
		if (logout) { // remove all effects on logout
			effects = getAllEffects();
		} else {
			effects = new ArrayList<>();
			lock.readLock().lock();
			try {
				for (Effect effect : abnormalEffectMap.values()) {
					if (effect.canRemoveOnDie() && (!keepBuffsOnDie || effect.getTargetSlot() == SkillTargetSlot.DEBUFF))
						effects.add(effect);
				}
				effects.addAll(noshowEffects.values());
			} finally {
				lock.readLock().unlock();
			}
		}
		for (Effect effect : effects) // end outside lock so broadcasting effects can't cause deadlocks
			effect.endEffect(false);
		if (!logout)
			broadCastEffects(null);
	}

	public boolean isUnderFear() {
		return isAbnormalSet(AbnormalState.FEAR);
	}

	public boolean isConfused() {
		return isAbnormalSet(AbnormalState.CONFUSE);
	}

	/**
	 * @return copy of abnormals list
	 */
	public List<Effect> getAbnormalEffects() {
		lock.readLock().lock();
		try {
			return new ArrayList<>(abnormalEffectMap.values());
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * @return list of effects to display as top icons
	 */
	public List<Effect> getAbnormalEffectsToTargetSlot(final int slot) {
		lock.readLock().lock();
		try {
			return abnormalEffectMap.values().stream().filter(e -> e.getTargetSlot().getId() == slot).collect(Collectors.toList());
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * @return list of effects to display as top icons
	 */
	public List<Effect> getAbnormalEffectsToShow() {
		lock.readLock().lock();
		try {
			return abnormalEffectMap.values().stream().filter(e -> e.getTargetSlot() != SkillTargetSlot.NOSHOW).collect(Collectors.toList());
		} finally {
			lock.readLock().unlock();
		}
	}

	public void setAbnormal(AbnormalState state) {
		owner.getObserveController().notifyAbnormalSettedObservers(state);
		abnormals |= state.getId();

		// TODO move to observer?
		// if player is sitting when setting certain abnormal states, he is forcefully made to stand up
		if (owner instanceof Player && owner.isInState(CreatureState.RESTING)) {
			if (isInAnyAbnormalState(AbnormalState.AUTOMATICALLY_STANDUP)) {
				owner.unsetState(CreatureState.RESTING);
				PacketSendUtility.broadcastPacket((Player) owner, new SM_EMOTION(owner, EmotionType.STAND), true);
			}
		}
	}

	public void unsetAbnormal(AbnormalState state) {
		int count = 0;
		lock.readLock().lock();
		try {
			for (Effect effect : abnormalEffectMap.values()) {
				if ((effect.getAbnormals() & state.getId()) == state.getId()) {
					if (++count == 2)
						return;
				}
			}
		} finally {
			lock.readLock().unlock();
		}
		abnormals &= ~state.getId();
	}

	/**
	 * Used for checking unique abnormal states
	 */
	public boolean isAbnormalSet(AbnormalState state) {
		if (state == AbnormalState.NONE)
			return abnormals == 0;
		return (abnormals & state.getId()) == state.getId();
	}

	/**
	 * Used for compound abnormal state checks
	 */
	public boolean isInAnyAbnormalState(AbnormalState state) {
		if (state == AbnormalState.NONE)
			return abnormals == 0;
		return (abnormals & state.getId()) != 0;
	}

	public int getAbnormals() {
		return abnormals;
	}

	public boolean isEmpty() {
		return abnormalEffectMap.isEmpty();
	}

	public void setKeepBuffsOnDie(boolean keepBuffsOnDie) {
		this.keepBuffsOnDie = keepBuffsOnDie;
	}

	public void resetDesignatedDispelEffect(Effect effect) {
		lock.readLock().lock();
		try {
			for (Effect ef : abnormalEffectMap.values()) {
				if (effect.equals(ef.getDesignatedDispelEffect())) {
					ef.resetDesignatedDispelEffect();
				}
			}
		} finally {
			lock.readLock().unlock();
		}
	}
}
