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

import cz.cuni.amis.pogamut.base.exceptions.PogamutException;
import cz.cuni.amis.pogamut.base.factory.guice.AgentScoped;

import java.util.LinkedList;
import java.util.Queue;

import com.google.inject.Inject;
import com.google.inject.cglib.beans.ImmutableBean;

import cz.cuni.amis.pogamut.base.agent.worldview.AbstractWorldView;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.IWorldObject;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.IWorldObjectUpdateEvent;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.WorldObjectAppearedEvent;
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.WorldObjectFirstEncounteredEvent;
import cz.cuni.amis.pogamut.base.agent.worldview.objects.WorldObjectUpdatedEvent;
import cz.cuni.amis.pogamut.base.communication.mediator.IMediator;
import cz.cuni.amis.pogamut.base.communication.translator.IWorldChangeEvent;
import cz.cuni.amis.pogamut.base.communication.translator.IWorldEventWrapper;
import cz.cuni.amis.pogamut.base.utils.logging.AgentLogger;
import cz.cuni.amis.utils.ExceptionToString;
import cz.cuni.amis.utils.flag.Flag;
import cz.cuni.amis.utils.flag.ImmutableFlag;

/**
 * Schema: "real" world | ... some communication ... | IWorldViewEventInput | EventDrivenWorldView | Agent (most probably listening for events)
 * <p><p>
 * EventDrivenWorldView assumes that everything is driven by the events that are received
 * through IWorldViewEventInput - those events surely must contains "new object appears event",
 * "object update event", "object disappear event". Those three events are wrapped in one
 * interface IWorldObjectUpdateEvent that has three types of behavior (see it's javadoc).
 * <p><p>
 * Handling of events (that is incoming):
 * <ul>
 * <li>event <b>is NOT</b> IWorldObjectUpdateEvent - the event is raised (if is instance of IWorldViewEvent, otherwise an exception is thrown)
 * all listeners at this object for that type of event are informed sequentially as they were added</li>
 * <li>event <b>IS</b> IWorldObjectUpdateEvent - the event is consumed (processed) by the EventDrivenWorldView
 * the IWorldObjectUpdateEvent may have three already mentioned outcomes:
 * 		<ol>
 * 			<li>new object appears - an event NewWorldObjectAppearedEvent is raised</li>
 * 			<li>object was updated - the EventDrivenWorldView will process this event silently, not raising any other events</li>
 * 			<li>object disappeared - an even WorldObjectDisappearedEvent is raised</li>
 *      </ol>
 * </li>
 * </ul>
 * <p><p>
 * <b>
 * Note that the implementation of raising / receiving / notifying about event is
 * strictly time-ordered. Every event is fully processed before another is raised/received.
 * There is a possibility that raising one event may produce another one - in that case
 * processing of this new one is postponed until the previous event has been fully processed
 * (e.g. all listeners has been notified about it). That means that recursion of events is forbidden.
 * <p><p>
 * Note that we rely on method update() of the IWorldObjectUpdateEvent to be called
 * as the first before the event is propagated further. An original object must be passed
 * to the update() method and that method should save it as the original so the
 * event can return correct object via getObject() method.
 * 
 * @author Jimmy
 */
@AgentScoped
public class EventDrivenWorldView extends AbstractWorldView implements IStartableWorldView {
	
	private Flag<Boolean> running = new Flag<Boolean>(false);
 
    /**
     * Flag that is telling us whether there is an event being processed or not.
     * <p><p>
     * It is managed only by notify() method - DO NOT MODIFY OUTSIDE IT!
     */
    protected boolean receiveEventProcessing = false;
    /**
     * List of events we have to process.
     * <p><p>
     * It is managed only by notify() method - DO NOT MODIFY OUTSIDE IT!
     */
    protected Queue<IWorldChangeEvent> notifyEventsList = new LinkedList<IWorldChangeEvent>();
    /** Source of messages updating this world view. */
    protected IMediator messageSource = null;

    @Inject
    public EventDrivenWorldView(IMediator messageSource, AgentLogger log) {
        super(log);
        this.messageSource = messageSource;
        messageSource.setMediatorOutput(this);
    }
    
    @Override
    protected void raiseEvent(IWorldEvent event) {
    	try {
    		super.raiseEvent(event);
    	} catch (Exception e) {    	
    		log.severe(ExceptionToString.process("exception happend during raising event " + event, e));
    		this.stop();
    	}
    }

    /**
     * Used to process IWorldChangeEvent - it has to be either IWorldChangeEvent or IWorldObjectUpdateEvent. Forbids recursion.
     * <p>
     * DO NOT CALL SEPARATELY - should be called only from notifyEvent().
     *
     * @param event
     */
    protected void innerNotify(IWorldChangeEvent event) {
        if (event instanceof IWorldObjectUpdateEvent) {
            IWorldObjectUpdateEvent updateEvent = (IWorldObjectUpdateEvent) event;
            IWorldObject obj = getWorldObject(updateEvent.getId());
            if (obj == null) {
                // new object appeared in the world case
                obj = updateEvent.update(null);
                addWorldObject(obj);
                raiseEvent(new WorldObjectFirstEncounteredEvent<IWorldObject>(obj));
            } else {
                IWorldObject updatedObject = updateEvent.update(obj);
                if (updatedObject == null) {
                    // object disappeared from the world case
                    removeWorldObject(obj);
                    raiseEvent(new WorldObjectDestroyedEvent<IWorldObject>(obj));
                } else {
                    // object was updated
                    if (updatedObject != obj) {
                        throw new RuntimeException("Update event (class: " + event.getClass() + ") returned different world object, wrong behavior.");
                    }
                    objectUpdated(updatedObject);
                }
            }
        } else {
            if (event instanceof IWorldEventWrapper) {
                raiseEvent(((IWorldEventWrapper) event).getWorldEvent());
            } else {
                throw new RuntimeException("Unsupported event type received.");
            }

        }
    }

    /**
     * Called whenever an object was updated - should raiseEvent() that
     * the object has been updated.
     * <p><p>
     * Might be override to provide a mechanism that will forbid
     * update of certain objects (like items that can't move).
     * 
     * @param obj
     */
    protected void objectUpdated(IWorldObject obj) {
        raiseEvent(new WorldObjectUpdatedEvent<IWorldObject>(obj));
    }

    @Override
    public synchronized void notify(IWorldChangeEvent event) {
    	if (!running.getFlag()) {
    		log.severe(this + ": not running but received message");
    		stop();
    		return;
    	}
    	
        // is this method recursively called?
        if (receiveEventProcessing) {
            // 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)
            notifyEventsList.add(event);
            return;
        } else {
            // no it is not ... so raise the flag that we're inside the method
            receiveEventProcessing = true;
        }
        // process event
        innerNotify(event);
        // check the events list size, do we have more events to process?
        while (notifyEventsList.size() != 0) {
            // yes -> do it!
            innerNotify(notifyEventsList.poll());
        }
        // all events has been processed, drop the flag that we're inside the method
        receiveEventProcessing = false;
    }

    public void start() throws PogamutException {
    	if (running.getFlag()) return;
        messageSource.start();
        running.setFlag(true);
    }

    public void stop() {
    	if (!running.getFlag()) return;
    	running.setFlag(false);
        messageSource.stop();        
    }

    public void kill() {
    	running.setFlag(false);
        messageSource.kill();        
    }
    
    public ImmutableFlag<Boolean> getRunning() {
    	return running.getImmutable();
    }
}
