/*
 * Copyright (C) 2004-2014 L2J Server
 * 
 * This file is part of L2J Server.
 * 
 * L2J Server is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * L2J Server is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.l2jserver.gameserver.model.entity;

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import javolution.util.FastList;
import javolution.util.FastMap;

import com.l2jserver.Config;
import com.l2jserver.gameserver.cache.HtmCache;
import com.l2jserver.gameserver.datatables.NpcData;
import com.l2jserver.gameserver.datatables.SpawnTable;
import com.l2jserver.gameserver.instancemanager.AntiFeedManager;
import com.l2jserver.gameserver.model.L2Spawn;
import com.l2jserver.gameserver.model.L2World;
import com.l2jserver.gameserver.model.actor.L2Npc;
import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate;
import com.l2jserver.gameserver.model.holders.PlayerEventHolder;
import com.l2jserver.gameserver.model.interfaces.IProcedure;
import com.l2jserver.gameserver.network.serverpackets.CharInfo;
import com.l2jserver.gameserver.network.serverpackets.ExBrExtraUserInfo;
import com.l2jserver.gameserver.network.serverpackets.MagicSkillUse;
import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage;
import com.l2jserver.gameserver.network.serverpackets.UserInfo;

/**
 * @since $Revision: 1.3.4.1 $ $Date: 2005/03/27 15:29:32 $ This ancient thingie got reworked by Nik at $Date: 2011/05/17 21:51:39 $ Yeah, for 6 years no one bothered reworking this buggy event engine.
 */
public class L2Event
{
	protected static final Logger _log = Logger.getLogger(L2Event.class.getName());
	public static EventState eventState = EventState.OFF;
	public static String _eventName = "";
	public static String _eventCreator = "";
	public static String _eventInfo = "";
	public static int _teamsNumber = 0;
	public static final Map<Integer, String> _teamNames = new FastMap<>();
	public static final List<L2PcInstance> _registeredPlayers = new FastList<>();
	public static final Map<Integer, List<L2PcInstance>> _teams = new FastMap<>();
	public static int _npcId = 0;
	// public static final List<L2Npc> _npcs = new FastList<L2Npc>();
	private static final Map<L2PcInstance, PlayerEventHolder> _connectionLossData = new FastMap<>();
	
	public enum EventState
	{
		OFF, // Not running
		STANDBY, // Waiting for participants to register
		ON // Registration is over and the event has started.
	}
	
	/**
	 * @param player
	 * @return The team ID where the player is in, or -1 if player is null or team not found.
	 */
	public static int getPlayerTeamId(L2PcInstance player)
	{
		if (player == null)
		{
			return -1;
		}
		
		for (Entry<Integer, List<L2PcInstance>> team : _teams.entrySet())
		{
			if (team.getValue().contains(player))
			{
				return team.getKey();
			}
		}
		
		return -1;
	}
	
	public static List<L2PcInstance> getTopNKillers(int n)
	{
		final Map<L2PcInstance, Integer> tmp = new HashMap<>();
		for (List<L2PcInstance> teamList : _teams.values())
		{
			for (L2PcInstance player : teamList)
			{
				if (player.getEventStatus() == null)
				{
					continue;
				}
				tmp.put(player, player.getEventStatus().getKills().size());
			}
		}
		
		sortByValue(tmp);
		
		// If the map size is less than "n", n will be as much as the map size
		if (tmp.size() <= n)
		{
			return new ArrayList<>(tmp.keySet());
		}
		
		final List<L2PcInstance> toReturn = new ArrayList<>(tmp.keySet());
		return toReturn.subList(1, n);
	}
	
	public static void showEventHtml(L2PcInstance player, String objectid)
	{
		// TODO: work on this
		if (eventState == EventState.STANDBY)
		{
			try
			{
				final String htmContent;
				final NpcHtmlMessage html = new NpcHtmlMessage(Integer.parseInt(objectid));
				
				if (_registeredPlayers.contains(player))
				{
					htmContent = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), "data/html/mods/EventEngine/Participating.htm");
				}
				else
				{
					htmContent = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), "data/html/mods/EventEngine/Participation.htm");
				}
				
				if (htmContent != null)
				{
					html.setHtml(htmContent);
				}
				
				html.replace("%objectId%", objectid); // Yeah, we need this.
				html.replace("%eventName%", _eventName);
				html.replace("%eventCreator%", _eventCreator);
				html.replace("%eventInfo%", _eventInfo);
				player.sendPacket(html);
			}
			catch (Exception e)
			{
				_log.log(Level.WARNING, "Exception on showEventHtml(): " + e.getMessage(), e);
			}
		}
	}
	
	/**
	 * Spawns an event participation NPC near the player. The npc id used to spawning is L2Event._npcId
	 * @param target
	 */
	public static void spawnEventNpc(L2PcInstance target)
	{
		
		L2NpcTemplate template = NpcData.getInstance().getTemplate(_npcId);
		
		try
		{
			L2Spawn spawn = new L2Spawn(template);
			
			spawn.setX(target.getX() + 50);
			spawn.setY(target.getY() + 50);
			spawn.setZ(target.getZ());
			spawn.setAmount(1);
			spawn.setHeading(target.getHeading());
			spawn.stopRespawn();
			SpawnTable.getInstance().addNewSpawn(spawn, false);
			
			spawn.init();
			spawn.getLastSpawn().setCurrentHp(999999999);
			spawn.getLastSpawn().setTitle(_eventName);
			spawn.getLastSpawn().setEventMob(true);
			// spawn.getLastSpawn().decayMe();
			// spawn.getLastSpawn().spawnMe(spawn.getLastSpawn().getX(), spawn.getLastSpawn().getY(), spawn.getLastSpawn().getZ());
			
			spawn.getLastSpawn().broadcastPacket(new MagicSkillUse(spawn.getLastSpawn(), spawn.getLastSpawn(), 1034, 1, 1, 1));
			
			// _npcs.add(spawn.getLastSpawn());
			
		}
		catch (Exception e)
		{
			_log.log(Level.WARNING, "Exception on spawn(): " + e.getMessage(), e);
		}
		
	}
	
	/**
	 * Zoey76: TODO: Rewrite this in a way that doesn't iterate over all spawns.
	 */
	public static void unspawnEventNpcs()
	{
		SpawnTable.getInstance().forEachSpawn(new IProcedure<L2Spawn, Boolean>()
		{
			@Override
			public Boolean execute(L2Spawn spawn)
			{
				L2Npc npc = spawn.getLastSpawn();
				if ((npc != null) && npc.isEventMob())
				{
					npc.deleteMe();
					spawn.stopRespawn();
					SpawnTable.getInstance().deleteSpawn(spawn, false);
				}
				return true;
			}
		});
	}
	
	/**
	 * @param player
	 * @return False: If player is null, his event status is null or the event state is off. True: if the player is inside the _registeredPlayers list while the event state is STANDBY. If the event state is ON, it will check if the player is inside in one of the teams.
	 */
	public static boolean isParticipant(L2PcInstance player)
	{
		if ((player == null) || (player.getEventStatus() == null))
		{
			return false;
		}
		
		switch (eventState)
		{
			case OFF:
				return false;
			case STANDBY:
				return _registeredPlayers.contains(player);
			case ON:
				for (List<L2PcInstance> teamList : _teams.values())
				{
					if (teamList.contains(player))
					{
						return true;
					}
				}
		}
		return false;
		
	}
	
	/**
	 * Adds the player to the list of participants. If the event state is NOT STANDBY, the player wont be registered.
	 * @param player
	 */
	public static void registerPlayer(L2PcInstance player)
	{
		if (eventState != EventState.STANDBY)
		{
			player.sendMessage("The registration period for this event is over.");
			return;
		}
		
		if ((Config.L2JMOD_DUALBOX_CHECK_MAX_L2EVENT_PARTICIPANTS_PER_IP == 0) || AntiFeedManager.getInstance().tryAddPlayer(AntiFeedManager.L2EVENT_ID, player, Config.L2JMOD_DUALBOX_CHECK_MAX_L2EVENT_PARTICIPANTS_PER_IP))
		{
			_registeredPlayers.add(player);
		}
		else
		{
			player.sendMessage("You have reached the maximum allowed participants per IP.");
			return;
		}
		
	}
	
	/**
	 * Removes the player from the participating players and the teams and restores his init stats before he registered at the event (loc, pvp, pk, title etc)
	 * @param player
	 */
	public static void removeAndResetPlayer(L2PcInstance player)
	{
		
		try
		{
			if (isParticipant(player))
			{
				if (player.isDead())
				{
					player.restoreExp(100.0);
					player.doRevive();
					player.setCurrentHpMp(player.getMaxHp(), player.getMaxMp());
					player.setCurrentCp(player.getMaxCp());
				}
				
				player.getPoly().setPolyInfo(null, "1");
				player.decayMe();
				player.spawnMe(player.getX(), player.getY(), player.getZ());
				CharInfo info1 = new CharInfo(player);
				player.broadcastPacket(info1);
				UserInfo info2 = new UserInfo(player);
				player.sendPacket(info2);
				player.broadcastPacket(new ExBrExtraUserInfo(player));
				
				player.stopTransformation(true);
			}
			
			if (player.getEventStatus() != null)
			{
				player.getEventStatus().restorePlayerStats();
			}
			
			player.setEventStatus(null);
			
			_registeredPlayers.remove(player);
			int teamId = getPlayerTeamId(player);
			if (_teams.containsKey(teamId))
			{
				_teams.get(teamId).remove(player);
			}
		}
		catch (Exception e)
		{
			_log.log(Level.WARNING, "Error at unregisterAndResetPlayer in the event:" + e.getMessage(), e);
		}
	}
	
	/**
	 * The player's event status will be saved at _connectionLossData
	 * @param player
	 */
	public static void savePlayerEventStatus(L2PcInstance player)
	{
		_connectionLossData.put(player, player.getEventStatus());
	}
	
	/**
	 * If _connectionLossData contains the player, it will restore the player's event status. Also it will remove the player from the _connectionLossData.
	 * @param player
	 */
	public static void restorePlayerEventStatus(L2PcInstance player)
	{
		if (_connectionLossData.containsKey(player))
		{
			player.setEventStatus(_connectionLossData.get(player));
			_connectionLossData.remove(player);
		}
	}
	
	/**
	 * If the event is ON or STANDBY, it will not start. Sets the event state to STANDBY and spawns registration NPCs
	 * @return a string with information if the event participation has been successfully started or not.
	 */
	public static String startEventParticipation()
	{
		try
		{
			switch (eventState)
			{
				case ON:
					return "Cannot start event, it is already on.";
				case STANDBY:
					return "Cannot start event, it is on standby mode.";
				case OFF: // Event is off, so no problem turning it on.
					eventState = EventState.STANDBY;
					break;
			}
			
			// Register the event at AntiFeedManager and clean it for just in case if the event is already registered.
			AntiFeedManager.getInstance().registerEvent(AntiFeedManager.L2EVENT_ID);
			AntiFeedManager.getInstance().clear(AntiFeedManager.L2EVENT_ID);
			
			// Just in case
			unspawnEventNpcs();
			_registeredPlayers.clear();
			// _npcs.clear();
			
			if (NpcData.getInstance().getTemplate(_npcId) == null)
			{
				return "Cannot start event, invalid npc id.";
			}
			
			try (FileReader fr = new FileReader(Config.DATAPACK_ROOT + "/data/events/" + _eventName);
				BufferedReader br = new BufferedReader(fr))
			{
				_eventCreator = br.readLine();
				_eventInfo = br.readLine();
			}
			
			List<L2PcInstance> temp = new FastList<>();
			for (L2PcInstance player : L2World.getInstance().getPlayers())
			{
				if (!player.isOnline())
				{
					continue;
				}
				
				if (!temp.contains(player))
				{
					spawnEventNpc(player);
					temp.add(player);
				}
				for (L2PcInstance playertemp : player.getKnownList().getKnownPlayers().values())
				{
					if ((Math.abs(playertemp.getX() - player.getX()) < 1000) && (Math.abs(playertemp.getY() - player.getY()) < 1000) && (Math.abs(playertemp.getZ() - player.getZ()) < 1000))
					{
						temp.add(playertemp);
					}
				}
			}
		}
		catch (Exception e)
		{
			_log.warning("L2Event: " + e.getMessage());
			return "Cannot start event participation, an error has occured.";
		}
		
		return "The event participation has been successfully started.";
	}
	
	/**
	 * If the event is ON or OFF, it will not start. Sets the event state to ON, creates the teams, adds the registered players ordered by level at the teams and adds a new event status to the players.
	 * @return a string with information if the event has been successfully started or not.
	 */
	public static String startEvent()
	{
		try
		{
			switch (eventState)
			{
				case ON:
					return "Cannot start event, it is already on.";
				case STANDBY:
					eventState = EventState.ON;
					break;
				case OFF: // Event is off, so no problem turning it on.
					return "Cannot start event, it is off. Participation start is required.";
			}
			
			// Clean the things we will use, just in case.
			unspawnEventNpcs();
			_teams.clear();
			_connectionLossData.clear();
			
			// Insert empty lists at _teams.
			for (int i = 0; i < _teamsNumber; i++)
			{
				_teams.put(i + 1, new FastList<L2PcInstance>());
			}
			
			int i = 0;
			while (!_registeredPlayers.isEmpty())
			{
				// Get the player with the biggest level
				int max = 0;
				L2PcInstance biggestLvlPlayer = null;
				for (L2PcInstance player : _registeredPlayers)
				{
					if (player == null)
					{
						continue;
					}
					
					if (max < player.getLevel())
					{
						max = player.getLevel();
						biggestLvlPlayer = player;
					}
				}
				
				if (biggestLvlPlayer == null)
				{
					continue;
				}
				
				_registeredPlayers.remove(biggestLvlPlayer);
				_teams.get(i + 1).add(biggestLvlPlayer);
				biggestLvlPlayer.setEventStatus();
				i = (i + 1) % _teamsNumber;
			}
			
		}
		catch (Exception e)
		{
			_log.warning("L2Event: " + e.getMessage());
			return "Cannot start event, an error has occured.";
		}
		
		return "The event has been successfully started.";
	}
	
	/**
	 * If the event state is OFF, it will not finish. Sets the event state to OFF, unregisters and resets the players, unspawns and clers the event NPCs, clears the teams, registered players, connection loss data, sets the teams number to 0, sets the event name to empty.
	 * @return a string with information if the event has been successfully stopped or not.
	 */
	public static String finishEvent()
	{
		switch (eventState)
		{
			case OFF:
				return "Cannot finish event, it is already off.";
			case STANDBY:
				for (L2PcInstance player : _registeredPlayers)
				{
					removeAndResetPlayer(player);
				}
				
				unspawnEventNpcs();
				// _npcs.clear();
				_registeredPlayers.clear();
				_teams.clear();
				_connectionLossData.clear();
				_teamsNumber = 0;
				_eventName = "";
				eventState = EventState.OFF;
				return "The event has been stopped at STANDBY mode, all players unregistered and all event npcs unspawned.";
			case ON:
				for (List<L2PcInstance> teamList : _teams.values())
				{
					for (L2PcInstance player : teamList)
					{
						removeAndResetPlayer(player);
					}
				}
				
				eventState = EventState.OFF;
				AntiFeedManager.getInstance().clear(AntiFeedManager.TVT_ID);
				unspawnEventNpcs(); // Just in case
				// _npcs.clear();
				_registeredPlayers.clear();
				_teams.clear();
				_connectionLossData.clear();
				_teamsNumber = 0;
				_eventName = "";
				_npcId = 0;
				_eventCreator = "";
				_eventInfo = "";
				return "The event has been stopped, all players unregistered and all event npcs unspawned.";
		}
		
		return "The event has been successfully finished.";
	}
	
	private static final Map<L2PcInstance, Integer> sortByValue(Map<L2PcInstance, Integer> unsortMap)
	{
		final List<Entry<L2PcInstance, Integer>> list = new LinkedList<>(unsortMap.entrySet());
		Collections.sort(list, new Comparator<Entry<L2PcInstance, Integer>>()
		{
			@Override
			public int compare(Entry<L2PcInstance, Integer> e1, Entry<L2PcInstance, Integer> e2)
			{
				return e1.getValue().compareTo(e2.getValue());
			}
		});
		
		final Map<L2PcInstance, Integer> sortedMap = new LinkedHashMap<>();
		for (Entry<L2PcInstance, Integer> entry : list)
		{
			sortedMap.put(entry.getKey(), entry.getValue());
		}
		return sortedMap;
	}
}
