package cz.cuni.amis.pogamut.base.agent.worldview;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import cz.cuni.amis.pogamut.base.agent.worldview.objects.IWorldObject;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.IWorldObjectEvent;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.IWorldObjectId;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.WorldObjectAppearedEvent;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.WorldObjectFirstEncounteredEvent;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.WorldObjectDestroyedEvent;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.WorldObjectDisappearedEvent;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.WorldObjectUpdatedEvent;
import cz.cuni.amis.pogamut.base.communication.translator.IWorldChangeEvent;
import cz.cuni.amis.pogamut.base.exceptions.PogamutException;
import cz.cuni.amis.pogamut.base.factory.guice.AgentScoped;
import cz.cuni.amis.pogamut.base.utils.logging.AgentLogger;
import cz.cuni.amis.pogamut.base.utils.logging.LogCategory;
import cz.cuni.amis.pogamut.base3d.worldview.objects.IViewable;
import cz.cuni.amis.utils.ClassUtils;
import cz.cuni.amis.utils.ExceptionToString;
import cz.cuni.amis.utils.listener.IListener;
import cz.cuni.amis.utils.listener.Listeners;
import cz.cuni.amis.utils.listener.ListenersMap;

/**
 * Abstract world view is implementing some of the tedious things every WorldView will surely
 * implement -&gt; maps for holding the references to all world objects either according to their
 * id and type (class). It also implements a map of listeners for events that may
 * be generated in the world.
 * <p><p>
 * For raising new IWorldViewEvent in descendants call protected method raiseEvent(IWorldViewEvent event).
 * <p><p>
 * Note that there is a big advantage in how the listeners are called and how objects are stored. 
 * <p>
 * The event notifying method (raiseEvent()) is respecting 
 * the class/interface hierarchy thus informing listeners hooked on all
 * levels of the hierarchy.
 * <p>
 * The items are stored according the the class/interface hierarchy as well!
 * <p><p>
 * <b>Example:</b>You have interface ItemEvent (extends IWorldViewObjectEvent) and it's implementation WeaponEvent and HealthEvent. Perheps
 * you want to listen for all events on WeaponEvent, so you will create WorldEventListener&lt;WeaponEvent&gt;.
 * But hey - you may want to listen on both WeaponEvent and HealthEvent (and perheps any ItemEvent there is),
 * that's easy - just create WorldEventListener&lt;ItemEvent&gt; and you will receive both WeaponEvent and HealthEvent.
 * That's because during event handling we're probing the event class ancestors / interfaces and informing
 * all listeners on all class-hierarchy levels. 
 * <p><p>
 * Ultimately you may create WorldEventListener&lt;IWorldViewEvent&gt; to
 * catch all events the world view produce (be careful because it may cause serious performance hit if you
 * process those events slowly).
 * <p><p>
 * Same goes for storing the items under it's class in the 'worldObjects'.
 * 
 * @author Jimmy
 */
@AgentScoped
@SuppressWarnings("unchecked")
public abstract class AbstractWorldView implements IWorldView {
	
	/**
	 * Class that notifies listeners about the world view event.
	 * @author Jimmy
	 */
	private static class ListenerNotifier<T> implements
		Listeners.ListenerNotifier<IListener> {

		/**
		 * Event that is being processed.
		 */
		private T event = null;
		
		public void setEvent(T event) {
			this.event = event;			
		}

		/**
		 * Method that is used to notify the listener.
		 */
		@Override
		public void notify(IListener listener) {
			listener.notify(event);
		}
		
	}
	
	/**
	 * Notifier object - preallocated, this will raise events on the listeners.
	 */
	private ListenerNotifier notifier = new ListenerNotifier();
	
	/**
	 * Synchronized map that holds all the objects according to their type
	 * in the maps.
	 * <p><p>
	 * Due to nature of generics we can't typed this field, it holds maps of objects
	 * according to their classes. 
	 * <p>
	 * Map &lt; Class, Map &lt; IWorldViewObjectId, IWorldObject of Class &gt; &gt;
	 */
	private Map worldObjects =
		Collections.synchronizedMap(
			new HashMap()
		);
	
	/**
	 * E.g. worldObjects but contains immutable (unmodifiable) version of map.
	 */
	private Map immutableWorldObjects =
		Collections.unmodifiableMap(
			worldObjects
		);
		
	/**
	 * Synchronized map of all the world objects that are present in the worldview.
	 */
	private Map<IWorldObjectId, IWorldObject> knownObjects =
		Collections.synchronizedMap(
			new HashMap<IWorldObjectId, IWorldObject>()
		);
	
	
	/**
	 * Map of the event listeners, key is the event class where the listener is hooked to.
	 */
	private ListenersMap<Class> eventListeners = new ListenersMap<Class>();	

    /**
	 * Map of object appeared listeners, key is the event class where the listener is hooked to.
	 */
    private ListenersMap<Class> appearedListeners = new ListenersMap<Class>();

    /**
	 * Map of object disappeared listeners, key is the event class where the listener is hooked to.
	 */
    private ListenersMap<Class> disappearedListeners = new ListenersMap<Class>();

    /**
	 * Map of object update listeners, key is the event class where the listener is hooked to.
	 */
    private ListenersMap<Class> updatedListeners = new ListenersMap<Class>();
	/**
	 * Flag that is telling us whether there is an event being processed or not.
	 * <p><p>
	 * It is managed only by raiseEvent() method - DO NOT MODIFY OUTSIDE IT!
	 */
	private boolean raiseEventProcessing = false;
	
	/**
	 * List of events we have to process.
	 * <p><p>
	 * It is managed only by raiseEvent() method - DO NOT MODIFY OUTSIDE IT!
	 */
	private Queue<IWorldEvent> raiseEventsList = new ConcurrentLinkedQueue<IWorldEvent>();
	
	/**
	 * Map with listeners hooked on a certain object.
	 */
	private Map<IWorldObjectId, ListenersMap<Class>> objectListeners = new HashMap<IWorldObjectId, ListenersMap<Class>>();
	
	/**
	 * Map with listeners on the object features.
	 */
	private Map<Class, ListenersMap<Class>> objectFeatureListeners = new HashMap<Class, ListenersMap<Class>>();
	
//	/**
//	 * Notifier object - preallocated, this will raise events on the listeners.
//	 */
//	private ListenerNotifier<IWorldObjectEvent> objectNotifier = new ListenerNotifier<IWorldObjectEvent>();
//	
	protected AgentLogger agentLogger;
	
	protected LogCategory log;
	
	public AbstractWorldView(AgentLogger logger) {
		agentLogger = logger;
		log = agentLogger.platform();
	}
	
	/**
	 * Used to introduce new object category into worldObjects and immutableWorldObjects.
	 * <p><p>
	 * It will create new synchronized Map&lt;IWorldViewObjectId, T&gt; in the worldObjects and it's immutable
	 * counterpart in immutableWorldObjects under key of 'cls'.
	 * <p><p>
	 * Returns modifiable version of created map.
	 * 
	 * @param <T>
	 * @param cls
	 * @return
	 */
	protected synchronized <T> Map<IWorldObjectId, T> addNewObjectCategory(Class<T> cls) {
		Map<IWorldObjectId, T> objects = Collections.synchronizedMap(new HashMap<IWorldObjectId, T>());			
		worldObjects.put(cls, objects);
		return objects;
	}
	
	/**
	 * Method that adds a new world object to the object maps. It will be called from
	 * the descendant whenever new object appears in the world view.
	 * @param worldObject
	 */
	protected void addWorldObject(IWorldObject worldObject) {
		knownObjects.put(worldObject.getId(), worldObject);
		for (Class cls : ClassUtils.getSubclasses(worldObject.getClass())) {
			Map objects;
			synchronized(worldObjects) {
				objects = (Map) worldObjects.get(cls);
				if (objects == null) objects = addNewObjectCategory(cls);
			}
			objects.put(worldObject.getId(), worldObject);
		}		
	}
	
	/**
	 * Returns world object of the given id or null if the object is not yet in the world view.
	 * @param objectId
	 * @return
	 */
	protected IWorldObject getWorldObject(IWorldObjectId objectId) {
		return knownObjects.get(objectId);
	}
	
	/**
	 * Removes world object from the world view - this will be called from the descendants
	 * of the AbstractWorldView whenever world object should disappear from the world view.
	 * @param worldObject
	 */
	protected void removeWorldObject(IWorldObject worldObject) {
		knownObjects.remove(worldObject.getId());
		for (Class<?> cls : ClassUtils.getSubclasses(worldObject.getClass())) {
			Map objects = (Map) worldObjects.get(cls);
			if (objects != null) {
				objects.remove(worldObject.getId());
			}		
		}			
	}
	
	@Override
	public Map<Class, Map<IWorldObjectId, IWorldObject>> getAll() {
		return immutableWorldObjects;
	}

	@Override
	public <T> Map<IWorldObjectId, T> getAll(Class<T> type) {
		// WE HAVE TO SYNCHRONIZE on worldObjects NOT immutableWorldObjects,
		// because we're adding new category the world objects depends on that!
		// see addWorldObject()
		synchronized(worldObjects) {
			Map<IWorldObjectId, T> objects = (Map<IWorldObjectId, T>) immutableWorldObjects.get(type);
			if (objects == null) {
				return Collections.unmodifiableMap(addNewObjectCategory(type));
			} else {
				return Collections.unmodifiableMap(objects);
			}
		}
	}
	
	@Override
	public void addListener(Class<? extends IWorldEvent> event, WorldEventListener<? extends IWorldEvent> listener) {
		eventListeners.add(event, listener);		
	}

    @Override
    public void addAppearedListener(Class<? extends IViewable> objectClass, WorldObjectEventListener<? extends IViewable> listener) {
        appearedListeners.add(objectClass, listener);
    }

    @Override
    public void addDisappearedListener(Class<? extends IViewable> objectClass, WorldObjectEventListener<? extends IViewable> listener) {
        disappearedListeners.add(objectClass, listener);
    }

    @Override
    public void addUpdatedListener(Class<? extends IWorldObject> objectClass, WorldObjectEventListener<? extends IWorldObject> listener) {
        updatedListeners.add(objectClass, listener);
    }


	@Override
	public boolean isListening(WorldEventListener listener) {
		if (eventListeners.isListening(listener)) return true;
		
		synchronized(objectListeners) {
			for (ListenersMap<Class> map : objectListeners.values()) {
				if (map.isListening(listener)) return true;
			}
		}
		synchronized(objectFeatureListeners) {
			for (ListenersMap<Class> map : objectFeatureListeners.values()) {
				if (map.isListening(listener)) return true;
			}
		}
		return false;
	}

	@Override
	public void removeListener(Class<? extends IWorldEvent> event, WorldEventListener<? extends IWorldEvent> listener) {
			
		eventListeners.remove(event, listener);
				
		synchronized(objectListeners) {
			for (ListenersMap<Class> map : objectListeners.values()) {
				map.remove(listener);
			}
		}
		synchronized(objectFeatureListeners) {
			for (ListenersMap<Class> map : objectFeatureListeners.values()) {
				map.remove(listener);
			}
		}
	}
	
	@Override
	public void addListener(IWorldObjectId objectId, Class<? extends IWorldObjectEvent> event, WorldEventListener<? extends IWorldObjectEvent> listener) {
		ListenersMap<Class> listenersMap;
		synchronized(objectListeners) {
			listenersMap = objectListeners.get(objectId);
			if (listenersMap == null) {
				listenersMap = new ListenersMap<Class>();
				objectListeners.put(objectId, listenersMap);
			}			
		}
		listenersMap.add(event, listener);			
	}

	@Override
	public void addListener(Class objectFeature, Class<? extends IWorldObjectEvent> event, WorldEventListener<? extends IWorldObjectEvent> listener) {
		ListenersMap<Class> listenersMap;
		synchronized(objectFeatureListeners) {
			listenersMap = objectFeatureListeners.get(objectFeature);
			if (listenersMap == null) {
				listenersMap = new ListenersMap<Class>();
				objectFeatureListeners.put(objectFeature, listenersMap);
			}
		}
		listenersMap.add(event, listener);		
	}
	
	/**
	 * Helper method used ONLY FROM innerRaiseEvent. DO NOT USE OUTSIDE THAT METHOD!
	 * @param event
	 * @param map
	 */
	private void notifyOnMap(Object event, ListenersMap map) {
		Collection<Class> eventClasses = ClassUtils.getSubclasses(event.getClass());
		for (Class eventClass : eventClasses) {
			notifier.setEvent(event);			
			map.notify(eventClass, notifier);
		}
	}
	
	/**
	 * Process new IWorldChangeEvent - DO NOT CALL SEPARATELY - must be called only from raiseEvent(),
	 * that forbids recursion of it's calls.
	 * @param event
	 */
	private void innerRaiseEvent(IWorldEvent event) {
		
		// ---------------
		// event listeners
		// ---------------
		
		// get the listeners for the current event
		notifyOnMap(event, eventListeners);		

		if (event instanceof IWorldObjectEvent) {
			IWorldObjectEvent objectEvent = (IWorldObjectEvent)event;
			ListenersMap<Class> listenersMap;
            final IWorldObject worldObject = objectEvent.getObject();

            // -------------------------
            //  object change listeners
            // -------------------------

            if(event instanceof WorldObjectAppearedEvent) {
                notifyOnMap(worldObject, appearedListeners);
            }
            if(event instanceof WorldObjectUpdatedEvent) {
                notifyOnMap(worldObject, updatedListeners);
            }
            if(event instanceof WorldObjectDisappearedEvent) {
                notifyOnMap(worldObject, disappearedListeners);
            }

			// ------------------------
			// object feature listeners
			// ------------------------
			
			for(Class objectFeature : ClassUtils.getSubclasses(worldObject.getClass())) {
				
				synchronized(objectFeatureListeners) {
					listenersMap = objectFeatureListeners.get(objectFeature);
					if (listenersMap == null) continue;
				}
				notifyOnMap(event, listenersMap);				
			}
			
			// -------------------
			// object id listeners
			// -------------------
			
			synchronized(objectListeners) {
				listenersMap = objectListeners.get(objectEvent.getId());
				if (listenersMap == null) return;
			}
			notifyOnMap(event, listenersMap);


		}
	}
	
	/**
	 * Process new IWorldEvent - notify all the listeners about it. Forbids recursion.
	 * <p><p>
	 * Use in the descendants to process new IWorldChangeEvent.
	 * 
	 * @param event
	 */
	protected synchronized void raiseEvent(IWorldEvent event) {
		// is this method recursively called? 
		if (raiseEventProcessing) {
			// yes it is -> that means the previous event has not been
			// processed! ... store this event and allows the previous one
			// to be fully processed (e.g. postpone raising this event)
			raiseEventsList.add(event);
			return;
		} else {
			// no it is not ... so raise the flag that we're inside the method
			raiseEventProcessing = true;
		}
		// process event
				
		innerRaiseEvent(event);
		
		// check the events list size, do we have more events to process?
		while(raiseEventsList.size() != 0) {
			// yes we do -> do it!
			innerRaiseEvent(raiseEventsList.poll());			
		}
		// all events has been processed, drop the flag that we're inside the method
		raiseEventProcessing = false;			
	}
	
}
