//----------------------------------------------------------------------------
// 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.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import bdi4jade.belief.BeliefBase;
import bdi4jade.core.Intention;
import bdi4jade.extension.remediation.logics.Predicate;
import bdi4jade.goal.GoalStatus;
import bdi4jade.goal.PredicateGoal;

/**
 * This class stores the information regarding changes performed during the
 * achievement of a related {@link Goal} in order to allow such changes to be
 * later reverted.
 * 
 * @author João Faccin
 */
public class GoalAchievementMetadata {

	private final static Integer MAX_EXECUTED_PLANS = 500;
	private final static Integer MAX_TIME = Integer.MAX_VALUE;
	private Map<Predicate, List<Pair<Boolean, Long>>> beliefChangeTrace;
	private Long createdTime;
	private PredicateGoal<?> goal;
	private Intention intention;
	private Integer maxExecutedPlans;
	private Integer maxTime;
	private Integer planCounter;
	private Map<Predicate, Boolean> reversionTrigger;
	private Boolean rollback;

	/**
	 * Initializes a goal achievement metadata with its related {@link Goal} and
	 * {@link Intention}.
	 * 
	 * @param goal
	 *            the goal related to the goal achievement metadata.
	 * @param intention
	 *            the intention related to the goal achievement metadata.
	 */
	public GoalAchievementMetadata(PredicateGoal<?> goal, Intention intention) {
		this(goal, intention, true);
	}

	/**
	 * Initializes a goal achievement metadata with its related {@link Goal} and
	 * {@link Intention}. It also sets the rollback flag.
	 * 
	 * @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.
	 */
	public GoalAchievementMetadata(PredicateGoal<?> goal, Intention intention, Boolean rollback) {
		this(goal, intention, rollback, null, MAX_EXECUTED_PLANS, MAX_TIME);
	}

	/**
	 * Initializes a goal achievement metadata with its related goal and intention.
	 * It sets a reversion trigger (if it is not null), a rollback flag, and the
	 * discarding conditions (maxExecutedPlans and maxTime).
	 * 
	 * @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 map of {@link Predicate} and boolean values 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.
	 */
	public GoalAchievementMetadata(PredicateGoal<?> goal, Intention intention, Boolean rollback,
			Map<Predicate, Boolean> reversionTrigger, Integer maxExecutedPlans, Integer maxTime) {
		// TODO Refactor: Created time must be refactored to achieved time.
		this.createdTime = System.currentTimeMillis();
		this.beliefChangeTrace = new HashMap<>();
		this.goal = goal;
		this.intention = intention;
		this.maxExecutedPlans = maxExecutedPlans;
		this.maxTime = maxTime;
		this.planCounter = 0;
		this.reversionTrigger = new HashMap<>();
		this.rollback = rollback;

		if (reversionTrigger != null) {
			this.reversionTrigger = reversionTrigger;
		}
	}

	/**
	 * Selects stored changes that must be actually reverted. It selects only those
	 * changes that do not correspond to the goal itself and whose first and last
	 * changed values are equal to each other.
	 * 
	 * @param beliefBase
	 *            the belief base.
	 * @return the list of {@link Predicate} to be reverted.
	 */
	public ArrayList<Predicate> filterBeliefChanges(BeliefBase beliefBase) {
		ArrayList<Predicate> filteredBeliefs = new ArrayList<>();
		PredicateGoal<?> goal = (PredicateGoal<?>) this.goal;

		for (Predicate predicate : this.getBeliefChangeTrace().keySet()) {
			LinkedList<Pair<Boolean, Long>> trace = (LinkedList<Pair<Boolean, Long>>) this.getBeliefChangeTrace()
					.get(predicate);
			if (!predicate.equals(goal.getBeliefName()) || (predicate.equals(goal.getBeliefName())
					&& !goal.getValue().equals(predicate.getValue(beliefBase)))) {
				if (trace.getFirst().getKey().equals(trace.getLast().getKey())) {
					filteredBeliefs.add(predicate);
				}
			}
		}
		return filteredBeliefs;
	}

	/**
	 * Returns the mapping of predicates and their corresponding list of changes.
	 * 
	 * @return the beliefChangeTrace.
	 */
	public Map<Predicate, List<Pair<Boolean, Long>>> getBeliefChangeTrace() {
		return beliefChangeTrace;
	}

	/**
	 * Returns the time in which the goal related to this goal achievement metadata
	 * was achieved.
	 * 
	 * @return the createdTime.
	 */
	public Long getCreatedTime() {
		return createdTime;
	}

	/**
	 * Returns the {@link PredicateGoal} related to this goal achievement metadata.
	 * 
	 * @return the goal.
	 */
	public PredicateGoal<?> getGoal() {
		return goal;
	}

	/**
	 * Returns the {@link Intention} related to this goal achievement metadata.
	 * 
	 * @return the intention.
	 */
	public Intention getIntention() {
		return intention;
	}

	/**
	 * Returns the maximum number of successfully executed plans after the
	 * achievement of the related goal before the reversion process becomes
	 * deactivated.
	 * 
	 * @return the maxExecutedPlans.
	 */
	public Integer getMaxExecutedPlans() {
		return maxExecutedPlans;
	}

	/**
	 * Returns the maximum time passed since the achievement of the related goal
	 * before the reversion process becomes deactivated.
	 * 
	 * @return the maxTime.
	 */
	public Integer getMaxTime() {
		return maxTime;
	}

	/**
	 * Returns the number of successfully executed plans after the achievement of
	 * the related goal.
	 * 
	 * @return the planCounter.
	 */
	public Integer getPlanCounter() {
		return planCounter;
	}

	/**
	 * Return a list of predicates that indicates the condition in which the
	 * reversion process is activated.
	 * 
	 * @return the reversionTrigger.
	 */
	public Map<Predicate, Boolean> getReversionTrigger() {
		return reversionTrigger;
	}

	/**
	 * Returns a boolean value that indicates the desire of a rollback in case of
	 * plan failure.
	 * 
	 * @return true if the rollback is desired.
	 */
	public Boolean getRollback() {
		return rollback;
	}

	/**
	 * Increments the counter of successfully executed plans after the achievement
	 * of the related goal.
	 */
	public void incrementPlanCounter() {
		this.planCounter++;
	}

	/**
	 * Returns a boolean value that indicates if the reversion process is activated.
	 * It returns true if its trigger condition is achieved or a rollback is
	 * triggered due to a plan failure.
	 * 
	 * @param beliefBase
	 *            the {@link BeliefBase} in which reversionTrigger will be
	 *            evaluated.
	 * @return true if the reversion process is activated.
	 */
	public Boolean isReversionActivated(BeliefBase beliefBase) {
		return isReversionTriggered(beliefBase) || isRollbackTriggered();
	}

	/**
	 * Returns a boolean value that indicates if reversion is deactivated. It
	 * returns true if planCounter exceeds maxExecutedPlans or the time passed since
	 * createdTime exceeds maxTime.
	 * 
	 * @return true if reversion is deactivated.
	 */
	public Boolean isReversionDeactivated() {
		if (this.planCounter > this.maxExecutedPlans) {
			return true;
		}

		if ((System.currentTimeMillis() - this.createdTime) > this.maxTime) {
			return true;
		}

		return this.intention.getStatus().isFinished() && !this.intention.getStatus().equals(GoalStatus.ACHIEVED);
	}

	/**
	 * Returns a boolean value that indicates if the reversion process is activated
	 * due to the achievement of its trigger condition. It returns true if every
	 * predicate into reversionTrigger holds true in the given {@link BeliefBase}.
	 * 
	 * @param beliefBase
	 *            the {@link BeliefBase} in which reversionTrigger will be
	 *            evaluated.
	 * @return true if reversionTrigger holds true in beliefBase.
	 */
	public Boolean isReversionTriggered(BeliefBase beliefBase) {
		Boolean triggered = true;

		if (!this.intention.getStatus().equals(GoalStatus.ACHIEVED) || this.reversionTrigger.isEmpty()) {
			triggered = false;
		} else {
			for (Predicate predicate : this.reversionTrigger.keySet()) {
				Boolean value = predicate.getValue(beliefBase);
				if (value == null || value != this.reversionTrigger.get(predicate)) {
					triggered = false;
				}
			}
		}

		return triggered;
	}

	/**
	 * Returns a boolean value that indicates if the reversion process is activated
	 * due to a plan failure. It returns true if a plan fails to achieve the related
	 * goal and a rollback is desired.
	 * 
	 * @return true if a rollback is triggered.
	 */
	public Boolean isRollbackTriggered() {
		return this.rollback && this.intention.getStatus().equals(GoalStatus.PLAN_FAILED);
	}

	/**
	 * Sets the intention related to this goal achievement metadata.
	 * 
	 * @param intention
	 *            the intention to be set.
	 */
	public void setIntention(Intention intention) {
		this.intention = intention;
	}

	/**
	 * Sets the reversion trigger that indicates the condition in which recorded
	 * belief changes must be reverted.
	 * 
	 * @param reversionTrigger
	 *            the reversionTrigger to be set.
	 */
	public void setReversionTrigger(HashMap<Predicate, Boolean> reversionTrigger) {
		this.reversionTrigger = reversionTrigger;
	}

	/**
	 * Records a change of the evaluation value of a given predicate, which was made
	 * during the achievement of the goal related to this goal achievement metadata.
	 * It also stores the time in which the change was recorded.
	 * 
	 * @param predicate
	 *            the predicate whose evaluation value changed.
	 * @param value
	 *            the new evaluation value of the given predicate.
	 */
	public void trackBeliefChange(Predicate predicate, Boolean value) {
		if (!this.beliefChangeTrace.containsKey(predicate)) {
			this.beliefChangeTrace.put(predicate, new LinkedList<Pair<Boolean, Long>>());
		}
		LinkedList<Pair<Boolean, Long>> trace = (LinkedList<Pair<Boolean, Long>>) this.beliefChangeTrace.get(predicate);
		trace.add(new Pair<Boolean, Long>(value, System.currentTimeMillis()));
	}

	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append("< ").append(goal);
		sb.append(", maxExecutedPlans = ").append(maxExecutedPlans);
		sb.append(", maxTime = ").append(maxTime);
		sb.append(", \n reversionTrigger = ").append(reversionTrigger);
		sb.append(", \n beliefChangeTrace = ").append(beliefChangeTrace);
		sb.append(", rollback = ").append(rollback).append(" >");
		return sb.toString();
	}
}
