/**
   * Calculates the Black-Scholes option implied volatility of a call, i.e., the payoff
   *
   * <p><i>max(S(T)-K,0)</i>, where <i>S</i> follows a log-normal process with constant
   * log-volatility.
   *
   * @param forward The forward of the underlying.
   * @param optionMaturity The option maturity T.
   * @param optionStrike The option strike. If the option strike is &le; 0.0 the method returns the
   *     value of the forward contract paying S(T)-K in T.
   * @param payoffUnit The payoff unit (e.g., the discount factor)
   * @param optionValue The option value.
   * @return Returns the implied volatility of a European call option under the Black-Scholes model.
   */
  public static double blackScholesOptionImpliedVolatility(
      double forward,
      double optionMaturity,
      double optionStrike,
      double payoffUnit,
      double optionValue) {
    // Limit the maximum number of iterations, to ensure this calculation returns fast, e.g. in
    // cases when there is no such thing as an implied vol
    // TODO: An exception should be thrown, when there is no implied volatility for the given value.
    int maxIterations = 500;
    double maxAccuracy = 1E-15;

    if (optionStrike <= 0.0) {
      // Actually it is not an option
      return 0.0;
    } else {
      // Calculate an lower and upper bound for the volatility
      double p =
          NormalDistribution.inverseCumulativeDistribution(
                  (optionValue / payoffUnit + optionStrike) / (forward + optionStrike))
              / Math.sqrt(optionMaturity);
      double q = 2.0 * Math.abs(Math.log(forward / optionStrike)) / optionMaturity;

      double volatilityLowerBound = p + Math.sqrt(Math.max(p * p - q, 0.0));
      double volatilityUpperBound = p + Math.sqrt(p * p + q);

      // If strike is close to forward the two bounds are close to the analytic solution
      if (Math.abs(volatilityLowerBound - volatilityUpperBound) < maxAccuracy)
        return (volatilityLowerBound + volatilityUpperBound) / 2.0;

      // Solve for implied volatility
      NewtonsMethod solver =
          new NewtonsMethod(0.5 * (volatilityLowerBound + volatilityUpperBound) /* guess */);
      while (solver.getAccuracy() > maxAccuracy
          && !solver.isDone()
          && solver.getNumberOfIterations() < maxIterations) {
        double volatility = solver.getNextPoint();

        // Calculate analytic value
        double dPlus =
            (Math.log(forward / optionStrike) + 0.5 * volatility * volatility * optionMaturity)
                / (volatility * Math.sqrt(optionMaturity));
        double dMinus = dPlus - volatility * Math.sqrt(optionMaturity);
        double valueAnalytic =
            (forward * NormalDistribution.cumulativeDistribution(dPlus)
                    - optionStrike * NormalDistribution.cumulativeDistribution(dMinus))
                * payoffUnit;
        double derivativeAnalytic =
            forward
                * Math.sqrt(optionMaturity)
                * Math.exp(-0.5 * dPlus * dPlus)
                / Math.sqrt(2.0 * Math.PI)
                * payoffUnit;

        double error = valueAnalytic - optionValue;

        solver.setValueAndDerivative(error, derivativeAnalytic);
      }

      return solver.getBestPoint();
    }
  }