class ImpliedTrinomialTreeLocalVolatilityCalculator implements LocalVolatilityCalculator {

  @Override
  public InterpolatedNodalSurface localVolatilityFromPrice(
      Surface callPriceSurface,
      double spot,
      Function<Double, Double> interestRate,
      Function<Double, Double> dividendRate) {

    double[][] stateValue = new double[nSteps + 1][];
    double[] df = new double[nSteps];
    List<DoubleMatrix> probability = new ArrayList<DoubleMatrix>(nSteps);
    int nTotal = (nSteps - 1) * (nSteps - 1) + 1;
    double[] timeRes = new double[nTotal];
    double[] spotRes = new double[nTotal];
    double[] volRes = new double[nTotal];
    // uniform grid based on TrigeorgisLatticeSpecification, using reference values
    double refPrice = callPriceSurface.zValue(maxTime, spot) * Math.exp(interestRate.apply(maxTime) * maxTime);
    double refForward = spot * Math.exp((interestRate.apply(maxTime) - dividendRate.apply(maxTime)) * maxTime);
    double refVolatility = BlackFormulaRepository.impliedVolatility(refPrice, refForward, spot, maxTime, true);
    double dt = maxTime / nSteps;
    double dx = refVolatility * Math.sqrt(3d * dt);
    double upFactor = Math.exp(dx);
    double downFactor = Math.exp(-dx);
    double[] adSec = new double[2 * nSteps + 1];
    double[] assetPrice = new double[2 * nSteps + 1];
    for (int i = nSteps; i > -1; --i) {
      if (i == 0) {
        resolveFirstLayer(interestRate, dividendRate, nTotal, dt, spot, adSec, assetPrice, timeRes, spotRes, volRes,
            df, stateValue, probability);
      } else {
        double time = dt * i;
        double zeroRate = interestRate.apply(time);
        double zeroDividendRate = dividendRate.apply(time);
        int nNodes = 2 * i + 1;
        double[] assetPriceLocal = new double[nNodes];
        double[] callOptionPrice = new double[nNodes];
        double[] putOptionPrice = new double[nNodes];
        int position = i - 1;
        double assetTmp = spot * Math.pow(upFactor, i);
        // call options for upper half nodes
        for (int j = nNodes - 1; j > position - 1; --j) {
          assetPriceLocal[j] = assetTmp;
          callOptionPrice[j] = callPriceSurface.zValue(time, assetPriceLocal[j]);
          assetTmp *= downFactor;
        }
        // put options for lower half nodes
        assetTmp = spot * Math.pow(downFactor, i);
        for (int j = 0; j < position + 2; ++j) {
          assetPriceLocal[j] = assetTmp;
          putOptionPrice[j] = callPriceSurface.zValue(time, assetPriceLocal[j]) - spot * Math.exp(-zeroDividendRate * time) +
              Math.exp(-zeroRate * time) * assetPriceLocal[j];
          assetTmp *= upFactor;
        }
        resolveLayer(interestRate, dividendRate, i, nTotal, position, dt, zeroRate, zeroDividendRate, callOptionPrice,
            putOptionPrice, adSec, assetPrice, assetPriceLocal, timeRes, spotRes, volRes, df, stateValue, probability);
      }
    }
    SurfaceMetadata metadata = DefaultSurfaceMetadata.builder()
        .xValueType(ValueType.YEAR_FRACTION)
        .yValueType(ValueType.STRIKE)
        .zValueType(ValueType.LOCAL_VOLATILITY)
        .surfaceName(SurfaceName.of("localVol_" + callPriceSurface.getName()))
        .build();
    return InterpolatedNodalSurface.ofUnsorted(
        metadata,
        DoubleArray.ofUnsafe(timeRes),
        DoubleArray.ofUnsafe(spotRes),
        DoubleArray.ofUnsafe(volRes),
        interpolator);
  }

  public  ImpliedTrinomialTreeLocalVolatilityCalculator();
  public  ImpliedTrinomialTreeLocalVolatilityCalculator(int nSteps, double maxTime);
  public  ImpliedTrinomialTreeLocalVolatilityCalculator(int nSteps, double maxTime, SurfaceInterpolator interpolator);

}