/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

package org.apache.commons.math.ode.events;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.apache.commons.math.ConvergenceException;
import org.apache.commons.math.ode.DerivativeException;
import org.apache.commons.math.ode.IntegratorException;
import org.apache.commons.math.ode.sampling.StepInterpolator;

/**
 * This class manages several {@link EventHandler event handlers} during
 * integration.
 *
 * @see EventHandler
 * @see EventState
 * @version $Revision$ $Date$
 * @since 1.2
 */

public class CombinedEventsManager {

	/** Events states. */
	private final List<EventState> states;

	/** First active event. */
	private EventState first;

	/** Initialization indicator. */
	private boolean initialized;

	/**
	 * Simple constructor. Create an empty manager
	 */
	public CombinedEventsManager() {
		states = new ArrayList<EventState>();
		first = null;
		initialized = false;
	}

	/**
	 * Add an events handler.
	 * 
	 * @param handler
	 *            event handler
	 * @param maxCheckInterval
	 *            maximal time interval between events checks (this interval
	 *            prevents missing sign changes in case the integration steps
	 *            becomes very large)
	 * @param convergence
	 *            convergence threshold in the event time search
	 * @param maxIterationCount
	 *            upper limit of the iteration count in the event time search
	 * @see #getEventsHandlers()
	 * @see #clearEventsHandlers()
	 */
	public void addEventHandler(final EventHandler handler, final double maxCheckInterval, final double convergence,
			final int maxIterationCount) {
		states.add(new EventState(handler, maxCheckInterval, convergence, maxIterationCount));
	}

	/**
	 * Get all the events handlers that have been added to the manager.
	 * 
	 * @return an unmodifiable collection of the added event handlers
	 * @see #addEventHandler(EventHandler, double, double, int)
	 * @see #clearEventsHandlers()
	 * @see #getEventsStates()
	 */
	public Collection<EventHandler> getEventsHandlers() {
		final List<EventHandler> list = new ArrayList<EventHandler>();
		for (EventState state : states) {
			list.add(state.getEventHandler());
		}
		return Collections.unmodifiableCollection(list);
	}

	/**
	 * Remove all the events handlers that have been added to the manager.
	 * 
	 * @see #addEventHandler(EventHandler, double, double, int)
	 * @see #getEventsHandlers()
	 */
	public void clearEventsHandlers() {
		states.clear();
	}

	/**
	 * Get all the events state wrapping the handlers that have been added to
	 * the manager.
	 * 
	 * @return a collection of the events states
	 * @see #getEventsHandlers()
	 */
	public Collection<EventState> getEventsStates() {
		return states;
	}

	/**
	 * Check if the manager does not manage any event handlers.
	 * 
	 * @return true if manager is empty
	 */
	public boolean isEmpty() {
		return states.isEmpty();
	}

	/**
	 * Evaluate the impact of the proposed step on all managed event handlers.
	 * 
	 * @param interpolator
	 *            step interpolator for the proposed step
	 * @return true if at least one event handler triggers an event before the
	 *         end of the proposed step (this implies the step should be
	 *         rejected)
	 * @exception DerivativeException
	 *                if the interpolator fails to compute the function
	 *                somewhere within the step
	 * @exception IntegratorException
	 *                if an event cannot be located
	 */
	public boolean evaluateStep(final StepInterpolator interpolator) throws DerivativeException, IntegratorException {

		try {

			first = null;
			if (states.isEmpty()) {
				// there is nothing to do, return now to avoid setting the
				// interpolator time (and hence avoid unneeded calls to the
				// user function due to interpolator finalization)
				return false;
			}

			if (!initialized) {

				// initialize the events states
				final double t0 = interpolator.getPreviousTime();
				interpolator.setInterpolatedTime(t0);
				final double[] y = interpolator.getInterpolatedState();
				for (EventState state : states) {
					state.reinitializeBegin(t0, y);
				}

				initialized = true;

			}

			// check events occurrence
			for (EventState state : states) {

				if (state.evaluateStep(interpolator)) {
					if (first == null) {
						first = state;
					} else {
						if (interpolator.isForward()) {
							if (state.getEventTime() < first.getEventTime()) {
								first = state;
							}
						} else {
							if (state.getEventTime() > first.getEventTime()) {
								first = state;
							}
						}
					}
				}

			}

			return first != null;

		} catch (EventException se) {
			throw new IntegratorException(se);
		} catch (ConvergenceException ce) {
			throw new IntegratorException(ce);
		}

	}

	/**
	 * Get the occurrence time of the first event triggered in the last
	 * evaluated step.
	 * 
	 * @return occurrence time of the first event triggered in the last
	 *         evaluated step, or </code>Double.NaN</code> if no event is
	 *         triggered
	 */
	public double getEventTime() {
		return (first == null) ? Double.NaN : first.getEventTime();
	}

	/**
	 * Inform the event handlers that the step has been accepted by the
	 * integrator.
	 * 
	 * @param t
	 *            value of the independent <i>time</i> variable at the end of
	 *            the step
	 * @param y
	 *            array containing the current value of the state vector at the
	 *            end of the step
	 * @exception IntegratorException
	 *                if the value of one of the events states cannot be
	 *                evaluated
	 */
	public void stepAccepted(final double t, final double[] y) throws IntegratorException {
		try {
			for (EventState state : states) {
				state.stepAccepted(t, y);
			}
		} catch (EventException se) {
			throw new IntegratorException(se);
		}
	}

	/**
	 * Check if the integration should be stopped at the end of the current
	 * step.
	 * 
	 * @return true if the integration should be stopped
	 */
	public boolean stop() {
		for (EventState state : states) {
			if (state.stop()) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Let the event handlers reset the state if they want.
	 * 
	 * @param t
	 *            value of the independent <i>time</i> variable at the beginning
	 *            of the next step
	 * @param y
	 *            array were to put the desired state vector at the beginning of
	 *            the next step
	 * @return true if the integrator should reset the derivatives too
	 * @exception IntegratorException
	 *                if one of the events states that should reset the state
	 *                fails to do it
	 */
	public boolean reset(final double t, final double[] y) throws IntegratorException {
		try {
			boolean resetDerivatives = false;
			for (EventState state : states) {
				if (state.reset(t, y)) {
					resetDerivatives = true;
				}
			}
			return resetDerivatives;
		} catch (EventException se) {
			throw new IntegratorException(se);
		}
	}

}
