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

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

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.ItemType;
import cz.cuni.amis.pogamut.ut2004.communication.messages.UnrealId;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.Item;
import cz.cuni.amis.pogamut.ut2004.communication.translator.events.MapPointListObtained;
import cz.cuni.amis.utils.HashMapMap;

/**
 * Memory module specialized on items on the map.
 *
 * @author Juraj 'Loque' Simlovic
 * @author Jimmy
 */
public class Items extends AgentModule
{
	/**
	 * Retreives list of all items, which includes all known pickups and all
	 * visible thrown items.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of items from this module, the Map
	 * will get updated based on what happens within the map.
	 *
	 * @return List of all items. Note: Spawned items are included only.
	 */
	public Map<UnrealId, Item> getAllItems()
	{
		return Collections.unmodifiableMap(items.all);
	}
	
	/**
	 * Retreives list of all items <b>of specific type</b>, which includes all known pickups and all
	 * visible thrown items.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of items from this module, the Map
	 * will get updated based on what happens within the map.
	 *
	 * @return List of all items of specific type. Note: Spawned items are included only.
	 */
	public Map<UnrealId, Item> getAllItems(ItemType type) 
	{
		return Collections.unmodifiableMap(items.allCategories.get(type));
	}
	
	/**
	 * Retrieves a specific item from the all items in the map.
	 * <p><p>
	 * Once obtained it is self-updating based on what happens in the game. 
	 * 
	 * @param id
	 * @return A specific Item be it Spawned or Dropped (Dropped item must be visible though!).
	 */
	public Item getItem(UnrealId id) {
		Item item = items.all.get(id);
		if (item == null) item = items.visible.get(id);
		return item;
	}
	
	/**
	 * Retrieves a specific item from the all items in the map.
	 * <p><p>
	 * Once obtained it is self-updating based on what happens in the game. 
	 * 
	 * @param stringUnrealId
	 * @return A specific Item be it Spawned or Dropped (Dropped item must be visible though!).
	 */
	public Item getItem(String stringUnrealId) {
		return getItem(UnrealId.get(stringUnrealId));
	}

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

	/**
	 * Retreives list of all visible items, which includes all visible known
	 * pickups and all visible thrown items.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of items from this module, the Map
	 * will get updated based on what happens within the map.
	 *
	 * @return List of all visible items. Note: Spawned items are included only.
	 */
	public Map<UnrealId, Item> getVisibleItems()
	{
		return Collections.unmodifiableMap(items.visible);
	}

	/**
	 * Retreives list of all visible items <b> of specific type</b>, which includes all visible known
	 * pickups and all visible thrown items.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of items from this module, the Map
	 * will get updated based on what happens within the map.
	 *
	 * @return List of all visible items of specific type. Note: Spawned items are included only.
	 */
	public Map<UnrealId, Item> getVisibleItems(ItemType type) 
	{
		return Collections.unmodifiableMap(items.visibleCategories.get(type));
	}
	
	/**
	 * Retrieves a specific item from the visible items in the map. If item of specified
	 * id is not visible returns null.
	 * <p><p>
	 * Once obtained it is self-updating based on what happens in the game. 
	 * 
	 * @param id
	 * @return A specific Item be it Spawned or Dropped.
	 */
	public Item getVisibleItem(UnrealId id) {
		Item item = items.visible.get(id);
		return item;
	}
	
	/**
	 * Retrieves a specific item from the visible items in the map. If item of specified
	 * id is not visible returns null.
	 * <p><p>
	 * Once obtained it is self-updating based on what happens in the game. 
	 * 
	 * @param stringUnrealId
	 * @return A specific Item be it Spawned or Dropped.
	 */
	public Item getVisibleItem(String stringUnrealId) {
		return getVisibleItem(UnrealId.get(stringUnrealId));
	}

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

	/**
	 * Retreives list of all reachable items, which includes all reachable
	 * known pickups and all reachable and visible thrown items.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of items from this module, the Map
	 * will get updated based on what happens within the map.
	 *
	 * @return List of all reachable items. Note: Spawned items are included only.
	 */
	public Map<UnrealId, Item> getReachableItems()
	{
		return Collections.unmodifiableMap(items.reachable);
	}
	
	/**
	 * Retreives list of all reachable items <b>of specific type</b>, which includes all reachable
	 * known pickups and all reachable and visible thrown items.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of items from this module, the Map
	 * will get updated based on what happens within the map.
	 *
	 * @return List of all reachable items of specific type. Note: Spawned items are included only.
	 */
	public Map<UnrealId, Item> getReachableItems(ItemType type) 
	{
		return Collections.unmodifiableMap(items.reachableCategories.get(type));
	}
	
	/**
	 * Retrieves a specific item from the all items in the map that is currently reachable.
	 * If item of specified item is not reachable returns null.
	 * <p><p>
	 * Once obtained it is self-updating based on what happens in the game. 
	 * 
	 * @param id
	 * @return A specific Item be it Spawned or Dropped (Dropped item must be visible though!).
	 */
	public Item getReachableItem(UnrealId id) {
		Item item = items.reachable.get(id);
		if (item == null) {
			item = items.visible.get(id);
			if (!item.isReachable()) return null;
		}
		return item;
	}
	
	/**
	 * Retrieves a specific item from the all items in the map that is currently reachable.
	 * If item of specified item is not reachable returns null.
	 * <p><p>
	 * Once obtained it is self-updating based on what happens in the game. 
	 * 
	 * @param stringUnrealId
	 * @return A specific Item be it Spawned or Dropped (Dropped item must be visible though!).
	 */
	public Item getReachableItem(String stringUnrealId) {
		return getReachableItem(UnrealId.get(stringUnrealId));
	}

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

	/**
	 * Retreives list of all known item pickup points.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of items from this module, the Map
	 * will get updated based on what happens within the map.
	 *
	 * @return List of all items. Note: Empty pickups are included as well.
	 *
	 * @see isPickupSpawned(Item)
	 */
	public Map<UnrealId, Item> getKnownPickups()
	{
		return Collections.unmodifiableMap(items.known);
	}

	/**
	 * Retreives list of all known item pickup points <b>of specific type</b>.
	 *
	 * <p>Note: The returned Map is unmodifiable and self updating throughout
	 * time. Once you obtain a specific Map of items from this module, the Map
	 * will get updated based on what happens within the map.
	 *
	 * @return List of all items of specific type. Note: Empty pickups are included as well.
	 *
	 * @see isPickupSpawned(Item)
	 */
	public Map<UnrealId, Item> getKnownPickups(ItemType type) 
	{
		return Collections.unmodifiableMap(items.knownCategories.get(type));
	}
	
	/**
	 * Retrieves a specific pickup point.
	 * <p><p>
	 * Once obtained it is self-updating based on what happens in the game. 
	 * 
	 * @param id
	 * @return A specific Item be it Spawned or Dropped (Dropped item must be visible though!).
	 */
	public Item getKnownPickup(UnrealId id) {
		return items.known.get(id);
	}
	
	/**
	 * Retrieves a specific pickup point.
	 * <p><p>
	 * Once obtained it is self-updating based on what happens in the game. 
	 * 
	 * @param stringUnrealId
	 * @return A specific Item be it Spawned or Dropped (Dropped item must be visible though!).
	 */
	public Item getKnownPickup(String stringUnrealId) {
		return getKnownPickup(UnrealId.get(stringUnrealId));
	}

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

	/**
	 * Tells, whether the given pickup point contains a spawned item.
	 *
	 * @param item Item, for which its pickup point is to be examined.
	 * @return True, if the item is spawned; false if the pickup is empty.
	 *
	 * @see getKnownPickups(boolean,boolean)
	 */
	public boolean isPickupSpawned(Item item)
	{
		// FIXME[js]: implement when available
		throw new UnsupportedOperationException("Not supported yet");
	}

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

	/**
	 * Maps of items of specific type.
	 */
	private class ItemMaps
	{
		/** Map of all items (known and thrown). */
		private HashMap<UnrealId, Item> all = new HashMap<UnrealId, Item> ();
		/** Map of visible items of the specific type. */
		private HashMap<UnrealId, Item> visible = new HashMap<UnrealId, Item> ();
		/** Map of visible items of the specific type. */
		private HashMap<UnrealId, Item> reachable = new HashMap<UnrealId, Item> ();
		/** Map of all known items of the specific type. */
		private HashMap<UnrealId, Item> known = new HashMap<UnrealId, Item> ();
		/** Map of all items (known and thrown) of specific categories. */ 
		private HashMapMap<ItemType, UnrealId, Item> allCategories = new HashMapMap<ItemType, UnrealId, Item>();
		/** Map of visible items of the specific type. */
		private HashMapMap<ItemType, UnrealId, Item> visibleCategories = new HashMapMap<ItemType, UnrealId, Item> ();
		/** Map of visible items of the specific type. */
		private HashMapMap<ItemType, UnrealId, Item> reachableCategories = new HashMapMap<ItemType, UnrealId, Item> ();
		/** Map of all known items of the specific type. */
		private HashMapMap<ItemType, UnrealId, Item> knownCategories = new HashMapMap<ItemType, UnrealId, Item> ();

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

			// be sure to be within all
			if (!all.containsKey(uid)) {
				all.put(uid, item);
				allCategories.put(item.getType(), item.getId(), item);
			}

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

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

			// remove non-visible thrown items
			if (!isVisible && item.isDropped()) {
				all.remove(uid);
				allCategories.remove(item.getType(), item.getId());
			}

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

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

		/**
		 * Processes events.
		 * @param items Map of known items to process.
		 */
		private void notify(Map<UnrealId, Item> items)
		{
			// register all known items
			known.putAll(items);
			for (Item item : items.values()) {
				knownCategories.put(item.getType(), item.getId(), item);
			}
		}
	}

	/** Maps of all items. */
	private ItemMaps items = new ItemMaps ();

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

	protected class ItemsListener implements WorldObjectEventListener<Item> {
		
		public ItemsListener(IWorldView worldView) {
			worldView.addUpdatedListener(Item.class, this);
		}

        public void notify(Item event) {
            items.notify(event);
        }

    }

	protected ItemsListener itemsListener;
	
	/**
	 *  This method is called from the constructor to hook a listener that updated the items field.
	 *  <p><p>
	 *  It must:
	 *  <ol>
	 *  <li>initialize itemsListener field</li>
	 *  <li>hook the listener to the world view</li>
	 *  </ol>
	 *  <p><p>
	 *  By overriding this method you may provide your own listener that may wrap Items with your class
	 *  adding new fields into them.
	 *  
	 *  @param worldView
	 **/
	protected ItemsListener createItemsListener(IWorldView worldView) {
		return new ItemsListener(worldView);
	}

	/*========================================================================*/
	
	/**
	 * MapPointsListObtained listener.
	 */
	protected class MapPointsListener implements WorldEventListener<MapPointListObtained>
	{
		/**
		 * Constructor. Registers itself on the given WorldView object.
		 * @param worldView WorldView object to listen to.
		 */
		public MapPointsListener(IWorldView worldView)
		{
			worldView.addListener(MapPointListObtained.class, this);
		}
		
		@Override
		public void notify(MapPointListObtained event)
		{
			items.notify(event.getItems());
			worldView.removeListener(MapPointListObtained.class, this);
		}
		
	}
	
	/** MapPointsListObtained listener */
	protected MapPointsListener mapPointsListener;
	
	/**
	 *  This method is called from the constructor to create a listener that initialize items field from
	 *  the MapPointListObtained event.
	 *  <p><p>
	 *  By overriding this method you may provide your own listener that may wrap Items with your class
	 *  adding new fields into them.
	 *  
	 *  @param worldView
	 * @return 
	 **/
	protected MapPointsListener createMapPointsListener(IWorldView worldView) {
		return new MapPointsListener(worldView);		
	}

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

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

	/** Reference to the world view we're listening on. */
	protected IWorldView worldView;

	/**
	 * 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 Items(IWorldView worldView, AgentInfo agentInfo, Logger log)
	{
		super(log);
		
		this.worldView = worldView;

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

		// create listeners
		itemsListener = createItemsListener(worldView);
		mapPointsListener = createMapPointsListener(worldView);				
	}
	
}