/*
 * Copyright (c) 2013 L2jMobius
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.l2jmobius.gameserver.managers;

import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.gameserver.entity.Location;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Attackable;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.instance.Monster;
import org.l2jmobius.gameserver.entity.groups.Party;
import org.l2jmobius.gameserver.entity.item.enums.ItemProcessType;
import org.l2jmobius.gameserver.entity.item.instance.Item;
import org.l2jmobius.gameserver.managers.FakePlayerCombatData.BotSkill;
import org.l2jmobius.gameserver.managers.FakePlayerCombatData.Profile;
import org.l2jmobius.gameserver.mechanics.skill.Skill;

/**
 * Game sense for headless bots: what to do right now.
 * Priority order (first match wins), the way real farming bots are built:
 * survival, upkeep (rest / self buffs), party role, combat rotation, sweeping,
 * loot, farming (target choice with a leash to the spot).
 * @author Mobius SPP
 */
public class FakePlayerBrain
{
	private static final int HEAL_POTION = 1061;
	private static final int GREATER_HEAL_POTION = 1539;
	private static final int SPOIL = 254;
	private static final int SWEEPER = 42;
	private static final int MELEE_RANGE = 80;
	private static final int BOW_RANGE = 500;
	private static final int PREY_RANGE = 1100;
	private static final int LOOT_RANGE = 300;
	private static final int LEASH = 1500; // Wander this far from the spot before walking back.
	private static final int LEASH_TELEPORT = 7000; // Too far to walk (fell off a cliff, chased someone) - teleport back.

	/**
	 * Runtime combat memory of a bot.
	 */
	public static class CombatState
	{
		public long nextSelfBuff = 0;
		public long restingUntil = 0;
		public long lastPotion = 0;
		public long fleeingUntil = 0;
		public int sweepCorpseId = 0;
		public long nextLeashCheck = 0;
		public long stuckSince = 0;
	}

	/**
	 * Tries to act. Returns true when the brain took control this tick.
	 * @param bot the headless bot.
	 * @param botState its state.
	 * @param hunts true when the bot may pick monsters on its own.
	 * @return true when an action was taken.
	 */
	public static boolean act(Player bot, FakePlayerBotState botState, boolean hunts)
	{
		final long now = System.currentTimeMillis();
		final CombatState state = botState.combat;
		final Profile profile = FakePlayerCombatData.getInstance().getProfile(bot.getPlayerClass().getId());
		final int hpPercent = (int) ((bot.getCurrentHp() * 100) / Math.max(1, bot.getMaxHp()));
		final int mpPercent = (int) ((bot.getCurrentMp() * 100) / Math.max(1, bot.getMaxMp()));

		// 1. Survival: heal, drink, run away.
		if (hpPercent < 55)
		{
			if (castHeal(bot, bot, profile, hpPercent, botState))
			{
				return true;
			}
			if ((hpPercent < 45) && ((now - state.lastPotion) > 8000) && drinkPotion(bot))
			{
				state.lastPotion = now;
				botState.action("potion");
				return true;
			}
			if (hpPercent < 25)
			{
				final Creature threat = nearestThreat(bot);
				if ((threat != null) && (threat.isMonster() || (threat.getLevel() > (bot.getLevel() + 3))))
				{
					state.fleeingUntil = now + 12000;
					botState.action("flee");
					flee(bot, threat);
					return true;
				}
			}
		}
		if (now < state.fleeingUntil)
		{
			return true; // Keep running.
		}

		// 2. Upkeep: rest when safe and low, refresh self buffs.
		if (state.restingUntil > now)
		{
			if ((hpPercent > 85) && (mpPercent > 75))
			{
				state.restingUntil = 0;
				bot.standUp();
			}
			else if (nearestThreat(bot) != null)
			{
				state.restingUntil = 0;
				bot.standUp();
				return false;
			}
			return true;
		}
		if (!bot.isInCombat() && !bot.isInStoreMode() && (nearestThreat(bot) == null) && ((hpPercent < 45) || (mpPercent < 35)))
		{
			state.restingUntil = now + 120000;
			botState.rests.incrementAndGet();
			botState.action("rest");
			bot.sitDown();
			return true;
		}
		if ((now > state.nextSelfBuff) && !bot.isInCombat())
		{
			state.nextSelfBuff = now + 180000;
			if (castSelfBuffs(bot, profile, botState))
			{
				return true;
			}
		}

		// 3. Party role: healers keep the party alive.
		final Party party = bot.getParty();
		if ((party != null) && "healer".equals(profile.role))
		{
			Player wounded = null;
			int worst = 100;
			for (Player member : party.getMembers())
			{
				final int memberHp = (int) ((member.getCurrentHp() * 100) / Math.max(1, member.getMaxHp()));
				if (!member.isDead() && (memberHp < worst) && (bot.calculateDistance2D(member) < 900))
				{
					worst = memberHp;
					wounded = member;
				}
			}
			if ((wounded != null) && (worst < 75) && castHeal(bot, wounded, profile, worst, botState))
			{
				return true;
			}
		}

		// 4. Combat: use skills by condition, keep proper distance.
		final WorldObject target = bot.getTarget();
		if ((target instanceof Creature victim) && !victim.isDead() && (bot.isInCombat() || bot.isAttackingNow() || (victim.getTarget() == bot)))
		{
			final int targetHp = (int) ((victim.getCurrentHp() * 100) / Math.max(1, victim.getMaxHp()));
			if ("spoiler".equals(profile.role) && (victim instanceof Attackable attackable) && !attackable.isSpoiled() && (targetHp > 30) && castSkill(bot, victim, SPOIL, botState))
			{
				return true;
			}
			if ((victim instanceof Attackable attackable) && attackable.isSpoiled() && (attackable.getSpoilerObjectId() == bot.getObjectId()))
			{
				state.sweepCorpseId = victim.getObjectId();
			}
			if (castOffensive(bot, victim, profile, targetHp, mpPercent, botState))
			{
				return true;
			}
			keepDistance(bot, victim, profile);
			return true;
		}

		// 4b. Under attack by something we are not fighting: fight back (or run from a monster far above us).
		final Creature aggressor = nearestThreat(bot);
		if ((aggressor != null) && (aggressor != target))
		{
			if (aggressor.isMonster() && (aggressor.getLevel() > (bot.getLevel() + 7)) && (hpPercent < 70))
			{
				state.fleeingUntil = now + 10000;
				botState.action("flee " + aggressor.getName());
				flee(bot, aggressor);
				return true;
			}
			bot.setTarget(aggressor);
			bot.getAI().setIntentionAttack(aggressor);
			botState.action("fight back " + aggressor.getName());
			return true;
		}

		// 5. Sweep the spoiled corpse before it decays.
		if (state.sweepCorpseId != 0)
		{
			final WorldObject corpse = World.findObject(state.sweepCorpseId);
			state.sweepCorpseId = 0;
			if ((corpse instanceof Attackable attackable) && attackable.isDead() && attackable.isSweepActive() && (bot.calculateDistance2D(corpse) < 400))
			{
				if (castSkill(bot, attackable, SWEEPER, botState))
				{
					botState.action("sweep");
					return true;
				}
			}
		}

		// 6. Loot: pick up what dropped from our kills (a real inventory, a real store later).
		if (!bot.isInCombat() && (bot.getInventory().getSize() < 70))
		{
			final Item loot = World.getNearestVisibleObjectInRange(bot, Item.class, LOOT_RANGE, item -> item.isSpawned() && ((item.getOwnerId() == 0) || (item.getOwnerId() == bot.getObjectId()) || ((party != null) && (World.findObject(item.getOwnerId()) instanceof Player owner) && party.getMembers().contains(owner))));
			if (loot != null)
			{
				if (bot.calculateDistance2D(loot) > 40)
				{
					bot.getAI().setIntentionMoveTo(loot.getLocation());
				}
				else
				{
					bot.doPickupItem(loot);
					FakePlayerHeadlessManager.getInstance().onLootPicked(bot, loot);
					botState.action("loot " + loot.getName());
				}
				return true;
			}
		}

		// 7. Farming: pick a sensible monster, stay on the spot. Only when fit for a fight.
		if (hunts && !bot.isInCombat())
		{
			if ((hpPercent < 60) || (mpPercent < 25))
			{
				if (nearestThreat(bot) == null)
				{
					state.restingUntil = now + 90000;
					botState.rests.incrementAndGet();
					botState.action("rest");
					bot.sitDown();
				}
				return true;
			}
			final Monster prey = pickPrey(bot);
			if (prey != null)
			{
				bot.setTarget(prey);
				bot.setRunning();
				bot.getAI().setIntentionAttack(prey);
				botState.action("hunt " + prey.getName());
				return true;
			}
			// Nothing worth fighting here: walk back to the spot if we drifted (or teleport if far away).
			if ((botState.spot != null) && (now > state.nextLeashCheck))
			{
				state.nextLeashCheck = now + 15000;
				final double distance = bot.calculateDistance2D(botState.spot);
				if (distance > LEASH_TELEPORT)
				{
					FakePlayerHeadlessManager.getInstance().teleport(bot, botState.spot.getX() + Rnd.get(-300, 300), botState.spot.getY() + Rnd.get(-300, 300), botState.spot.getZ());
					botState.action("back to spot (tp)");
					return true;
				}
				if (distance > LEASH)
				{
					bot.setRunning();
					bot.getAI().setIntentionMoveTo(new Location(botState.spot.getX() + Rnd.get(-300, 300), botState.spot.getY() + Rnd.get(-300, 300), botState.spot.getZ()));
					botState.action("back to spot");
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Monster worth attacking: close to our level, reachable, not a raid, nobody else's.
	 * @param bot the bot.
	 * @return the prey or null.
	 */
	private static Monster pickPrey(Player bot)
	{
		final int level = bot.getLevel();
		final java.util.List<Monster> candidates = World.getVisibleObjectsInRange(bot, Monster.class, PREY_RANGE, monster -> !monster.isDead() && !monster.isRaid() && !monster.isRaidMinion() && !monster.isFakePlayer() //
			&& (monster.getLevel() <= (level + 1)) && (monster.getLevel() >= (level - 9)) // не бьём слишком сильных и не тратим время на серых
			&& (Math.abs(monster.getZ() - bot.getZ()) < 200) // не пытаемся достать тех, кто выше/ниже
			&& ((monster.getTarget() == null) || (monster.getTarget() == bot) || ((bot.getParty() != null) && bot.getParty().getMembers().contains(monster.getTarget())))); // не воруем чужую цель
		if (candidates.isEmpty())
		{
			return null;
		}
		// A player pulls the lonely one, not the one in the middle of a pack: score = distance + 350 per neighbour.
		Monster best = null;
		double bestScore = Double.MAX_VALUE;
		for (Monster monster : candidates)
		{
			int neighbours = 0;
			for (Monster other : candidates)
			{
				if ((other != monster) && (other.calculateDistance2D(monster) < 350))
				{
					neighbours++;
				}
			}
			final double score = bot.calculateDistance2D(monster) + (neighbours * 350) + ((monster.getLevel() > level) ? 200 : 0);
			if (score < bestScore)
			{
				bestScore = score;
				best = monster;
			}
		}
		return best;
	}

	private static Creature nearestThreat(Player bot)
	{
		return World.getNearestVisibleObjectInRange(bot, Creature.class, 600, creature -> !creature.isDead() && (creature.getTarget() == bot) && (creature.isMonster() || creature.isPlayer()) && creature.isInCombat());
	}

	private static void flee(Player bot, Creature threat)
	{
		final int dx = bot.getX() - threat.getX();
		final int dy = bot.getY() - threat.getY();
		final double length = Math.max(1, Math.sqrt((dx * dx) + (dy * dy)));
		final int targetX = bot.getX() + (int) ((dx / length) * 900);
		final int targetY = bot.getY() + (int) ((dy / length) * 900);
		bot.setRunning();
		bot.getAI().setIntentionMoveTo(new Location(targetX, targetY, bot.getZ()));
	}

	private static boolean drinkPotion(Player bot)
	{
		Item potion = bot.getInventory().getItemByItemId(GREATER_HEAL_POTION);
		if (potion == null)
		{
			potion = bot.getInventory().getItemByItemId(HEAL_POTION);
		}
		if (potion == null)
		{
			// Out of potions in the field: the cheapest one is always in a real player's bag.
			bot.addItem(ItemProcessType.NONE, HEAL_POTION, 5, bot, false);
			potion = bot.getInventory().getItemByItemId(HEAL_POTION);
		}
		if (potion == null)
		{
			return false;
		}
		// Potions are used through their item handler, like a click in the inventory.
		final org.l2jmobius.gameserver.handler.IItemHandler handler = org.l2jmobius.gameserver.handler.ItemHandler.getInstance().getHandler(potion.getEtcItem());
		if (handler == null)
		{
			return false;
		}
		return handler.onItemUse(bot, potion, false);
	}

	private static boolean castSelfBuffs(Player bot, Profile profile, FakePlayerBotState botState)
	{
		for (BotSkill entry : profile.self)
		{
			final Skill skill = bot.getKnownSkill(entry.id);
			if ((skill == null) || bot.isSkillDisabled(skill) || (bot.getCurrentMp() < skill.getMpConsume()))
			{
				continue;
			}
			if (bot.isAffectedBySkill(entry.id))
			{
				continue;
			}
			bot.setTarget(bot);
			if (bot.useMagic(skill, true, false))
			{
				botState.skillsCast.incrementAndGet();
				botState.action("buff");
				return true;
			}
		}
		return false;
	}

	private static boolean castHeal(Player bot, Player patient, Profile profile, int patientHp, FakePlayerBotState botState)
	{
		for (BotSkill entry : profile.heal)
		{
			if (patientHp > entry.minHp)
			{
				continue;
			}
			final Skill skill = bot.getKnownSkill(entry.id);
			if ((skill == null) || bot.isSkillDisabled(skill) || (bot.getCurrentMp() < skill.getMpConsume()))
			{
				continue;
			}
			bot.setTarget(patient);
			if (bot.useMagic(skill, true, false))
			{
				botState.skillsCast.incrementAndGet();
				botState.action((patient == bot) ? "heal self" : ("heal " + patient.getName()));
				return true;
			}
		}
		return false;
	}

	private static boolean castSkill(Player bot, Creature target, int skillId, FakePlayerBotState botState)
	{
		final Skill skill = bot.getKnownSkill(skillId);
		if ((skill == null) || bot.isSkillDisabled(skill) || (bot.getCurrentMp() < skill.getMpConsume()))
		{
			return false;
		}
		if ((skill.getCastRange() > 0) && (bot.calculateDistance2D(target) > skill.getCastRange()))
		{
			return false;
		}
		bot.setTarget(target);
		if (bot.useMagic(skill, true, false))
		{
			botState.skillsCast.incrementAndGet();
			return true;
		}
		return false;
	}

	private static boolean castOffensive(Player bot, Creature victim, Profile profile, int targetHp, int mpPercent, FakePlayerBotState botState)
	{
		for (BotSkill entry : profile.offensive)
		{
			if ((targetHp > entry.maxTargetHp) || (mpPercent < entry.minMp))
			{
				continue;
			}
			final Skill skill = bot.getKnownSkill(entry.id);
			if ((skill == null) || bot.isSkillDisabled(skill) || (bot.getCurrentMp() < skill.getMpConsume()))
			{
				continue;
			}
			if ((skill.getCastRange() > 0) && (bot.calculateDistance2D(victim) > skill.getCastRange()))
			{
				continue;
			}
			bot.setTarget(victim);
			if (bot.useMagic(skill, true, false))
			{
				botState.skillsCast.incrementAndGet();
				return true;
			}
		}
		return false;
	}

	/**
	 * Archers and mages should not stand in melee; fighters should close in.
	 * @param bot the bot.
	 * @param victim its target.
	 * @param profile combat profile.
	 */
	private static void keepDistance(Player bot, Creature victim, Profile profile)
	{
		// Kiting a monster only trades hits for nothing: mages nuke point blank, archers shoot, fighters close in.
		final boolean ranged = "archer".equals(profile.role) || "mage".equals(profile.role) || "healer".equals(profile.role);
		final double distance = bot.calculateDistance2D(victim);
		if (ranged)
		{
			if ((distance > BOW_RANGE) || (!bot.isAttackingNow() && !bot.isCastingNow()))
			{
				bot.getAI().setIntentionAttack(victim);
			}
			return;
		}
		if ((distance > MELEE_RANGE) || !bot.isAttackingNow())
		{
			bot.getAI().setIntentionAttack(victim);
		}
	}

	/**
	 * @param bot the bot.
	 * @return short human readable description of what the brain would do now.
	 */
	public static String describe(Player bot)
	{
		final Profile profile = FakePlayerCombatData.getInstance().getProfile(bot.getPlayerClass().getId());
		final int hp = (int) ((bot.getCurrentHp() * 100) / Math.max(1, bot.getMaxHp()));
		final int mp = (int) ((bot.getCurrentMp() * 100) / Math.max(1, bot.getMaxMp()));
		final StringBuilder sb = new StringBuilder();
		sb.append(bot.getName()).append(" lvl").append(bot.getLevel()).append(' ').append(bot.getPlayerClass()).append('\n');
		sb.append("  роль: ").append(profile.role).append(" | HP ").append(hp).append("% MP ").append(mp).append("%\n");
		sb.append("  скиллы боя: ");
		for (BotSkill entry : profile.offensive)
		{
			final Skill skill = bot.getKnownSkill(entry.id);
			sb.append(entry.id).append(skill != null ? "+" : "-").append(' ');
		}
		sb.append("\n  лечение: ");
		for (BotSkill entry : profile.heal)
		{
			final Skill skill = bot.getKnownSkill(entry.id);
			sb.append(entry.id).append(skill != null ? "+" : "-").append(' ');
		}
		sb.append("\n  самобафы: ");
		for (BotSkill entry : profile.self)
		{
			final Skill skill = bot.getKnownSkill(entry.id);
			sb.append(entry.id).append(skill != null ? "+" : "-").append(' ');
		}
		final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot);
		if (state != null)
		{
			sb.append("\n  действие: ").append(state.lastAction).append(" | спот: ").append(state.zone).append(" (").append((state.spot != null) ? (int) bot.calculateDistance2D(state.spot) : -1).append(" от центра)");
			final WorldObject target = bot.getTarget();
			sb.append("\n  цель: ").append((target != null) ? target.getName() : "-");
			if (target instanceof Creature creature)
			{
				sb.append(" lvl").append(creature.getLevel()).append(" hp").append((int) (creature.getCurrentHp() * 100 / Math.max(1, creature.getMaxHp()))).append("% dist ").append((int) bot.calculateDistance2D(creature));
			}
			sb.append(" | в бою: ").append(bot.isInCombat()).append(" бьёт: ").append(bot.isAttackingNow()).append(" кастует: ").append(bot.isCastingNow()).append(" сидит: ").append(bot.isSitting());
			final Item weapon = bot.getActiveWeaponInstance();
			sb.append("\n  оружие: ").append((weapon != null) ? weapon.getName() : "кулаки").append(" | автошоты: ").append(bot.getAutoSoulShot());
			for (int shotId : bot.getAutoSoulShot())
			{
				final Item shots = bot.getInventory().getItemByItemId(shotId);
				sb.append(" x").append((shots != null) ? shots.getCount() : 0);
			}
			sb.append(" | бафов: ").append(bot.getEffectList().getBuffCount()).append(" | hp ").append((int) bot.getCurrentHp()).append('/').append(bot.getMaxHp()).append(" p.def ").append((int) bot.getPDef(null)).append(" p.atk ").append((int) bot.getPAtk(null)).append(" m.atk ").append((int) bot.getMAtk(null, null));
		}
		sb.append("\n  (+ есть у бота, - не выучен)");
		return sb.toString();
	}

	private FakePlayerBrain()
	{
	}
}
