/**
   * Performs a non-linear - yet somewhat intelligent - search for the best values for the smoothing
   * coefficients alpha and beta for the given data set.
   *
   * <p>For the given data set, and models with a small, medium and large value of the alpha
   * smoothing constant, returns the best fit model where the value of the alpha and beta (trend)
   * smoothing constants are within the given tolerances.
   *
   * <p>Note that the descriptions of the parameters below include a discussion of valid values.
   * However, since this is a private method and to help improve performance, we don't provide any
   * validation of these parameters. Using invalid values may lead to unexpected results.
   *
   * @param dataSet the data set for which a best fit model is required.
   * @param modelMin the pre-initialized best fit model with the smallest value of the alpha
   *     smoothing constant found so far.
   * @param modelMid the pre-initialized best fit model with the value of the alpha smoothing
   *     constant between that of modelMin and modelMax.
   * @param modelMax the pre-initialized best fit model with the largest value of the alpha
   *     smoothing constant found so far.
   * @param alphaTolerance the tolerance within which the alpha value is required. Must be
   *     considerably less than 1.0. However, note that the smaller this value the longer it will
   *     take to diverge on a best fit model.
   * @param betaTolerance the tolerance within which the beta value is required. Must be
   *     considerably less than 1.0. However, note that the smaller this value the longer it will
   *     take to diverge on a best fit model. This value can be the same as, greater than or less
   *     than the value of the alphaTolerance parameter. It makes no difference - at least to this
   *     code.
   */
  private static TripleExponentialSmoothingModel findBest(
      DataSet dataSet,
      TripleExponentialSmoothingModel modelMin,
      TripleExponentialSmoothingModel modelMid,
      TripleExponentialSmoothingModel modelMax,
      double alphaTolerance,
      double betaTolerance) {
    double alphaMin = modelMin.getAlpha();
    double alphaMid = modelMid.getAlpha();
    double alphaMax = modelMax.getAlpha();

    // If we're not making much ground, then we're done
    if (Math.abs(alphaMid - alphaMin) < alphaTolerance
        && Math.abs(alphaMax - alphaMid) < alphaTolerance) return modelMid;

    TripleExponentialSmoothingModel model[] = new TripleExponentialSmoothingModel[5];
    model[0] = modelMin;
    model[1] = findBestBeta(dataSet, (alphaMin + alphaMid) / 2.0, 0.0, 1.0, betaTolerance);
    model[2] = modelMid;
    model[3] = findBestBeta(dataSet, (alphaMid + alphaMax) / 2.0, 0.0, 1.0, betaTolerance);
    model[4] = modelMax;

    for (int m = 0; m < 5; m++) model[m].init(dataSet);

    int bestModelIndex = 0;
    for (int m = 1; m < 5; m++)
      if (model[m].getMSE() < model[bestModelIndex].getMSE()) bestModelIndex = m;

    switch (bestModelIndex) {
      case 1:
        // Reduce maximums
        // Can discard models 3 and 4
        model[3] = null;
        model[4] = null;
        return findBest(dataSet, model[0], model[1], model[2], alphaTolerance, betaTolerance);

      case 2:
        // Can discard models 0 and 4
        model[0] = null;
        model[4] = null;
        return findBest(dataSet, model[1], model[2], model[3], alphaTolerance, betaTolerance);

      case 3:
        // Reduce minimums
        // Can discard models 0 and 1
        model[0] = null;
        model[1] = null;
        return findBest(dataSet, model[2], model[3], model[4], alphaTolerance, betaTolerance);

      case 0:
      case 4:
        // We're done???
        break;
    }

    // Release all but the best model constructed so far
    for (int m = 0; m < 5; m++) if (m != bestModelIndex) model[m] = null;

    return model[bestModelIndex];
  }
  /**
   * For the given value of the alpha smoothing constant, returns the best fit model where the value
   * of the beta (trend) smoothing constant is between betaMin and betaMax. This method will
   * continually try to refine the estimate of beta until a tolerance of less than betaTolerance is
   * achieved.
   *
   * <p>Note that the descriptions of the parameters below include a discussion of valid values.
   * However, since this is a private method and to help improve performance, we don't provide any
   * validation of these parameters. Using invalid values may lead to unexpected results.
   *
   * @param dataSet the data set for which a best fit model is required.
   * @param alpha the (fixed) value of the alpha smoothing constant to use for the best fit model.
   * @param betaMin the minimum value of the beta (trend) smoothing constant accepted in the
   *     resulting best fit model. Must be greater than (or equal to) 0.0 and less than betaMax.
   * @param betaMax the maximum value of the beta (trend) smoothing constant accepted in the
   *     resulting best fit model. Must be greater than betaMin and less than (or equal to) 1.0.
   * @param betaTolerance the tolerance within which the beta value is required. Must be
   *     considerably less than 1.0. However, note that the smaller this value the longer it will
   *     take to diverge on a best fit model.
   */
  private static TripleExponentialSmoothingModel findBestBeta(
      DataSet dataSet, double alpha, double betaMin, double betaMax, double betaTolerance) {
    int stepsPerIteration = 10;

    if (betaMin < 0.0) betaMin = 0.0;
    if (betaMax > 1.0) betaMax = 1.0;

    TripleExponentialSmoothingModel bestModel =
        new TripleExponentialSmoothingModel(alpha, betaMin, 0.0);
    bestModel.init(dataSet);

    double initialMSE = bestModel.getMSE();

    boolean betaImproving = true;
    double betaStep = (betaMax - betaMin) / stepsPerIteration;
    double beta = betaMin + betaStep;
    for (; beta <= betaMax || betaImproving; ) {
      TripleExponentialSmoothingModel model = new TripleExponentialSmoothingModel(alpha, beta, 0.0);
      model.init(dataSet);

      if (model.getMSE() < bestModel.getMSE()) bestModel = model;
      else betaImproving = false;

      beta += betaStep;
      if (beta > 1.0) betaImproving = false;
    }

    // If we're making progress, then try to refine the beta estimate
    if (bestModel.getMSE() < initialMSE && betaStep > betaTolerance) {
      // Can this be further refined - improving efficiency - by
      //  only searching in the range beta-betaStep/2 to
      //  beta+betaStep/2 ?
      return findBestBeta(
          dataSet,
          bestModel.getAlpha(),
          bestModel.getBeta() - betaStep,
          bestModel.getBeta() + betaStep,
          betaTolerance);
    }

    return bestModel;
  }