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

import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.gameserver.config.custom.FakePlayersConfig;
import org.l2jmobius.gameserver.data.xml.ItemData;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.enums.player.PlayerClass;
import org.l2jmobius.gameserver.entity.item.ItemTemplate;
import org.l2jmobius.gameserver.entity.item.enums.ItemProcessType;
import org.l2jmobius.gameserver.entity.item.instance.Item;
import org.l2jmobius.gameserver.entity.item.type.WeaponType;
import org.l2jmobius.gameserver.data.xml.SkillData;
import org.l2jmobius.gameserver.entity.item.enums.BodyPart;
import org.l2jmobius.gameserver.mechanics.skill.Skill;

/**
 * The money side of a bot's life, done with real inventory and real adena:
 * junk from the field is sold to the town merchant (half of the reference
 * price, as NPCs pay), shots / potions / arrows are bought with that adena
 * before going back to the spot, and at 20 / 40 / 76 the bot takes its next
 * profession with the skills that come with it. Everything is counted in the
 * bot's telemetry so it can be verified with do=stats.
 * @author Mobius SPP
 */
public class FakePlayerEconomy
{
	private static final Logger LOGGER = Logger.getLogger(FakePlayerEconomy.class.getName());

	private static final int ADENA = 57;
	private static final int[] SOULSHOTS = { 1835, 1463, 1464, 1465, 1466, 1467 };
	private static final int[] SPIRITSHOTS = { 3947, 3948, 3949, 3950, 3951, 3952 };
	private static final int[] ARROWS = { 17, 1341, 1342, 1343, 1344, 1345 };
	private static final int HEALING_POTION = 1061;
	private static final int GREATER_HEALING_POTION = 1539;
	private static final int SHOT_STOCK = 2500;
	private static final int ARROW_STOCK = 3000;
	private static final int POTION_STOCK = 10;
	private static final int JUNK_PRICE = 1500; // Cheaper things go to the NPC, dearer ones to the private store.

	protected FakePlayerEconomy()
	{
	}

	/**
	 * Everything a player does in town between two farming sessions.
	 * @param bot the bot (already standing in a town).
	 * @param state its state.
	 * @return a short description of what happened (for the action log).
	 */
	public String visitTown(Player bot, FakePlayerBotState state)
	{
		if (!FakePlayersConfig.FAKE_PLAYER_ECONOMY)
		{
			return "";
		}
		state.lastTownVisit = System.currentTimeMillis();
		final int sold = sellJunk(bot, state);
		final int bought = restock(bot, state);
		buffUp(bot);
		final String summary = "town: sold " + sold + ", bought " + bought + ", adena " + bot.getAdena();
		state.action(summary);
		return summary;
	}

	/**
	 * Sells the low value part of the loot to the merchant: NPCs pay half of the reference price.
	 * @param bot the bot.
	 * @param state its state.
	 * @return number of item stacks sold.
	 */
	public int sellJunk(Player bot, FakePlayerBotState state)
	{
		int sold = 0;
		long earned = 0;
		final List<Item> junk = new ArrayList<>();
		for (Item item : bot.getInventory().getItems())
		{
			if ((item == null) || item.isEquipped() || (item.getId() == ADENA) || !item.isSellable() || isConsumable(item.getId()))
			{
				continue;
			}
			final int price = item.getReferencePrice();
			if ((price < JUNK_PRICE) || (item.getCount() > 50) || (bot.getInventory().getSize() > 55))
			{
				junk.add(item);
			}
		}
		for (Item item : junk)
		{
			final long value = Math.max(1L, item.getReferencePrice() / 2L) * item.getCount();
			if (bot.destroyItem(ItemProcessType.SELL, item, bot, false))
			{
				bot.addAdena(ItemProcessType.SELL, (int) Math.min(Integer.MAX_VALUE, value), bot, false);
				earned += value;
				sold++;
			}
		}
		if (sold > 0)
		{
			state.itemsSold.addAndGet(sold);
			state.adenaEarned.addAndGet(earned);
		}
		return sold;
	}

	/**
	 * Buys shots, arrows and potions with the bot's own adena.
	 * @param bot the bot.
	 * @param state its state.
	 * @return number of purchases.
	 */
	public int restock(Player bot, FakePlayerBotState state)
	{
		int bought = 0;
		final Item weapon = bot.getActiveWeaponInstance();
		final int grade = (weapon != null) ? Math.min(5, Math.max(0, weapon.getTemplate().getCrystalType().getLevel())) : 0;
		final boolean mage = bot.isMageClass();
		final int shotId = mage ? SPIRITSHOTS[grade] : SOULSHOTS[grade];
		if (buyUpTo(bot, state, shotId, SHOT_STOCK))
		{
			bought++;
		}
		bot.addAutoSoulShot(shotId);
		if ((weapon != null) && (weapon.getItemType() == WeaponType.BOW) && buyUpTo(bot, state, ARROWS[grade], ARROW_STOCK))
		{
			bought++;
		}
		if (buyUpTo(bot, state, (bot.getLevel() >= 40) ? GREATER_HEALING_POTION : HEALING_POTION, POTION_STOCK))
		{
			bought++;
		}
		return bought;
	}

	/**
	 * Buys the missing part of a stack, when the bot can afford it.
	 * @param bot the bot.
	 * @param state its state.
	 * @param itemId the item.
	 * @param stock the stock to keep.
	 * @return true when something was bought.
	 */
	private boolean buyUpTo(Player bot, FakePlayerBotState state, int itemId, int stock)
	{
		final ItemTemplate template = ItemData.getInstance().getTemplate(itemId);
		if (template == null)
		{
			return false;
		}
		final Item existing = bot.getInventory().getItemByItemId(itemId);
		final int have = (existing != null) ? existing.getCount() : 0;
		if (have >= (stock / 2))
		{
			return false;
		}
		final int missing = stock - have;
		final long unit = Math.max(1, template.getReferencePrice());
		long affordable = Math.min(missing, bot.getAdena() / unit);
		if (affordable <= 0)
		{
			// A fresh character has no adena yet: the first pack is a gift so it can farm at all.
			if ((have == 0) && !state.giftedStock)
			{
				state.giftedStock = true;
				bot.addItem(ItemProcessType.BUY, itemId, Math.min(missing, stock / 5), bot, false);
				return true;
			}
			return false;
		}
		final int count = (int) affordable;
		if (bot.reduceAdena(ItemProcessType.BUY, (int) Math.min(Integer.MAX_VALUE, unit * count), bot, false))
		{
			bot.addItem(ItemProcessType.BUY, itemId, count, bot, false);
			state.adenaSpent.addAndGet(unit * count);
			return true;
		}
		return false;
	}

	/**
	 * Makes sure the auto shots are switched on for the equipped weapon (cheap, every gear check).
	 * @param bot the bot.
	 */
	public void ensureShots(Player bot)
	{
		final Item weapon = bot.getActiveWeaponInstance();
		final int grade = (weapon != null) ? Math.min(5, Math.max(0, weapon.getTemplate().getCrystalType().getLevel())) : 0;
		final int shotId = bot.isMageClass() ? SPIRITSHOTS[grade] : SOULSHOTS[grade];
		if (!bot.getAutoSoulShot().contains(shotId))
		{
			bot.getAutoSoulShot().clear();
			bot.addAutoSoulShot(shotId);
		}
	}

	private static boolean isConsumable(int itemId)
	{
		for (int id : SOULSHOTS)
		{
			if (id == itemId)
			{
				return true;
			}
		}
		for (int id : SPIRITSHOTS)
		{
			if (id == itemId)
			{
				return true;
			}
		}
		for (int id : ARROWS)
		{
			if (id == itemId)
			{
				return true;
			}
		}
		return (itemId == HEALING_POTION) || (itemId == GREATER_HEALING_POTION);
	}

	/**
	 * Class transfer at 20 / 40 / 76, like a player visiting the class master:
	 * the next class is chosen deterministically from the character id, so the
	 * same bot always grows into the same profession.
	 * @param bot the bot.
	 * @param state its state.
	 * @return the new class, or null when nothing changed.
	 */
	public PlayerClass checkProfession(Player bot, FakePlayerBotState state)
	{
		if (!FakePlayersConfig.FAKE_PLAYER_PROFESSION_CHANGE || bot.isSubClassActive())
		{
			return null;
		}
		final PlayerClass current = bot.getPlayerClass();
		final int level = bot.getLevel();
		final int classLevel = current.level();
		final boolean due = ((classLevel == 0) && (level >= 20)) || ((classLevel == 1) && (level >= 40)) || ((classLevel == 2) && (level >= 76));
		if (!due || current.getNextClasses().isEmpty())
		{
			return null;
		}
		final List<PlayerClass> options = new ArrayList<>(current.getNextClasses());
		options.sort((a, b) -> Integer.compare(a.getId(), b.getId()));
		final PlayerClass next = options.get(Math.floorMod(FakePlayerIdiolect.hash32("prof" + state.seedId + ":" + classLevel), options.size()));
		try
		{
			bot.setPlayerClass(next.getId());
			bot.setBaseClass(bot.getActiveClass());
			bot.giveAvailableSkills(true, true, true);
			bot.store(false);
			bot.broadcastUserInfo();
			bot.sendSkillList();
			state.professions.incrementAndGet();
			FakePlayerBotContext.setEvent(state.seedId, "profession");
			state.action("profession:" + next);
			LOGGER.info(getClass().getSimpleName() + ": " + bot.getName() + " lvl" + level + " became " + next + " (was " + current + ").");
			return next;
		}
		catch (Exception e)
		{
			LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Could not change class of " + bot.getName(), e);
			return null;
		}
	}

	/**
	 * @param bot the bot.
	 * @return farmed items worth a private store (no adena, nothing equipped, no consumables, not junk).
	 */
	public List<Item> storeGoods(Player bot)
	{
		final List<Item> result = new ArrayList<>();
		for (Item item : bot.getInventory().getItems())
		{
			if ((item == null) || item.isEquipped() || (item.getId() == ADENA) || !item.isSellable() || !item.isTradeable() || isConsumable(item.getId()))
			{
				continue;
			}
			if (item.getReferencePrice() >= JUNK_PRICE)
			{
				result.add(item);
			}
		}
		return result;
	}

	/**
	 * Store title slots: the dearest item and its price, for the store_title seam.
	 * @param goods store goods.
	 * @return slots.
	 */
	public Map<String, String> storeSlots(List<Item> goods)
	{
		Item best = null;
		for (Item item : goods)
		{
			if ((best == null) || (item.getReferencePrice() > best.getReferencePrice()))
			{
				best = item;
			}
		}
		if (best == null)
		{
			return Map.of();
		}
		final int price = best.getReferencePrice() * (80 + Rnd.get(90)) / 100;
		return Map.of("item", best.getName(), "price", (price >= 1000000) ? ((price / 1000000) + "кк") : (price >= 1000) ? ((price / 1000) + "к") : String.valueOf(price));
	}

	// ======================== Jewelry and buffs ========================

	private static final Map<Integer, int[]> JEWELRY = new java.util.concurrent.ConcurrentHashMap<>();

	/**
	 * The best necklace / earring / ring of a grade (by reference price), found once in the item data.
	 * @param grade crystal grade level (0 = NG ... 5 = S).
	 * @return item ids {necklace, earring, ring}.
	 */
	private static int[] jewelryOf(int grade)
	{
		return JEWELRY.computeIfAbsent(grade, g ->
		{
			final int[] best = new int[3];
			final int[] price = new int[3];
			for (ItemTemplate template : ItemData.getInstance().getAllItems())
			{
				if ((template == null) || !(template instanceof org.l2jmobius.gameserver.entity.item.Armor) || (template.getCrystalType().getLevel() != g) || !template.isSellable() || !template.isTradeable())
				{
					continue;
				}
				final BodyPart part = template.getBodyPart();
				final int slot = (part == BodyPart.NECK) ? 0 : ((part == BodyPart.R_EAR) || (part == BodyPart.L_EAR) || (part == BodyPart.LR_EAR)) ? 1 : ((part == BodyPart.R_FINGER) || (part == BodyPart.L_FINGER) || (part == BodyPart.LR_FINGER)) ? 2 : -1;
				if ((slot < 0) || (template.getReferencePrice() <= price[slot]) || (template.getReferencePrice() > 50000000))
				{
					continue;
				}
				price[slot] = template.getReferencePrice();
				best[slot] = template.getId();
			}
			return best;
		});
	}

	/**
	 * Gives and wears a full jewelry set of the bot's grade (real players never farm without one).
	 * @param bot the bot.
	 * @param grade crystal grade level.
	 */
	public void wearJewelry(Player bot, int grade)
	{
		final int[] set = jewelryOf(Math.max(0, Math.min(5, grade)));
		final int[] wanted = { set[0], set[1], set[1], set[2], set[2] };
		for (int itemId : wanted)
		{
			if (itemId <= 0)
			{
				continue;
			}
			int worn = 0;
			int owned = 0;
			for (Item item : bot.getInventory().getItems())
			{
				if (item.getId() == itemId)
				{
					owned++;
					if (item.isEquipped())
					{
						worn++;
					}
				}
			}
			final int needed = (itemId == set[1]) || (itemId == set[2]) ? 2 : 1;
			if (worn >= needed)
			{
				continue;
			}
			Item item = null;
			for (Item candidate : bot.getInventory().getItems())
			{
				if ((candidate.getId() == itemId) && !candidate.isEquipped())
				{
					item = candidate;
					break;
				}
			}
			if ((item == null) && (owned < needed))
			{
				item = bot.addItem(ItemProcessType.NONE, itemId, 1, bot, false);
			}
			if ((item != null) && !item.isEquipped())
			{
				bot.useEquippableItem(item, false);
			}
		}
	}

	/** Town buffs (the scheme buffer everybody uses): id, level for fighters / mages. */
	private static final int[][] FIGHTER_BUFFS = { { 1204, 2 }, { 1040, 3 }, { 1068, 3 }, { 1077, 3 }, { 1086, 2 }, { 1062, 2 }, { 1035, 4 }, { 1045, 6 }, { 1268, 4 }, { 1240, 3 }, { 1242, 3 } };
	private static final int[][] MAGE_BUFFS = { { 1204, 2 }, { 1040, 3 }, { 1059, 3 }, { 1085, 3 }, { 1078, 6 }, { 1035, 4 }, { 1045, 6 }, { 1062, 2 }, { 1303, 2 }, { 1048, 6 } };

	/**
	 * The full buff set a player takes from the town buffer before going to farm. Levels are
	 * clamped to what exists; buffs the character would not have at its level are skipped.
	 * @param bot the bot.
	 */
	public void buffUp(Player bot)
	{
		final int[][] buffs = bot.isMageClass() ? MAGE_BUFFS : FIGHTER_BUFFS;
		for (int[] entry : buffs)
		{
			final int maxLevel = SkillData.getInstance().getMaxLevel(entry[0]);
			if (maxLevel <= 0)
			{
				continue;
			}
			int level = Math.min(entry[1], maxLevel);
			// Low levels get weaker buffs, like a real buffer's level restriction.
			if (bot.getLevel() < 20)
			{
				level = Math.min(level, 1);
			}
			else if (bot.getLevel() < 40)
			{
				level = Math.min(level, 2);
			}
			final Skill skill = SkillData.getInstance().getSkill(entry[0], level);
			if ((skill != null) && !bot.isAffectedBySkill(entry[0]))
			{
				try
				{
					skill.applyEffects(bot, bot);
				}
				catch (Exception e)
				{
					// A buff that cannot be applied is just skipped.
				}
			}
		}
	}

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

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