//----------------------------------------------------------------------------
// Copyright (C) 2011  Ingrid Nunes
// 
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// 
// This library 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
// Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
// 
// To contact the authors:
// http://inf.ufrgs.br/prosoft/bdi4jade/
//
//----------------------------------------------------------------------------

package bdi4jade.core;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import bdi4jade.annotation.GoalOwner;
import bdi4jade.event.GoalListener;
import bdi4jade.exception.PlanInstantiationException;
import bdi4jade.goal.Goal;
import bdi4jade.goal.GoalStatus;
import bdi4jade.plan.Plan;
import bdi4jade.plan.Plan.EndState;
import bdi4jade.plan.planbody.PlanBody;

/**
 * This class represents the intention abstraction from the BDI model. It
 * represents a goal that the agent is committed to achieve. It has the
 * associated goal and tries to execute plans to achieve it. It keeps a list of
 * the executed plans, and after using all plans unsuccessfully, the goal is
 * considered unachievable. When a plan fails, the BDI-interpreter cycle may
 * invoke the {@link #tryToAchive()} method again, so the intention tries
 * another plan. During its execution, the intention can be set to no longer
 * desired. This occurs during the agent reasoning cycle or when a goal is
 * dropped ({@link BDIAgent#dropGoal(Goal)}).
 * 
 * @author Ingrid Nunes
 */
public class Intention {

	private static final Log log = LogFactory.getLog(Intention.class);

	private PlanBody currentPlan;
	private final Capability dispatcher;
	private final Set<Plan> executedPlans;
	private final Goal goal;
	private final List<GoalListener> goalListeners;
	private final AbstractBDIAgent myAgent;
	private boolean noLongerDesired;
	private final Set<Capability> owners;
	private boolean unachievable;
	private boolean waiting;

	/**
	 * Creates a new intention. It is associated with an agent and the goal that
	 * it is committed to achieve.
	 * 
	 * @param goal
	 *            the goal to be achieved.
	 * @param bdiAgent
	 *            the bdiAgent associated with this intention.
	 * 
	 * @throws IllegalAccessException
	 *             if the goal was dispatched by a capability that has no access
	 *             to the goal to be achieved.
	 */
	public Intention(Goal goal, AbstractBDIAgent bdiAgent)
			throws IllegalAccessException {
		this(goal, bdiAgent, null);
	}

	/**
	 * Creates a new intention. It is associated with an agent and the goal that
	 * it is committed to achieve. It also receives a {@link Capability} as
	 * parameter indicating the owner of the goal (dispatched the goal).
	 * 
	 * @param goal
	 *            the goal to be achieved.
	 * @param bdiAgent
	 *            the bdiAgent associated with this intention.
	 * @param dispatcher
	 *            the Capability that dispatched the goal.
	 * 
	 * @throws IllegalAccessException
	 *             if the goal was dispatched by a capability that has no access
	 *             to the goal to be achieved.
	 */
	public Intention(Goal goal, AbstractBDIAgent bdiAgent, Capability dispatcher)
			throws IllegalAccessException {
		this.goal = goal;
		this.myAgent = bdiAgent;
		this.unachievable = false;
		this.noLongerDesired = false;
		this.waiting = true;
		this.executedPlans = new HashSet<>();
		this.currentPlan = null;
		this.goalListeners = new LinkedList<>();
		this.dispatcher = dispatcher;

		Class<? extends Capability> owner = null;
		boolean internal = false;

		if (goal.getClass().isAnnotationPresent(GoalOwner.class)) {
			GoalOwner ownerAnnotation = goal.getClass().getAnnotation(
					GoalOwner.class);
			owner = ownerAnnotation.capability();
			internal = ownerAnnotation.internal();
		} else {
			Class<?> enclosingClass = goal.getClass().getEnclosingClass();
			if (enclosingClass != null
					&& Capability.class.isAssignableFrom(goal.getClass()
							.getEnclosingClass())) {
				owner = (Class<Capability>) enclosingClass;
			}
		}

		if (owner == null) {
			this.owners = new HashSet<>();
		} else {
			if (dispatcher == null) {
				this.owners = myAgent.getGoalOwner(owner, internal);
			} else {
				this.owners = dispatcher.getGoalOwner(owner, internal);
				if (owners.isEmpty()) {
					throw new IllegalAccessException("Capability " + dispatcher
							+ " has no access to goal "
							+ goal.getClass().getName() + " of capability "
							+ owner.getName());
				}
			}
		}
	}

	/**
	 * Adds a listener to be notified when about goal events.
	 * 
	 * @param goalListener
	 *            the listener to be notified.
	 */
	public void addGoalListener(GoalListener goalListener) {
		synchronized (goalListeners) {
			goalListeners.add(goalListener);
		}
	}

	/**
	 * Dispatches a new plan to try to achieve the intention goal. It looks for
	 * plans that can achieve the goal that were not already tried and then
	 * starts the plan. If all possible plans were already executed, the
	 * intention is set to unachievable.
	 */
	private synchronized void dispatchPlan() {
		Map<Capability, Set<Plan>> options = new HashMap<>();

		if (owners.isEmpty()) {
			for (Capability capability : myAgent.getCapabilities()) {
				capability.addCandidatePlans(goal, options);
			}
		} else {
			for (Capability capability : owners) {
				capability.addCandidatePlans(goal, options);
			}
		}

		Iterator<Capability> it = options.keySet().iterator();
		while (it.hasNext()) {
			Set<Plan> plans = options.get(it.next());
			plans.removeAll(executedPlans);
			if (plans.isEmpty()) {
				it.remove();
			}
		}

		while (this.currentPlan == null && !options.isEmpty()) {
			Plan selectedPlan = myAgent.getPlanSelectionStrategy().selectPlan(
					goal, options);
			try {
				this.currentPlan = selectedPlan.createPlanBody();
				currentPlan.init(selectedPlan, this);
			} catch (PlanInstantiationException e) {
				log.error("Plan " + selectedPlan.getId()
						+ " could not be instantiated.");
				e.printStackTrace();
				this.currentPlan = null;
				for (Set<Plan> plans : options.values()) {
					plans.remove(selectedPlan);
				}
			}
		}

		if (options.isEmpty()) {
			this.unachievable = true;
		} else {
			this.currentPlan.start();
		}
	}

	/**
	 * Sets this intention to the {@link GoalStatus#WAITING} status. It may come
	 * from the {@link GoalStatus#PLAN_FAILED} or
	 * {@link GoalStatus#TRYING_TO_ACHIEVE} states.
	 */
	public synchronized void doWait() {
		GoalStatus status = getStatus();
		switch (status) {
		case WAITING:
			break;
		case TRYING_TO_ACHIEVE:
			this.waiting = true;
			this.currentPlan.block();
			break;
		case PLAN_FAILED:
			this.waiting = true;
			this.executedPlans.add(this.currentPlan.getPlan());
			this.currentPlan = null;
			break;
		default:
			assert false : status;
			break;
		}
	}

	/**
	 * Returns the capability that dispatched this goal.
	 * 
	 * @return the dispatcher.
	 */
	public Capability getDispatcher() {
		return dispatcher;
	}

	/**
	 * Returns the goal associated with this intention.
	 * 
	 * @return the goal.
	 */
	public Goal getGoal() {
		return goal;
	}

	/**
	 * Returns all goal listeners.
	 * 
	 * @return the goalListeners.
	 */
	public List<GoalListener> getGoalListeners() {
		return goalListeners;
	}

	/**
	 * Returns the agent associated with this intention.
	 * 
	 * @return the myAgent.
	 */
	public AbstractBDIAgent getMyAgent() {
		return myAgent;
	}

	/**
	 * Returns the set of capabilities that own this goal.
	 * 
	 * @return the owners.
	 */
	public Set<Capability> getOwners() {
		return owners;
	}

	/**
	 * Returns the current goal status that this capability is committed to
	 * achieve.
	 * 
	 * @return the current goal status.
	 * 
	 * @see GoalStatus
	 */
	public synchronized GoalStatus getStatus() {
		if (this.unachievable) {
			return GoalStatus.UNACHIEVABLE;
		} else if (this.noLongerDesired) {
			return GoalStatus.NO_LONGER_DESIRED;
		} else if (this.waiting) {
			return GoalStatus.WAITING;
		} else if (this.currentPlan == null) {
			return GoalStatus.TRYING_TO_ACHIEVE;
		} else {
			EndState endState = this.currentPlan.getEndState();
			if (EndState.FAILED.equals(endState)) {
				return GoalStatus.PLAN_FAILED;
			} else if (EndState.SUCCESSFUL.equals(endState)) {
				return GoalStatus.ACHIEVED;
			} else {
				return GoalStatus.TRYING_TO_ACHIEVE;
			}
		}
	}

	/**
	 * Sets this intention as no longer desired. It stops the current plan
	 * execution. It changes the goal status from {@link GoalStatus#WAITING},
	 * {@link GoalStatus#PLAN_FAILED} or {@link GoalStatus#TRYING_TO_ACHIEVE} to
	 * {@link GoalStatus#NO_LONGER_DESIRED}.
	 */
	public synchronized void noLongerDesire() {
		GoalStatus status = getStatus();
		switch (status) {
		case WAITING:
			this.noLongerDesired = true;
			if (currentPlan != null) {
				this.currentPlan.stop();
				this.currentPlan = null;
			}
			break;
		case TRYING_TO_ACHIEVE:
			this.noLongerDesired = true;
			this.currentPlan.stop();
			this.currentPlan = null;
			break;
		case PLAN_FAILED:
			this.noLongerDesired = true;
			this.executedPlans.add(this.currentPlan.getPlan());
			this.currentPlan = null;
			break;
		default:
			assert false : status;
			break;
		}
	}

	/**
	 * Removes a goal listener, so it will not be notified about the goal events
	 * anymore.
	 * 
	 * @param goalListener
	 *            the goal listener to be removed.
	 */
	public void removeGoalListener(GoalListener goalListener) {
		synchronized (goalListeners) {
			goalListeners.remove(goalListener);
		}
	}

	/**
	 * Makes this intention starts to try to achieve the goal. It changes the
	 * goal status from {@link GoalStatus#WAITING} or
	 * {@link GoalStatus#PLAN_FAILED} to {@link GoalStatus#TRYING_TO_ACHIEVE}.
	 */
	public synchronized void tryToAchive() {
		GoalStatus status = getStatus();
		switch (status) {
		case TRYING_TO_ACHIEVE:
			break;
		case WAITING:
			this.waiting = false;
			if (currentPlan != null) {
				this.currentPlan.restart();
			} else {
				dispatchPlan();
			}
			break;
		case PLAN_FAILED:
			this.executedPlans.add(this.currentPlan.getPlan());
			this.currentPlan = null;
			dispatchPlan();
			break;
		default:
			assert false : status;
			break;
		}
	}

}
