package org.apache.maven.werkz;

/* ====================================================================
 *   Copyright 2001-2005 The Apache Software Foundation.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 * ====================================================================
 */

/*
 $Id$

 Copyright 2002 (C) The Werken Company. All Rights Reserved.
 
 Redistribution and use of this software and associated documentation
 ("Software"), with or without modification, are permitted provided
 that the following conditions are met:

 1. Redistributions of source code must retain copyright
 statements and notices.  Redistributions must also contain a
 copy of this document.
 
 2. Redistributions in binary form must reproduce the
 above copyright notice, this list of conditions and the
 following disclaimer in the documentation and/or other
 materials provided with the distribution.
 
 3. The name "werkz" must not be used to endorse or promote
 products derived from this Software without prior written
 permission of The Werken Company.  For written permission,
 please contact bob@werken.com.
 
 4. Products derived from this Software may not be called "werkz"
 nor may "werkz" appear in their names without prior written
 permission of The Werken Company. "werkz" is a registered
 trademark of The Werken Company.
 
 5. Due credit should be given to "the werkz project"
 ( http://werkz.werken.com/ ).
 
 THIS SOFTWARE IS PROVIDED BY THE WERKEN COMPANY AND CONTRIBUTORS
 ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 THE WERKEN COMPANY OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 OF THE POSSIBILITY OF SUCH DAMAGE.
 
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * Abstract Goal to attain.
 * 
 * <p>
 * A <code>Goal</code> embodies both an action and the precursors for that
 * action. A <code>Goal</code>'s precursors will be satisfied before
 * attempting to perform the target <code>Goal</code>'s action. There may be
 * a case that once precursors have been satisfied there is no further action
 * required to be perform for a particular <code>Goal</code>.
 * </p>
 * 
 * <p>
 * A <code>Goal</code>'s postcursors are also tracked so that if a
 * <code>Goal</code>'s state has been changed and the <code>Goal</code>s
 * ahead of it in the hierarchy need to be notified, it can
 * <code>percolate</code> forward and have it's postcursors satisfied.
 * </p>
 * 
 * @see WerkzProject
 * @see Action
 * 
 * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
 */
public class Goal {
	// ------------------------------------------------------------
	// Constants
	// ------------------------------------------------------------

	/** Empty <code>Goal</code> array. */
	public static final Goal[] EMPTY_ARRAY = new Goal[0];

	// ------------------------------------------------------------
	// Class members
	// ------------------------------------------------------------

	/** Unique name. */
	private String name;

	/** Ordered list of precursor <code>Goal</code>s. */
	private List precursors;

	/** Ordered list of postcursor <code>Goal</code>s. */
	private List postcursors;

	/** Action to perform. */
	private Action action;

	/** Pre-goal callbacks. */
	private List preGoalCallbacks;

	/** Post-goal callbacks. */
	private List postGoalCallbacks;

	/** Pre-action callbacks. */
	private List preActionCallbacks;

	/** Post-action callbacks. */
	private List postActionCallbacks;

	/** Description of the goal (for auto-documenting). */
	private String description;

	// ------------------------------------------------------------
	// Constructors
	// ------------------------------------------------------------

	/**
	 * Construct a new <code>Goal</code> with the specified name.
	 * 
	 * @param name
	 *            The name of the <code>Goal</code>.
	 */
	public Goal(String name) {
		this.name = name;
		this.precursors = Collections.EMPTY_LIST;
		this.postcursors = Collections.EMPTY_LIST;

		this.preGoalCallbacks = Collections.EMPTY_LIST;
		this.postGoalCallbacks = Collections.EMPTY_LIST;
		this.preActionCallbacks = Collections.EMPTY_LIST;
		this.postActionCallbacks = Collections.EMPTY_LIST;
	}

	/**
	 * Construct a new <code>Goal</code> with the specified name and
	 * <code>Action</code>.
	 * 
	 * @param name
	 *            The name of the <code>Goal</code>.
	 * @param action
	 *            The <code>Action</code> for this <code>Goal</code>.
	 */
	public Goal(String name, Action action) {
		this(name);

		setAction(action);
	}

	// ------------------------------------------------------------
	// Instance methods
	// ------------------------------------------------------------

	/**
	 * Retrieve the name of this <code>Goal</code>.
	 * 
	 * @return This <code>Goal</code>'s name.
	 */
	public String getName() {
		return this.name;
	}

	/**
	 * Retrieve the description of this <code>Goal</code>.
	 * 
	 * @return The description of this goal.
	 */
	public String getDescription() {
		return this.description;
	}

	/**
	 * Set the description for this <code>Goal</code>.
	 * 
	 * @param description
	 *            The description of this goal.
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * Retrieve the <code>Action</code> of this <code>Goal</code>.
	 * 
	 * @return The <code>Action</code> of this <code>Goal</code>.
	 */
	public Action getAction() {
		return this.action;
	}

	/**
	 * Set the <code>Action</code> of this <code>Goal</code>.
	 * 
	 * @param action
	 *            The <code>Action</code> of this <code>Goal</code>.
	 */
	public void setAction(Action action) {
		this.action = action;
	}

	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	// PRE GOAL
	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	/**
	 * Add a <b>pre-goal</b> callback to this <code>Goal</code>.
	 * 
	 * @param callback
	 *            The callback to add.
	 */
	public void addPreGoalCallback(PreGoalCallback callback) {
		if (Collections.EMPTY_LIST.equals(this.preGoalCallbacks)) {
			this.preGoalCallbacks = new ArrayList(3);
		}

		this.preGoalCallbacks.add(callback);
	}

	/**
	 * Remove all occurences of a <b>pre-goal</b> callback from this
	 * <code>Goal</code>.
	 * 
	 * @param callback
	 *            The callback to remove.
	 */
	public void removePreGoalCallback(PreGoalCallback callback) {
		while (this.preGoalCallbacks.remove(callback)) {
			// intentionally left blank;
		}
	}

	/**
	 * Retrieve an unmodifiable list of the <b>pre-goal</b> callbacks.
	 * 
	 * @return An unmodifiable <code>List</code> of
	 *         <code>PreGoalCallback</code>s.
	 */
	public List getPreGoalCallbacks() {
		return Collections.unmodifiableList(this.preGoalCallbacks);
	}

	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	// POST GOAL
	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	/**
	 * Add a <b>post-goal</b> callback to this <code>Goal</code>.
	 * 
	 * @param callback
	 *            The callback to add.
	 */
	public void addPostGoalCallback(PostGoalCallback callback) {
		if (Collections.EMPTY_LIST.equals(this.postGoalCallbacks)) {
			this.postGoalCallbacks = new ArrayList(3);
		}

		this.postGoalCallbacks.add(callback);
	}

	/**
	 * Remove all occurences of a <b>post-goal</b> callback from this
	 * <code>Goal</code>.
	 * 
	 * @param callback
	 *            The callback to remove.
	 */
	public void removePostGoalCallback(PostGoalCallback callback) {
		while (this.postGoalCallbacks.remove(callback)) {
			// intentionally left blank;
		}
	}

	/**
	 * Retrieve an unmodifiable list of the <b>post-goal</b> callbacks.
	 * 
	 * @return An unmodifiable <code>List</code> of
	 *         <code>PostGoalCallback</code>s.
	 */
	public List getPostGoalCallbacks() {
		return Collections.unmodifiableList(this.postGoalCallbacks);
	}

	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	// PRE ACTION
	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	/**
	 * Add a <b>pre-action</b> callback to this <code>Goal</code>.
	 * 
	 * @param callback
	 *            The callback to add.
	 */
	public void addPreActionCallback(PreActionCallback callback) {
		if (this.preActionCallbacks.equals(Collections.EMPTY_LIST)) {
			this.preActionCallbacks = new ArrayList(3);
		}

		this.preActionCallbacks.add(callback);
	}

	/**
	 * Remove all occurences of a <b>pre-action</b> callback from this
	 * <code>Goal</code>.
	 * 
	 * @param callback
	 *            The callback to remove.
	 */
	public void removePreActionCallback(PreActionCallback callback) {
		while (this.preActionCallbacks.remove(callback)) {
			// intentionally left blank;
		}
	}

	/**
	 * Retrieve an unmodifiable list of the <b>pre-action</b> callbacks.
	 * 
	 * @return An unmodifiable <code>List</code> of
	 *         <code>PreActionCallback</code>s.
	 */
	public List getPreActionCallbacks() {
		return Collections.unmodifiableList(this.preActionCallbacks);
	}

	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	// POST ACTION
	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	/**
	 * Add a <b>post-action</b> callback to this <code>Goal</code>.
	 * 
	 * @param callback
	 *            The callback to add.
	 */
	public void addPostActionCallback(PostActionCallback callback) {
		if (this.postActionCallbacks.equals(Collections.EMPTY_LIST)) {
			this.postActionCallbacks = new ArrayList(3);
		}

		this.postActionCallbacks.add(callback);
	}

	/**
	 * Remove all occurences of a <b>post-action</b> callback from this
	 * <code>Goal</code>.
	 * 
	 * @param callback
	 *            The callback to remove.
	 */
	public void removePostActionCallback(PostActionCallback callback) {
		while (this.postActionCallbacks.remove(callback)) {
			// intentionally left blank;
		}
	}

	/**
	 * Retrieve an unmodifiable list of the <b>post-action</b> callbacks.
	 * 
	 * @return An unmodifiable <code>List</code> of
	 *         <code>PostActionCallback</code>s.
	 */
	public List getPostActionCallbacks() {
		return Collections.unmodifiableList(this.postActionCallbacks);
	}

	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	/**
	 * Determine if this <code>Goal</code> has been satisfied for the
	 * specified <code>Session</code>.
	 * 
	 * @param session
	 *            The <code>Session</code> context in which to test for goal
	 *            satisfaction.
	 * 
	 * @return <code>true</code> if this <code>Goal</code> has been
	 *         satisfied within the <code>Session</code>, otherwise
	 *         <code>false</code>.
	 */
	public boolean isSatisfied(Session session) {
		return session.isGoalSatisfied(this);
	}

	/**
	 * Add a precursor <code>Goal</code> to this <code>Goal</code>.
	 * 
	 * @param precursor
	 *            The precursor <code>Goal</code> that must be satisfied
	 *            before performing this <code>Goal</code>.
	 * 
	 * @throws CyclicGoalChainException
	 *             if adding the precursor would result in a cyclic dependency.
	 */
	public void addPrecursor(Goal precursor) throws CyclicGoalChainException {
		if (Collections.EMPTY_LIST.equals(this.precursors)) {
			this.precursors = new ArrayList();
		}

		this.precursors.add(precursor);

		try {
			checkForCycles();

			precursor.addInternalPostcursor(this);
		} catch (CyclicGoalChainException e) {
			e.fillInStackTrace();
			this.precursors.remove(precursor);
			throw e;
		}
	}

	/**
	 * Add a postcursor <code>Goal</code> to this <code>Goal</code>.
	 * 
	 * @param postcursor
	 *            The postcursor <code>Goal</code> that depends on this
	 *            <code>Goal</code>.
	 * 
	 * @throws CyclicGoalChainException
	 *             if adding the postcursor would result in a cyclic dependency.
	 */
	public void addPostcursor(Goal postcursor) throws CyclicGoalChainException {
		postcursor.addPrecursor(this);
	}

	/**
	 * Adds a postcursor <code>Goal</code> without looping back to the
	 * <code>postcursor.addPrecursor</code> and entering an infinite loop.
	 * 
	 * @param postcursor
	 *            The postcursor <code>Goal</code> that is already tracking
	 *            this <code>Goal</goal> as a precursor and is letting this 
	 *      <code>Goal</code> know that it is a postcursor.
	 */
	private void addInternalPostcursor(Goal postcursor) {
		if (Collections.EMPTY_LIST.equals(this.postcursors)) {
			this.postcursors = new ArrayList();
		}

		this.postcursors.add(postcursor);
	}

	/**
	 * Retrieve an unmodifiable <code>List</code> of this <code>Goal</code>'s
	 * precursor <code>Goal</code>s.
	 * 
	 * @return The <code>List<code> of precursor <code>Goal</code>s.
	 */
	public List getPrecursors() {
		return Collections.unmodifiableList(this.precursors);
	}

	/**
	 * Retrive an unmodifiable <code>List</code> of this <code>Goal</code>'s
	 * postcursor <code>Goal</code>s.
	 * 
	 * @return The <code>List</code> of postcursor <code>Goal</code>s.
	 */
	public List getPostcursors() {
		return Collections.unmodifiableList(this.postcursors);
	}

	/**
	 * Attempt to attain this <code>Goal</code>'s precursor <code>Goal</code>s.
	 * 
	 * @param session
	 *            The context in which to attain goals.
	 * 
	 * @throws UnattainableGoalException
	 *             if unable to satisfy a precursor.
	 * @throws NoActionDefinitionException
	 *             if this goal contains no action definition.
	 */
	void attainPrecursors(Session session) throws UnattainableGoalException,
			NoActionDefinitionException {
		Iterator precursorIter = getPrecursors().iterator();
		Goal eachPrereq = null;

		while (precursorIter.hasNext()) {
			eachPrereq = (Goal) precursorIter.next();

			eachPrereq.attain(session);
		}
	}

	/**
	 * Attempt to attain this <code>Goal</code>'s postcursor
	 * <code>Goal</code>s.
	 * 
	 * @param session
	 *            The context in which to attain goals.
	 * 
	 * @throws UnattainableGoalException
	 *             if unable to satisfy a postcursor.
	 * @throws NoActionDefinitionException
	 *             if this goal contains no action definition.
	 */
	void percolatePostcursors(Session session)
			throws UnattainableGoalException, NoActionDefinitionException {
		Iterator postreqIter = getPostcursors().iterator();
		Goal eachPostreq = null;

		while (postreqIter.hasNext()) {
			eachPostreq = (Goal) postreqIter.next();

			eachPostreq.percolate(session);
		}
	}

	/**
	 * Perform a cyclic dependency check.
	 * 
	 * @throws CyclicGoalChainException
	 *             if a dependency cycle is discovered.
	 */
	void checkForCycles() throws CyclicGoalChainException {
		Set visited = new HashSet();

		checkForCyclesFast(this, visited);
	}

	/**
	 * Perform a cyclic dependency check.
	 * 
	 * @param initialGoal
	 *            The root <code>Goal</code> initiating the cycle check.
	 * @param visited
	 *            The <code>Set</code> of all <code>Goal</code>s visited
	 *            between the root <code>initialGoal</code> and this
	 *            <code>Goal</code>.
	 * 
	 * @throws CyclicGoalChainException
	 *             if a cyclic dependency is detected.
	 */
	void checkForCycles(Goal initialGoal, Set visited)
			throws CyclicGoalChainException {
		if (visited.contains(this)) {
			throw new CyclicGoalChainException(initialGoal);
		}

		visited.add(this);

		Iterator precursorIter = getPrecursors().iterator();
		Goal eachPrereq = null;

		while (precursorIter.hasNext()) {
			eachPrereq = (Goal) precursorIter.next();

			eachPrereq.checkForCycles(initialGoal, new HashSet(visited));			
		}
	}

	void checkForCyclesFast(Goal initialGoal, Set visited)
			throws CyclicGoalChainException {
		if (visited.contains(this)) {
			throw new CyclicGoalChainException(initialGoal);
		}

		visited.add(this);

		Iterator precursorIter = getPrecursors().iterator();
		Goal eachPrereq = null;

		while (precursorIter.hasNext()) {
			eachPrereq = (Goal) precursorIter.next();

			eachPrereq.checkForCyclesFast(initialGoal, visited);
			visited.remove(eachPrereq);
		}
	}

	/**
	 * Attempt to attain this <code>Goal</code>.
	 * 
	 * @param session
	 *            The context in which to attain goals.
	 * 
	 * @throws UnattainableGoalException
	 *             if unable to attain this <code>Goal</code> or one of its
	 *             precursor <code>Goal</code>s.
	 * @throws NoActionDefinitionException
	 *             if this goal contains no action definition.
	 */
	public final void attain(Session session) throws UnattainableGoalException,
			NoActionDefinitionException {
		if (session.isGoalSatisfied(this)) {
			// session.info( getName() + ":" );
			// session.info( "" );
			return;
		}

		// session.info( getName() + ": checking precursors." );

		attainPrecursors(session);

		fire(session);
	}

	/**
	 * Attempt to percolate this <code>Goal</code> through to its Postcursors.
	 * 
	 * @param session
	 *            The context in which to percolate goals.
	 * 
	 * @throws UnattainableGoalException
	 *             if unable to attain this <code>Goal</code> or one of its
	 *             precursor <code>Goal</code>s.
	 * @throws NoActionDefinitionException
	 *             if this goal contains no action definition.
	 */
	public final void percolate(Session session)
			throws UnattainableGoalException, NoActionDefinitionException {
		if (session.isGoalSatisfied(this)) {
			return;
		}

		fire(session);

		percolatePostcursors(session);
	}

	/**
	 * Fires pre-goal callbacks, the <code>Goal</code>'s action, if need be,
	 * and the post-goal callbacks.
	 * 
	 * @param session
	 *            The context in which to fire the goal.
	 * 
	 * @throws UnattainableGoalException
	 *             if unable to attain this <code>Goal</code> or one of its
	 *             precursor <code>Goal</code>s.
	 * @throws NoActionDefinitionException
	 *             if this goal contains no action definition.
	 */
	private final void fire(Session session) throws UnattainableGoalException,
			NoActionDefinitionException {
		session.info(getName() + ":");

		// log.info( "begin goal" );

		try {
			// session.info( getName() + ": firing pre-goal callbacks." );
			firePreGoalCallbacks();
		} catch (Exception e) {
			throw new UnattainableGoalException(getName(), e);
		}

		Action action = getAction();

		if (action == null) {
			throw new NoActionDefinitionException(this);
		}

		if (action.requiresAction()) {
			try {
				// session.info( getName() + ": firing pre-action callbacks." );
				// log.info( "begin action" );
				firePreActionCallbacks();
				// session.info( getName() + ": firing goal action." );
				getAction().performAction(session);
				// session.info( getName() + ": firing post-action callbacks."
				// );
				firePostActionCallbacks();
				// log.info( "end action" );
			} catch (Exception e) {
				throw new UnattainableGoalException(getName(), e);
			}
		}

		try {
			// session.info( getName() + ": firing post-goal callbacks." );
			firePostGoalCallbacks();
		} catch (Exception e) {
			throw new UnattainableGoalException(getName(), e);
		}

		// log.info( "end goal" );

		// session.info( getName() + ": checking done." );

		session.addSatisfiedGoal(this);

		session.info("");
	}

	/**
	 * Fire the pre-goal callbacks.
	 * 
	 * @throws Exception
	 *             if an error occurs while firing a callback.
	 */
	void firePreGoalCallbacks() throws Exception {
		Iterator callbackIter = this.preGoalCallbacks.iterator();
		PreGoalCallback eachCallback = null;

		if (!callbackIter.hasNext()) {
			return;
		}

		// log.info( "begin pre-goal callbacks" );

		while (callbackIter.hasNext()) {
			eachCallback = (PreGoalCallback) callbackIter.next();

			eachCallback.firePreGoal(this);
		}

		// log.info( "end pre-goal callbacks" );
	}

	/**
	 * Fire the post-goal callbacks.
	 * 
	 * @throws Exception
	 *             if an error occurs while firing a callback.
	 */
	void firePostGoalCallbacks() throws Exception {
		Iterator callbackIter = this.postGoalCallbacks.iterator();
		PostGoalCallback eachCallback = null;

		while (callbackIter.hasNext()) {
			eachCallback = (PostGoalCallback) callbackIter.next();

			eachCallback.firePostGoal(this);
		}
	}

	/**
	 * Fire the pre-action callbacks.
	 * 
	 * @throws Exception
	 *             if an error occurs while firing a callback.
	 */
	void firePreActionCallbacks() throws Exception {
		Iterator callbackIter = this.preActionCallbacks.iterator();
		PreActionCallback eachCallback = null;

		while (callbackIter.hasNext()) {
			eachCallback = (PreActionCallback) callbackIter.next();

			eachCallback.firePreAction(this);
		}
	}

	/**
	 * Fire the post-action callbacks.
	 * 
	 * @throws Exception
	 *             if an error occurs while firing a callback.
	 */
	void firePostActionCallbacks() throws Exception {
		Iterator callbackIter = this.postActionCallbacks.iterator();
		PostActionCallback eachCallback = null;

		while (callbackIter.hasNext()) {
			eachCallback = (PostActionCallback) callbackIter.next();

			eachCallback.firePostAction(this);
		}
	}

	// ------------------------------------------------------------
	// java.lang.Object implementation
	// ------------------------------------------------------------

	/**
	 * Retrieve the hash-code of this object.
	 * 
	 * <p>
	 * The hash-code is derrived <b>only</b> from the name of the
	 * <code>Goal</code> as returned by {@link #getName}.
	 * </p>
	 * 
	 * @return The hash-code of this object.
	 */
	public int hashCode() {
		return getName().hashCode();
	}

	/**
	 * Determine if two <code>Goal</code> objects are equivelant.
	 * 
	 * <p>
	 * Equivelancy is determined <b>only</b> from the names of the
	 * <code>Goal</code>s as return by {@link #getName}.
	 * </p>
	 * 
	 * @param thatObj
	 *            The object to compare to this object.
	 * 
	 * @return The hash-code of this object.
	 */
	public boolean equals(Object thatObj) {
		Goal that = (Goal) thatObj;

		return this.getName().equals(that.getName());
	}

	/**
	 * Produce a textual representation suitable for debugging.
	 * 
	 * @return A textual representation suitable for debugging.
	 */
	public String toString() {
		return "[Goal: name=\"" + getName() + "\"]" + "; precursor="
				+ getPrecursors() + "]";
	}

}
