/*
 * 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
 *
 *      https://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.statistics.inference;

import java.util.function.DoubleUnaryOperator;
import org.apache.commons.numbers.core.Precision;

/**
 * For a function defined on some interval {@code (lo, hi)}, this class
 * finds an approximation {@code x} to the point at which the function
 * attains its minimum.
 * It implements Richard Brent's algorithm (from his book "Algorithms for
 * Minimization without Derivatives", p. 79) for finding minima of real
 * univariate functions.
 *
 * <P>This code is an adaptation, partly based on the Python code from SciPy
 * (module "optimize.py" v0.5); the original algorithm is also modified:
 * <ul>
 *  <li>to use an initial guess provided by the user,</li>
 *  <li>to ensure that the best point encountered is the one returned.</li>
 * </ul>
 *
 * <p>This class has been extracted from {@code o.a.c.math4.optim.univariate}
 * and simplified to remove support for the UnivariateOptimizer interface.
 * This removed the options: to find the maximum; use a custom convergence checker
 * on the function value; and remove the maximum function evaluation count.
 * The class now implements a single optimize method within the provided bracket
 * from the given start position (with value).
 *
 * @since 1.1
 */
final class BrentOptimizer {
    /** Golden section. (3 - sqrt(5)) / 2. */
    private static final double GOLDEN_SECTION = 0.3819660112501051;
    /** Minimum relative tolerance. 2 * eps = 2^-51. */
    private static final double MIN_RELATIVE_TOLERANCE = 0x1.0p-51;

    /** Relative threshold. */
    private final double relativeThreshold;
    /** Absolute threshold. */
    private final double absoluteThreshold;
    /** The number of function evaluations from the most recent call to optimize. */
    private int evaluations;

    /**
     * This class holds a point and the value of an objective function at this
     * point. This is a simple immutable container.
     *
     * @since 1.1
     */
    static final class PointValuePair {
        /** Point. */
        private final double point;
        /** Value of the objective function at the point. */
        private final double value;

        /**
         * @param point Point.
         * @param value Value of an objective function at the point.
         */
        private PointValuePair(double point, double value) {
            this.point = point;
            this.value = value;
        }

        /**
         * Create a point/objective function value pair.
         *
         * @param point Point.
         * @param value Value of an objective function at the point.
         * @return the pair
         */
        static PointValuePair of(double point, double value) {
            return new PointValuePair(point, value);
        }

        /**
         * Get the point.
         *
         * @return the point.
         */
        double getPoint() {
            return point;
        }

        /**
         * Get the value of the objective function.
         *
         * @return the stored value of the objective function.
         */
        double getValue() {
            return value;
        }
    }

    /**
     * The arguments are used to implement the original stopping criterion
     * of Brent's algorithm.
     * {@code abs} and {@code rel} define a tolerance
     * {@code tol = rel |x| + abs}. {@code rel} should be no smaller than
     * <em>2 macheps</em> and preferably not much less than <em>sqrt(macheps)</em>,
     * where <em>macheps</em> is the relative machine precision. {@code abs} must
     * be positive.
     *
     * @param rel Relative threshold.
     * @param abs Absolute threshold.
     * @throws IllegalArgumentException if {@code abs <= 0}; or if {@code rel < 2 * Math.ulp(1.0)}
     */
    BrentOptimizer(double rel, double abs) {
        if (rel >= MIN_RELATIVE_TOLERANCE) {
            relativeThreshold = rel;
            absoluteThreshold = Arguments.checkStrictlyPositive(abs);
        } else {
            // relative too small, or NaN
            throw new InferenceException(InferenceException.X_LT_Y, rel, MIN_RELATIVE_TOLERANCE);
        }
    }

    /**
     * Gets the number of function evaluations from the most recent call to
     * {@link #optimize(DoubleUnaryOperator, double, double, double, double) optimize}.
     *
     * @return the function evaluations
     */
    int getEvaluations() {
        return evaluations;
    }

    /**
     * Search for the minimum inside the provided interval. The bracket must satisfy
     * the equalities {@code lo < mid < hi} or {@code hi < mid < lo}.
     *
     * <p>Note: This function accepts the initial guess and the function value at that point.
     * This is done for convenience as this internal class is used where the caller already
     * knows the function value.
     *
     * @param func Function to solve.
     * @param lo Lower bound of the search interval.
     * @param hi Higher bound of the search interval.
     * @param mid Start point.
     * @param fMid Function value at the start point.
     * @return the value where the function is minimum.
     * @throws IllegalArgumentException if start point is not within the search interval
     * @throws IllegalStateException if the maximum number of iterations is exceeded
     */
    PointValuePair optimize(DoubleUnaryOperator func,
                            double lo, double hi,
                            double mid, double fMid) {
        double a;
        double b;
        if (lo < hi) {
            a = lo;
            b = hi;
        } else {
            a = hi;
            b = lo;
        }
        if (!(a < mid && mid < b)) {
            throw new InferenceException("Invalid bounds: (%s, %s) with start %s", a, b, mid);
        }
        double x = mid;
        double v = x;
        double w = x;
        double d = 0;
        double e = 0;
        double fx = fMid;
        double fv = fx;
        double fw = fx;

        // Best point encountered so far (which is the initial guess).
        double bestX = x;
        double bestFx = fx;

        // No test for iteration count.
        // Note that the termination criterion is based purely on the size of the current
        // bracket and the current point x. If the function evaluates NaN then golden
        // section steps are taken.
        evaluations = 0;
        for (;;) {
            final double m = 0.5 * (a + b);
            final double tol1 = relativeThreshold * Math.abs(x) + absoluteThreshold;
            final double tol2 = 2 * tol1;

            // Default termination (Brent's criterion).
            if (Math.abs(x - m) <= tol2 - 0.5 * (b - a)) {
                return PointValuePair.of(bestX, bestFx);
            }

            if (Math.abs(e) > tol1) {
                // Fit parabola.
                double r = (x - w) * (fx - fv);
                double q = (x - v) * (fx - fw);
                double p = (x - v) * q - (x - w) * r;
                q = 2 * (q - r);

                if (q > 0) {
                    p = -p;
                } else {
                    q = -q;
                }

                r = e;
                e = d;

                if (p > q * (a - x) &&
                    p < q * (b - x) &&
                    Math.abs(p) < Math.abs(0.5 * q * r)) {
                    // Parabolic interpolation step.
                    d = p / q;
                    final double u = x + d;

                    // f must not be evaluated too close to a or b.
                    if (u - a < tol2 || b - u < tol2) {
                        if (x <= m) {
                            d = tol1;
                        } else {
                            d = -tol1;
                        }
                    }
                } else {
                    // Golden section step.
                    if (x < m) {
                        e = b - x;
                    } else {
                        e = a - x;
                    }
                    d = GOLDEN_SECTION * e;
                }
            } else {
                // Golden section step.
                if (x < m) {
                    e = b - x;
                } else {
                    e = a - x;
                }
                d = GOLDEN_SECTION * e;
            }

            // Update by at least "tol1".
            // Here d is never NaN so the evaluation point u is always finite.
            final double u;
            if (Math.abs(d) < tol1) {
                if (d >= 0) {
                    u = x + tol1;
                } else {
                    u = x - tol1;
                }
            } else {
                u = x + d;
            }

            evaluations++;
            final double fu = func.applyAsDouble(u);

            // Maintain the best encountered result
            if (fu < bestFx) {
                bestX = u;
                bestFx = fu;
            }

            // Note:
            // Here the use of a convergence checker on f(x) previous vs current has been removed.
            // Typically when the checker requires a very small relative difference
            // the optimizer will stop before, or soon after, on Brent's criterion when that is
            // configured with the smallest recommended convergence criteria.

            // Update a, b, v, w and x.
            if (fu <= fx) {
                if (u < x) {
                    b = x;
                } else {
                    a = x;
                }
                v = w;
                fv = fw;
                w = x;
                fw = fx;
                x = u;
                fx = fu;
            } else {
                if (u < x) {
                    a = u;
                } else {
                    b = u;
                }
                if (fu <= fw ||
                    Precision.equals(w, x)) {
                    v = w;
                    fv = fw;
                    w = u;
                    fw = fu;
                } else if (fu <= fv ||
                           Precision.equals(v, x) ||
                           Precision.equals(v, w)) {
                    v = u;
                    fv = fu;
                }
            }
        }
    }
}
