/*
 * 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.analysis.solvers;

import org.apache.commons.math3.analysis.QuinticFunction;
import org.apache.commons.math3.analysis.UnivariateFunction;
import org.apache.commons.math3.analysis.XMinus5Function;
import org.apache.commons.math3.analysis.function.Sin;
import org.apache.commons.math3.exception.NoBracketingException;
import org.apache.commons.math3.exception.NumberIsTooLargeException;
import org.apache.commons.math3.util.FastMath;
import org.junit.Assert;
import org.junit.Test;

/**
 * Base class for root-finding algorithms tests derived from
 * {@link BaseSecantSolver}.
 *
 */
public abstract class BaseSecantSolverAbstractTest {
    /** Returns the solver to use to perform the tests.
     * @return the solver to use to perform the tests
     */
    protected abstract UnivariateSolver getSolver();

    /** Returns the expected number of evaluations for the
     * {@link #testQuinticZero} unit test. A value of {@code -1} indicates that
     * the test should be skipped for that solver.
     * @return the expected number of evaluations for the
     * {@link #testQuinticZero} unit test
     */
    protected abstract int[] getQuinticEvalCounts();

    @Test
    public void testSinZero() {
        // The sinus function is behaved well around the root at pi. The second
        // order derivative is zero, which means linear approximating methods
        // still converge quadratically.
        UnivariateFunction f = new Sin();
        double result;
        UnivariateSolver solver = getSolver();

        result = solver.solve(100, f, 3, 4);
        //System.out.println(
        //    "Root: " + result + " Evaluations: " + solver.getEvaluations());
        Assert.assertEquals(result, FastMath.PI, solver.getAbsoluteAccuracy());
        Assert.assertTrue(solver.getEvaluations() <= 6);
        result = solver.solve(100, f, 1, 4);
        //System.out.println(
        //    "Root: " + result + " Evaluations: " + solver.getEvaluations());
        Assert.assertEquals(result, FastMath.PI, solver.getAbsoluteAccuracy());
        Assert.assertTrue(solver.getEvaluations() <= 7);
    }

    @Test
    public void testQuinticZero() {
        // The quintic function has zeros at 0, +-0.5 and +-1.
        // Around the root of 0 the function is well behaved, with a second
        // derivative of zero a 0.
        // The other roots are less well to find, in particular the root at 1,
        // because the function grows fast for x>1.
        // The function has extrema (first derivative is zero) at 0.27195613
        // and 0.82221643, intervals containing these values are harder for
        // the solvers.
        UnivariateFunction f = new QuinticFunction();
        double result;
        UnivariateSolver solver = getSolver();
        double atol = solver.getAbsoluteAccuracy();
        int[] counts = getQuinticEvalCounts();

        // Tests data: initial bounds, and expected solution, per test case.
        double[][] testsData = {{-0.2,  0.2,  0.0},
                                {-0.1,  0.3,  0.0},
                                {-0.3,  0.45, 0.0},
                                { 0.3,  0.7,  0.5},
                                { 0.2,  0.6,  0.5},
                                { 0.05, 0.95, 0.5},
                                { 0.85, 1.25, 1.0},
                                { 0.8,  1.2,  1.0},
                                { 0.85, 1.75, 1.0},
                                { 0.55, 1.45, 1.0},
                                { 0.85, 5.0,  1.0},
                               };
        int maxIter = 500;

        for(int i = 0; i < testsData.length; i++) {
            // Skip test, if needed.
            if (counts[i] == -1) continue;

            // Compute solution.
            double[] testData = testsData[i];
            result = solver.solve(maxIter, f, testData[0], testData[1]);
            //System.out.println(
            //    "Root: " + result + " Evaluations: " + solver.getEvaluations());

            // Check solution.
            Assert.assertEquals(result, testData[2], atol);
            Assert.assertTrue(solver.getEvaluations() <= counts[i] + 1);
        }
    }

    @Test
    public void testRootEndpoints() {
        UnivariateFunction f = new XMinus5Function();
        UnivariateSolver solver = getSolver();

        // End-point is root. This should be a special case in the solver, and
        // the initial end-point should be returned exactly.
        double result = solver.solve(100, f, 5.0, 6.0);
        Assert.assertEquals(5.0, result, 0.0);

        result = solver.solve(100, f, 4.0, 5.0);
        Assert.assertEquals(5.0, result, 0.0);

        result = solver.solve(100, f, 5.0, 6.0, 5.5);
        Assert.assertEquals(5.0, result, 0.0);

        result = solver.solve(100, f, 4.0, 5.0, 4.5);
        Assert.assertEquals(5.0, result, 0.0);
    }

    @Test
    public void testBadEndpoints() {
        UnivariateFunction f = new Sin();
        UnivariateSolver solver = getSolver();
        try {  // bad interval
            solver.solve(100, f, 1, -1);
            Assert.fail("Expecting NumberIsTooLargeException - bad interval");
        } catch (NumberIsTooLargeException ex) {
            // expected
        }
        try {  // no bracket
            solver.solve(100, f, 1, 1.5);
            Assert.fail("Expecting NoBracketingException - non-bracketing");
        } catch (NoBracketingException ex) {
            // expected
        }
        try {  // no bracket
            solver.solve(100, f, 1, 1.5, 1.2);
            Assert.fail("Expecting NoBracketingException - non-bracketing");
        } catch (NoBracketingException ex) {
            // expected
        }
    }

    @Test
    public void testSolutionLeftSide() {
        UnivariateFunction f = new Sin();
        UnivariateSolver solver = getSolver();
        double left = -1.5;
        double right = 0.05;
        for(int i = 0; i < 10; i++) {
            // Test whether the allowed solutions are taken into account.
            double solution = getSolution(solver, 100, f, left, right, AllowedSolution.LEFT_SIDE);
            if (!Double.isNaN(solution)) {
                Assert.assertTrue(solution <= 0.0);
            }

            // Prepare for next test.
            left -= 0.1;
            right += 0.3;
        }
    }

    @Test
    public void testSolutionRightSide() {
        UnivariateFunction f = new Sin();
        UnivariateSolver solver = getSolver();
        double left = -1.5;
        double right = 0.05;
        for(int i = 0; i < 10; i++) {
            // Test whether the allowed solutions are taken into account.
            double solution = getSolution(solver, 100, f, left, right, AllowedSolution.RIGHT_SIDE);
            if (!Double.isNaN(solution)) {
                Assert.assertTrue(solution >= 0.0);
            }

            // Prepare for next test.
            left -= 0.1;
            right += 0.3;
        }
    }
    @Test
    public void testSolutionBelowSide() {
        UnivariateFunction f = new Sin();
        UnivariateSolver solver = getSolver();
        double left = -1.5;
        double right = 0.05;
        for(int i = 0; i < 10; i++) {
            // Test whether the allowed solutions are taken into account.
            double solution = getSolution(solver, 100, f, left, right, AllowedSolution.BELOW_SIDE);
            if (!Double.isNaN(solution)) {
                Assert.assertTrue(f.value(solution) <= 0.0);
            }

            // Prepare for next test.
            left -= 0.1;
            right += 0.3;
        }
    }

    @Test
    public void testSolutionAboveSide() {
        UnivariateFunction f = new Sin();
        UnivariateSolver solver = getSolver();
        double left = -1.5;
        double right = 0.05;
        for(int i = 0; i < 10; i++) {
            // Test whether the allowed solutions are taken into account.
            double solution = getSolution(solver, 100, f, left, right, AllowedSolution.ABOVE_SIDE);
            if (!Double.isNaN(solution)) {
                Assert.assertTrue(f.value(solution) >= 0.0);
            }

            // Prepare for next test.
            left -= 0.1;
            right += 0.3;
        }
    }

    private double getSolution(UnivariateSolver solver, int maxEval, UnivariateFunction f,
                               double left, double right, AllowedSolution allowedSolution) {
        try {
            @SuppressWarnings("unchecked")
            BracketedUnivariateSolver<UnivariateFunction> bracketing =
            (BracketedUnivariateSolver<UnivariateFunction>) solver;
            return bracketing.solve(100, f, left, right, allowedSolution);
        } catch (ClassCastException cce) {
            double baseRoot = solver.solve(maxEval, f, left, right);
            if ((baseRoot <= left) || (baseRoot >= right)) {
                // the solution slipped out of interval
                return Double.NaN;
            }
            PegasusSolver bracketing =
                    new PegasusSolver(solver.getRelativeAccuracy(), solver.getAbsoluteAccuracy(),
                                      solver.getFunctionValueAccuracy());
            return UnivariateSolverUtils.forceSide(maxEval - solver.getEvaluations(),
                                                       f, bracketing, baseRoot, left, right,
                                                       allowedSolution);
        }
    }

}
