/**
   * @param pp
   * @param xKeys
   * @return Second derivatives of piecewise polynomial functions at xKeys When _dim in
   *     PiecewisePolynomialResult is greater than 1, i.e., the struct contains multiple piecewise
   *     polynomials, a row vector of return value corresponds to each piecewise polynomial
   */
  public DoubleMatrix2D differentiateTwice(
      final PiecewisePolynomialResult pp, final double[] xKeys) {
    ArgumentChecker.notNull(pp, "pp");
    ArgumentChecker.isFalse(pp.getOrder() < 3, "polynomial degree < 2");

    final double[][] coefs = pp.getCoefMatrix().getData();
    final double[] knots = pp.getKnots().getData();

    final int nKnots = pp.getNumberOfIntervals() + 1;
    final int nCoefs = pp.getOrder();
    final int dim = pp.getDimensions();

    double[][] res = new double[dim * (nKnots - 1)][nCoefs - 2];
    for (int i = 0; i < dim * (nKnots - 1); ++i) {
      Arrays.fill(res[i], 0.);
    }

    for (int i = 0; i < dim * (nKnots - 1); ++i) {
      for (int j = 0; j < nCoefs - 2; ++j) {
        res[i][j] = coefs[i][j] * (nCoefs - j - 1) * (nCoefs - j - 2);
      }
    }

    PiecewisePolynomialResult ppDiff =
        new PiecewisePolynomialResult(
            new DoubleMatrix1D(knots), DoubleMatrix2D.noCopy(res), nCoefs - 1, pp.getDimensions());

    return evaluate(ppDiff, xKeys);
  }
  /**
   * @param pp PiecewisePolynomialResult
   * @param initialKey
   * @param xKeys
   * @return Integral of piecewise polynomial between initialKey and xKeys
   */
  public DoubleMatrix1D integrate(
      final PiecewisePolynomialResult pp, final double initialKey, final double[] xKeys) {
    ArgumentChecker.notNull(pp, "pp");
    ArgumentChecker.notNull(xKeys, "xKeys");

    ArgumentChecker.isFalse(Double.isNaN(initialKey), "initialKey containing NaN");
    ArgumentChecker.isFalse(Double.isInfinite(initialKey), "initialKey containing Infinity");
    ArgumentChecker.isTrue(pp.getDimensions() == 1, "Dimension should be 1");

    final double[] knots = pp.getKnots().getData();
    final int nCoefs = pp.getOrder();
    final int nKnots = pp.getNumberOfIntervals() + 1;
    final double[][] coefMatrix = pp.getCoefMatrix().getData();

    double[][] res = new double[nKnots - 1][nCoefs + 1];
    for (int i = 0; i < nKnots - 1; ++i) {
      Arrays.fill(res[i], 0.);
    }

    for (int i = 0; i < nKnots - 1; ++i) {
      for (int j = 0; j < nCoefs; ++j) {
        res[i][j] = coefMatrix[i][j] / (nCoefs - j);
      }
    }

    double[] constTerms = new double[nKnots - 1];
    Arrays.fill(constTerms, 0.);

    int indicator = 0;
    if (initialKey <= knots[1]) {
      indicator = 0;
    } else {
      for (int i = 1; i < nKnots - 1; ++i) {
        if (knots[i] < initialKey) {
          indicator = i;
        }
      }
    }

    double sum = getValue(res[indicator], initialKey, knots[indicator]);
    for (int i = indicator; i < nKnots - 2; ++i) {
      constTerms[i + 1] = constTerms[i] + getValue(res[i], knots[i + 1], knots[i]) - sum;
      sum = 0.;
    }

    constTerms[indicator] = -getValue(res[indicator], initialKey, knots[indicator]);
    for (int i = indicator - 1; i > -1; --i) {
      constTerms[i] = constTerms[i + 1] - getValue(res[i], knots[i + 1], knots[i]);
    }
    for (int i = 0; i < nKnots - 1; ++i) {
      res[i][nCoefs] = constTerms[i];
    }

    final PiecewisePolynomialResult ppInt =
        new PiecewisePolynomialResult(
            new DoubleMatrix1D(knots), DoubleMatrix2D.noCopy(res), nCoefs + 1, 1);

    return new DoubleMatrix1D(evaluate(ppInt, xKeys).getData()[0]);
  }
  /**
   * @param pp PiecewisePolynomialResult
   * @param xKeys
   * @return Values of piecewise polynomial functions at xKeys When _dim in
   *     PiecewisePolynomialResult is greater than 1, i.e., the struct contains multiple piecewise
   *     polynomials, one element of return vector of DoubleMatrix2D corresponds to each piecewise
   *     polynomial
   */
  public DoubleMatrix2D[] evaluate(final PiecewisePolynomialResult pp, final double[][] xKeys) {
    ArgumentChecker.notNull(pp, "pp");
    ArgumentChecker.notNull(xKeys, "xKeys");

    final int keyLength = xKeys[0].length;
    final int keyDim = xKeys.length;
    for (int j = 0; j < keyDim; ++j) {
      for (int i = 0; i < keyLength; ++i) {
        ArgumentChecker.isFalse(Double.isNaN(xKeys[j][i]), "xKeys containing NaN");
        ArgumentChecker.isFalse(Double.isInfinite(xKeys[j][i]), "xKeys containing Infinity");
      }
    }

    final double[] knots = pp.getKnots().getData();
    final int nKnots = knots.length;
    final DoubleMatrix2D coefMatrix = pp.getCoefMatrix();
    final int dim = pp.getDimensions();

    double[][][] res = new double[dim][keyDim][keyLength];

    for (int k = 0; k < dim; ++k) {
      for (int l = 0; l < keyDim; ++l) {
        for (int j = 0; j < keyLength; ++j) {
          int indicator = 0;
          if (xKeys[l][j] < knots[1]) {
            indicator = 0;
          } else {
            for (int i = 1; i < nKnots - 1; ++i) {
              if (knots[i] <= xKeys[l][j]) {
                indicator = i;
              }
            }
          }

          final double[] coefs = coefMatrix.getRowVector(dim * indicator + k, false).getData();
          res[k][l][j] = getValue(coefs, xKeys[l][j], knots[indicator]);
          ArgumentChecker.isFalse(Double.isInfinite(res[k][l][j]), "Too large input");
          ArgumentChecker.isFalse(Double.isNaN(res[k][l][j]), "Too large input");
        }
      }
    }

    DoubleMatrix2D[] resMat = new DoubleMatrix2D[dim];
    for (int i = 0; i < dim; ++i) {
      resMat[i] = DoubleMatrix2D.noCopy(res[i]);
    }

    return resMat;
  }
  @Override
  public PiecewisePolynomialResult interpolate(
      final double[] xValues, final double[][] yValuesMatrix) {
    ArgumentChecker.notNull(xValues, "xValues");
    ArgumentChecker.notNull(yValuesMatrix, "yValuesMatrix");

    ArgumentChecker.isTrue(
        xValues.length == yValuesMatrix[0].length | xValues.length + 2 == yValuesMatrix[0].length,
        "(xValues length = yValuesMatrix's row vector length) or (xValues length + 2 = yValuesMatrix's row vector length)");
    ArgumentChecker.isTrue(xValues.length > 2, "Data points should be more than 2");

    final int nDataPts = xValues.length;
    final int yValuesLen = yValuesMatrix[0].length;
    final int dim = yValuesMatrix.length;

    for (int i = 0; i < nDataPts; ++i) {
      ArgumentChecker.isFalse(Double.isNaN(xValues[i]), "xValues containing NaN");
      ArgumentChecker.isFalse(Double.isInfinite(xValues[i]), "xValues containing Infinity");
    }
    for (int i = 0; i < yValuesLen; ++i) {
      for (int j = 0; j < dim; ++j) {
        ArgumentChecker.isFalse(Double.isNaN(yValuesMatrix[j][i]), "yValuesMatrix containing NaN");
        ArgumentChecker.isFalse(
            Double.isInfinite(yValuesMatrix[j][i]), "yValuesMatrix containing Infinity");
      }
    }
    for (int i = 0; i < nDataPts; ++i) {
      for (int j = i + 1; j < nDataPts; ++j) {
        ArgumentChecker.isFalse(xValues[i] == xValues[j], "xValues should be distinct");
      }
    }

    double[] xValuesSrt = new double[nDataPts];
    DoubleMatrix2D[] coefMatrix = new DoubleMatrix2D[dim];

    for (int i = 0; i < dim; ++i) {
      xValuesSrt = Arrays.copyOf(xValues, nDataPts);
      double[] yValuesSrt = new double[nDataPts];
      if (nDataPts == yValuesLen) {
        yValuesSrt = Arrays.copyOf(yValuesMatrix[i], nDataPts);
      } else {
        yValuesSrt = Arrays.copyOfRange(yValuesMatrix[i], 1, nDataPts + 1);
      }
      ParallelArrayBinarySort.parallelBinarySort(xValuesSrt, yValuesSrt);

      final double[] intervals = _solver.intervalsCalculator(xValuesSrt);
      final double[] slopes = _solver.slopesCalculator(yValuesSrt, intervals);
      final PiecewisePolynomialResult result = _method.interpolate(xValues, yValuesMatrix[i]);

      ArgumentChecker.isTrue(result.getOrder() >= 3, "Primary interpolant should be degree >= 2");

      final double[] initialFirst = _function.differentiate(result, xValuesSrt).getData()[0];
      final double[] initialSecond = _function.differentiateTwice(result, xValuesSrt).getData()[0];
      final double[] first = firstDerivativeCalculator(yValuesSrt, intervals, slopes, initialFirst);

      boolean modFirst = false;
      int k;
      double[] aValues = aValuesCalculator(slopes, first);
      double[] bValues = bValuesCalculator(slopes, first);
      double[][] intervalsA = getIntervalsA(intervals, slopes, first, bValues);
      double[][] intervalsB = getIntervalsB(intervals, slopes, first, aValues);
      while (modFirst == false) {
        k = 0;
        for (int j = 0; j < nDataPts - 2; ++j) {
          if (first[j + 1] > 0.) {
            if (intervalsA[j + 1][1] + Math.abs(intervalsA[j + 1][1]) * ERROR
                    < intervalsB[j][0] - Math.abs(intervalsB[j][0]) * ERROR
                | intervalsA[j + 1][0] - Math.abs(intervalsA[j + 1][0]) * ERROR
                    > intervalsB[j][1] + Math.abs(intervalsB[j][1]) * ERROR) {
              ++k;
              first[j + 1] =
                  firstDerivativesRecalculator(intervals, slopes, aValues, bValues, j + 1);
            }
          }
        }
        if (k == 0) {
          modFirst = true;
        }
        aValues = aValuesCalculator(slopes, first);
        bValues = bValuesCalculator(slopes, first);
        intervalsA = getIntervalsA(intervals, slopes, first, bValues);
        intervalsB = getIntervalsB(intervals, slopes, first, aValues);
      }
      final double[] second = secondDerivativeCalculator(initialSecond, intervalsA, intervalsB);

      coefMatrix[i] =
          new DoubleMatrix2D(_solver.solve(yValuesSrt, intervals, slopes, first, second));
    }

    final int nIntervals = coefMatrix[0].getNumberOfRows();
    final int nCoefs = coefMatrix[0].getNumberOfColumns();
    double[][] resMatrix = new double[dim * nIntervals][nCoefs];

    for (int i = 0; i < nIntervals; ++i) {
      for (int j = 0; j < dim; ++j) {
        resMatrix[dim * i + j] = coefMatrix[j].getRowVector(i, false).getData();
      }
    }

    for (int i = 0; i < (nIntervals * dim); ++i) {
      for (int j = 0; j < nCoefs; ++j) {
        ArgumentChecker.isFalse(Double.isNaN(resMatrix[i][j]), "Too large input");
        ArgumentChecker.isFalse(Double.isInfinite(resMatrix[i][j]), "Too large input");
      }
    }

    return new PiecewisePolynomialResult(
        new DoubleMatrix1D(xValuesSrt, false), DoubleMatrix2D.noCopy(resMatrix), nCoefs, dim);
  }