/*
 * 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;

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

import org.apache.commons.math.MaxEvaluationsExceededException;
import org.apache.commons.math.ode.events.CombinedEventsManager;
import org.apache.commons.math.ode.events.EventHandler;
import org.apache.commons.math.ode.events.EventState;
import org.apache.commons.math.ode.sampling.StepHandler;

/**
 * Base class managing common boilerplate for all integrators.
 * @version $Revision$ $Date$
 * @since 2.0
 */
public abstract class AbstractIntegrator implements FirstOrderIntegrator {

    
    /** Name of the method. */
    private final String name;

    /** Maximal number of evaluations allowed. */
    private int maxEvaluations;

    /** Number of evaluations already performed. */
    private int evaluations;

    /** Differential equations to integrate. */
    private transient FirstOrderDifferentialEquations equations;

    /** Step handler. */
    protected Collection<StepHandler> stepHandlers;

    /** Current step start time. */
    protected double stepStart;

    /** Current stepsize. */
    protected double stepSize;

    /** Events handlers manager. */
    protected CombinedEventsManager eventsHandlersManager;

    /** Build an instance.
     * @param name name of the method
     */
    public AbstractIntegrator(final String name) {
        this.name = name;
        stepHandlers = new ArrayList<StepHandler>();
        stepStart = Double.NaN;
        stepSize  = Double.NaN;
        eventsHandlersManager = new CombinedEventsManager();
        setMaxEvaluations(-1);
        resetEvaluations();
    }

    /** Build an instance with a null name.
     */
    protected AbstractIntegrator() {
        this(null);
    }
    
    /** {@inheritDoc} */
    public String getName() {
        return name;
    }

    /** {@inheritDoc} */
    public void addStepHandler(final StepHandler handler) {
        stepHandlers.add(handler);
    }

    /** {@inheritDoc} */
    public Collection<StepHandler> getStepHandlers() {
        return Collections.unmodifiableCollection(stepHandlers);
    }

    /** {@inheritDoc} */
    public void clearStepHandlers() {
        stepHandlers.clear();
    }

    /** {@inheritDoc} */
    public void addEventHandler(final EventHandler function,
                                final double maxCheckInterval,
                                final double convergence,
                                final int maxIterationCount) {
        eventsHandlersManager.addEventHandler(function, maxCheckInterval,
                                              convergence, maxIterationCount);
    }

    /** {@inheritDoc} */
    public Collection<EventHandler> getEventHandlers() {
        return eventsHandlersManager.getEventsHandlers();
    }

    /** {@inheritDoc} */
    public void clearEventHandlers() {
        eventsHandlersManager.clearEventsHandlers();
    }

    /** Check if one of the step handlers requires dense output.
     * @return true if one of the step handlers requires dense output
     */
    protected boolean requiresDenseOutput() {
        for (StepHandler handler : stepHandlers) {
            if (handler.requiresDenseOutput()) {
                return true;
            }
        }
        return false;
    }

    /** {@inheritDoc} */
    public double getCurrentStepStart() {
        return stepStart;
    }

    /** {@inheritDoc} */
    public double getCurrentSignedStepsize() {
        return stepSize;
    }

    /** {@inheritDoc} */
    public void setMaxEvaluations(int maxEvaluations) {
        this.maxEvaluations = (maxEvaluations < 0) ? Integer.MAX_VALUE : maxEvaluations;
    }

    /** {@inheritDoc} */
    public int getMaxEvaluations() {
        return maxEvaluations;
    }

    /** {@inheritDoc} */
    public int getEvaluations() {
        return evaluations;
    }

    /** Reset the number of evaluations to zero.
     */
    protected void resetEvaluations() {
        evaluations = 0;
    }

    /** Set the differential equations.
     * @param equations differential equations to integrate
     * @see #computeDerivatives(double, double[], double[])
     */
    protected void setEquations(final FirstOrderDifferentialEquations equations) {
        this.equations = equations;
    }

    /** Compute the derivatives and check the number of evaluations.
     * @param t current value of the independent <I>time</I> variable
     * @param y array containing the current value of the state vector
     * @param yDot placeholder array where to put the time derivative of the state vector
     * @throws DerivativeException this exception is propagated to the caller if the
     * underlying user function triggers one
     */
    public void computeDerivatives(final double t, final double[] y, final double[] yDot)
        throws DerivativeException {
        if (++evaluations > maxEvaluations) {
            throw new DerivativeException(new MaxEvaluationsExceededException(maxEvaluations));
        }
        equations.computeDerivatives(t, y, yDot);
    }

    /** Perform some sanity checks on the integration parameters.
     * @param equations differential equations set
     * @param t0 start time
     * @param y0 state vector at t0
     * @param t target time for the integration
     * @param y placeholder where to put the state vector
     * @exception IntegratorException if some inconsistency is detected
     */
    protected void sanityChecks(final FirstOrderDifferentialEquations equations,
                                final double t0, final double[] y0,
                                final double t, final double[] y)
        throws IntegratorException {

        if (equations.getDimension() != y0.length) {
            throw new IntegratorException(
                    "dimensions mismatch: ODE problem has dimension {0}," +
                    " initial state vector has dimension {1}",
                    equations.getDimension(), y0.length);
        }

        if (equations.getDimension() != y.length) {
            throw new IntegratorException(
                    "dimensions mismatch: ODE problem has dimension {0}," +
                    " final state vector has dimension {1}",
                    equations.getDimension(), y.length);
        }

        if (Math.abs(t - t0) <= 1.0e-12 * Math.max(Math.abs(t0), Math.abs(t))) {
            throw new IntegratorException(
                    "too small integration interval: length = {0}",
                    Math.abs(t - t0));
        }

    }

    /** Add an event handler for end time checking.
     * <p>This method can be used to simplify handling of integration end time.
     * It leverages the nominal stop condition with the exceptional stop
     * conditions.</p>
     * @param startTime integration start time
     * @param endTime desired end time
     * @param manager manager containing the user-defined handlers
     * @return a new manager containing all the user-defined handlers plus a
     * dedicated manager triggering a stop event at entTime
     */
    protected CombinedEventsManager addEndTimeChecker(final double startTime,
                                                      final double endTime,
                                                      final CombinedEventsManager manager) {
        CombinedEventsManager newManager = new CombinedEventsManager();
        for (final EventState state : manager.getEventsStates()) {
            newManager.addEventHandler(state.getEventHandler(),
                                       state.getMaxCheckInterval(),
                                       state.getConvergence(),
                                       state.getMaxIterationCount());
        }
        newManager.addEventHandler(new EndTimeChecker(endTime),
                                   Double.POSITIVE_INFINITY,
                                   Math.ulp(Math.max(Math.abs(startTime), Math.abs(endTime))),
                                   100);
        return newManager;
    }

    /** Specialized event handler to stop integration. */
    private static class EndTimeChecker implements EventHandler {

        /** Desired end time. */
        private final double endTime;

        /** Build an instance.
         * @param endTime desired time
         */
        public EndTimeChecker(final double endTime) {
            this.endTime = endTime;
        }

        /** {@inheritDoc} */
        public int eventOccurred(double t, double[] y, boolean increasing) {
            return STOP;
        }

        /** {@inheritDoc} */
        public double g(double t, double[] y) {
            return t - endTime;
        }

        /** {@inheritDoc} */
        public void resetState(double t, double[] y) {
        }
        
    }

}