public void start(IGoogleLandscapeInterpolatorUserConfiguration userConfig) throws Exception {

    while (!Thread.interrupted()) {

      LOGGER.info("Interpolate based on downloaded landscape");
      try {
        processData(userConfig);
      } catch (Exception e) {
        throw new RuntimeException("Something wrong while first interpolation", e);
      }

      if (userConfig.getGoogleLandscapeInterpolator().interpolateInterval > 0) {
        try {
          Thread.sleep(
              userConfig.getGoogleLandscapeInterpolator().interpolateInterval * 60 * 60 * 1000L);
        } catch (InterruptedException e) {
          // do nothing
        }
      } else {
        break;
      }
    }
  }
  private List<LandscapeData> loadLandscape(
      IGoogleLandscapeInterpolatorUserConfiguration userConfig, Map<String, Account> accounts)
      throws Exception {
    List<LandscapeData> result = new CopyOnWriteArrayList<>();

    List<String> headers =
        Arrays.asList(
            "AccountID",
            "CriterionId",
            "CampaignId",
            "AdGroupId",
            "StartDate",
            "EndDate",
            "Bid",
            "LocalClicks",
            "LocalCost",
            "LocalImpressions");

    if (userConfig.getGoogleLandscapeInterpolator().pullLandscapeBeforeInterpolate) {
      LOGGER.info("Download landscape before interpolation");
      landscapeReportService.report(
          accounts,
          new BasicReportDataConsumer() {
            @Override
            public void accept(String accountId, DataRow dataRow) {
              readLandscape(accountId, result, dataRow);
            }
          });
      LOGGER.info("Landscape downloaded");
    } else {
      File landscape = new File(userConfig.getGoogleLandscapeReport().filePath);
      if (!landscape.exists()) {
        googleLandscapeService.start(userConfig);
      }

      csvService.loadCsv(
          landscape,
          headers,
          0,
          dataRow -> {
            readLandscape(dataRow.apply("AccountId"), result, column -> dataRow.apply(column));
          });
    }

    // order X values
    result.forEach(ld -> ld.bids.sort((bid1, bid2) -> bid1.bid.compareTo(bid2.bid)));

    return result;
  }
  private void report(
      IGoogleLandscapeInterpolatorUserConfiguration userConfig,
      List<LandscapeData> landscapeData,
      Map<String, Account> accounts)
      throws Exception {

    Map<CriterionId, Criterion> criterions = getCriterionsMap(accounts);

    settingsLoader.downloadSettings(
        accounts,
        criterion -> {
          Long id = getLongValue(criterion.apply("Id"));
          Long groupId = getLongValue(criterion.apply("AdGroupId"));

          Criterion cr = criterions.get(new CriterionId(id, groupId));
          if (cr != null) {
            ((BidMetrics) cr.getMetrics())
                .setMaxBid(getDoubleValue(criterion.apply("CpcBid")) / 1_000_000);
          }
        });

    Map<Long, Map<Long, Double>> conversionsRate = getKeywordConversionsRate(accounts);

    File linearReportFile = fsUtils.getResultFile(userConfig, LINEAR);
    File cubicReportFile = fsUtils.getResultFile(userConfig, CUBIC);

    for (String interpolationType : userConfig.getGoogleLandscapeInterpolator().interpolationType) {
      switch (interpolationType) {
        case LINEAR:
          {
            try (ICsvListWriter writer =
                new CsvListWriter(
                    new FileWriter(linearReportFile), CsvPreference.STANDARD_PREFERENCE)) {
              writer.write(Arrays.asList(linear_report_fields));
            }
            break;
          }
        case CUBIC:
          {
            try (ICsvListWriter writer =
                new CsvListWriter(
                    new FileWriter(cubicReportFile), CsvPreference.STANDARD_PREFERENCE)) {
              writer.write(Arrays.asList(cubic_report_fields));
            }
          }
      }
    }

    for (LandscapeData ld : landscapeData) {
      Criterion cr = criterions.get(new CriterionId(ld.keywordId, ld.adGroupId));

      Double bid = ((BidMetrics) cr.getMetrics()).getMaxBid();

      Double increasedBidValue =
          bid * (100 + userConfig.getGoogleLandscapeInterpolator().bidFactor) / 100;
      Double decreasedBidValue =
          bid * (100 - userConfig.getGoogleLandscapeInterpolator().bidFactor) / 100;

      Map<Long, Double> conversions = conversionsRate.get(ld.adGroupId);

      for (String interpolationType :
          userConfig.getGoogleLandscapeInterpolator().interpolationType) {
        switch (interpolationType) {
          case LINEAR:
            writeLinearInterpolationBids(
                linearReportFile, bid, ld, increasedBidValue, decreasedBidValue, conversions);
            break;
          case CUBIC:
            writeCubicInterpolationBids(
                cubicReportFile, bid, ld, increasedBidValue, decreasedBidValue, conversions);
        }
      }
    }
    switch (userConfig.getGoogleLandscapeInterpolator().interpolationType.get(0)) {
      case LINEAR:
        {
          userConfig.setResultFile(linearReportFile.getAbsolutePath());
          break;
        }
      case CUBIC:
        {
          userConfig.setResultFile(cubicReportFile.getAbsolutePath());
        }
    }
  }