/*
 * 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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
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.data.sql.ClanTable;
import org.l2jmobius.gameserver.entity.Location;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.instance.Artefact;
import org.l2jmobius.gameserver.entity.actor.instance.Door;
import org.l2jmobius.gameserver.entity.clan.Clan;
import org.l2jmobius.gameserver.geoengine.GeoEngine;
import org.l2jmobius.gameserver.mechanics.siege.Castle;
import org.l2jmobius.gameserver.mechanics.siege.Siege;
import org.l2jmobius.gameserver.mechanics.siege.SiegeClan;
import org.l2jmobius.gameserver.mechanics.skill.CommonSkill;
import org.l2jmobius.gameserver.mechanics.skill.Skill;

/**
 * Castle sieges played by the bot clans with the real siege engine:
 * <ul>
 * <li>while registration is open, bot clans with enough high level members online register as attackers of
 * castles owned by bot clans (and, when allowed, of castles owned by real players);</li>
 * <li>when the siege starts, attackers gather outside the outer gate and defenders inside by the holy artifact;</li>
 * <li>attackers fight defenders, break the gates, walk to the artifact and their clan leader engraves it with Seal
 * of Ruler (3 minutes of casting the others have to cover); defenders hold the artifact and hunt attackers;</li>
 * <li>battle cries at the start, at the gate, on engraving, and win / lose lines at the end; the castle really
 * changes hands because the engine does the engraving, not us.</li>
 * </ul>
 * Everything runs on the real siege schedule (SiegeSchedule.xml); do=siege&arg=Giran forces one right now.
 * @author Mobius SPP
 */
public class FakePlayerSiegeManager
{
	private static final Logger LOGGER = Logger.getLogger(FakePlayerSiegeManager.class.getName());

	private static final long TICK = 10000;
	private static final int MAX_PER_CLAN = 12;
	private static final int MIN_LEVEL = 40;
	private static final int MIN_MEMBERS = 3;

	private static class Battle
	{
		Siege siege;
		Castle castle;
		int firstOwnerId;
		Location inner; // By the artifact.
		Location staging; // Outside the outer gate.
		final Set<Integer> attackerClans = new HashSet<>();
		final Set<Integer> defenderClans = new HashSet<>();
		final List<Player> participants = new ArrayList<>();
		final Map<Integer, Long> lastCry = new HashMap<>();
		boolean engraveAnnounced;
	}

	private final Map<Integer, Battle> _battles = new ConcurrentHashMap<>();
	private final Map<Integer, Long> _lastRegisterTry = new ConcurrentHashMap<>();

	protected FakePlayerSiegeManager()
	{
		if (!FakePlayersConfig.FAKE_PLAYERS_ENABLED || !FakePlayersConfig.FAKE_PLAYER_SIEGES || (FakePlayersConfig.FAKE_PLAYER_HEADLESS_COUNT <= 0))
		{
			LOGGER.info(getClass().getSimpleName() + ": Disabled.");
			return;
		}
		ThreadPool.scheduleAtFixedRate(this::tick, 60000, TICK);
		LOGGER.info(getClass().getSimpleName() + ": Bot clans will register for castle sieges (" + FakePlayersConfig.FAKE_PLAYER_SIEGE_ATTACKER_CLANS + " attacker clans per castle).");
	}

	private void tick()
	{
		try
		{
			for (Siege siege : SiegeManager.getInstance().getSieges())
			{
				final int castleId = siege.getCastle().getResidenceId();
				final Battle battle = _battles.get(castleId);
				if (siege.isInProgress())
				{
					if (battle == null)
					{
						onSiegeStarted(siege);
					}
				}
				else
				{
					if (battle != null)
					{
						onSiegeEnded(battle);
					}
					else
					{
						maybeRegister(siege);
					}
				}
			}
		}
		catch (Exception e)
		{
			LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": tick failed.", e);
		}
	}

	// ======================== Registration ========================

	private static boolean isBotClan(int clanId)
	{
		return (clanId >= 300000001) && (clanId <= 300000999);
	}

	/**
	 * Registers bot clans as attackers while registration is open. Retried every 10 minutes.
	 * @param siege the siege.
	 */
	private void maybeRegister(Siege siege)
	{
		final int castleId = siege.getCastle().getResidenceId();
		final long now = System.currentTimeMillis();
		final Long last = _lastRegisterTry.get(castleId);
		if ((last != null) && ((now - last) < 600000))
		{
			return;
		}
		_lastRegisterTry.put(castleId, now);
		if (siege.isRegistrationOver())
		{
			return;
		}
		final int ownerId = siege.getCastle().getOwnerId();
		if ((ownerId > 0) && !isBotClan(ownerId) && !FakePlayersConfig.FAKE_PLAYER_SIEGE_ATTACK_PLAYER_CASTLES)
		{
			return; // A real clan's castle stays in peace unless allowed.
		}
		int registered = 0;
		for (SiegeClan siegeClan : siege.getAttackerClans())
		{
			if (isBotClan(siegeClan.getClanId()))
			{
				registered++;
			}
		}
		final int wanted = FakePlayersConfig.FAKE_PLAYER_SIEGE_ATTACKER_CLANS - registered;
		if (wanted <= 0)
		{
			return;
		}
		// Candidate clans: enough strong members online, no castle, not registered anywhere else that day.
		final Map<Integer, List<Player>> byClan = membersByClan();
		final List<Integer> candidates = new ArrayList<>();
		for (Map.Entry<Integer, List<Player>> entry : byClan.entrySet())
		{
			final int clanId = entry.getKey();
			final Clan clan = ClanTable.getInstance().getClan(clanId);
			if ((clan == null) || (clanId == ownerId) || (clan.getCastleId() > 0) || (entry.getValue().size() < MIN_MEMBERS))
			{
				continue;
			}
			if ((ownerId > 0) && (clan.getAllyId() != 0))
			{
				final Clan owner = ClanTable.getInstance().getClan(ownerId);
				if ((owner != null) && (owner.getAllyId() == clan.getAllyId()))
				{
					continue; // Allies do not attack each other.
				}
			}
			if (SiegeManager.getInstance().checkIsRegistered(clan, castleId) || siege.checkIfAlreadyRegisteredForSameDay(clan))
			{
				continue;
			}
			candidates.add(clanId);
		}
		if (candidates.isEmpty())
		{
			return;
		}
		// Deterministic per castle and siege date, so the same clans keep coming back to "their" castle.
		final long seed = castleId * 31L + (siege.getSiegeDate().getTimeInMillis() / 86400000L);
		candidates.sort((a, b) -> Integer.compare(FakePlayerIdiolect.hash32(seed + ":" + a), FakePlayerIdiolect.hash32(seed + ":" + b)));
		int done = 0;
		for (int clanId : candidates)
		{
			if (done >= wanted)
			{
				break;
			}
			final Player member = byClan.get(clanId).get(0);
			siege.registerAttacker(member, true);
			if (SiegeManager.getInstance().checkIsRegistered(member.getClan(), castleId))
			{
				done++;
				final Clan clan = ClanTable.getInstance().getClan(clanId);
				FakePlayerHeadlessManager.getInstance().ensureOnline(clan.getLeaderId());
				LOGGER.info(getClass().getSimpleName() + ": " + clan.getName() + " registered to attack " + siege.getCastle().getName() + " (siege " + siege.getSiegeDate().getTime() + ").");
			}
		}
		if ((done > 0) && (ownerId > 0) && isBotClan(ownerId))
		{
			// The defending lord should be there too.
			final Clan owner = ClanTable.getInstance().getClan(ownerId);
			if (owner != null)
			{
				FakePlayerHeadlessManager.getInstance().ensureOnline(owner.getLeaderId());
			}
		}
	}

	private Map<Integer, List<Player>> membersByClan()
	{
		final Map<Integer, List<Player>> byClan = new HashMap<>();
		for (Player bot : FakePlayerHeadlessManager.getInstance().bots())
		{
			if ((bot.getClanId() > 0) && isBotClan(bot.getClanId()) && (bot.getLevel() >= MIN_LEVEL) && !bot.isDead())
			{
				byClan.computeIfAbsent(bot.getClanId(), k -> new ArrayList<>()).add(bot);
			}
		}
		return byClan;
	}

	// ======================== Battle ========================

	private void onSiegeStarted(Siege siege)
	{
		final Castle castle = siege.getCastle();
		final Battle battle = new Battle();
		battle.siege = siege;
		battle.castle = castle;
		battle.firstOwnerId = castle.getOwnerId();
		for (SiegeClan siegeClan : siege.getAttackerClans())
		{
			if (isBotClan(siegeClan.getClanId()))
			{
				battle.attackerClans.add(siegeClan.getClanId());
			}
		}
		for (SiegeClan siegeClan : siege.getDefenderClans())
		{
			if (isBotClan(siegeClan.getClanId()))
			{
				battle.defenderClans.add(siegeClan.getClanId());
			}
		}
		if (isBotClan(castle.getOwnerId()))
		{
			battle.defenderClans.add(castle.getOwnerId());
		}
		if (battle.attackerClans.isEmpty() && battle.defenderClans.isEmpty())
		{
			return; // Not our siege.
		}
		if (!computeLocations(battle))
		{
			LOGGER.warning(getClass().getSimpleName() + ": " + castle.getName() + " has no artifact or gates loaded, bots skip this siege.");
			return;
		}
		_battles.put(castle.getResidenceId(), battle);

		// Participants: online headless members of the registered clans.
		final Map<Integer, Integer> perClan = new HashMap<>();
		for (Player bot : FakePlayerHeadlessManager.getInstance().bots())
		{
			final int clanId = bot.getClanId();
			final boolean attacker = battle.attackerClans.contains(clanId);
			final boolean defender = battle.defenderClans.contains(clanId);
			if ((!attacker && !defender) || (bot.getLevel() < (MIN_LEVEL - 10)))
			{
				continue;
			}
			final int count = perClan.getOrDefault(clanId, 0);
			if ((count >= MAX_PER_CLAN) && !bot.isClanLeader())
			{
				continue;
			}
			perClan.put(clanId, count + 1);
			final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot);
			if (state == null)
			{
				continue;
			}
			state.siegeSide = attacker ? (byte) 1 : (byte) 2;
			state.siegeCastleId = castle.getResidenceId();
			state.siegeNextThink = 0;
			state.siegesJoined.incrementAndGet();
			state.action((attacker ? "siege attack " : "siege defend ") + castle.getName());
			bot.setSiegeState(state.siegeSide);
			FakePlayerBotContext.setPlan(state.seedId, "siege", 3 * 3600000L);
			FakePlayerBotContext.setSlot(state.seedId, "castle", castle.getName());
			battle.participants.add(bot);
			// Leave the farm party, close the shop, go to the castle in small waves.
			if ((bot.getParty() != null) && (FakePlayerSocial.humanPartyAnchor(bot) == null))
			{
				bot.leaveParty();
			}
			FakePlayerHeadlessManager.getInstance().closeStore(bot);
			final Location where = attacker ? battle.staging : battle.inner;
			final int delay = 2000 + (battle.participants.size() * 700);
			ThreadPool.schedule(() ->
			{
				if (!bot.isDead())
				{
					FakePlayerHeadlessManager.getInstance().teleport(bot, where.getX() + Rnd.get(-150, 150), where.getY() + Rnd.get(-150, 150), where.getZ());
				}
			}, delay);
		}
		// One battle cry per clan.
		final Set<Integer> cried = new HashSet<>();
		for (Player bot : battle.participants)
		{
			if (cried.add(bot.getClanId()))
			{
				final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot);
				final String seam = (state.siegeSide == 1) ? "siege_start" : "siege_defend";
				ThreadPool.schedule(() -> cry(bot, seam), 6000 + Rnd.get(4000));
			}
		}
		LOGGER.info(getClass().getSimpleName() + ": Siege of " + castle.getName() + " - " + battle.participants.size() + " bots joined (attacker clans " + battle.attackerClans + ", defender clans " + battle.defenderClans + ").");
	}

	/**
	 * @param battle the battle.
	 * @return true when the castle has enough geometry (artifact / gates) to play on.
	 */
	private boolean computeLocations(Battle battle)
	{
		final Castle castle = battle.castle;
		Location artefact = null;
		for (Artefact art : castle.getArtefacts())
		{
			artefact = art.getLocation();
			break;
		}
		Door outer = null;
		double best = -1;
		for (Door door : castle.getDoors())
		{
			if (!door.isShowHp())
			{
				continue;
			}
			final double distance = (artefact != null) ? door.calculateDistance2D(artefact) : 0;
			if (distance > best)
			{
				best = distance;
				outer = door;
			}
		}
		if ((artefact == null) && (outer == null))
		{
			return false;
		}
		battle.inner = (artefact != null) ? new Location(artefact.getX(), artefact.getY(), artefact.getZ()) : outer.getLocation();
		if ((outer != null) && (artefact != null))
		{
			final double dx = outer.getX() - artefact.getX();
			final double dy = outer.getY() - artefact.getY();
			final double length = Math.max(1, Math.sqrt((dx * dx) + (dy * dy)));
			final int x = outer.getX() + (int) ((dx / length) * 900);
			final int y = outer.getY() + (int) ((dy / length) * 900);
			int z = outer.getZ();
			try
			{
				z = GeoEngine.getInstance().getHeight(x, y, z);
			}
			catch (Exception e)
			{
				// Keep the door height.
			}
			battle.staging = new Location(x, y, z);
		}
		else
		{
			battle.staging = battle.inner;
		}
		return true;
	}

	/**
	 * Called every headless tick for a bot taking part in a siege.
	 * @param bot the bot.
	 * @param state its state.
	 * @param now current time.
	 * @return true when the siege logic took the tick.
	 */
	public boolean handle(Player bot, FakePlayerBotState state, long now)
	{
		final Battle battle = _battles.get(state.siegeCastleId);
		if ((battle == null) || !battle.siege.isInProgress())
		{
			release(bot, state, battle);
			return false;
		}
		if (now < state.siegeNextThink)
		{
			return bot.isInCombat() || bot.isAttackingNow() || bot.isCastingNow();
		}
		state.siegeNextThink = now + 3000 + Rnd.get(2000);
		if (bot.isCastingNow())
		{
			return true; // Engraving.
		}
		final boolean attacker = state.siegeSide == 1;
		final Location post = attacker ? battle.staging : battle.inner;
		if (bot.calculateDistance2D(post) > 4000)
		{
			// Came back from the village after dying: rejoin the battle.
			FakePlayerHeadlessManager.getInstance().teleport(bot, post.getX() + Rnd.get(-150, 150), post.getY() + Rnd.get(-150, 150), post.getZ());
			return true;
		}

		// Enemies first: players of the other side inside / around the castle.
		final byte enemySide = attacker ? (byte) 2 : (byte) 1;
		final int ownerId = battle.castle.getOwnerId();
		final Player enemy = World.getNearestVisibleObjectInRange(bot, Player.class, attacker ? 900 : 1200, other -> (other != bot) && !other.isDead() && !other.isInvisible() && ((other.getSiegeState() == enemySide) || (attacker && (other.getClanId() == ownerId) && (ownerId > 0))));
		if (enemy != null)
		{
			if ((bot.getTarget() != enemy) || !bot.isAttackingNow())
			{
				bot.setTarget(enemy);
				bot.getAI().setIntentionAttack(enemy);
			}
			state.action("siege fight " + enemy.getName());
			return true;
		}

		if (attacker)
		{
			// The leader engraves once the artifact is reachable.
			Artefact artefact = null;
			for (Artefact art : battle.castle.getArtefacts())
			{
				if ((artefact == null) || (bot.calculateDistance2D(art) < bot.calculateDistance2D(artefact)))
				{
					artefact = art;
				}
			}
			if ((artefact != null) && bot.isClanLeader() && (bot.calculateDistance2D(artefact) < 150) && (Math.abs(bot.getZ() - artefact.getZ()) < 45))
			{
				final Skill seal = bot.getKnownSkill(CommonSkill.SEAL_OF_RULER.getId());
				if (seal != null)
				{
					bot.setTarget(artefact);
					if (bot.useMagic(seal, false, false))
					{
						state.action("siege engrave");
						if (!battle.engraveAnnounced)
						{
							battle.engraveAnnounced = true;
							cry(bot, "siege_engrave");
						}
						return true;
					}
				}
			}
			// Break the nearest closed gate on the way in.
			Door gate = null;
			for (Door door : battle.castle.getDoors())
			{
				if (door.isOpen() || door.isDead() || !door.isAutoAttackable(bot))
				{
					continue;
				}
				if ((gate == null) || (bot.calculateDistance2D(door) < bot.calculateDistance2D(gate)))
				{
					gate = door;
				}
			}
			if ((gate != null) && (bot.calculateDistance2D(gate) < 1200))
			{
				if ((bot.getTarget() != gate) || !bot.isAttackingNow())
				{
					bot.setTarget(gate);
					bot.getAI().setIntentionAttack(gate);
				}
				state.action("siege gate");
				final Long last = battle.lastCry.get(bot.getClanId());
				if ((last == null) || ((now - last) > 90000))
				{
					battle.lastCry.put(bot.getClanId(), now);
					cry(bot, "siege_gate");
				}
				return true;
			}
			// Otherwise walk towards the artifact (the leader) or escort the leader / hold the yard.
			final Location goal = (artefact != null) ? artefact.getLocation() : battle.inner;
			if (bot.calculateDistance2D(goal) > 250)
			{
				bot.setRunning();
				bot.getAI().setIntentionMoveTo(new Location(goal.getX() + Rnd.get(-120, 120), goal.getY() + Rnd.get(-120, 120), goal.getZ()));
				state.action("siege advance");
			}
			return true;
		}

		// Defender: hold the artifact room.
		if (bot.calculateDistance2D(battle.inner) > 350)
		{
			bot.setRunning();
			bot.getAI().setIntentionMoveTo(new Location(battle.inner.getX() + Rnd.get(-150, 150), battle.inner.getY() + Rnd.get(-150, 150), battle.inner.getZ()));
			state.action("siege hold");
		}
		return true;
	}

	private void onSiegeEnded(Battle battle)
	{
		_battles.remove(battle.castle.getResidenceId());
		final int ownerNow = battle.castle.getOwnerId();
		final Set<Integer> cried = new HashSet<>();
		for (Player bot : battle.participants)
		{
			final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot);
			if (state == null)
			{
				continue;
			}
			final boolean won = (bot.getClanId() == ownerNow) || ((state.siegeSide == 2) && (ownerNow == battle.firstOwnerId) && (bot.getClanId() == battle.firstOwnerId));
			if (cried.add(bot.getClanId()))
			{
				ThreadPool.schedule(() -> cry(bot, won ? "siege_win" : "siege_lose"), 3000 + Rnd.get(5000));
			}
			release(bot, state, battle);
		}
		LOGGER.info(getClass().getSimpleName() + ": Siege of " + battle.castle.getName() + " finished, owner now " + ownerNow + (ownerNow != battle.firstOwnerId ? " (castle changed hands!)" : "") + ".");
	}

	private void release(Player bot, FakePlayerBotState state, Battle battle)
	{
		if (state.siegeSide == 0)
		{
			return;
		}
		state.siegeSide = 0;
		state.siegeCastleId = 0;
		FakePlayerBotContext.setPlan(state.seedId, "farm", 0);
		FakePlayerBotContext.setEvent(state.seedId, "siege_finished");
		state.action("siege over");
		// Back to the farming spot after a while (the engine already sends everybody to town).
		ThreadPool.schedule(() ->
		{
			if (!bot.isDead() && (FakePlayerHeadlessManager.getInstance().stateOf(bot) != null) && (FakePlayerHeadlessManager.getInstance().stateOf(bot).siegeSide == 0))
			{
				FakePlayerHeadlessManager.getInstance().returnToSpot(bot);
			}
		}, 60000 + Rnd.get(120000));
	}

	private void cry(Player bot, String seam)
	{
		try
		{
			if (bot.isDead())
			{
				return;
			}
			final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot);
			final Map<String, String> slots = new HashMap<>(2);
			slots.put("castle", (state != null) && (state.siegeCastleId > 0) && (CastleManager.getInstance().getCastleById(state.siegeCastleId) != null) ? CastleManager.getInstance().getCastleById(state.siegeCastleId).getName() : "замок");
			final Clan clan = bot.getClan();
			if (clan != null)
			{
				slots.put("clan", clan.getName());
			}
			final String line = FakePlayerUnderstanding.eventLine(FakePlayerBotRef.ofHeadless(bot), seam, null, slots);
			if (line != null)
			{
				FakePlayerHeadlessManager.getInstance().shout(bot, line, seam);
			}
		}
		catch (Exception e)
		{
			// Silence is fine.
		}
	}

	/**
	 * @param state a bot state.
	 * @return true while the bot is fighting a siege.
	 */
	public boolean isBusy(FakePlayerBotState state)
	{
		return state.siegeSide != 0;
	}

	// ======================== Debug ========================

	/**
	 * Forces a siege right now: registers bot clans and starts the engine's siege.
	 * @param castleName castle name or id.
	 * @return result text.
	 */
	public String forceSiege(String castleName)
	{
		final Castle castle = findCastle(castleName);
		if (castle == null)
		{
			return "castle not found: " + castleName;
		}
		final Siege siege = castle.getSiege();
		if (siege.isInProgress())
		{
			return castle.getName() + ": siege already in progress";
		}
		_lastRegisterTry.remove(castle.getResidenceId());
		final boolean allowPlayers = FakePlayersConfig.FAKE_PLAYER_SIEGE_ATTACK_PLAYER_CASTLES;
		FakePlayersConfig.FAKE_PLAYER_SIEGE_ATTACK_PLAYER_CASTLES = true;
		try
		{
			maybeRegister(siege);
		}
		finally
		{
			FakePlayersConfig.FAKE_PLAYER_SIEGE_ATTACK_PLAYER_CASTLES = allowPlayers;
		}
		int botAttackers = 0;
		for (SiegeClan siegeClan : siege.getAttackerClans())
		{
			if (isBotClan(siegeClan.getClanId()))
			{
				botAttackers++;
			}
		}
		if (botAttackers == 0)
		{
			return castle.getName() + ": no bot clan could register (need " + MIN_MEMBERS + " online members of level " + MIN_LEVEL + "+ in a clan without a castle)";
		}
		siege.startSiege();
		ThreadPool.schedule(this::tick, 3000);
		return castle.getName() + ": siege started, bot attacker clans: " + botAttackers + " (owner clan " + castle.getOwnerId() + "). Ends in " + SiegeManager.getInstance().getSiegeLength() + " min or do=siegeend";
	}

	/**
	 * Ends a forced siege.
	 * @param castleName castle name or id.
	 * @return result text.
	 */
	public String endSiege(String castleName)
	{
		final Castle castle = findCastle(castleName);
		if (castle == null)
		{
			return "castle not found: " + castleName;
		}
		if (!castle.getSiege().isInProgress())
		{
			return castle.getName() + ": no siege in progress";
		}
		castle.getSiege().endSiege();
		ThreadPool.schedule(this::tick, 2000);
		return castle.getName() + ": siege ended, owner " + castle.getOwnerId();
	}

	/**
	 * @return status of all sieges and bot registrations.
	 */
	public String info()
	{
		final StringBuilder sb = new StringBuilder(512);
		for (Siege siege : SiegeManager.getInstance().getSieges())
		{
			final Castle castle = siege.getCastle();
			sb.append(castle.getResidenceId()).append(' ').append(castle.getName()).append(": owner ").append(castle.getOwnerId());
			sb.append(siege.isInProgress() ? " IN PROGRESS" : (" next " + siege.getSiegeDate().getTime()));
			sb.append(", attackers ");
			for (SiegeClan siegeClan : siege.getAttackerClans())
			{
				final Clan clan = ClanTable.getInstance().getClan(siegeClan.getClanId());
				sb.append((clan != null) ? clan.getName() : siegeClan.getClanId()).append(' ');
			}
			final Battle battle = _battles.get(castle.getResidenceId());
			if (battle != null)
			{
				sb.append("| bots in battle: ").append(battle.participants.size());
			}
			sb.append('\n');
		}
		return sb.toString();
	}

	private static Castle findCastle(String name)
	{
		for (Castle castle : CastleManager.getInstance().getCastles())
		{
			if (castle.getName().equalsIgnoreCase(name) || String.valueOf(castle.getResidenceId()).equals(name))
			{
				return castle;
			}
		}
		return null;
	}

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

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