/*
 * 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.math3.ode.sampling;

import org.junit.Assert;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.math3.exception.DimensionMismatchException;
import org.apache.commons.math3.exception.MaxCountExceededException;
import org.apache.commons.math3.exception.NoBracketingException;
import org.apache.commons.math3.exception.NumberIsTooSmallException;
import org.apache.commons.math3.ode.FirstOrderDifferentialEquations;
import org.apache.commons.math3.ode.FirstOrderIntegrator;
import org.apache.commons.math3.ode.nonstiff.GraggBulirschStoerIntegrator;
import org.junit.Test;

/** Base class for step normalizer output tests. */
public abstract class StepNormalizerOutputTestBase
    implements FirstOrderDifferentialEquations, FixedStepHandler {

    /** The normalized output time values. */
    private List<Double> output;

    /**
     * Returns the start time.
     * @return the start time
     */
    protected abstract double getStart();

    /**
     * Returns the end time.
     * @return the end time
     */
    protected abstract double getEnd();

    /**
     * Returns the expected normalized output time values for increment mode.
     * @return the expected normalized output time values for increment mode
     */
    protected abstract double[] getExpInc();

    /**
     * Returns the expected reversed normalized output time values for
     * increment mode.
     * @return the expected reversed normalized output time values for
     * increment mode
     */
    protected abstract double[] getExpIncRev();

    /**
     * Returns the expected normalized output time values for multiples mode.
     * @return the expected normalized output time values for multiples mode
     */
    protected abstract double[] getExpMul();

    /**
     * Returns the expected reversed normalized output time values for
     * multiples mode.
     * @return the expected reversed normalized output time values for
     * multiples mode
     */
    protected abstract double[] getExpMulRev();

    /**
     * Returns the offsets for the unit tests below, in the order they are
     * given below. For each test, the left and right offsets are returned.
     * @return the offsets for the unit tests below, in the order they are
     * given below
     */
    protected abstract int[][] getO();

    /**
     * Get the array, given left and right offsets.
     * @param a the input array
     * @param offsetL the left side offset
     * @param offsetR the right side offset
     * @return the modified array
     */
    private double[] getArray(double[] a, int offsetL, int offsetR) {
        double[] copy = new double[a.length - offsetR - offsetL];
        System.arraycopy(a, offsetL, copy, 0, copy.length);
        return copy;
    }

    @Test
    public void testIncNeither()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpInc(), getO()[0][0], getO()[0][1]);
        doTest(StepNormalizerMode.INCREMENT, StepNormalizerBounds.NEITHER, exp, false);
    }

    @Test
    public void testIncNeitherRev()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpIncRev(), getO()[1][0], getO()[1][1]);
        doTest(StepNormalizerMode.INCREMENT, StepNormalizerBounds.NEITHER, exp, true);
    }

    @Test
    public void testIncFirst()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpInc(), getO()[2][0], getO()[2][1]);
        doTest(StepNormalizerMode.INCREMENT, StepNormalizerBounds.FIRST, exp, false);
    }

    @Test
    public void testIncFirstRev()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpIncRev(), getO()[3][0], getO()[3][1]);
        doTest(StepNormalizerMode.INCREMENT, StepNormalizerBounds.FIRST, exp, true);
    }

    @Test
    public void testIncLast()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpInc(), getO()[4][0], getO()[4][1]);
        doTest(StepNormalizerMode.INCREMENT, StepNormalizerBounds.LAST, exp, false);
    }

    @Test
    public void testIncLastRev()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpIncRev(), getO()[5][0], getO()[5][1]);
        doTest(StepNormalizerMode.INCREMENT, StepNormalizerBounds.LAST, exp, true);
    }

    @Test
    public void testIncBoth()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpInc(), getO()[6][0], getO()[6][1]);
        doTest(StepNormalizerMode.INCREMENT, StepNormalizerBounds.BOTH, exp, false);
    }

    @Test
    public void testIncBothRev()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpIncRev(), getO()[7][0], getO()[7][1]);
        doTest(StepNormalizerMode.INCREMENT, StepNormalizerBounds.BOTH, exp, true);
    }

    @Test
    public void testMulNeither()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpMul(), getO()[8][0], getO()[8][1]);
        doTest(StepNormalizerMode.MULTIPLES, StepNormalizerBounds.NEITHER, exp, false);
    }

    @Test
    public void testMulNeitherRev()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpMulRev(), getO()[9][0], getO()[9][1]);
        doTest(StepNormalizerMode.MULTIPLES, StepNormalizerBounds.NEITHER, exp, true);
    }

    @Test
    public void testMulFirst()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpMul(), getO()[10][0], getO()[10][1]);
        doTest(StepNormalizerMode.MULTIPLES, StepNormalizerBounds.FIRST, exp, false);
    }

    @Test
    public void testMulFirstRev()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpMulRev(), getO()[11][0], getO()[11][1]);
        doTest(StepNormalizerMode.MULTIPLES, StepNormalizerBounds.FIRST, exp, true);
    }

    @Test
    public void testMulLast()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpMul(), getO()[12][0], getO()[12][1]);
        doTest(StepNormalizerMode.MULTIPLES, StepNormalizerBounds.LAST, exp, false);
    }

    @Test
    public void testMulLastRev()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpMulRev(), getO()[13][0], getO()[13][1]);
        doTest(StepNormalizerMode.MULTIPLES, StepNormalizerBounds.LAST, exp, true);
    }

    @Test
    public void testMulBoth()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpMul(), getO()[14][0], getO()[14][1]);
        doTest(StepNormalizerMode.MULTIPLES, StepNormalizerBounds.BOTH, exp, false);
    }

    @Test
    public void testMulBothRev()
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        double[] exp = getArray(getExpMulRev(), getO()[15][0], getO()[15][1]);
        doTest(StepNormalizerMode.MULTIPLES, StepNormalizerBounds.BOTH, exp, true);
    }

    /**
     * The actual step normalizer output test code, shared by all the unit
     * tests.
     *
     * @param mode the step normalizer mode to use
     * @param bounds the step normalizer bounds setting to use
     * @param expected the expected output (normalized time points)
     * @param reverse whether to reverse the integration direction
     * @throws NoBracketingException 
     * @throws MaxCountExceededException 
     * @throws NumberIsTooSmallException 
     * @throws DimensionMismatchException 
     */
    private void doTest(StepNormalizerMode mode, StepNormalizerBounds bounds,
                        double[] expected, boolean reverse)
        throws DimensionMismatchException, NumberIsTooSmallException,
               MaxCountExceededException, NoBracketingException {
        // Forward test.
        FirstOrderIntegrator integ = new GraggBulirschStoerIntegrator(
                                                        1e-8, 1.0, 1e-5, 1e-5);
        integ.addStepHandler(new StepNormalizer(0.5, this, mode, bounds));
        double[] y   = {0.0};
        double start = reverse ? getEnd()   : getStart();
        double end   = reverse ? getStart() : getEnd();
        output       = new ArrayList<Double>();
        integ.integrate(this, start, y, end, y);
        double[] actual = new double[output.size()];
        for(int i = 0; i < actual.length; i++) {
            actual[i] = output.get(i);
        }
        Assert.assertArrayEquals(expected, actual, 1e-5);
    }

    /** {@inheritDoc} */
    public int getDimension() {
        return 1;
    }

    /** {@inheritDoc} */
    public void computeDerivatives(double t, double[] y, double[] yDot) {
        yDot[0] = y[0];
    }

    /** {@inheritDoc} */
    public void init(double t0, double[] y0, double t) {
    }

    /** {@inheritDoc} */
    public void handleStep(double t, double[] y, double[] yDot, boolean isLast) {
        output.add(t);
    }

}
