package cz.cuni.amis.pogamut.ut2004.agent.module.sensor;

import cz.cuni.amis.pogamut.base.agent.worldview.IWorldView;
import cz.cuni.amis.pogamut.base.agent.worldview.WorldEventListener;
import cz.cuni.amis.pogamut.base.agent.worldview.WorldObjectEventListener;
import cz.cuni.amis.pogamut.ut2004.agent.module.AgentModule;
import cz.cuni.amis.pogamut.ut2004.communication.messages.UnrealId;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.Player;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.PlayerLeft;

import java.util.logging.Logger;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Memory module specialized on whereabouts of other players.
 *
 * <h2>Auto updating</h2>
 *
 * <p>All Player objects returned by this memory module are always self-updating
 * throughout the time, until the associated player leaves the game. This means
 * that once a valid Player object is obtained, it is not necessary to call any
 * methods of this memory module to get the object's info updated (e.g. player's
 * location, visibility, reachability, etc.). The object will autoupdate itself.
 *
 * <p>The same principle is applied to all Maps returned by this memory module.
 * Each returned Map is self-updating throughout the time. Once a specific Map
 * is obtained (e.g. a map of visible enemies) from this memory module, the Map
 * will get updated based on actions of the players (e.g. joining or leaving
 * the game, changing their team, moving around the map, etc.) automatically.
 *
 * <p>Note: All Maps returned by this memory module are locked and can not be
 * modified outside this memory module. If you need to modify a Map returned by
 * this module (for your own specific purpose), create a duplicate first. Such
 * duplicates, however and of course, will not get updated.
 *
 * @author Juraj 'Loque' Simlovic
 */
public class Players extends AgentModule
{
	/**
	 * Retreives last known info about given player.
	 *
	 * <p>Note: The returned Player object is self updating throughout time.
	 * Once you have a valid Player object, you do not have to call this
	 * method to get updated info about that player.
	 *
	 * @param UnrealId Player UnrealId to be retreived.
	 * @return Last known player info; or null upon none.
	 *
	 * @see getVisiblePlayer(UnrealId)
	 * @see getReachablePlayer(UnrealId)
	 */
	public Player getPlayer(UnrealId UnrealId)
	{
		// retreive from map of all players
		return players.all.get(UnrealId);
	}

	/**
	 * Retreives info about given player, but only it the player is visible.
	 *
	 * <p>Note: The returned Player object is self updating throughout time.
	 * Once you have a valid Player object, you do not have to call this
	 * method to get updated info about visibility of that player.
	 *
	 * @param UnrealId Player UnrealId to be retreived.
	 * @return Player info; or null upon none or not visible.
	 *
	 * @see getPlayer(UnrealId)
	 * @see getReachablePlayer(UnrealId)
	 */
	public Player getVisiblePlayer(UnrealId UnrealId)
	{
		// retreive from map of all visible players
		return players.visible.get(UnrealId);
	}

	/**
	 * Retreives info about given player, but only it the player is reachable.
	 *
	 * <p>Note: The returned Player object is self updating throughout time.
	 * Once you have a valid Player object, you do not have to call this
	 * method to get updated info about reachability of that player.
	 *
	 * @param UnrealId Player UnrealId to be retreived.
	 * @return Player info; or null upon none or not reachable.
	 *
	 * @see getPlayer(UnrealId)
	 * @see getVisiblePlayer(UnrealId)
	 */
	public Player getReachablePlayer(UnrealId UnrealId)
	{
		// retreive from map of all reachable players
		return players.reachable.get(UnrealId);
	}

	/*========================================================================*/

	/**
	 * Retreives a Map of all players.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of players from this memory module,
	 * the Map will get updated based on actions of the players (e.g. joining
	 * or leaving the game, changing their status, etc.).
	 *
	 * @return Map of all players, using their UnrealIds as keys.
	 *
	 * @see getEnemies()
	 * @see getFriends()
	 * @see getVisiblePlayers()
	 * @see getReachablePlayers()
	 */
	public Map<UnrealId, Player> getPlayers()
	{
		// publish map of all players
		return Collections.unmodifiableMap(players.all);
	}

	/**
	 * Retreives a Map of all enemies.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of enemies from this memory module,
	 * the Map will get updated based on actions of the players (e.g. joining
	 * or leaving the game, changing their team or status, etc.).
	 *
	 * @return Map of all enemies, using their UnrealIds as keys.
	 *
	 * @see getPlayers()
	 * @see getFriends()
	 * @see getVisibleEnemies()
	 * @see getReachableEnemies()
	 */
	public Map<UnrealId, Player> getEnemies()
	{
		// publish map of all enemies
		return Collections.unmodifiableMap(enemies.all);
	}

	/**
	 * Retreives a Map of all friends.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of friends from this memory module,
	 * the Map will get updated based on actions of the players (e.g. joining
	 * or leaving the game, changing their team or status, etc.).
	 *
	 * @return Map of all friends, using their UnrealIds as keys.
	 *
	 * @see getPlayers()
	 * @see getEnemies()
	 * @see getVisibleFriends()
	 * @see getReachableFriends()
	 */
	public Map<UnrealId, Player> getFriends()
	{
		// publish map of all friends
		return Collections.unmodifiableMap(friends.all);
	}

	/*========================================================================*/

	/**
	 * Retreives a Map of all visible players.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of players from this memory module,
	 * the Map will get updated based on actions of the players (e.g. joining
	 * or leaving the game, or changing their visibility, etc.).
	 *
	 * @return Map of all visible players, using their UnrealIds as keys.
	 *
	 * @see getPlayers()
	 * @see getVisibleEnemies()
	 * @see getVisibleFriends()
	 * @see canSeePlayers()
	 */
	public Map<UnrealId, Player> getVisiblePlayers()
	{
		// publish map of all visible players
		return Collections.unmodifiableMap(players.visible);
	}

	/**
	 * Retreives a Map of all visible enemies.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of enemies from this memory module,
	 * the Map will get updated based on actions of the players (e.g. joining
	 * or leaving the game, changing their team, status or visibility, etc.).
	 *
	 * @return Map of all visible enemies, using their UnrealIds as keys.
	 *
	 * @see getEnemies()
	 * @see getVisiblePlayers()
	 * @see getVisibleFriends()
	 * @see canSeeEnemies()
	 */
	public Map<UnrealId, Player> getVisibleEnemies()
	{
		// publish map of all visible enemies
		return Collections.unmodifiableMap(enemies.visible);
	}

	/**
	 * Retreives a Map of all visible friends.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of friends from this memory module,
	 * the Map will get updated based on actions of the players (e.g. joining
	 * or leaving the game, changing their team, status or visibility, etc.).
	 *
	 * @return Map of all visible friends, using their UnrealIds as keys.
	 *
	 * @see getFriends()
	 * @see getVisiblePlayers()
	 * @see getVisibleEnemies()
	 * @see canSeeFriends()
	 */
	public Map<UnrealId, Player> getVisibleFriends()
	{
		// publish map of all visible friends
		return Collections.unmodifiableMap(friends.visible);
	}

	/*========================================================================*/

	/**
	 * Retreives a Map of all reachable players.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of players from this memory module,
	 * the Map will get updated based on actions of the players (e.g. joining
	 * or leaving the game, or changing their visibility, etc.).
	 *
	 * @return Map of all reachable players, using their UnrealIds as keys.
	 *
	 * @see getPlayers()
	 * @see getReachableEnemies()
	 * @see getReachableFriends()
	 * @see canReachPlayers()
	 */
	public Map<UnrealId, Player> getReachablePlayers()
	{
		// publish map of all reachable players
		return Collections.unmodifiableMap(players.reachable);
	}

	/**
	 * Retreives a Map of all reachable enemies.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of enemies from this memory module,
	 * the Map will get updated based on actions of the players (e.g. joining
	 * or leaving the game, changing their team, status or visibility, etc.).
	 *
	 * @return Map of all reachable enemies, using their UnrealIds as keys.
	 *
	 * @see getEnemies()
	 * @see getReachablePlayers()
	 * @see getReachableFriends()
	 * @see canReachEnemies()
	 */
	public Map<UnrealId, Player> getReachableEnemies()
	{
		// publish map of all reachable enemies
		return Collections.unmodifiableMap(enemies.reachable);
	}

	/**
	 * Retreives a Map of all reachable friends.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of friends from this memory module,
	 * the Map will get updated based on actions of the players (e.g. joining
	 * or leaving the game, changing their team, status or visibility, etc.).
	 *
	 * @return Map of all reachable friends, using their UnrealIds as keys.
	 *
	 * @see getFriends()
	 * @see getReachablePlayers()
	 * @see getReachableEnemies()
	 * @see canReachFriends()
	 */
	public Map<UnrealId, Player> getReachableFriends()
	{
		// publish map of all reachable friends
		return Collections.unmodifiableMap(friends.reachable);
	}

	/*========================================================================*/
	
	public Player getNearestPlayer(Collection<Player> players) {
		double distance = Double.MAX_VALUE;
		Iterator<Player> iterator = players.iterator();
		Player player = null;
		while (iterator.hasNext()) {
			Player nextPlayer = iterator.next();
			double temp = agentInfo.getLocation().getPoint3d().distance(nextPlayer.getLocation().getPoint3d()); 
			if (temp < distance) {
				distance = temp;
				player = nextPlayer; 
			}
		}
		return player;
	}
	
	/*========================================================================*/

	/**
	 * Tells, whether the agent sees any other players.
	 *
	 * @return True, if at least one other player is visible; false otherwise.
	 *
	 * @see getVisiblePlayers()
	 */
	public boolean canSeePlayers()
	{
		// search map of all visible players
		return (players.visible.size() > 0);
	}

	/**
	 * Tells, whether the agent sees any other enemies.
	 *
	 * @return True, if at least one other enemy is visible; false otherwise.
	 *
	 * @see getVisibleEnemies()
	 */
	public boolean canSeeEnemies()
	{
		// search map of all visible enemies
		return (enemies.visible.size() > 0);
	}

	/**
	 * Tells, whether the agent sees any other friends.
	 *
	 * @return True, if at least one other friend is visible; false otherwise.
	 *
	 * @see getVisibleFriends()
	 */
	public boolean canSeeFriends()
	{
		// search map of all visible friends
		return (friends.visible.size() > 0);
	}

	/*========================================================================*/

	/**
	 * Tells, whether the agent can reach any other players.
	 *
	 * @return True, if at least one other player is reachable; false otherwise.
	 *
	 * @see getReachablePlayers()
	 */
	public boolean canReachPlayers()
	{
		// search map of all reachable players
		return (players.reachable.size() > 0);
	}

	/**
	 * Tells, whether the agent can reach any other enemies.
	 *
	 * @return True, if at least one other enemy is reachable; false otherwise.
	 *
	 * @see getReachableEnemies()
	 */
	public boolean canReachEnemies()
	{
		// search map of all reachable enemies
		return (enemies.reachable.size() > 0);
	}

	/**
	 * Tells, whether the agent can reach any other friends.
	 *
	 * @return True, if at least one other friend is reachable; false otherwise.
	 *
	 * @see getReachableFriends()
	 */
	public boolean canReachFriends()
	{
		// search map of all reachable friends
		return (friends.reachable.size() > 0);
	}

	/*========================================================================*/

	/**
	 * Maps of players of specific type.
	 */
	private class PlayerMaps
	{
		/** Map of all players of the specific type. */
		private HashMap<UnrealId, Player> all = new HashMap<UnrealId, Player> ();
		/** Map of visible players of the specific type. */
		private HashMap<UnrealId, Player> visible = new HashMap<UnrealId, Player> ();
		/** Map of reachable players of the specific type. */
		private HashMap<UnrealId, Player> reachable = new HashMap<UnrealId, Player> ();

		/**
		 * Processes events.
		 * @param player Player to process.
		 */
		private void notify(Player player)
		{
			UnrealId uid = player.getId();

			// be sure to be within all
			if (!all.containsKey(uid))
				all.put(uid, player);

			// previous visibility
			boolean wasVisible = visible.containsKey(uid);
			boolean isVisible = player.isVisible();

			// refresh visible
			if (isVisible && !wasVisible)
			{
				// add to visibles
				visible.put(uid, player);
			}
			else if (!isVisible && wasVisible)
			{
				// remove from visibles
				visible.remove(uid);
			}

			// previous reachability
			boolean wasReachable = reachable.containsKey(uid);
			boolean isReachable = player.isReachable();

			// refresh reachable
			if (isReachable && !wasReachable)
			{
				// add to reachables
				reachable.put(uid, player);
			}
			else if (!isReachable && wasReachable)
			{
				// remove from reachables
				reachable.remove(uid);
			}
		}

		/**
		 * Removes player from all maps.
		 * @param uid UnrealId of player to be removed.
		 */
		private void remove(UnrealId uid)
		{
			// remove from all maps
			all.remove(uid);
			visible.remove(uid);
			reachable.remove(uid);
		}
	}

	/** Maps of all players. */
	private PlayerMaps players = new PlayerMaps ();
	/** Maps of all enemies. */
	private PlayerMaps enemies = new PlayerMaps ();
	/** Maps of all friends. */
	private PlayerMaps friends = new PlayerMaps ();

	/*========================================================================*/

	/**
	 * Player listener.
	 */
	private class PlayerListener implements WorldObjectEventListener<Player>
	{
		@Override
		public void notify(Player event)
		{
			// do the job in map of players
			players.notify(event);
			// do the job in map of enemies
			if (agentInfo.isEnemy(event))
				enemies.notify(event);
			// do the job in map of friends
			if (agentInfo.isFriend(event))
				friends.notify(event);
		}

		/**
		 * Constructor. Registers itself on the given WorldView object.
		 * @param worldView WorldView object to listent to.
		 */
		public PlayerListener(IWorldView worldView)
		{
			worldView.addUpdatedListener(Player.class, this);
		}
	}

	/** Player listener */
	PlayerListener playerListener;

	/*========================================================================*/

	/**
	 * PlayerLeft listener.
	 */
	private class PlayerLeftListener implements WorldEventListener<PlayerLeft>
	{
		@Override
		public void notify(PlayerLeft event)
		{
			UnrealId uid = event.getId();

			// remove from all maps
			players.remove(uid);
			enemies.remove(uid);
			friends.remove(uid);
		}

		/**
		 * Constructor. Registers itself on the given WorldView object.
		 * @param worldView WorldView object to listent to.
		 */
		public PlayerLeftListener(IWorldView worldView)
		{
			worldView.addListener(PlayerLeft.class, this);
		}
	}

	/** PlayerLeft listener */
	PlayerLeftListener playerLeftListener;

	/*========================================================================*/

	/** AgentInfo memory module. */
	protected AgentInfo agentInfo;

	/**
	 * Constructor. Setups the memory module based on given WorldView.
	 * @param worldView WorldView object to read the info from.
	 * @param agentInfo AgentInfo memory module. Note: If <i>null</i> is
	 * provided, this memory module creates its own AgentInfo memory module.
	 * Provide shared AgentInfo memory module to economize CPU time and other
	 * resources.
	 * @param log Logger to be used for logging runtime/debug info.
	 */
	public Players(IWorldView worldView, AgentInfo agentInfo, Logger log)
	{
		super(log);

		// set or create AgentInfo memory module
		if (agentInfo != null) this.agentInfo = agentInfo;
		else this.agentInfo = new AgentInfo(worldView, null, log);

		// create listeners
		playerListener = new PlayerListener(worldView);
		playerLeftListener = new PlayerLeftListener(worldView);
	}
}
