/*
 * 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.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;

import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.entity.actor.Npc;
import org.l2jmobius.gameserver.entity.actor.Player;

/**
 * The "ear" of the fake player population: every public bot line passes
 * through here, and nearby bots sometimes pick the topic up - agreeing with a
 * complaint, shrugging at a question, congratulating a level up. One pickup
 * per line at most (depth 1), commercial/functional lines are never picked up.
 * Sandbox-tested logic (overheard / overheard_q / grats seams).
 * @author Mobius SPP
 */
public class FakePlayerHeardManager
{
	private static final int HEAR_RANGE = 1250;
	private static final int PICKUP_CHANCE = 10; // Percent.
	private static final int GRATS_CHANCE = 12; // Percent, per neighbour.
	private static final int GRATS_MAX = 2;
	private static final long GLOBAL_THROTTLE = 2500; // At most one pickup per N ms server-wide.
	private static final long LISTENER_COOLDOWN = 25000; // A bot picks something up at most this often.
	
	private static final Pattern QUESTION = Pattern.compile("^(кто|где|когда|скок|сколько|почем)\\b.*|.*\\?.*", Pattern.UNICODE_CHARACTER_CLASS);
	private static final Pattern COMMERCIAL = Pattern.compile(".*(лавка|продаю|продает|скупа|распродажа|налетай|торгую|акци|цены|меняю|покупай|торговец|зацени|ждет (своего )?хозяина|открыл точку|ищу (кп|пати)|лфп|нужн|формирую пати|собран|собираю|стучите|(кп|пати).{0,25}\\bго\\b|\\bго\\b.{0,25}(кп|пати)|за \\d+к|по \\d+к|\\d+ ?кк?\\b|лям|сотк|грейд|соски|шоты|бсое|свитки|кристалл|рецепт|уголь|бижа|банки|дешевле|отдам|вх\\b|под завязку|гк вызову).*", Pattern.UNICODE_CHARACTER_CLASS);
	private static final Pattern LEVELUP = Pattern.compile(".*\\b\\d{1,2}\\b.*", Pattern.UNICODE_CHARACTER_CLASS);
	
	private final AtomicLong _lastPickup = new AtomicLong();
	private final Map<Integer, Long> _listenerCooldown = new java.util.concurrent.ConcurrentHashMap<>();
	
	protected FakePlayerHeardManager()
	{
	}
	
	public static boolean isQuestionLine(String text)
	{
		return QUESTION.matcher(text).matches();
	}
	
	public static boolean isCommercialLine(String text)
	{
		return COMMERCIAL.matcher(text).matches();
	}
	
	public static boolean isLevelupLine(String text)
	{
		return LEVELUP.matcher(text).matches();
	}
	
	/**
	 * Called for every public line a bot says (never for pickup replies - depth 1).
	 * @param speaker the bot that spoke (npc fake player or headless player).
	 * @param text the said line.
	 */
	public void onSpeech(Creature speaker, String text)
	{
		onSpeech(speaker, text, "");
	}
	
	/**
	 * @param speaker the bot that spoke.
	 * @param text the said line.
	 * @param seam the seam the line came from ("" when unknown, e.g. an LLM line -
	 *            then text heuristics decide).
	 */
	public void onSpeech(Creature speaker, String text, String seam)
	{
		if ((speaker == null) || (text == null) || text.isEmpty())
		{
			return;
		}
		
		// The seam is the ground truth; heuristics only cover seamless (LLM) lines.
		final boolean known = (seam != null) && !seam.isEmpty();
		final boolean levelup = known ? "levelup".equals(seam) : isLevelupLine(text);
		final boolean commercial = known ? ("trade_shout".equals(seam) || "lfp".equals(seam) || "store_title".equals(seam) || "call_help".equals(seam) || "grats".equals(seam) || "overheard".equals(seam) || "overheard_q".equals(seam) || "dialogue".equals(seam) || "bot_talk".equals(seam) || "reply".equals(seam) || "invite_player".equals(seam) || "party_accept".equals(seam) || seam.startsWith("siege_") || seam.startsWith("reply_")) : isCommercialLine(text);
		if (!levelup && commercial)
		{
			return; // Nobody "agrees" with an advertisement (or a pickup - depth 1).
		}
		
		final long now = System.currentTimeMillis();
		if (!levelup)
		{
			if (Rnd.get(100) >= PICKUP_CHANCE)
			{
				return;
			}
			final long last = _lastPickup.get();
			if (((now - last) < GLOBAL_THROTTLE) || !_lastPickup.compareAndSet(last, now))
			{
				return;
			}
		}
		
		final List<Creature> listeners = new ArrayList<>();
		for (Npc npc : World.getVisibleObjectsInRange(speaker, Npc.class, HEAR_RANGE))
		{
			if ((npc != speaker) && npc.getTemplate().isFakePlayer() && !npc.isDead() && !onCooldown(npc.getObjectId(), now))
			{
				listeners.add(npc);
			}
		}
		for (Player player : World.getVisibleObjectsInRange(speaker, Player.class, HEAR_RANGE))
		{
			if ((player != speaker) && FakePlayerHeadlessManager.getInstance().isBot(player) && !player.isDead() && !player.isInStoreMode() && !onCooldown(player.getObjectId(), now))
			{
				listeners.add(player);
			}
		}
		if (listeners.isEmpty())
		{
			return;
		}
		
		if (levelup)
		{
			int congratulated = 0;
			for (Creature listener : listeners)
			{
				if (congratulated >= GRATS_MAX)
				{
					break;
				}
				if (Rnd.get(100) >= GRATS_CHANCE)
				{
					continue;
				}
				congratulated++;
				final Map<String, String> slots = new HashMap<>(1);
				slots.put("player", speaker.getName());
				scheduleReply(listener, "grats", slots);
			}
			return;
		}
		
		final Creature listener = listeners.get(Rnd.get(listeners.size()));
		_listenerCooldown.put(listener.getObjectId(), now);
		if (_listenerCooldown.size() > 2000)
		{
			_listenerCooldown.entrySet().removeIf(entry -> (now - entry.getValue()) > LISTENER_COOLDOWN);
		}
		final boolean question = isQuestionLine(text);
		final Map<String, String> slots = new HashMap<>(2);
		slots.put("msg", echoOf(text));
		slots.put("player", speaker.getName());
		scheduleReply(listener, question ? "overheard_q" : "overheard", slots);
	}
	
	private boolean onCooldown(int objectId, long now)
	{
		final Long last = _listenerCooldown.get(objectId);
		return (last != null) && ((now - last) < LISTENER_COOLDOWN);
	}
	
	/**
	 * @param text a said line.
	 * @return its first words, the way a neighbour echoes a topic ("да, {msg}, точно").
	 */
	public static String echoOf(String text)
	{
		final String[] words = text.split("\\s+");
		final StringBuilder echo = new StringBuilder();
		for (int i = 0; (i < words.length) && (i < 3); i++)
		{
			if (i > 0)
			{
				echo.append(' ');
			}
			echo.append(words[i]);
		}
		return echo.toString();
	}
	
	private void scheduleReply(Creature listener, String seam, Map<String, String> slots)
	{
		ThreadPool.schedule(() ->
		{
			final int botId;
			final String personality;
			if (listener instanceof Npc npc)
			{
				botId = FakePlayerProgressionManager.getInstance().getCharacterId(npc.getObjectId());
				personality = npc.getTemplate().getParameters().getString("fpcPersonality", "neutral");
			}
			else if (listener instanceof Player player)
			{
				botId = FakePlayerHeadlessManager.getInstance().seedIdOf(player);
				personality = FakePlayerHeadlessManager.getInstance().personalityOf(player);
			}
			else
			{
				return;
			}
			final Map<String, String> facts = new HashMap<>(1);
			facts.put("personality", personality);
			// Optional mode: silence beats repetition for background reactions.
			final String reply = FakePlayerChatLines.getInstance().speak(botId, seam, facts, slots, true, null);
			if (reply == null)
			{
				return;
			}
			if (listener instanceof Npc npc)
			{
				FakePlayerChatManager.getInstance().broadcastRaw(npc, reply);
			}
			else if (listener instanceof Player player)
			{
				FakePlayerHeadlessManager.getInstance().sayRaw(player, reply);
			}
		}, Rnd.get(2000, 6000));
	}
	
	public static FakePlayerHeardManager getInstance()
	{
		return SingletonHolder.INSTANCE;
	}
	
	private static class SingletonHolder
	{
		protected static final FakePlayerHeardManager INSTANCE = new FakePlayerHeardManager();
	}
}
