//----------------------------------------------------------------------------
// 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.belief;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import bdi4jade.core.Capability;
import bdi4jade.event.BeliefEvent;
import bdi4jade.event.BeliefEvent.Action;
import bdi4jade.event.BeliefListener;
import bdi4jade.exception.BeliefAlreadyExistsException;

/**
 * This class represents a belief base of a capability. It aggregates its
 * knowledge.
 * 
 * @author Ingrid Nunes
 */
public class BeliefBase implements Serializable {

	private static final long serialVersionUID = -6411530721625492882L;

	private final Set<BeliefListener> beliefListeners;
	private final Map<Object, Belief<?, ?>> beliefs;
	private final Map<Class<?>, Set<Belief<?, ?>>> beliefsByType;
	private Capability capability;

	/**
	 * The default constructor. It should be only used if persistence frameworks
	 * are used.
	 */
	protected BeliefBase() {
		this.beliefListeners = new HashSet<>();
		this.beliefs = new HashMap<>();
		this.beliefsByType = new HashMap<>();
	}

	/**
	 * Creates a belief base associated with a capability.
	 * 
	 * @param capability
	 *            the capability to which this belief base belongs.
	 */
	public BeliefBase(final Capability capability) {
		this(capability, null);
	}

	/**
	 * Creates a belief base associated with a capability and adds the beliefs
	 * in the provided belief set as the initial beliefs of this belief base.
	 * 
	 * @param capability
	 *            the capability to which this belief base belongs.
	 * @param beliefs
	 *            the initial beliefs.
	 */
	public BeliefBase(final Capability capability, Set<Belief<?, ?>> beliefs) {
		if (capability == null)
			throw new NullPointerException("Capability must be not null.");

		this.capability = capability;
		this.beliefListeners = new HashSet<>();
		this.beliefs = new HashMap<>();
		this.beliefsByType = new HashMap<>();
		if (beliefs != null) {
			for (Belief<?, ?> belief : beliefs) {
				putBelief(belief);
			}
		}
	}

	/**
	 * Adds a belief to the belief base.
	 * 
	 * @param belief
	 *            the belief to be added.
	 */
	public void addBelief(Belief<?, ?> belief) {
		if (!hasBelief(belief.getName())) {
			belief.addBeliefBase(this);
			putBelief(belief);
			notifyBeliefChanged(new BeliefEvent(belief, Action.BELIEF_ADDED));
		} else {
			throw new BeliefAlreadyExistsException(belief);
		}
	}

	/**
	 * Adds a belief listener to be notified about changes in the belief base.
	 * 
	 * @param beliefListener
	 *            the listener to be added.
	 */
	public void addBeliefListener(BeliefListener beliefListener) {
		synchronized (beliefListeners) {
			this.beliefListeners.add(beliefListener);
		}
	}

	/**
	 * Adds a belief to the belief base. It updates the belief value, if it
	 * already exists.
	 * 
	 * @param belief
	 *            the belief to be added or updated.
	 */
	public void addOrUpdateBelief(Belief<?, ?> belief) {
		if (hasBelief(belief.getName())) {
			updateBelief(belief.getName(), belief.getValue());
		} else {
			addBelief(belief);
		}
	}

	/**
	 * Retrieves a belief from the belief base. If this belief base does not
	 * contain it, the method checks whole-capabilities' belief base
	 * recursively.
	 * 
	 * @param name
	 *            the name of the belief to be retrieved.
	 * @return the belief, or null if no belief is found.
	 */
	public Belief<?, ?> getBelief(Object name) {
		Belief<?, ?> belief = this.beliefs.get(name);
		if (belief == null && capability.getWholeCapability() != null) {
			belief = capability.getWholeCapability().getBeliefBase()
					.getBelief(name);
		}
		return belief;
	}

	/**
	 * Returns all the current belief listeners of this belief base.
	 * 
	 * @return the belief listeners.
	 */
	public Set<BeliefListener> getBeliefListeners() {
		synchronized (beliefListeners) {
			return new HashSet<>(beliefListeners);
		}
	}

	/**
	 * Gets all beliefs of this belief base and the belief bases of the
	 * whole-capabilities of the capability that this belief base belongs to.
	 * 
	 * @return the beliefs of this capability and all of its whole-capabilities.
	 */
	public Collection<Belief<?, ?>> getBeliefs() {
		Collection<Belief<?, ?>> beliefs = new LinkedList<>();
		getBeliefs(beliefs);
		return beliefs;
	}

	/**
	 * This is a recursive method to implement the {@link #getAllBeliefs()}
	 * method.
	 * 
	 * @param beliefs
	 *            the set to which beliefs are added.
	 */
	private void getBeliefs(final Collection<Belief<?, ?>> beliefs) {
		beliefs.addAll(this.beliefs.values());
		if (capability.getWholeCapability() != null) {
			capability.getWholeCapability().getBeliefBase().getBeliefs(beliefs);
		}
	}

	/**
	 * Returns all beliefs whose name is of the given class or any other class
	 * that is assignable to this class. It also searches beliefs in belief
	 * bases of whole capabilities.
	 * 
	 * @param beliefNameType
	 *            the class of the name of beliefs.
	 * @return the set of beliefs assignable from the given class.
	 */
	public Set<Belief<?, ?>> getBeliefsAssignableFrom(Class<?> beliefNameType) {
		Set<Belief<?, ?>> beliefs = new HashSet<>();
		getBeliefsAssignableFrom(beliefNameType, beliefs);
		return beliefs;
	}

	private void getBeliefsAssignableFrom(Class<?> beliefNameType,
			Collection<Belief<?, ?>> beliefs) {
		for (Class<?> beliefSupertype : beliefsByType.keySet()) {
			if (beliefSupertype.isAssignableFrom(beliefNameType)) {
				Set<Belief<?, ?>> beliefsOfType = beliefsByType
						.get(beliefSupertype);
				beliefs.addAll(beliefsOfType);
			}
		}

		if (capability.getWholeCapability() != null) {
			capability.getWholeCapability().getBeliefBase()
					.getBeliefsAssignableFrom(beliefNameType, beliefs);
		}
	}

	/**
	 * Returns all beliefs whose name is of the given class. It also searches
	 * beliefs in belief bases of whole capabilities.
	 * 
	 * @param beliefNameType
	 *            the class of the name of beliefs.
	 * @return the set of beliefs of the given class.
	 */
	public Set<Belief<?, ?>> getBeliefsByType(Class<?> beliefNameType) {
		Set<Belief<?, ?>> beliefs = new HashSet<>();
		getBeliefsByType(beliefNameType, beliefs);
		return beliefs;
	}

	private void getBeliefsByType(Class<?> beliefNameType,
			Collection<Belief<?, ?>> beliefs) {
		Set<Belief<?, ?>> beliefsOfType = beliefsByType.get(beliefNameType);
		if (beliefsOfType != null) {
			beliefs.addAll(beliefsOfType);
		}

		if (capability.getWholeCapability() != null) {
			capability.getWholeCapability().getBeliefBase()
					.getBeliefsByType(beliefNameType, beliefs);
		}
	}

	/**
	 * Returns the capability with which this belief base is associated.
	 * 
	 * @return the capability.
	 */
	public Capability getCapability() {
		return capability;
	}

	/**
	 * Gets all beliefs of this specific belief base.
	 * 
	 * @return the beliefs
	 */
	public Set<Belief<?, ?>> getLocalBeliefs() {
		return new HashSet<>(beliefs.values());
	}

	/**
	 * Returns all beliefs whose name is of the given class or any other class
	 * that is assignable to this class.
	 * 
	 * @param beliefNameType
	 *            the class of the name of beliefs.
	 * @return the set of beliefs assignable from the given class.
	 */
	public Set<Belief<?, ?>> getLocalBeliefsAssignableFrom(
			Class<?> beliefNameType) {
		Set<Belief<?, ?>> beliefs = new HashSet<>();
		for (Class<?> beliefsubtype : beliefsByType.keySet()) {
			if (beliefNameType.isAssignableFrom(beliefsubtype)) {
				Set<Belief<?, ?>> beliefsOfType = beliefsByType
						.get(beliefsubtype);
				beliefs.addAll(beliefsOfType);
			}
		}
		return beliefs;
	}

	/**
	 * Returns beliefs whose name is of the given class.
	 * 
	 * @param beliefNameType
	 *            the class of the name of beliefs.
	 * @return the set of beliefs of the given class.
	 */
	public Set<Belief<?, ?>> getLocalBeliefsByType(Class<?> beliefNameType) {
		Set<Belief<?, ?>> beliefs = new HashSet<>();
		Set<Belief<?, ?>> beliefsOfType = beliefsByType.get(beliefNameType);
		if (beliefsOfType != null) {
			beliefs.addAll(beliefsOfType);
		}
		return beliefs;
	}

	/**
	 * Returns a list of belief values from this belief base.
	 * 
	 * @return the beliefValues
	 */
	public List<Object> getLocalBeliefValues() {
		List<Object> beliefValues = new ArrayList<>(beliefs.size());
		for (Belief<?, ?> belief : beliefs.values())
			beliefValues.add(belief.getValue());
		return beliefValues;
	}

	/**
	 * Checks whether a belief is part of the belief base. If this belief base
	 * does not contain it, the method checks whole-capabilities' belief base
	 * recursively.
	 * 
	 * @param name
	 *            the belief to be checked
	 * @return true if the belief base contains the belief.
	 */
	public boolean hasBelief(Object name) {
		boolean hasBelief = this.beliefs.containsKey(name);
		if (!hasBelief && capability.getWholeCapability() != null) {
			hasBelief = capability.getWholeCapability().getBeliefBase()
					.hasBelief(name);
		}
		return hasBelief;
	}

	/**
	 * Checks whether a belief is part of the belief base with an specific
	 * value. If this belief base does not contain it, the method checks
	 * whole-capabilities' belief base recursively.
	 * 
	 * @param name
	 *            the belief to be checked
	 * @param value
	 *            the value to be checked
	 * @return true if the belief base contains the belief.
	 */
	public boolean hasBelief(Object name, Object value) {
		Belief<?, ?> belief = getBelief(name);
		if (belief == null) {
			return false;
		}
		if (belief.getValue() == null) {
			return value == null;
		} else {
			return belief.getValue() == value;
		}
	}

	/**
	 * Notifies the capability associated with this belief base that a belief
	 * was modified. It also recursively notifies belief listeners of part
	 * capabilities.
	 * 
	 * @param beliefChanged
	 *            the belief that was changed
	 */
	protected void notifyBeliefChanged(BeliefEvent beliefChanged) {
		Set<BeliefListener> beliefListeners = getBeliefListeners();
		for (BeliefListener beliefListener : beliefListeners) {
			beliefListener.eventOccurred(beliefChanged);
		}
		for (Capability part : capability.getPartCapabilities()) {
			part.getBeliefBase().notifyBeliefChanged(beliefChanged);
		}
	}

	private void putBelief(Belief<?, ?> belief) {
		Class<?> beliefNameType = belief.getName().getClass();
		Set<Belief<?, ?>> beliefTypeSet = beliefsByType.get(beliefNameType);
		if (beliefTypeSet == null) {
			beliefTypeSet = new HashSet<>();
			beliefsByType.put(beliefNameType, beliefTypeSet);
		}
		beliefTypeSet.add(belief);
		this.beliefs.put(belief.getName(), belief);
	}

	/**
	 * Removes a belief from the belief base. If this belief base does not
	 * contain it, the method checks whole-capabilities' belief base recursively
	 * to remove this belief..
	 * 
	 * @param name
	 *            the name of the belief to be removed.
	 * @return the belief was removed, null if it is not part of the belief
	 *         base.
	 */
	public Belief<?, ?> removeBelief(Object name) {
		Belief<?, ?> belief = this.beliefs.remove(name);
		if (belief != null) {
			Class<?> beliefNameType = belief.getName().getClass();
			Set<Belief<?, ?>> beliefTypeSet = beliefsByType.get(beliefNameType);
			boolean removed = beliefTypeSet.remove(belief);
			assert removed;
			if (beliefTypeSet.isEmpty()) {
				beliefsByType.remove(beliefNameType);
			}
			belief.removeBeliefBase(this);
			notifyBeliefChanged(new BeliefEvent(belief, Action.BELIEF_REMOVED));
		} else {
			if (capability.getWholeCapability() != null) {
				belief = capability.getWholeCapability().getBeliefBase()
						.removeBelief(name);
			}
		}
		return belief;
	}

	/**
	 * Removes a belief listener.
	 * 
	 * @param beliefListener
	 *            the listener to be removed.
	 */
	public void removeBeliefListener(BeliefListener beliefListener) {
		synchronized (beliefListeners) {
			this.beliefListeners.remove(beliefListener);
		}
	}

	/**
	 * Associates a capability with this belief base. Ideally, the capability
	 * should be final and initialized in the constructor. This method should be
	 * only used if persistence frameworks are used.
	 * 
	 * @param capability
	 *            the capability to set.
	 */
	protected void setCapability(Capability capability) {
		this.capability = capability;
	}

	/**
	 * Gets the size of this specific belief base (the number of beliefs).
	 * 
	 * @return the size of this belief base.
	 */
	public int size() {
		return this.beliefs.size();
	}

	/**
	 * Returns this belief base as a string in the form:
	 * "Belief base of Capability ID = [ BELIEFS ]".
	 * 
	 * @return the string representation of this belief base.
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer("Belief base of Capability ");
		if (capability == null)
			sb.append(" NO ID");
		else
			sb.append(capability.getId());
		sb.append(" = ").append(beliefs.values());
		return sb.toString();
	}

	/**
	 * Updates the value of a belief in the belief base. In case the belief is
	 * not present in the belief base (of in its whole-capabilities' belief
	 * bases), nothing is performed and the method returns false. If the type of
	 * the new value being provided does not match the current type, the method
	 * still subscribes the previous value.
	 * 
	 * @param name
	 *            the belief to be updated.
	 * @param value
	 *            the new value to the belief.
	 * @return true if the belief was updated.
	 */
	@SuppressWarnings("unchecked")
	public boolean updateBelief(Object name, Object value) {
		Belief belief = this.beliefs.get(name);
		if (belief != null) {
			belief.setValue(value);
			return true;
		} else if (capability.getWholeCapability() != null) {
			return capability.getWholeCapability().getBeliefBase()
					.updateBelief(name, value);
		}
		return false;
	}

}
