/**
 * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */
package com.opengamma.analytics.math.rootfinding;

import com.opengamma.analytics.math.MathException;
import com.opengamma.analytics.math.function.Function1D;

/**
 *
 */
public class BrentSingleRootFinder extends RealSingleRootFinder {
  private static final int MAX_ITER = 100;
  private static final double ZERO = 1e-16;
  private final double _accuracy;

  /**
   * Sets the accuracy to 10<sup>-15</sup>.
   */
  public BrentSingleRootFinder() {
    this(1e-15);
  }

  /**
   * @param accuracy
   *          The accuracy of the root
   */
  public BrentSingleRootFinder(final double accuracy) {
    _accuracy = accuracy;
  }

  @Override
  public Double getRoot(final Function1D<Double, Double> function, final Double xLower, final Double xUpper) {
    checkInputs(function, xLower, xUpper);
    if (Double.compare(xLower, xUpper) == 0) {
      return xLower;
    }
    double x1 = xLower;
    double x2 = xUpper;
    double x3 = xUpper;
    double delta = 0;
    double oldDelta = 0;
    double f1 = function.evaluate(x1);
    double f2 = function.evaluate(x2);
    double f3 = f2;
    double r1, r2, r3, r4, eps, xMid, min1, min2;
    for (int i = 0; i < MAX_ITER; i++) {
      if (f2 > 0 && f3 > 0 || f2 < 0 && f3 < 0) {
        x3 = x1;
        f3 = f1;
        delta = x2 - x1;
        oldDelta = delta;
      }
      if (Math.abs(f3) < Math.abs(f2)) {
        x1 = x2;
        x2 = x3;
        x3 = x1;
        f1 = f2;
        f2 = f3;
        f3 = f1;
      }
      eps = 2 * ZERO * Math.abs(x2) + 0.5 * _accuracy;
      xMid = (x3 - x2) / 2;
      if (Math.abs(xMid) <= eps) {
        return x2;
      }
      if (Math.abs(oldDelta) >= eps && Math.abs(f1) > Math.abs(f2)) {
        r4 = f2 / f1;
        if (Math.abs(x1 - x3) < ZERO) {
          r1 = 2 * xMid * r4;
          r2 = 1 - r4;
        } else {
          r2 = f1 / f3;
          r3 = f2 / f3;
          r1 = r4 * (2 * xMid * r2 * (r2 - r3) - (x2 - x1) * (r3 - 1));
          r2 = (r2 - 1) * (r3 - 1) * (r4 - 1);
        }
        if (r1 > 0) {
          r2 *= -1;
        }
        r1 = Math.abs(r1);
        min1 = 3 * xMid * r2 - Math.abs(eps * r2);
        min2 = Math.abs(oldDelta * r2);
        if (2 * r1 < (min1 < min2 ? min1 : min2)) {
          oldDelta = delta;
          delta = r1 / r2;
        } else {
          delta = xMid;
          oldDelta = delta;
        }
      } else {
        delta = xMid;
        oldDelta = delta;
      }
      x1 = x2;
      f1 = f2;
      if (Math.abs(delta) > eps) {
        x2 += delta;
      } else {
        x2 += Math.copySign(eps, xMid);
      }
      f1 = function.evaluate(x1);
      f2 = function.evaluate(x2);
      f3 = function.evaluate(x3);
    }
    throw new MathException("Could not converge to root in " + MAX_ITER + " attempts");
  }
}
