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

import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.gameserver.config.custom.FakePlayersConfig;
import org.l2jmobius.gameserver.entity.actor.Player;

/**
 * Proof of life for the bot population: every counter a bot increments while
 * it farms, loots, trades, talks and fights is summed here, printed to the
 * server log every few minutes and served by the dashboard (/api/stats,
 * do=stats). If the numbers do not move, the bots are pretending.
 * @author Mobius SPP
 */
public class FakePlayerTelemetry
{
	private static final Logger LOGGER = Logger.getLogger(FakePlayerTelemetry.class.getName());

	private final long _startedAt = System.currentTimeMillis();
	private Map<String, Long> _lastTotals = new LinkedHashMap<>();

	protected FakePlayerTelemetry()
	{
		if (!FakePlayersConfig.FAKE_PLAYERS_ENABLED || (FakePlayersConfig.FAKE_PLAYER_STATS_LOG_MINUTES <= 0))
		{
			return;
		}
		final long interval = FakePlayersConfig.FAKE_PLAYER_STATS_LOG_MINUTES * 60000L;
		ThreadPool.scheduleAtFixedRate(this::logSummary, interval, interval);
	}

	/**
	 * @return population totals in a fixed order.
	 */
	public Map<String, Long> totals()
	{
		final Map<String, Long> totals = new LinkedHashMap<>();
		long kills = 0;
		long exp = 0;
		long deaths = 0;
		long pvp = 0;
		long loot = 0;
		long sold = 0;
		long earned = 0;
		long spent = 0;
		long stores = 0;
		long lines = 0;
		long replies = 0;
		long casts = 0;
		long rests = 0;
		long sieges = 0;
		long raids = 0;
		long migrations = 0;
		long professions = 0;
		long parties = 0;
		int alive = 0;
		int fighting = 0;
		int trading = 0;
		int inParty = 0;
		int sitting = 0;
		int besieging = 0;
		int levels = 0;
		for (Player bot : FakePlayerHeadlessManager.getInstance().bots())
		{
			final FakePlayerBotState state = FakePlayerHeadlessManager.getInstance().stateOf(bot);
			if (state == null)
			{
				continue;
			}
			kills += state.kills.get();
			exp += state.expGained.get();
			deaths += state.deaths.get();
			pvp += state.pvpKills.get();
			loot += state.lootPicked.get();
			sold += state.itemsSold.get();
			earned += state.adenaEarned.get();
			spent += state.adenaSpent.get();
			stores += state.storesOpened.get();
			lines += state.linesSaid.get();
			replies += state.repliesGiven.get();
			casts += state.skillsCast.get();
			rests += state.rests.get();
			sieges += state.siegesJoined.get();
			raids += state.raidsWon.get();
			migrations += state.migrations.get();
			professions += state.professions.get();
			parties += state.partiesJoined.get();
			levels += bot.getLevel();
			if (!bot.isDead())
			{
				alive++;
			}
			if (bot.isInCombat())
			{
				fighting++;
			}
			if (bot.isInStoreMode())
			{
				trading++;
			}
			if (bot.getParty() != null)
			{
				inParty++;
			}
			if (bot.isSitting())
			{
				sitting++;
			}
			if (state.siegeSide != 0)
			{
				besieging++;
			}
		}
		final int count = FakePlayerHeadlessManager.getInstance().size();
		totals.put("bots", (long) count);
		totals.put("alive", (long) alive);
		totals.put("fighting", (long) fighting);
		totals.put("sitting", (long) sitting);
		totals.put("trading", (long) trading);
		totals.put("inParty", (long) inParty);
		totals.put("besieging", (long) besieging);
		totals.put("avgLevel", (count > 0) ? (levels / count) : 0L);
		totals.put("kills", kills);
		totals.put("exp", exp);
		totals.put("pvpKills", pvp);
		totals.put("deaths", deaths);
		totals.put("loot", loot);
		totals.put("itemsSold", sold);
		totals.put("adenaEarned", earned);
		totals.put("adenaSpent", spent);
		totals.put("stores", stores);
		totals.put("skillsCast", casts);
		totals.put("rests", rests);
		totals.put("lines", lines);
		totals.put("replies", replies);
		totals.put("parties", parties);
		totals.put("migrations", migrations);
		totals.put("professions", professions);
		totals.put("sieges", sieges);
		totals.put("raidsWon", raids);
		totals.put("uptimeMin", (System.currentTimeMillis() - _startedAt) / 60000);
		return totals;
	}

	private void logSummary()
	{
		try
		{
			final Map<String, Long> totals = totals();
			final StringBuilder sb = new StringBuilder(256);
			sb.append(getClass().getSimpleName()).append(": last ").append(FakePlayersConfig.FAKE_PLAYER_STATS_LOG_MINUTES).append(" min ->");
			for (Map.Entry<String, Long> entry : totals.entrySet())
			{
				final String key = entry.getKey();
				if (key.equals("bots") || key.equals("alive") || key.equals("fighting") || key.equals("sitting") || key.equals("trading") || key.equals("inParty") || key.equals("besieging") || key.equals("avgLevel") || key.equals("uptimeMin"))
				{
					continue;
				}
				final long delta = entry.getValue() - _lastTotals.getOrDefault(key, 0L);
				sb.append(' ').append(key).append("=+").append(delta);
			}
			sb.append(" | now: alive ").append(totals.get("alive")).append('/').append(totals.get("bots")).append(", fighting ").append(totals.get("fighting")).append(", sitting ").append(totals.get("sitting")).append(", trading ").append(totals.get("trading")).append(", in party ").append(totals.get("inParty")).append(", besieging ").append(totals.get("besieging")).append(", avg lvl ").append(totals.get("avgLevel"));
			LOGGER.info(sb.toString());
			_lastTotals = totals;
		}
		catch (Exception e)
		{
			// Logging must never break anything.
		}
	}

	/**
	 * @return totals as JSON for the dashboard.
	 */
	public String statsJson()
	{
		final StringBuilder sb = new StringBuilder(512);
		sb.append('{');
		boolean first = true;
		for (Map.Entry<String, Long> entry : totals().entrySet())
		{
			if (!first)
			{
				sb.append(',');
			}
			first = false;
			sb.append('"').append(entry.getKey()).append("\":").append(entry.getValue());
		}
		return sb.append('}').toString();
	}

	/**
	 * @return human readable totals (do=stats).
	 */
	public String statsText()
	{
		final StringBuilder sb = new StringBuilder(512);
		for (Map.Entry<String, Long> entry : totals().entrySet())
		{
			sb.append(entry.getKey()).append('=').append(entry.getValue()).append('\n');
		}
		return sb.toString();
	}

	/**
	 * @param bot a bot.
	 * @param state its state.
	 * @return one bot's counters (do=stats&name=).
	 */
	public String statsText(Player bot, FakePlayerBotState state)
	{
		final StringBuilder sb = new StringBuilder(256);
		sb.append(bot.getName()).append(" lvl").append(bot.getLevel()).append(' ').append(bot.getPlayerClass()).append(" [").append(state.personality).append("] zone=").append(state.zone).append(" plan=").append(FakePlayerBotContext.planOf(state.seedId)).append('\n');
		sb.append("  last action: ").append(state.lastAction).append(" (").append((System.currentTimeMillis() - state.lastActionAt) / 1000).append("s ago)\n");
		sb.append("  kills=").append(state.kills.get()).append(" exp=").append(state.expGained.get()).append(" pvp=").append(state.pvpKills.get()).append(" deaths=").append(state.deaths.get()).append('\n');
		sb.append("  loot=").append(state.lootPicked.get()).append(" sold=").append(state.itemsSold.get()).append(" earned=").append(state.adenaEarned.get()).append(" spent=").append(state.adenaSpent.get()).append(" adena=").append(bot.getAdena()).append(" stores=").append(state.storesOpened.get()).append('\n');
		sb.append("  casts=").append(state.skillsCast.get()).append(" rests=").append(state.rests.get()).append(" lines=").append(state.linesSaid.get()).append(" replies=").append(state.repliesGiven.get()).append(" parties=").append(state.partiesJoined.get()).append('\n');
		sb.append("  migrations=").append(state.migrations.get()).append(" professions=").append(state.professions.get()).append(" sieges=").append(state.siegesJoined.get()).append(" siegeSide=").append(state.siegeSide).append(" raidsWon=").append(state.raidsWon.get()).append(" raidBoss=").append(state.raidBossId).append('\n');
		sb.append("  shots=").append(bot.getAutoSoulShot()).append(" inventory=").append(bot.getInventory().getSize()).append(" hp=").append((int) bot.getCurrentHp()).append('/').append(bot.getMaxHp());
		return sb.toString();
	}

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

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