/*
 	OrpheusMS: MapleStory Private Server based on OdinMS
    Copyright (C) 2012 Aaron Weiss <aaron@deviant-core.net>
    				Patrick Huy <patrick.huy@frz.cc>
					Matthias Butz <matze@odinms.de>
					Jan Christian Meyer <vimes@odinms.de>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package server.life;

import client.ISkill;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import client.MapleBuffStat;
import client.MapleCharacter;
import client.MapleClient;
import client.MapleJob;
import client.SkillFactory;
import client.status.MonsterStatus;
import client.status.MonsterStatusEffect;
import java.awt.Point;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.concurrent.locks.ReentrantLock;
import tools.Randomizer;
import net.MaplePacket;
import net.server.Channel;
import net.server.MapleParty;
import net.server.MaplePartyCharacter;
import scripting.event.EventInstanceManager;
import server.TimerManager;
import server.life.MapleLifeFactory.BanishInfo;
import server.maps.MapleMap;
import server.maps.MapleMapObject;
import server.maps.MapleMapObjectType;
import server.maps.TimeMobEntry;
import tools.ArrayMap;
import tools.MaplePacketCreator;
import tools.Output;

public class MapleMonster extends AbstractLoadedMapleLife {

	private MapleMonsterStats stats;
	private int hp, mp;
	private WeakReference<MapleCharacter> controller = new WeakReference<MapleCharacter>(null);
	private boolean controllerHasAggro, controllerKnowsAboutAggro;
	private Collection<AttackerEntry> attackers = new LinkedList<AttackerEntry>();
	private EventInstanceManager eventInstance = null;
	private Collection<MonsterListener> listeners = new LinkedList<MonsterListener>();
	private MapleCharacter highestDamageChar;
	private EnumMap<MonsterStatus, MonsterStatusEffect> stati = new EnumMap<MonsterStatus, MonsterStatusEffect>(MonsterStatus.class);
	private MapleMap map;
	private int VenomMultiplier = 0;
	private boolean fake = false;
	private boolean dropsDisabled = false;
	private List<MobSkillEntry> usedSkills = new ArrayList<MobSkillEntry>();
	private Map<MobSkillEntry, Integer> skillsUsed = new HashMap<MobSkillEntry, Integer>();
	private List<Integer> stolenItems = new ArrayList<Integer>();
	private int team;
	public ReentrantLock monsterLock = new ReentrantLock();

	public MapleMonster(int id, MapleMonsterStats stats) {
		super(id);
		initWithStats(stats);
	}

	public MapleMonster(MapleMonster monster) {
		super(monster);
		initWithStats(monster.stats);
	}

	private void initWithStats(MapleMonsterStats stats) {
		setStance(5);
		this.stats = stats;
		hp = stats.getHp();
		mp = stats.getMp();
	}

	public void disableDrops() {
		this.dropsDisabled = true;
	}

	public boolean dropsDisabled() {
		return dropsDisabled;
	}

	public void setMap(MapleMap map) {
		this.map = map;
	}

	public int getHp() {
		return hp;
	}

	public void setHp(int hp) {
		this.hp = hp;
	}

	public int getMaxHp() {
		return stats.getHp();
	}

	public int getMp() {
		return mp;
	}

	public void setMp(int mp) {
		if (mp < 0) {
			mp = 0;
		}
		this.mp = mp;
	}

	public int getMaxMp() {
		return stats.getMp();
	}

	public int getExp() {
		return stats.getExp();
	}

	int getLevel() {
		return stats.getLevel();
	}

	public int getCP() {
		return stats.getCP();
	}

	public int getTeam() {
		return team;
	}

	public void setTeam(int team) {
		this.team = team;
	}

	public int getVenomMulti() {
		return this.VenomMultiplier;
	}

	public void setVenomMulti(int multiplier) {
		this.VenomMultiplier = multiplier;
	}

	public MapleMonsterStats getStats() {
		return stats;
	}

	public boolean isBoss() {
		return stats.isBoss() || isHT();
	}

	public int getAnimationTime(String name) {
		return stats.getAnimationTime(name);
	}

	private List<Integer> getRevives() {
		return stats.getRevives();
	}

	private byte getTagColor() {
		return stats.getTagColor();
	}

	private byte getTagBgColor() {
		return stats.getTagBgColor();
	}

	/**
	 * 
	 * @param from
	 *            the player that dealt the damage
	 * @param damage
	 * @param updateAttackTime
	 */
	public void damage(MapleCharacter from, int damage, boolean updateAttackTime) {
		AttackerEntry attacker = null;
		if (from.getParty() != null) {
			attacker = new PartyAttackerEntry(from.getParty().getId(), from.getClient().getChannelServer());
		} else {
			attacker = new SingleAttackerEntry(from, from.getClient().getChannelServer());
		}
		boolean replaced = false;
		for (AttackerEntry aentry : attackers) {
			if (aentry.equals(attacker)) {
				attacker = aentry;
				replaced = true;
				break;
			}
		}
		if (!replaced) {
			attackers.add(attacker);
		}
		int rDamage = Math.max(0, Math.min(damage, this.hp));
		attacker.addDamage(from, rDamage, updateAttackTime);
		this.hp -= rDamage;
		int remhppercentage = (int) Math.ceil((this.hp * 100.0) / getMaxHp());
		if (remhppercentage < 1) {
			remhppercentage = 1;
		}
		long okTime = System.currentTimeMillis() - 4000;
		if (hasBossHPBar()) {
			from.getMap().broadcastMessage(makeBossHPBarPacket(), getPosition());
		} else if (!isBoss()) {
			for (AttackerEntry mattacker : attackers) {
				for (AttackingMapleCharacter cattacker : mattacker.getAttackers()) {
					if (cattacker.getAttacker().getMap() == from.getMap()) {
						if (cattacker.getLastAttackTime() >= okTime) {
							cattacker.getAttacker().getClient().getSession().write(MaplePacketCreator.showMonsterHP(getObjectId(), remhppercentage));
						}
					}
				}
			}
		}
	}

	public void heal(int hp, int mp) {
		int hp2Heal = getHp() + hp;
		int mp2Heal = getMp() + mp;
		if (hp2Heal >= getMaxHp()) {
			hp2Heal = getMaxHp();
		}
		if (mp2Heal >= getMaxMp()) {
			mp2Heal = getMaxMp();
		}
		setHp(hp2Heal);
		setMp(mp2Heal);
		getMap().broadcastMessage(MaplePacketCreator.healMonster(getObjectId(), hp));
	}

	public boolean isAttackedBy(MapleCharacter chr) {
		for (AttackerEntry aentry : attackers) {
			if (aentry.contains(chr)) {
				return true;
			}
		}
		return false;
	}

	public void giveExpToCharacter(MapleCharacter attacker, int exp, boolean highestDamage, int numExpSharers) {
		if (highestDamage) {
			if (eventInstance != null) {
				eventInstance.monsterKilled(attacker, this);
			}
			highestDamageChar = attacker;
		}
		if (attacker.getHp() > 0) {
			int personalExp = exp;
			if (exp > 0) {
				Integer holySymbol = attacker.getBuffedValue(MapleBuffStat.HOLY_SYMBOL);
				if (holySymbol != null) {
					if (numExpSharers == 1) {
						personalExp *= 1.0 + (holySymbol.doubleValue() / 500.0);
					} else {
						personalExp *= 1.0 + (holySymbol.doubleValue() / 100.0);
					}
				}
				if (stati.containsKey(MonsterStatus.SHOWDOWN)) {
					personalExp *= (stati.get(MonsterStatus.SHOWDOWN).getStati().get(MonsterStatus.SHOWDOWN).doubleValue() / 100.0 + 1.0);
				}
			}
			if (exp < 0) {// O.O ><
				personalExp = Integer.MAX_VALUE;
			}
			attacker.gainExp(personalExp, true, false, highestDamage);
			attacker.increaseEquipExp(personalExp);// better place
			attacker.mobKilled(this.getId());
		}
	}

	public MapleCharacter killBy(MapleCharacter killer) {
		long totalBaseExpL = (this.getExp() * killer.getClient().getPlayer().getExpRate());
		int totalBaseExp = (int) (Math.min(Integer.MAX_VALUE, totalBaseExpL));
		AttackerEntry highest = null;
		int highdamage = 0;
		for (AttackerEntry attackEntry : attackers) {
			if (attackEntry.getDamage() > highdamage) {
				highest = attackEntry;
				highdamage = attackEntry.getDamage();
			}
		}
		for (AttackerEntry attackEntry : attackers) {
			attackEntry.killedMob(killer.getMap(), (int) Math.ceil(totalBaseExp * ((double) attackEntry.getDamage() / getMaxHp())), attackEntry == highest);
		}
		if (this.getController() != null) { // this can/should only happen when
											// a hidden gm attacks the monster
			getController().getClient().getSession().write(MaplePacketCreator.stopControllingMonster(this.getObjectId()));
			getController().stopControllingMonster(this);
		}
		final List<Integer> toSpawn = this.getRevives();
		if (toSpawn != null) {
			final MapleMap reviveMap = killer.getMap();
			if (toSpawn.contains(9300216) && reviveMap.getId() > 925000000 && reviveMap.getId() < 926000000) {
				reviveMap.broadcastMessage(MaplePacketCreator.playSound("Dojang/clear"));
				reviveMap.broadcastMessage(MaplePacketCreator.showEffect("dojang/end/clear"));
			}
			TimeMobEntry timeMob = reviveMap.getTimeMob();
			if (timeMob != null) {
				if (toSpawn.contains(timeMob.id)) {
					reviveMap.broadcastMessage(MaplePacketCreator.serverNotice(6, timeMob.message));
				}

				if (timeMob.id == 9300338 && (reviveMap.getId() >= 922240100 && reviveMap.getId() <= 922240119)) {
					if (!reviveMap.containsNPC(9001108)) {
						MapleNPC npc = MapleLifeFactory.getNPC(9001108);
						npc.setPosition(new Point(172, 9));
						npc.setCy(9);
						npc.setRx0(172 + 50);
						npc.setRx1(172 - 50);
						npc.setFh(27);
						reviveMap.addMapObject(npc);
						reviveMap.broadcastMessage(MaplePacketCreator.spawnNPC(npc));
					} else {
						reviveMap.toggleHiddenNPC(9001108);
					}
				}
			}
			for (Integer mid : toSpawn) {
				final MapleMonster mob = MapleLifeFactory.getMonster(mid);
				if (eventInstance != null) {
					eventInstance.registerMonster(mob);
				}
				mob.setPosition(getPosition());
				if (dropsDisabled()) {
					mob.disableDrops();
				}
				TimerManager.getInstance().schedule(new Runnable() {

					@Override
					public void run() {
						reviveMap.spawnMonster(mob);
					}
				}, getAnimationTime("die1"));
			}
		}
		if (eventInstance != null) {
			eventInstance.unregisterMonster(this);
		}
		for (MonsterListener listener : listeners.toArray(new MonsterListener[listeners.size()])) {
			listener.monsterKilled(this, highestDamageChar);
		}
		MapleCharacter ret = highestDamageChar;
		highestDamageChar = null; // may not keep hard references to chars
									// outside of PlayerStorage or MapleMap
		return ret;
	}

	public boolean isAlive() {
		return this.hp > 0;
	}

	public MapleCharacter getController() {
		return controller.get();
	}

	public void setController(MapleCharacter controller) {
		this.controller = new WeakReference<MapleCharacter>(controller);
	}

	public void switchController(MapleCharacter newController, boolean immediateAggro) {
		MapleCharacter controllers = getController();
		if (controllers == newController) {
			return;
		}
		if (controllers != null) {
			controllers.stopControllingMonster(this);
			controllers.getClient().getSession().write(MaplePacketCreator.stopControllingMonster(getObjectId()));
		}
		newController.controlMonster(this, immediateAggro);
		setController(newController);
		if (immediateAggro) {
			setControllerHasAggro(true);
		}
		setControllerKnowsAboutAggro(false);
	}

	public void addListener(MonsterListener listener) {
		listeners.add(listener);
	}

	public boolean isControllerHasAggro() {
		return fake ? false : controllerHasAggro;
	}

	public void setControllerHasAggro(boolean controllerHasAggro) {
		if (fake) {
			return;
		}
		this.controllerHasAggro = controllerHasAggro;
	}

	public boolean isControllerKnowsAboutAggro() {
		return fake ? false : controllerKnowsAboutAggro;
	}

	public void setControllerKnowsAboutAggro(boolean controllerKnowsAboutAggro) {
		if (fake) {
			return;
		}
		this.controllerKnowsAboutAggro = controllerKnowsAboutAggro;
	}

	public MaplePacket makeBossHPBarPacket() {
		return MaplePacketCreator.showBossHP(getId(), getHp(), getMaxHp(), getTagColor(), getTagBgColor());
	}

	public boolean hasBossHPBar() {
		return (isBoss() && getTagColor() > 0) || isHT();
	}

	private boolean isHT() {
		return getId() == 8810018;
	}

	@Override
	public void sendSpawnData(MapleClient c) {
		if (!isAlive()) {
			return;
		}
		if (isFake()) {
			c.getSession().write(MaplePacketCreator.spawnFakeMonster(this, 0));
		} else {
			c.getSession().write(MaplePacketCreator.spawnMonster(this, false));
		}
		if (stati.size() > 0) {
			for (final MonsterStatusEffect mse : this.stati.values()) {
				c.getSession().write(MaplePacketCreator.applyMonsterStatus(getObjectId(), mse));
			}
		}
		if (hasBossHPBar()) {
			if (this.getMap().countMonster(8810026) > 2 && this.getMap().getId() == 240060200) {
				this.getMap().killAllMonsters();
				return;
			}
			c.getSession().write(makeBossHPBarPacket());
		}
	}

	@Override
	public void sendDestroyData(MapleClient client) {
		client.getSession().write(MaplePacketCreator.killMonster(getObjectId(), false));
	}

	@Override
	public MapleMapObjectType getType() {
		return MapleMapObjectType.MONSTER;
	}

	public void setEventInstance(EventInstanceManager eventInstance) {
		this.eventInstance = eventInstance;
	}

	public boolean isMobile() {
		return stats.isMobile();
	}

	public ElementalEffectiveness getEffectiveness(Element e) {
		if (stati.size() > 0 && stati.get(MonsterStatus.DOOM) != null) {
			return ElementalEffectiveness.NORMAL; // like blue snails
		}
		return stats.getEffectiveness(e);
	}

	public boolean applyStatus(MapleCharacter from, final MonsterStatusEffect status, boolean poison, long duration) {
		return applyStatus(from, status, poison, duration, false);
	}

	public boolean applyStatus(MapleCharacter from, final MonsterStatusEffect status, boolean poison, long duration, boolean venom) {
		switch (stats.getEffectiveness(status.getSkill().getElement())) {
			case IMMUNE:
			case STRONG:
			case NEUTRAL:
				return false;
			case NORMAL:
			case WEAK:
				break;
			default: {
				Output.print("Unknown elemental effectiveness: " + stats.getEffectiveness(status.getSkill().getElement()));
				return false;
			}
		}
		if (status.getSkill().getId() == 2111006) { // fp compo
			ElementalEffectiveness effectiveness = stats.getEffectiveness(Element.POISON);
			if (effectiveness == ElementalEffectiveness.IMMUNE || effectiveness == ElementalEffectiveness.STRONG) {
				return false;
			}
		} else if (status.getSkill().getId() == 2211006) { // il compo
			ElementalEffectiveness effectiveness = stats.getEffectiveness(Element.ICE);
			if (effectiveness == ElementalEffectiveness.IMMUNE || effectiveness == ElementalEffectiveness.STRONG) {
				return false;
			}
		} else if (status.getSkill().getId() == 4120005 || status.getSkill().getId() == 4220005 || status.getSkill().getId() == 14110004) {// venom
			if (stats.getEffectiveness(Element.POISON) == ElementalEffectiveness.WEAK) {
				return false;
			}
		}
		if (poison && getHp() <= 1) {
			return false;
		}

		final Map<MonsterStatus, Integer> statis = status.getStati();
		if (stats.isBoss()) {
			if (!(statis.containsKey(MonsterStatus.SPEED) && statis.containsKey(MonsterStatus.NINJA_AMBUSH) && statis.containsKey(MonsterStatus.WATK))) {
				return false;
			}
		}
		for (MonsterStatus stat : statis.keySet()) {
			final MonsterStatusEffect oldEffect = stati.get(stat);
			if (oldEffect != null) {
				oldEffect.removeActiveStatus(stat);
				if (oldEffect.getStati().isEmpty()) {
					oldEffect.cancelTask();
					oldEffect.cancelDamageSchedule();
				}
			}
		}
		TimerManager timerManager = TimerManager.getInstance();
		final Runnable cancelTask = new Runnable() {

			@Override
			public void run() {
				if (isAlive()) {
					MaplePacket packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), status.getStati());
					map.broadcastMessage(packet, getPosition());
					if (getController() != null && !getController().isMapObjectVisible(MapleMonster.this)) {
						getController().getClient().getSession().write(packet);
					}
				}
				for (MonsterStatus stat : status.getStati().keySet()) {
					stati.remove(stat);
				}
				setVenomMulti(0);
				status.cancelDamageSchedule();
			}
		};
		if (poison) {
			int poisonLevel = from.getSkillLevel(status.getSkill());
			int poisonDamage = Math.min(Short.MAX_VALUE, (int) (getMaxHp() / (70.0 - poisonLevel) + 0.999));
			status.setValue(MonsterStatus.POISON, Integer.valueOf(poisonDamage));
			status.setDamageSchedule(timerManager.register(new DamageTask(poisonDamage, from, status, cancelTask, 0), 1000, 1000));
		} else if (venom) {
			if (from.getJob() == MapleJob.NIGHTLORD || from.getJob() == MapleJob.SHADOWER || from.getJob().isA(MapleJob.NIGHTWALKER3)) {
				int poisonLevel = 0, matk = 0, id = from.getJob().getId();
				int skill = (id == 412 ? 4120005 : (id == 422 ? 4220005 : 14110004));
				poisonLevel = from.getSkillLevel(SkillFactory.getSkill(skill));
				if (poisonLevel <= 0) {
					return false;
				}
				matk = SkillFactory.getSkill(skill).getEffect(poisonLevel).getMatk();
				int luk = from.getLuk();
				int maxDmg = (int) Math.ceil(Math.min(Short.MAX_VALUE, 0.2 * luk * matk));
				int minDmg = (int) Math.ceil(Math.min(Short.MAX_VALUE, 0.1 * luk * matk));
				int gap = maxDmg - minDmg;
				if (gap == 0) {
					gap = 1;
				}
				int poisonDamage = 0;
				for (int i = 0; i < getVenomMulti(); i++) {
					poisonDamage += (Randomizer.nextInt(gap) + minDmg);
				}
				poisonDamage = Math.min(Short.MAX_VALUE, poisonDamage);
				status.setValue(MonsterStatus.POISON, Integer.valueOf(poisonDamage));
				status.setDamageSchedule(timerManager.register(new DamageTask(poisonDamage, from, status, cancelTask, 0), 1000, 1000));
			} else {
				return false;
			}

		} else if (status.getSkill().getId() == 4111003 || status.getSkill().getId() == 14111001) { // Shadow
																									// Web
			status.setDamageSchedule(timerManager.schedule(new DamageTask((int) (getMaxHp() / 50.0 + 0.999), from, status, cancelTask, 1), 3500));
		} else if (status.getSkill().getId() == 4121004 || status.getSkill().getId() == 4221004) { // Ninja
																									// Ambush
			final ISkill skill = SkillFactory.getSkill(status.getSkill().getId());
			final byte level = from.getSkillLevel(skill);
			final int damage = (int) ((from.getStr() + from.getLuk()) * (1.5 + (level * 0.05)) * skill.getEffect(level).getDamage());
			/*
			 * if (getHp() - damage <= 1) { make hp 1 betch damage = getHp() -
			 * (getHp() - 1); }
			 */

			status.setValue(MonsterStatus.NINJA_AMBUSH, Integer.valueOf(damage));
			status.setDamageSchedule(timerManager.register(new DamageTask(damage, from, status, cancelTask, 2), 1000, 1000));
		}
		for (MonsterStatus stat : status.getStati().keySet()) {
			stati.put(stat, status);
		}
		int animationTime = status.getSkill().getAnimationTime();
		MaplePacket packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), status);
		map.broadcastMessage(packet, getPosition());
		if (getController() != null && !getController().isMapObjectVisible(this)) {
			getController().getClient().getSession().write(packet);
		}
		status.setCancelTask(timerManager.schedule(cancelTask, duration + animationTime));
		return true;
	}

	public void applyMonsterBuff(final Map<MonsterStatus, Integer> stats, final int x, int skillId, long duration, MobSkill skill, final List<Integer> reflection) {
		TimerManager timerManager = TimerManager.getInstance();
		final Runnable cancelTask = new Runnable() {

			@Override
			public void run() {
				if (isAlive()) {
					MaplePacket packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), stats);
					map.broadcastMessage(packet, getPosition());
					if (getController() != null && !getController().isMapObjectVisible(MapleMonster.this)) {
						getController().getClient().getSession().write(packet);
					}
					for (final MonsterStatus stat : stats.keySet()) {
						stati.remove(stat);
					}
				}
			}
		};
		final MonsterStatusEffect effect = new MonsterStatusEffect(stats, null, skill, true);
		MaplePacket packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), effect);
		map.broadcastMessage(packet, getPosition());
		if (getController() != null && !getController().isMapObjectVisible(this)) {
			getController().getClient().getSession().write(packet);
		}
		timerManager.schedule(cancelTask, duration);
	}

	public boolean isBuffed(MonsterStatus status) {
		return stati.containsKey(status);
	}

	public void setFake(boolean fake) {
		this.fake = fake;
	}

	public boolean isFake() {
		return fake;
	}

	public MapleMap getMap() {
		return map;
	}

	public List<MobSkillEntry> getSkills() {
		return stats.getSkills();
	}

	public boolean hasSkill(int skillId, int level) {
		return stats.hasSkill(skillId, level);
	}

	public boolean canUseSkill(MobSkill toUse) {
		if (toUse == null) {
			return false;
		}
		for (MobSkillEntry skill : usedSkills) {
			if (skill.skillId == toUse.getSkillId() && skill.level == toUse.getSkillLevel()) {
				return false;
			}
		}
		if (toUse.getLimit() > 0) {
			if (this.skillsUsed.containsKey(new MobSkillEntry(toUse.getSkillId(), toUse.getSkillLevel()))) {
				int times = this.skillsUsed.get(new MobSkillEntry(toUse.getSkillId(), toUse.getSkillLevel()));
				if (times >= toUse.getLimit()) {
					return false;
				}
			}
		}
		if (toUse.getSkillId() == 200) {
			Collection<MapleMapObject> mmo = getMap().getMapObjects();
			int i = 0;
			for (MapleMapObject mo : mmo) {
				if (mo.getType() == MapleMapObjectType.MONSTER) {
					i++;
				}
			}
			if (i > 100) {
				return false;
			}
		}
		return true;
	}

	public void usedSkill(final int skillId, final int level, long cooltime) {
		this.usedSkills.add(new MobSkillEntry(skillId, level));
		if (this.skillsUsed.containsKey(new MobSkillEntry(skillId, level))) {
			int times = this.skillsUsed.get(new MobSkillEntry(skillId, level)) + 1;
			this.skillsUsed.remove(new MobSkillEntry(skillId, level));
			this.skillsUsed.put(new MobSkillEntry(skillId, level), times);
		} else {
			this.skillsUsed.put(new MobSkillEntry(skillId, level), 1);
		}
		final MapleMonster mons = this;
		TimerManager tMan = TimerManager.getInstance();
		tMan.schedule(new Runnable() {

			@Override
			public void run() {
				mons.clearSkill(skillId, level);
			}
		}, cooltime);
	}

	public void clearSkill(int skillId, int level) {
		int index = -1;
		for (MobSkillEntry skill : usedSkills) {
			if (skill.skillId == skillId && skill.level == level) {
				index = usedSkills.indexOf(skill);
				break;
			}
		}
		if (index != -1) {
			usedSkills.remove(index);
		}
	}

	public int getNoSkills() {
		return this.stats.getNoSkills();
	}

	public boolean isFirstAttack() {
		return this.stats.isFirstAttack();
	}

	public int getBuffToGive() {
		return this.stats.getBuffToGive();
	}

	private final class DamageTask implements Runnable {

		private final int dealDamage;
		private final MapleCharacter chr;
		private final MonsterStatusEffect status;
		private final Runnable cancelTask;
		private final int type;
		private final MapleMap map;

		private DamageTask(int dealDamage, MapleCharacter chr, MonsterStatusEffect status, Runnable cancelTask, int type) {
			this.dealDamage = dealDamage;
			this.chr = chr;
			this.status = status;
			this.cancelTask = cancelTask;
			this.type = type;
			this.map = chr.getMap();
		}

		@Override
		public void run() {
			int damage = dealDamage;
			if (damage >= hp) {
				damage = hp - 1;
				if (type == 1 || type == 2) {
					map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition());
					cancelTask.run();
					status.getCancelTask().cancel(false);
				}
			}
			if (hp > 1 && damage > 0) {
				damage(chr, damage, false);
				if (type == 1) {
					map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition());
				}
			}
		}
	}

	public String getName() {
		return stats.getName();
	}

	private class AttackingMapleCharacter {

		private MapleCharacter attacker;
		private long lastAttackTime;

		public AttackingMapleCharacter(MapleCharacter attacker, long lastAttackTime) {
			super();
			this.attacker = attacker;
			this.lastAttackTime = lastAttackTime;
		}

		public long getLastAttackTime() {
			return lastAttackTime;
		}

		public MapleCharacter getAttacker() {
			return attacker;
		}
	}

	private interface AttackerEntry {

		List<AttackingMapleCharacter> getAttackers();

		public void addDamage(MapleCharacter from, int damage, boolean updateAttackTime);

		public int getDamage();

		public boolean contains(MapleCharacter chr);

		public void killedMob(MapleMap map, int baseExp, boolean mostDamage);
	}

	private class SingleAttackerEntry implements AttackerEntry {

		private int damage;
		private int chrid;
		private long lastAttackTime;
		private Channel cserv;

		public SingleAttackerEntry(MapleCharacter from, Channel cserv) {
			this.chrid = from.getId();
			this.cserv = cserv;
		}

		@Override
		public void addDamage(MapleCharacter from, int damage, boolean updateAttackTime) {
			if (chrid == from.getId()) {
				this.damage += damage;
			} else {
				throw new IllegalArgumentException("Not the attacker of this entry");
			}
			if (updateAttackTime) {
				lastAttackTime = System.currentTimeMillis();
			}
		}

		@Override
		public List<AttackingMapleCharacter> getAttackers() {
			MapleCharacter chr = cserv.getPlayerStorage().getCharacterById(chrid);
			if (chr != null) {
				return Collections.singletonList(new AttackingMapleCharacter(chr, lastAttackTime));
			} else {
				return Collections.emptyList();
			}
		}

		@Override
		public boolean contains(MapleCharacter chr) {
			return chrid == chr.getId();
		}

		@Override
		public int getDamage() {
			return damage;
		}

		@Override
		public void killedMob(MapleMap map, int baseExp, boolean mostDamage) {
			MapleCharacter chr = map.getCharacterById(chrid);
			if (chr != null) {
				giveExpToCharacter(chr, baseExp, mostDamage, 1);
			}
		}

		@Override
		public int hashCode() {
			return chrid;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}
			final SingleAttackerEntry other = (SingleAttackerEntry) obj;
			return chrid == other.chrid;
		}
	}

	private static class OnePartyAttacker {

		public MapleParty lastKnownParty;
		public int damage;
		public long lastAttackTime;

		public OnePartyAttacker(MapleParty lastKnownParty, int damage) {
			this.lastKnownParty = lastKnownParty;
			this.damage = damage;
			this.lastAttackTime = System.currentTimeMillis();
		}
	}

	private class PartyAttackerEntry implements AttackerEntry {

		private int totDamage;
		private Map<Integer, OnePartyAttacker> attackers;
		private Channel cserv;
		private int partyid;

		public PartyAttackerEntry(int partyid, Channel cserv) {
			this.partyid = partyid;
			this.cserv = cserv;
			attackers = new HashMap<Integer, OnePartyAttacker>(6);
		}

		@Override
		public List<AttackingMapleCharacter> getAttackers() {
			List<AttackingMapleCharacter> ret = new ArrayList<AttackingMapleCharacter>(attackers.size());
			for (Entry<Integer, OnePartyAttacker> entry : attackers.entrySet()) {
				MapleCharacter chr = cserv.getPlayerStorage().getCharacterById(entry.getKey());
				if (chr != null) {
					ret.add(new AttackingMapleCharacter(chr, entry.getValue().lastAttackTime));
				}
			}
			return ret;
		}

		private Map<MapleCharacter, OnePartyAttacker> resolveAttackers() {
			Map<MapleCharacter, OnePartyAttacker> ret = new HashMap<MapleCharacter, OnePartyAttacker>(attackers.size());
			for (Entry<Integer, OnePartyAttacker> aentry : attackers.entrySet()) {
				MapleCharacter chr = cserv.getPlayerStorage().getCharacterById(aentry.getKey());
				if (chr != null) {
					ret.put(chr, aentry.getValue());
				}
			}
			return ret;
		}

		@Override
		public boolean contains(MapleCharacter chr) {
			return attackers.containsKey(chr.getId());
		}

		@Override
		public int getDamage() {
			return totDamage;
		}

		@Override
		public void addDamage(MapleCharacter from, int damage, boolean updateAttackTime) {
			OnePartyAttacker oldPartyAttacker = attackers.get(from.getId());
			if (oldPartyAttacker != null) {
				oldPartyAttacker.damage += damage;
				oldPartyAttacker.lastKnownParty = from.getParty();
				if (updateAttackTime) {
					oldPartyAttacker.lastAttackTime = System.currentTimeMillis();
				}
			} else {
				// TODO actually this causes wrong behaviour when the party
				// changes between attacks
				// only the last setup will get exp - but otherwise we'd have to
				// store the full party
				// constellation for every attack/everytime it changes, might be
				// wanted/needed in the
				// future but not now
				OnePartyAttacker onePartyAttacker = new OnePartyAttacker(from.getParty(), damage);
				attackers.put(from.getId(), onePartyAttacker);
				if (!updateAttackTime) {
					onePartyAttacker.lastAttackTime = 0;
				}
			}
			totDamage += damage;
		}

		@Override
		public void killedMob(MapleMap map, int baseExp, boolean mostDamage) {
			Map<MapleCharacter, OnePartyAttacker> attackers_ = resolveAttackers();
			MapleCharacter highest = null;
			int highestDamage = 0;
			Map<MapleCharacter, Integer> expMap = new ArrayMap<MapleCharacter, Integer>(6);
			for (Entry<MapleCharacter, OnePartyAttacker> attacker : attackers_.entrySet()) {
				MapleParty party = attacker.getValue().lastKnownParty;
				double averagePartyLevel = 0;
				List<MapleCharacter> expApplicable = new ArrayList<MapleCharacter>();
				for (MaplePartyCharacter partychar : party.getMembers()) {
					if (attacker.getKey().getLevel() - partychar.getLevel() <= 5 || getLevel() - partychar.getLevel() <= 5) {
						MapleCharacter pchr = cserv.getPlayerStorage().getCharacterByName(partychar.getName());
						if (pchr != null) {
							if (pchr.isAlive() && pchr.getMap() == map) {
								expApplicable.add(pchr);
								averagePartyLevel += pchr.getLevel();
							}
						}
					}
				}
				double expBonus = 1.0;
				if (expApplicable.size() > 1) {
					expBonus = 1.10 + 0.05 * expApplicable.size();
					averagePartyLevel /= expApplicable.size();
				}
				int iDamage = attacker.getValue().damage;
				if (iDamage > highestDamage) {
					highest = attacker.getKey();
					highestDamage = iDamage;
				}
				double innerBaseExp = baseExp * ((double) iDamage / totDamage);
				double expFraction = (innerBaseExp * expBonus) / (expApplicable.size() + 1);
				for (MapleCharacter expReceiver : expApplicable) {
					Integer oexp = expMap.get(expReceiver);
					int iexp;
					if (oexp == null) {
						iexp = 0;
					} else {
						iexp = oexp.intValue();
					}
					double expWeight = (expReceiver == attacker.getKey() ? 2.0 : 1.0);
					double levelMod = expReceiver.getLevel() / averagePartyLevel;
					if (levelMod > 1.0 || this.attackers.containsKey(expReceiver.getId())) {
						levelMod = 1.0;
					}
					iexp += (int) Math.round(expFraction * expWeight * levelMod);
					expMap.put(expReceiver, Integer.valueOf(iexp));
				}
			}
			for (Entry<MapleCharacter, Integer> expReceiver : expMap.entrySet()) {
				giveExpToCharacter(expReceiver.getKey(), expReceiver.getValue(), mostDamage ? expReceiver.getKey() == highest : false, expMap.size());
			}
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + partyid;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}
			final PartyAttackerEntry other = (PartyAttackerEntry) obj;
			if (partyid != other.partyid) {
				return false;
			}
			return true;
		}
	}

	public void addStolen(int itemId) {
		stolenItems.add(itemId);
	}

	public void setTempEffectiveness(Element e, ElementalEffectiveness ee, long milli) {
		final Element fE = e;
		final ElementalEffectiveness fEE = stats.getEffectiveness(e);
		if (!stats.getEffectiveness(e).equals(ElementalEffectiveness.WEAK)) {
			stats.setEffectiveness(e, ee);
			TimerManager.getInstance().schedule(new Runnable() {

				@Override
				public void run() {
					stats.removeEffectiveness(fE);
					stats.setEffectiveness(fE, fEE);
				}
			}, milli);
		}
	}

	public BanishInfo getBanish() {
		return stats.getBanishInfo();
	}

	public void setBoss(boolean boss) {
		this.stats.setBoss(boss);
	}

	public int getDropPeriodTime() {
		return stats.getDropPeriod();
	}

	public int getPADamage() {
		return stats.getPADamage();
	}

	public Map<MonsterStatus, MonsterStatusEffect> getStati() {
		return stati;
	}

	public final void empty() {
		try {
			this.monsterLock.unlock();
		} catch (Exception e) {
		} // who cares
		this.monsterLock = null;
		final Iterator<MonsterStatusEffect> mseIt = stati.values().iterator();
		MonsterStatusEffect mse;
		while (mseIt.hasNext()) {
			mse = mseIt.next();
			mse.cancelDamageSchedule();
			mse.cancelTask();
			mseIt.remove();
			mse = null;
		}
		this.stati = null;
		this.usedSkills = null;
		this.listeners = null;
		this.skillsUsed = null;
		this.stats = null;
		this.highestDamageChar = null;
		this.map = null;
		this.eventInstance = null;
		this.attackers = null;
		this.stolenItems = null;
		if (controller.get() != null) {
			controller.get().stopControllingMonster(this);
		}
		controller.clear();
		this.controller = null;
	}
}
