/*
 * 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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.gameserver.config.custom.FakePlayersConfig;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Npc;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.instance.RaidBoss;
import org.l2jmobius.gameserver.entity.groups.Party;
import org.l2jmobius.gameserver.entity.groups.PartyDistributionType;

/**
 * Raid boss hunts by headless bots: every 30-90 minutes a real party of 5-9
 * bots close to a live raid boss' level teleports to it, calls out the raid
 * and fights until the boss dies or 25 minutes pass, then everybody goes back
 * to farming. The party is a real L2 party (shared exp, loot rules), the boss
 * is a real raid (drop, raid points), the fight is the ordinary combat brain
 * with the boss forced as target. Grand bosses are never touched.
 * @author Mobius SPP
 */
public class FakePlayerRaidManager
{
	private static final Logger LOGGER = Logger.getLogger(FakePlayerRaidManager.class.getName());

	private static final long TICK = 60000;
	private static final long RAID_LENGTH = 25 * 60000L;
	private static final int PARTY_MIN = 5;
	private static final int PARTY_MAX = 9;

	private long _nextRaidAt = System.currentTimeMillis() + Rnd.get(1800000, 3600000);
	private long _raidEndsAt = 0;
	private int _bossObjectId = 0;
	private String _bossName = "";
	private final List<Player> _party = new ArrayList<>();

	protected FakePlayerRaidManager()
	{
		if (!FakePlayersConfig.FAKE_PLAYERS_ENABLED || !FakePlayersConfig.FAKE_PLAYER_RAIDS || (FakePlayersConfig.FAKE_PLAYER_HEADLESS_COUNT <= 0))
		{
			LOGGER.info(getClass().getSimpleName() + ": Disabled.");
			return;
		}
		ThreadPool.scheduleAtFixedRate(this::tick, TICK, TICK);
		LOGGER.info(getClass().getSimpleName() + ": Raid parties every 30-90 minutes.");
	}

	private void tick()
	{
		try
		{
			final long now = System.currentTimeMillis();
			if (_bossObjectId != 0)
			{
				final WorldObject boss = World.findObject(_bossObjectId);
				final boolean gone = !(boss instanceof Npc bossNpc) || bossNpc.isDead();
				if (gone || (now >= _raidEndsAt))
				{
					finish(gone);
				}
				return;
			}
			if (now >= _nextRaidAt)
			{
				start();
			}
		}
		catch (Exception e)
		{
			LOGGER.warning(getClass().getSimpleName() + ": tick failed: " + e);
		}
	}

	/**
	 * Picks a live raid boss and a party of bots of a matching level, sends them in.
	 * @return result text.
	 */
	public String start()
	{
		final long now = System.currentTimeMillis();
		final List<RaidBoss> bosses = new ArrayList<>();
		for (RaidBoss boss : RaidBossSpawnManager.getInstance().getBosses().values())
		{
			if ((boss != null) && !boss.isDead() && boss.isSpawned() && (boss.getLevel() >= 20) && (boss.getLevel() <= 80))
			{
				bosses.add(boss);
			}
		}
		if (bosses.isEmpty())
		{
			_nextRaidAt = now + 3600000;
			return "no live raid boss";
		}
		// Try a few bosses until one has enough bots of its level online and free.
		for (int attempt = 0; attempt < 6; attempt++)
		{
			final RaidBoss boss = bosses.get(Rnd.get(bosses.size()));
			final List<Player> party = new ArrayList<>();
			for (Player bot : FakePlayerHeadlessManager.getInstance().bots())
			{
				final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot);
				if ((state == null) || bot.isDead() || (state.siegeSide != 0) || (state.tradeUntil > 0) || ((bot.getParty() != null) && (FakePlayerSocial.humanPartyAnchor(bot) != null)) || (Math.abs(bot.getLevel() - boss.getLevel()) > 6) || (bot.getLevel() < (boss.getLevel() - 2)))
				{
					continue;
				}
				party.add(bot);
				if (party.size() >= PARTY_MAX)
				{
					break;
				}
			}
			if (party.size() < PARTY_MIN)
			{
				continue;
			}
			_bossObjectId = boss.getObjectId();
			_bossName = boss.getName();
			_raidEndsAt = now + RAID_LENGTH;
			_party.clear();
			_party.addAll(party);
			for (Player bot : party)
			{
				if (bot.getParty() != null)
				{
					bot.leaveParty(); // Farming squads dissolve into the raid party.
				}
			}
			final Player leader = party.get(0);
			final Party group = new Party(leader, PartyDistributionType.FINDERS_KEEPERS);
			leader.setParty(group);
			int index = 0;
			for (Player bot : party)
			{
				final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot);
				if (bot != leader)
				{
					bot.joinParty(group);
				}
				state.partiesJoined.incrementAndGet();
				state.raidBossId = boss.getObjectId();
				state.action("raid " + boss.getName());
				FakePlayerBotContext.setRaid(state.seedId, boss.getName(), RAID_LENGTH);
				FakePlayerHeadlessManager.getInstance().closeStore(bot);
				final int delay = 1000 + (index++ * 600);
				ThreadPool.schedule(() ->
				{
					if (!bot.isDead())
					{
						FakePlayerHeadlessManager.getInstance().teleport(bot, boss.getX() + Rnd.get(-250, 250), boss.getY() + Rnd.get(-250, 250), boss.getZ());
					}
				}, delay);
			}
			final String cry = FakePlayerUnderstanding.eventLine(FakePlayerBotRef.ofHeadless(leader), "raid_start", null, Map.of("boss", boss.getName()));
			if (cry != null)
			{
				ThreadPool.schedule(() -> FakePlayerHeadlessManager.getInstance().shout(leader, cry, "raid_start"), 6000);
			}
			LOGGER.info(getClass().getSimpleName() + ": Raid started - " + party.size() + " bots vs " + boss.getName() + " (lvl " + boss.getLevel() + ").");
			return "raid: " + party.size() + " bots vs " + boss.getName() + " lvl" + boss.getLevel();
		}
		_nextRaidAt = now + 1800000;
		return "no party of " + PARTY_MIN + "+ free bots for any live boss";
	}

	private void finish(boolean bossDead)
	{
		for (Player bot : _party)
		{
			final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot);
			if (state == null)
			{
				continue;
			}
			state.raidBossId = 0;
			if (bossDead)
			{
				state.raidsWon.incrementAndGet();
			}
			FakePlayerBotContext.setPlan(state.seedId, "farm", 0);
			FakePlayerBotContext.setEvent(state.seedId, "raid_finished");
			state.action(bossDead ? ("raid won " + _bossName) : "raid over");
			if (bot.getParty() != null)
			{
				bot.leaveParty();
			}
			ThreadPool.schedule(() -> FakePlayerHeadlessManager.getInstance().returnToSpot(bot), 15000 + Rnd.get(45000));
		}
		LOGGER.info(getClass().getSimpleName() + ": Raid finished (" + (bossDead ? "boss dead" : "timeout") + ") - " + _bossName + ".");
		_party.clear();
		_bossObjectId = 0;
		_bossName = "";
		_nextRaidAt = System.currentTimeMillis() + Rnd.get(1800000, 5400000);
	}

	/**
	 * @param bot the bot.
	 * @param state its state.
	 * @return true when the bot is on a raid and the raid logic took the tick.
	 */
	public boolean handle(Player bot, FakePlayerBotState state, long now)
	{
		if (state.raidBossId == 0)
		{
			return false;
		}
		final WorldObject boss = World.findObject(state.raidBossId);
		if (!(boss instanceof Npc bossNpc) || bossNpc.isDead() || (_bossObjectId != state.raidBossId))
		{
			state.raidBossId = 0;
			return false;
		}
		if (bot.calculateDistance2D(bossNpc) > 3000)
		{
			// Back from the village after dying: rejoin.
			FakePlayerHeadlessManager.getInstance().teleport(bot, bossNpc.getX() + Rnd.get(-250, 250), bossNpc.getY() + Rnd.get(-250, 250), bossNpc.getZ());
			return true;
		}
		// Survival and healing first, then the boss (the brain handles a targeted creature in combat).
		if ((bot.getTarget() != bossNpc) || (!bot.isAttackingNow() && !bot.isCastingNow()))
		{
			bot.setTarget(bossNpc);
			bot.getAI().setIntentionAttack(bossNpc);
		}
		return FakePlayerBrain.act(bot, state, false) || true;
	}

	public String info()
	{
		if (_bossObjectId != 0)
		{
			return "raid in progress: " + _party.size() + " bots vs " + _bossName + ", ends in " + Math.max(0, (_raidEndsAt - System.currentTimeMillis()) / 60000) + " min";
		}
		return "next raid in " + Math.max(0, (_nextRaidAt - System.currentTimeMillis()) / 60000) + " min";
	}

	public static FakePlayerRaidManager getInstance()
	{
		return SingletonHolder.INSTANCE;
	}

	private static class SingletonHolder
	{
		protected static final FakePlayerRaidManager INSTANCE = new FakePlayerRaidManager();
	}
}
