package com.aionemu.gameserver.controllers;

import static com.aionemu.gameserver.model.DialogAction.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.aionemu.gameserver.GameServer;
import com.aionemu.gameserver.ai.NpcAI;
import com.aionemu.gameserver.ai.handler.ShoutEventHandler;
import com.aionemu.gameserver.configs.administration.AdminConfig;
import com.aionemu.gameserver.configs.main.*;
import com.aionemu.gameserver.controllers.attack.AttackStatus;
import com.aionemu.gameserver.controllers.attack.AttackUtil;
import com.aionemu.gameserver.controllers.observer.StanceObserver;
import com.aionemu.gameserver.custom.pvpmap.PvpMapService;
import com.aionemu.gameserver.dataholders.DataManager;
import com.aionemu.gameserver.model.EmotionType;
import com.aionemu.gameserver.model.Race;
import com.aionemu.gameserver.model.TaskId;
import com.aionemu.gameserver.model.actions.PlayerMode;
import com.aionemu.gameserver.model.animations.ActionAnimation;
import com.aionemu.gameserver.model.animations.ObjectDeleteAnimation;
import com.aionemu.gameserver.model.gameobjects.*;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.gameobjects.state.CreatureState;
import com.aionemu.gameserver.model.gameobjects.state.CreatureVisualState;
import com.aionemu.gameserver.model.gameobjects.state.FlyState;
import com.aionemu.gameserver.model.house.House;
import com.aionemu.gameserver.model.stats.container.PlayerGameStats;
import com.aionemu.gameserver.model.summons.SummonMode;
import com.aionemu.gameserver.model.summons.UnsummonType;
import com.aionemu.gameserver.model.templates.QuestTemplate;
import com.aionemu.gameserver.model.templates.flypath.FlyPathEntry;
import com.aionemu.gameserver.model.templates.panels.SkillPanel;
import com.aionemu.gameserver.model.templates.zone.ZoneType;
import com.aionemu.gameserver.network.aion.serverpackets.*;
import com.aionemu.gameserver.network.aion.serverpackets.SM_ATTACK_STATUS.LOG;
import com.aionemu.gameserver.network.aion.serverpackets.SM_ATTACK_STATUS.TYPE;
import com.aionemu.gameserver.questEngine.QuestEngine;
import com.aionemu.gameserver.questEngine.model.QuestEnv;
import com.aionemu.gameserver.restrictions.PlayerRestrictions;
import com.aionemu.gameserver.services.*;
import com.aionemu.gameserver.services.conquerorAndProtectorSystem.ConquerorAndProtectorService;
import com.aionemu.gameserver.services.drop.DropService;
import com.aionemu.gameserver.services.instance.InstanceService;
import com.aionemu.gameserver.services.reward.StarterKitService;
import com.aionemu.gameserver.services.summons.SummonsService;
import com.aionemu.gameserver.skillengine.SkillEngine;
import com.aionemu.gameserver.skillengine.effect.EffectTemplate;
import com.aionemu.gameserver.skillengine.effect.RebirthEffect;
import com.aionemu.gameserver.skillengine.model.Effect;
import com.aionemu.gameserver.skillengine.model.HopType;
import com.aionemu.gameserver.skillengine.model.Skill;
import com.aionemu.gameserver.skillengine.model.Skill.SkillMethod;
import com.aionemu.gameserver.skillengine.model.SkillTemplate;
import com.aionemu.gameserver.taskmanager.tasks.PlayerMoveTaskManager;
import com.aionemu.gameserver.taskmanager.tasks.TeamMoveUpdater;
import com.aionemu.gameserver.taskmanager.tasks.TeamStatUpdater;
import com.aionemu.gameserver.utils.PacketSendUtility;
import com.aionemu.gameserver.utils.PositionUtil;
import com.aionemu.gameserver.utils.ThreadPoolManager;
import com.aionemu.gameserver.utils.audit.AuditLogger;
import com.aionemu.gameserver.world.MapRegion;
import com.aionemu.gameserver.world.WorldMapType;
import com.aionemu.gameserver.world.WorldType;
import com.aionemu.gameserver.world.geo.GeoService;
import com.aionemu.gameserver.world.zone.ZoneInstance;
import com.aionemu.gameserver.world.zone.ZoneName;

/**
 * This class is for controlling players.
 * 
 * @author -Nemesiss-, ATracer, xavier, Sarynth, RotO, xTz, KID, Sippolo
 */
public class PlayerController extends CreatureController<Player> {

	private static final Logger log = LoggerFactory.getLogger(PlayerController.class);
	private long lastAttackMillis = 0;
	private long lastAttackedMillis = 0;
	private StanceObserver stanceObserver;

	@Override
	public void see(VisibleObject object) {
		super.see(object);
		if (object instanceof Creature creature) {
			if (creature instanceof Npc npc) {
				PacketSendUtility.sendPacket(getOwner(), new SM_NPC_INFO(npc, getOwner()));
				if (npc instanceof Kisk) {
					if (getOwner().getRace() == ((Kisk) npc).getOwnerRace())
						PacketSendUtility.sendPacket(getOwner(), new SM_KISK_UPDATE((Kisk) npc));
				} else {
					QuestEngine.getInstance().onAtDistance(new QuestEnv(npc, getOwner(), 0));
				}
				DropService.getInstance().see(getOwner(), npc);
			} else if (creature instanceof Player player) {
				sendPlayerInfoPackets(player);
			} else if (creature instanceof Summon) {
				PacketSendUtility.sendPacket(getOwner(), new SM_NPC_INFO((Summon) creature, getOwner()));
			}
			if (!creature.getEffectController().isEmpty())
				PacketSendUtility.sendPacket(getOwner(), new SM_ABNORMAL_EFFECT(creature));
		} else if (object instanceof Gatherable || object instanceof StaticObject) {
			PacketSendUtility.sendPacket(getOwner(), new SM_GATHERABLE_INFO(object));
		} else if (object instanceof Pet pet) {
			PacketSendUtility.sendPacket(getOwner(), new SM_PET(pet));
			if (pet.getMaster().isInFlyingState())
				PacketSendUtility.sendPacket(getOwner(), new SM_PET_EMOTE(pet, PetEmote.FLY_START));
		} else if (object instanceof House) {
			PacketSendUtility.sendPacket(getOwner(), new SM_HOUSE_RENDER((House) object));
		} else if (object instanceof HouseObject) {
			PacketSendUtility.sendPacket(getOwner(), new SM_HOUSE_OBJECT((HouseObject<?>) object));
		}
	}

	private void sendPlayerInfoPackets(Player player) {
		PacketSendUtility.sendPacket(getOwner(), new SM_PLAYER_INFO(player, !player.equals(getOwner()) && getOwner().isAggroIconTo(player)));
		PacketSendUtility.sendPacket(getOwner(), new SM_MOTION(player.getObjectId(), player.getMotions().getActiveMotions()));
		if (player.isInPlayerMode(PlayerMode.RIDE))
			PacketSendUtility.sendPacket(getOwner(), new SM_EMOTION(player, EmotionType.RIDE, 0, player.ride.getNpcId()));
		if (player.getController().isUnderStance())
			PacketSendUtility.sendPacket(getOwner(), new SM_PLAYER_STANCE(player, 1));
	}

	@Override
	public void notSee(VisibleObject object, ObjectDeleteAnimation animation) {
		super.notSee(object, animation);
		if (object instanceof Pet) {
			PacketSendUtility.sendPacket(getOwner(), new SM_PET(object.getObjectId(), animation));
		} else if (object instanceof House) {
			PacketSendUtility.sendPacket(getOwner(), new SM_DELETE_HOUSE(((House) object).getAddress().getId()));
		} else if (object instanceof HouseObject) {
			PacketSendUtility.sendPacket(getOwner(), new SM_DELETE_HOUSE_OBJECT(object.getObjectId()));
		} else if (object instanceof Npc && ((Npc) object).isFlag()) {
			PacketSendUtility.sendPacket(getOwner(), new SM_DELETE(object, ObjectDeleteAnimation.DELAYED));
		} else {
			PacketSendUtility.sendPacket(getOwner(), new SM_DELETE(object, animation));
		}
	}

	@Override
	public void onHide() {
		super.onHide();
		DuelService.getInstance().fixTeamVisibility(getOwner());
	}

	@Override
	public void onHideEnd() {
		Pet pet = getOwner().getPet();
		if (pet != null && !PositionUtil.isInRange(getOwner(), pet, 3)) // client sends pet position only every 50m...
			pet.getPosition().setXYZH(getOwner().getX(), getOwner().getY(), getOwner().getZ(), getOwner().getHeading());
		super.onHideEnd();
	}

	public void updateNearbyQuests() {
		Map<Integer, Integer> nearbyQuestList = new HashMap<>();
		for (int questId : getOwner().getPosition().getMapRegion().getParent().getQuestIds()) {
			if (QuestService.checkStartConditions(getOwner(), questId, false, 2, false, false, false))
				nearbyQuestList.put(questId, QuestService.getLevelRequirementDiff(questId, getOwner().getCommonData().getLevel()));
		}
		PacketSendUtility.sendPacket(getOwner(), new SM_NEARBY_QUESTS(nearbyQuestList));
	}

	public void updateRepeatableQuests() {
		List<Integer> reapeatQuestList = new ArrayList<>();
		for (int questId : getOwner().getPosition().getMapRegion().getParent().getQuestIds()) {
			QuestTemplate template = DataManager.QUEST_DATA.getQuestById(questId);
			if (!template.isTimeBased())
				continue;
			if (QuestService.checkStartConditions(getOwner(), questId, false))
				reapeatQuestList.add(questId);
		}
		if (reapeatQuestList.size() > 0)
			PacketSendUtility.sendPacket(getOwner(), new SM_QUEST_REPEAT(reapeatQuestList));
	}

	@Override
	public void onEnterZone(ZoneInstance zone) {
		Player player = getOwner();
		if (!zone.canRide() && player.isInPlayerMode(PlayerMode.RIDE))
			player.unsetPlayerMode(PlayerMode.RIDE);
		ConquerorAndProtectorService.getInstance().onEnterZone(player, zone);
		InstanceService.onEnterZone(player, zone);
		ZoneName zoneName = zone.getAreaTemplate().getZoneName();
		if (zoneName == null)
			log.warn("No name found for a zone in map " + zone.getAreaTemplate().getWorldId() + " with xml name " + zone.getZoneTemplate().getXmlName());
		else
			QuestEngine.getInstance().onEnterZone(new QuestEnv(null, player, 0), zoneName);
	}

	@Override
	public void onLeaveZone(ZoneInstance zone) {
		Player player = getOwner();
		ConquerorAndProtectorService.getInstance().onLeaveZone(player, zone);
		InstanceService.onLeaveZone(player, zone);
		ZoneName zoneName = zone.getAreaTemplate().getZoneName();
		if (zoneName == null)
			log.warn("No name found for a zone in map " + zone.getAreaTemplate().getWorldId() + " with xml name " + zone.getZoneTemplate().getXmlName());
		else
			QuestEngine.getInstance().onLeaveZone(new QuestEnv(null, player, 0), zoneName);
	}

	/**
	 * Called when leaving a fly zone (like citadel of verteron) or a fly map (like the abyss).
	 */
	public void onLeaveFlyArea() {
		Player player = getOwner();
		if (!player.hasAccess(AdminConfig.FREE_FLIGHT)) {
			if (player.isInFlyingState()) {
				if (player.isInGlidingState()) {
					player.unsetFlyState(FlyState.FLYING);
					player.unsetState(CreatureState.FLYING);
					player.getLifeStats().triggerFpReduce();
					player.getGameStats().updateStatsAndSpeedVisually();
					PacketSendUtility.broadcastPacket(player, new SM_EMOTION(player, EmotionType.STOP_FLY), true);
				} else {
					player.getFlyController().endFly(true);
					if (player.isSpawned() && !player.isInsideZoneType(ZoneType.FLY)) // not spawned means leaving by teleporter
						AuditLogger.log(player, "left fly zone in fly state at " + player.getPosition());
				}
			} else if (player.isInGlidingState()) {
				player.getLifeStats().triggerFpReduce();
			}
		}
	}

	public void onEnterFlyArea() {
		getOwner().getLifeStats().triggerFpReduce();
	}

	/**
	 * Should only be triggered from one place (life stats)
	 */
	// TODO [AT] move
	public void onEnterWorld() {
		if (getOwner().getPosition().getWorldMapInstance().getParent().isExceptBuff()) {
			if (!PvpMapService.getInstance().isOnPvPMap(getOwner()))
				getOwner().getEffectController().removeAllEffects();
		}

		for (Effect ef : getOwner().getEffectController().getAbnormalEffects()) {
			if (ef.isDeityAvatar()) {
				// remove abyss transformation if worldtype != abyss && worldtype != balaurea && worldType != panesterra
				if (getOwner().getWorldType() != WorldType.ABYSS && getOwner().getWorldType() != WorldType.BALAUREA
					&& getOwner().getWorldType() != WorldType.PANESTERRA || getOwner().isInInstance()) {
					ef.endEffect();
				}
			}
		}
	}

	@Override
	public void onDie(Creature lastAttacker) {
		Player player = getOwner();
		player.getController().cancelCurrentSkill(null);
		setRebirthReviveInfo();
		Creature master = lastAttacker.getMaster();

		if (DuelService.getInstance().isDueling(player)) {
			boolean killedByOpponent = player.isDueling(master);
			DuelService.getInstance().loseDuel(player);
			if (killedByOpponent) {
				if (player.getLifeStats().getHpPercentage() < 33)
					player.getLifeStats().setCurrentHpPercent(33);
				if (player.getLifeStats().getMpPercentage() < 33)
					player.getLifeStats().setCurrentMpPercent(33);
				if (master.getLifeStats().getHpPercentage() < 33)
					master.getLifeStats().setCurrentHpPercent(33);
				if (master.getLifeStats().getMpPercentage() < 33)
					master.getLifeStats().setCurrentMpPercent(33);
				return;
			}
		}

		// Release summon
		Summon summon = player.getSummon();
		if (summon != null)
			SummonsService.doMode(SummonMode.RELEASE, summon, UnsummonType.UNSPECIFIED);

		// setIsFlyingBeforeDead for PlayerReviveService
		if (player.isInState(CreatureState.FLYING))
			player.setIsFlyingBeforeDeath(true);

		// ride
		player.setPlayerMode(PlayerMode.RIDE, null);
		player.unsetState(CreatureState.RESTING);
		player.unsetState(CreatureState.FLOATING_CORPSE);

		// unset flying
		player.unsetState(CreatureState.FLYING);
		player.unsetState(CreatureState.GLIDING);
		player.unsetFlyState(FlyState.FLYING);
		player.unsetFlyState(FlyState.GLIDING);

		player.resetFearCount();
		player.resetSleepCount();
		player.resetParalyzeCount();

		// Effects removed with super.onDie()
		super.onDie(lastAttacker);

		scheduleShowResurrectionOptions();

		if (player.getPosition().getWorldMapInstance().getInstanceHandler().onDie(player, lastAttacker))
			return;

		MapRegion mapRegion = player.getPosition().getMapRegion();
		if (mapRegion != null && mapRegion.onDie(lastAttacker, player))
			return;

		doReward();

		if (master instanceof Npc || master.equals(player)) {
			if (player.getLevel() > 4 && !player.getEffectController().hasAbnormalEffect(Effect::isNoDeathPenalty))
				player.getCommonData().calculateExpLoss();
		}

		QuestEngine.getInstance().onDie(new QuestEnv(null, player, 0));
	}

	private void setRebirthReviveInfo() {
		Player player = getOwner();
		// Store the effect info.
		List<Effect> effects = player.getEffectController().getAbnormalEffects();
		for (Effect effect : effects) {
			for (EffectTemplate template : effect.getEffectTemplates()) {
				if (template.getEffectId() == 160 && template instanceof RebirthEffect) {
					player.setRebirthEffect((RebirthEffect) template);
					return;
				}
			}
		}
		player.setRebirthEffect(null);
	}

	@Override
	public void onDespawn() {
		if (getOwner().isLooting())
			DropService.getInstance().closeDropList(getOwner(), getOwner().getLootingNpcOid());
		super.onDespawn();
	}

	public void scheduleShowResurrectionOptions() {
		ThreadPoolManager.getInstance().schedule(() -> {
			// teleportation task can be assigned shortly after death (see PlayerReviveService#scheduleReviveAtBase)
			if (getOwner().isDead() && !hasTask(TaskId.TELEPORT))
				showResurrectionOptions();
		}, 500);
	}

	public void showResurrectionOptions() {
		PacketSendUtility.sendPacket(getOwner(), new SM_DIE(getOwner()));
	}

	private boolean isInvader(Player player) {
		if (player.getRace().equals(Race.ASMODIANS)) {
			return player.getWorldId() == 210060000;
		} else {
			return player.getWorldId() == 220050000;
		}
	}

	@Override
	public void doReward() {
		PvpService.getInstance().doReward(getOwner());
	}

	@Override
	public void onBeforeSpawn() {
		super.onBeforeSpawn();
		if (!getOwner().isDead()) {
			if (getOwner().getIsFlyingBeforeDeath())
				getOwner().unsetState(CreatureState.FLOATING_CORPSE);
			else if (getOwner().isInState(CreatureState.DEAD))
				getOwner().unsetState(CreatureState.DEAD);
			getOwner().setState(CreatureState.ACTIVE);
		}
		getOwner().setHitTimeBoost(0, 0);
		if (getOwner().getPanesterraFaction() != null && !WorldMapType.isPanesterraMap(getOwner().getWorldId()))
			getOwner().setPanesterraFaction(null);
	}

	@Override
	public void attackTarget(Creature target, int time, boolean skipChecks) {
		if (!PlayerRestrictions.canAttack(getOwner(), target))
			return;

		PlayerGameStats gameStats = getOwner().getGameStats();
		// client allows attacking from +0.̅9 meters further away
		float attackRange = 1 + gameStats.getAttackRange().getCurrent() / 1000f;
		// client can send CM_ATTACK before in range (even before sending CM_MOVE). we only allow it on first hit to minimize exploit potential
		if (!target.getAggroList().isHating(getOwner()))
			attackRange += PositionUtil.calculateMaxCoveredDistance(getOwner(), 100);
		if (!PositionUtil.isInAttackRange(getOwner(), target, attackRange)) {
			PacketSendUtility.sendPacket(getOwner(), SM_ATTACK_RESPONSE.TARGET_TOO_FAR_AWAY(gameStats.getAttackCounter()));
			return;
		}

		if (!GeoService.getInstance().canSee(getOwner(), target)) {
			PacketSendUtility.sendPacket(getOwner(), SM_ATTACK_RESPONSE.STOP_OBSTACLE_IN_THE_WAY(gameStats.getAttackCounter()));
			return;
		}

		if (target instanceof Npc) {
			QuestEngine.getInstance().onAttack(new QuestEnv(target, getOwner(), 0));
		}

		int attackSpeed = gameStats.getAttackSpeed().getCurrent();

		long milis = System.currentTimeMillis();
		// network ping..
		if (milis - lastAttackMillis + 300 < attackSpeed) {
			// hack
			PacketSendUtility.sendPacket(getOwner(), SM_ATTACK_RESPONSE.STOP_WITHOUT_MESSAGE(gameStats.getAttackCounter()));
			return;
		}
		lastAttackMillis = milis;

		super.attackTarget(target, time, true);
	}

	@Override
	public void onAttack(Creature attacker, Effect effect, TYPE type, int damage, boolean notifyAttack, LOG logId, AttackStatus attackStatus,
		HopType hopType) {
		if (getOwner().isDead())
			return;

		if (getOwner().isProtectionActive())
			return;

		// avoid killing players after duel
		if (!getOwner().equals(attacker) && attacker.getActingCreature() instanceof Player && !getOwner().isEnemy(attacker))
			return;

		cancelUseItem();
		super.onAttack(attacker, effect, type, damage, notifyAttack, logId, attackStatus, hopType);

		if (attacker instanceof Npc) {
			ShoutEventHandler.onAttack((NpcAI) attacker.getAi(), getOwner());
			QuestEngine.getInstance().onAttack(new QuestEnv(attacker, getOwner(), 0));
		}

		lastAttackedMillis = System.currentTimeMillis();
	}

	public void useSkill(SkillTemplate template, int targetType, float x, float y, float z, int clientHitTime, int skillLevel) {
		Player player = getOwner();
		Skill skill = SkillEngine.getInstance().getSkillFor(player, template, player.getTarget());
		if (skill == null && player.isTransformed()) {
			SkillPanel panel = DataManager.PANEL_SKILL_DATA.getSkillPanel(player.getTransformModel().getPanelId());
			if (panel != null && panel.canUseSkill(template.getSkillId(), skillLevel)) {
				skill = SkillEngine.getInstance().getSkillFor(player, template, player.getTarget(), skillLevel);
			}
		}

		if (skill != null) {
			if (!PlayerRestrictions.canUseSkill(player, skill))
				return;

			skill.setTargetType(targetType, x, y, z);
			skill.setClientHitTime(clientHitTime);
			skill.useSkill();
		}
	}

	@Override
	public void onStartMove() {
		super.onStartMove();
		PlayerMoveTaskManager.getInstance().addPlayer(getOwner());
		cancelUseItem();
		cancelCurrentSkill(null);
	}

	@Override
	public void onMove() {
		super.onMove();
		if (getOwner().isInTeam())
			TeamMoveUpdater.getInstance().startTask(getOwner());
	}

	@Override
	public void onStopMove() {
		super.onStopMove();
		PlayerMoveTaskManager.getInstance().removePlayer(getOwner());
		cancelCurrentSkill(null);
		updateZone();
	}

	@Override
	public void cancelCurrentSkill(Creature lastAttacker) {
		cancelCurrentSkill(lastAttacker, SM_SYSTEM_MESSAGE.STR_SKILL_CANCELED());
	}

	@Override
	public void cancelCurrentSkill(Creature lastAttacker, SM_SYSTEM_MESSAGE message) {
		if (getOwner().getCastingSkill() == null) {
			return;
		}

		Player player = getOwner();
		Skill castingSkill = player.getCastingSkill();
		castingSkill.cancelCast();
		player.setCasting(null);
		if (castingSkill.allowAnimationBoostByCastSpeed())
			player.setHitTimeBoost(Long.MAX_VALUE, castingSkill.getCastSpeedForAnimationBoostAndChargeSkills()); // yes, this is retail client behavior
		else
			player.setHitTimeBoost(0, 0);
		if (castingSkill.getSkillMethod() == SkillMethod.CAST) {
			PacketSendUtility.broadcastPacket(player, new SM_SKILL_CANCEL(player, castingSkill.getSkillTemplate().getSkillId()), true);
			if (message != null)
				PacketSendUtility.sendPacket(player, message);
		} else if (castingSkill.getSkillMethod() == SkillMethod.ITEM) {
			PacketSendUtility.sendPacket(player, SM_SYSTEM_MESSAGE.STR_ITEM_CANCELED());
			player.removeItemCoolDown(castingSkill.getItemTemplate().getUseLimits().getDelayId());
			PacketSendUtility.broadcastPacket(player, new SM_ITEM_USAGE_ANIMATION(player.getObjectId(), castingSkill.getFirstTarget().getObjectId(),
				castingSkill.getItemObjectId(), castingSkill.getItemTemplate().getTemplateId(), 0, 3, 0), true);
		}

		if (lastAttacker instanceof Player && !lastAttacker.equals(getOwner())) {
			PacketSendUtility.sendPacket((Player) lastAttacker, SM_SYSTEM_MESSAGE.STR_SKILL_TARGET_SKILL_CANCELED());
		}
	}

	@Override
	public void cancelUseItem() {
		Player player = getOwner();
		Item usingItem = player.getUsingItem();
		player.setUsingItem(null);
		if (hasTask(TaskId.ITEM_USE)) {
			cancelTask(TaskId.ITEM_USE);
			PacketSendUtility.broadcastPacket(player, new SM_ITEM_USAGE_ANIMATION(player.getObjectId(), usingItem == null ? 0 : usingItem.getObjectId(),
				usingItem == null ? 0 : usingItem.getItemTemplate().getTemplateId(), 0, 3, 0), true);
		}
	}

	@Override
	public void onDialogSelect(int dialogActionId, int prevDialogId, Player player, int questId, int extendedRewardIndex) {
		switch (dialogActionId) {
			case BUY:
				PacketSendUtility.sendPacket(player, new SM_PRIVATE_STORE(getOwner().getStore(), player));
				break;
			case QUEST_ACCEPT_1:
			case QUEST_ACCEPT_SIMPLE:
				if (!getOwner().equals(player) && PositionUtil.isInRange(getOwner(), player, 100)) { // TODO check if owner really shared
					if (!DataManager.QUEST_DATA.getQuestById(questId).isCannotShare())
						QuestService.startQuest(new QuestEnv(null, player, questId, dialogActionId));
				}
				break;
		}
	}

	public void onLevelChange(int oldLevel, int newLevel) {
		if (oldLevel == newLevel)
			return;

		Player player = getOwner();
		int minNewLevel = oldLevel < newLevel ? oldLevel + 1 : oldLevel - 1; // for skill learning and other stuff that only wants the new level(s)

		if (GSConfig.ENABLE_RATIO_LIMITATION
			&& (player.getAccount().getNumberOf(player.getRace()) == 1 || player.getAccount().getMaxPlayerLevel() == newLevel)) {
			if (oldLevel < GSConfig.RATIO_MIN_REQUIRED_LEVEL && newLevel >= GSConfig.RATIO_MIN_REQUIRED_LEVEL)
				GameServer.updateRatio(player.getRace(), 1);
			else if (oldLevel >= GSConfig.RATIO_MIN_REQUIRED_LEVEL && newLevel < GSConfig.RATIO_MIN_REQUIRED_LEVEL)
				GameServer.updateRatio(player.getRace(), -1);
		}

		player.getGameStats().updateStatsTemplate();
		player.getCommonData().updateMaxRepose();
		player.getCommonData().resetSalvationPoints();
		upgradePlayer();
		PacketSendUtility.broadcastPacket(player, new SM_ACTION_ANIMATION(player.getObjectId(), ActionAnimation.LEVEL_UP, newLevel), true);

		player.getNpcFactions().onLevelUp();
		QuestEngine.getInstance().onLevelChanged(player);
		updateNearbyQuests();
		if (HTMLConfig.ENABLE_GUIDES && player.isSpawned())
			HTMLService.sendGuideHtml(player, minNewLevel, newLevel);
		SkillLearnService.learnNewSkills(player, minNewLevel, newLevel);
		BonusPackService.getInstance().addPlayerCustomReward(player);
		FactionPackService.getInstance().addPlayerCustomReward(player);
		if (CustomConfig.ENABLE_STARTER_KIT)
			StarterKitService.getInstance().onLevelUp(player, minNewLevel, newLevel);
	}

	public void upgradePlayer() {
		Player player = getOwner();
		player.getLifeStats().synchronizeWithMaxStats();
		player.getGameStats().updateStatsVisually();

		if (player.isInTeam() && !TeamStatUpdater.getInstance().hasTask(player)) // SM_GROUP_MEMBER_INFO / SM_ALLIANCE_MEMBER_INFO task
			TeamStatUpdater.getInstance().startTask(player);

		if (player.isLegionMember()) // SM_LEGION_UPDATE_MEMBER
			LegionService.getInstance().updateMemberInfo(player);
	}

	public void onChangedPlayerAttributes() {
		getOwner().clearKnownlist();
		sendPlayerInfoPackets(getOwner());
		if (getOwner().getSeeState() != 0)
			PacketSendUtility.sendPacket(getOwner(), new SM_PLAYER_STATE(getOwner())); // needed to see hidden creatures again
		getOwner().getEffectController().updatePlayerEffectIcons(null);
		getOwner().updateKnownlist();
	}

	/**
	 * After entering game player char is "blinking" which means that it's in under some protection, after making an action char stops blinking. -
	 * Starts protection active - Schedules task to end protection
	 */
	public void startProtectionActiveTask() {
		if (!getOwner().isProtectionActive()) {
			getOwner().setVisualState(CreatureVisualState.BLINKING);
			AttackUtil.cancelCastOn(getOwner());
			AttackUtil.removeTargetFrom(getOwner());
			PacketSendUtility.broadcastToSightedPlayers(getOwner(), new SM_PLAYER_STATE(getOwner()), true);
			addTask(TaskId.PROTECTION_ACTIVE, ThreadPoolManager.getInstance().schedule(this::stopProtectionActiveTask, 60000));
		}
	}

	/**
	 * Stops protection active task after first move or use skill
	 */
	public void stopProtectionActiveTask() {
		cancelTask(TaskId.PROTECTION_ACTIVE);
		Player player = getOwner();
		if (player.isSpawned()) {
			player.unsetVisualState(CreatureVisualState.BLINKING);
			PacketSendUtility.broadcastToSightedPlayers(player, new SM_PLAYER_STATE(player), true);
			notifyAIOnMove();
		}
	}

	/**
	 * When player arrives at destination point of flying teleport
	 */
	public void onFlyTeleportEnd() {
		Player player = getOwner();
		if (player.isInPlayerMode(PlayerMode.WINDSTREAM)) {
			player.unsetPlayerMode(PlayerMode.WINDSTREAM);
			player.unsetState(CreatureState.FLYING);
			player.unsetFlyState(FlyState.FLYING);
			player.setFlyState(FlyState.GLIDING);
			player.setState(CreatureState.ACTIVE);
			player.setState(CreatureState.GLIDING);
			player.getLifeStats().triggerFpReduce();
			player.getGameStats().updateStatsAndSpeedVisually();
		} else {
			player.unsetState(CreatureState.FLYING);
			player.setFlightTeleportId(0);

			if (SecurityConfig.ENABLE_FLYPATH_VALIDATOR) {
				long diff = (System.currentTimeMillis() - player.getFlyStartTime());
				FlyPathEntry path = player.getCurrentFlyPath();

				if (player.getWorldId() != path.getEndWorldId()) {
					AuditLogger.log(player, "tried to use flyPath #" + path.getId() + " from not native start world " + player.getWorldId() + " (expected "
						+ path.getEndWorldId() + ")");
				}

				if (diff < path.getTimeInMs()) {
					AuditLogger.log(player, "ended fly path too early: Fly duration " + diff + "ms instead of " + path.getTimeInMs() + "ms");
					/*
					 * todo if works teleport player to start_* xyz, or even ban
					 */
				}

				player.setCurrentFlypath(null);
			}

			player.setFlightDistance(0);
			player.setState(CreatureState.ACTIVE);
			updateZone();
		}
	}

	public void startStance(int skillId) {
		stopStance();
		stanceObserver = new StanceObserver(getOwner(), skillId);
		getOwner().getObserveController().addObserver(stanceObserver);
		PacketSendUtility.broadcastPacket(getOwner(), new SM_PLAYER_STANCE(getOwner(), 1), true);
	}

	public void stopStance() {
		if (stanceObserver != null) {
			getOwner().getObserveController().removeObserver(stanceObserver);
			getOwner().getEffectController().removeEffect(stanceObserver.getStanceSkillId());
			PacketSendUtility.broadcastPacket(getOwner(), new SM_PLAYER_STANCE(getOwner(), 0), true);
			stanceObserver = null;
		}
	}

	public int getStanceSkillId() {
		return stanceObserver == null ? 0 : stanceObserver.getStanceSkillId();
	}

	public boolean isUnderStance() {
		return stanceObserver != null;
	}

	public void updateSoulSickness(int skillId) {
		Player player = getOwner();
		House house = player.getActiveHouse();
		if (house != null)
			switch (house.getHouseType()) {
				case MANSION:
				case ESTATE:
				case PALACE:
					return;
			}

		if (!player.hasPermission(MembershipConfig.DISABLE_SOULSICKNESS)) {
			int deathCount = player.getCommonData().getDeathCount();
			if (deathCount < 10) {
				deathCount++;
				player.getCommonData().setDeathCount(deathCount);
			}

			if (skillId == 0)
				skillId = 8291;
			SkillEngine.getInstance().getSkill(player, skillId, deathCount, player).useSkill();
		}
	}

	/**
	 * Player is considered in combat if he's been attacked or has attacked less or equal 10s before
	 * 
	 * @return true if the player is actively in combat
	 */
	public boolean isInCombat() {
		return System.currentTimeMillis() - getLastCombatTime() <= 10000;
	}

	/**
	 * @return The last time, when the player attacked someone or got attacked
	 */
	public long getLastCombatTime() {
		return Math.max(lastAttackedMillis, lastAttackMillis);
	}

}
