//----------------------------------------------------------------------------
// Copyright (C) 2018  João Faccin
// 
// 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.extension.undo;

import java.util.HashMap;
import java.util.Map;

import bdi4jade.belief.PredicateBelief;
import bdi4jade.core.BDIAgent;
import bdi4jade.core.Capability;
import bdi4jade.core.Intention;
import bdi4jade.event.BeliefEvent;
import bdi4jade.event.BeliefEvent.Action;
import bdi4jade.event.BeliefListener;
import bdi4jade.event.GoalEvent;
import bdi4jade.event.GoalListener;
import bdi4jade.extension.remediation.logics.Predicate;
import bdi4jade.extension.undo.reasoning.RevertingOptionGenerationFunction;
import bdi4jade.goal.Goal;
import bdi4jade.goal.GoalStatus;
import bdi4jade.goal.PredicateGoal;

/**
 * This class is an extension of {@link Capability}. It implements the
 * {@link BeliefListener} and {@link GoalListener} interfaces and has the
 * {@link RevertingOptionGenerationFunction} as its default option generation
 * function. This capability provides agents with the ability to revert actions
 * performed with the aim of achieving particular goals. For this purpose, it
 * stores a mapping between goals and their corresponding achievement metadata,
 * as well as a mapping between child and parent goals.
 * 
 * @author João Faccin
 */
public class RevertingCapability extends Capability implements BeliefListener, GoalListener {

	private static final long serialVersionUID = -5556551069331273755L;
	private final Map<Goal, GoalAchievementMetadata> gams;

	protected Map<Goal, Goal> parentGoals;

	/**
	 * Creates a new capability and sets {@link RevertingOptionGenerationFunction}
	 * as its default option generation function.
	 */
	public RevertingCapability(BDIAgent agent) {
		this.gams = new HashMap<>();
		this.parentGoals = new HashMap<>();
		setOptionGenerationFunction(new RevertingOptionGenerationFunction(this));
		agent.addGoalListener(this);
		this.getBeliefBase().addBeliefListener(this);
	}

	/**
	 * Adds a mapping between the goal and the goal achievement metadata to the
	 * existing map.
	 * 
	 * @param goal
	 *            the goal to be associated with the goal achievement metadata.
	 * @param gam
	 *            the goal achievement metadata to which the goal will be
	 *            associated.
	 */
	public void addGoalAchievementMetadata(Goal goal, GoalAchievementMetadata gam) {
		this.gams.put(goal, gam);
	}

	/**
	 * Adds a mapping between the child goal and its parent to the existing map.
	 * 
	 * @param child
	 *            the child goal to be associated with the parent.
	 * @param parent
	 *            the parent goal to which the child will be associated.
	 */
	public void addParentGoal(Goal child, Goal parent) {
		this.parentGoals.put(child, parent);
	}

	/**
	 * Creates a new {@link GoalAchievementMetadata} with its related goal and
	 * intention, and adds the mapping between the goal and the goal achievement
	 * metadata to the existing map.
	 * 
	 * @param goal
	 *            the goal related to the goal achievement metadata.
	 * @param intention
	 *            the intention related to the goal achievement metadata.
	 * @return the created goal achievement metadata.
	 */
	public GoalAchievementMetadata createGoalAchievementMetadata(PredicateGoal<?> goal, Intention intention) {
		GoalAchievementMetadata gam = new GoalAchievementMetadata(goal, intention);
		addGoalAchievementMetadata(goal, gam);
		return gam;
	}

	/**
	 * Creates a new {@link GoalAchievementMetadata} with its related goal,
	 * intention, and rollback flag. It also adds the mapping between the goal and
	 * the goal achievement metadata to the existing map.
	 * 
	 * @param goal
	 *            the goal related to the goal achievement metadata
	 * @param intention
	 *            the intention related to the goal achievement metadata.
	 * @param rollback
	 *            a flag that indicates the desire of a rollback in case of a plan
	 *            failure.
	 * @return the created goal achievement metadata.
	 */
	public GoalAchievementMetadata createGoalAchievementMetadata(PredicateGoal<?> goal, Intention intention,
			Boolean rollback) {
		GoalAchievementMetadata gam = new GoalAchievementMetadata(goal, intention, rollback);
		addGoalAchievementMetadata(goal, gam);
		return gam;
	}

	/**
	 * Creates a new {@link GoalAchievementMetadata} with its related goal,
	 * intention, reversion trigger, rollback flag and discarding conditions. It
	 * also adds the mapping between the goal and the goal achievement metadata to
	 * the existing map.
	 * 
	 * @param goal
	 *            the goal related to the goal achievement metadata
	 * @param intention
	 *            the intention related to the goal achievement metadata.
	 * @param rollback
	 *            a flag that indicates the desire of a rollback in case of a plan
	 *            failure.
	 * @param reversionTrigger
	 *            a list of {@link Predicate} that specifies the condition in which
	 *            a reversion will be activated.
	 * @param maxExecutedPlans
	 *            the number of plans that can be executed after achieving the
	 *            related goal before this goal achievement metadata is discarded.
	 * @param maxTime
	 *            the time that can pass after achieving the related goal before
	 *            this goal achievement metadata is discarded.
	 * @return the created goal achievement metadata.
	 */
	public GoalAchievementMetadata createGoalAchievementMetadata(PredicateGoal<?> goal, Intention intention,
			Boolean rollback, Map<Predicate, Boolean> reversionTrigger, Integer maxExecutedPlans, Integer maxTime) {
		GoalAchievementMetadata gam = new GoalAchievementMetadata((PredicateGoal<?>) goal, intention, rollback,
				reversionTrigger, maxExecutedPlans, maxTime);
		addGoalAchievementMetadata(goal, gam);
		return gam;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see bdi4jade.event.BeliefListener#eventOccurred(bdi4jade.event.BeliefEvent)
	 */
	@Override
	public void eventOccurred(BeliefEvent beliefEvent) {
		if (beliefEvent.getAction().equals(Action.BELIEF_UPDATED) && beliefEvent.getArgs() != null) {
			Object args = beliefEvent.getArgs();
			if (args instanceof PredicateGoal<?>) {
				PredicateGoal<?> goal = (PredicateGoal<?>) args;
				if (beliefEvent.getBelief() instanceof PredicateBelief<?>) {
					PredicateBelief<?> belief = (PredicateBelief<?>) beliefEvent.getBelief();
					Goal oldestParent = getOldestReversibleParent(goal);
					if (oldestParent != null) {
						this.gams.get(oldestParent).trackBeliefChange((Predicate) belief.getName(), belief.getValue());
					}
				}
			}
		}
	}

	/**
	 * Returns the mapping between goals and their corresponding goal achievement
	 * metadata.
	 * 
	 * @return the goal achievement metadata map.
	 */
	public Map<Goal, GoalAchievementMetadata> getGoalAchievementMetadata() {
		return this.gams;
	}

	/**
	 * Returns the oldest parent of goal with a mapping in the goal achievement map.
	 * If there is no such parent and the goal is in the mapping, returns the goal
	 * itself, otherwise, returns null.
	 * 
	 * @param goal
	 *            the goal whose oldest reversible parent is to be found.
	 * @return the oldest reversible parent of goal if it exists, or null,
	 *         otherwise.
	 */
	private Goal getOldestReversibleParent(Goal goal) {
		Goal parent = null;
		Goal aux = goal;

		if (this.gams.containsKey(goal)) {
			parent = goal;
		}

		while (this.parentGoals.containsKey(aux)) {
			aux = this.parentGoals.get(aux);
			if (this.gams.containsKey(aux)) {
				parent = aux;
			}
		}

		return parent;
	}

	/**
	 * Returns the mapping between child goals and their parents.
	 * 
	 * @return the parentGols map.
	 */
	public Map<Goal, Goal> getParentGoals() {
		return parentGoals;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see bdi4jade.event.GoalListener#goalPerformed(bdi4jade.event.GoalEvent)
	 */
	@Override
	public void goalPerformed(GoalEvent event) {
		if (!event.isGoalAdded()) {
			for (GoalAchievementMetadata gam : this.gams.values()) {
				if (gam.getIntention().getStatus().equals(GoalStatus.ACHIEVED) && !event.getGoal().equals(gam.getGoal())
						&& event.getStatus().equals(GoalStatus.ACHIEVED)) {
					gam.incrementPlanCounter();
				}
			}
		}
	}
}
