@Override
 public void planSkipped(HRegionInfo hri, PlanType type) {
   skippedCount[type.ordinal()]++;
 }
/**
 * Simple implementation of region normalizer.
 *
 * <p>Logic in use:
 *
 * <ol>
 *   <li>get all regions of a given table
 *   <li>get avg size S of each region (by total size of store files reported in RegionLoad)
 *   <li>If biggest region is bigger than S * 2, it is kindly requested to split, and normalization
 *       stops
 *   <li>Otherwise, two smallest region R1 and its smallest neighbor R2 are kindly requested to
 *       merge, if R1 + R1 &lt; S, and normalization stops
 *   <li>Otherwise, no action is performed
 * </ol>
 *
 * <p>Region sizes are coarse and approximate on the order of megabytes. Additionally, "empty"
 * regions (less than 1MB, with the previous note) are not merged away. This is by design to prevent
 * normalization from undoing the pre-splitting of a table.
 */
@InterfaceAudience.Private
public class SimpleRegionNormalizer implements RegionNormalizer {

  private static final Log LOG = LogFactory.getLog(SimpleRegionNormalizer.class);
  private static final int MIN_REGION_COUNT = 3;
  private MasterServices masterServices;
  private static long[] skippedCount = new long[PlanType.values().length];

  /**
   * Set the master service.
   *
   * @param masterServices inject instance of MasterServices
   */
  @Override
  public void setMasterServices(MasterServices masterServices) {
    this.masterServices = masterServices;
  }

  @Override
  public void planSkipped(HRegionInfo hri, PlanType type) {
    skippedCount[type.ordinal()]++;
  }

  @Override
  public long getSkippedCount(NormalizationPlan.PlanType type) {
    return skippedCount[type.ordinal()];
  }

  // Comparator that gives higher priority to region Split plan
  private Comparator<NormalizationPlan> planComparator =
      new Comparator<NormalizationPlan>() {
        @Override
        public int compare(NormalizationPlan plan, NormalizationPlan plan2) {
          if (plan instanceof SplitNormalizationPlan) return -1;
          if (plan2 instanceof SplitNormalizationPlan) return 1;
          return 0;
        }
      };

  /**
   * Computes next most "urgent" normalization action on the table. Action may be either a split, or
   * a merge, or no action.
   *
   * @param table table to normalize
   * @param types desired types of NormalizationPlan
   * @return normalization plan to execute
   */
  @Override
  public List<NormalizationPlan> computePlanForTable(TableName table, List<PlanType> types)
      throws HBaseIOException {
    if (table == null || table.isSystemTable()) {
      LOG.debug("Normalization of system table " + table + " isn't allowed");
      return null;
    }

    List<NormalizationPlan> plans = new ArrayList<NormalizationPlan>();
    List<HRegionInfo> tableRegions =
        masterServices.getAssignmentManager().getRegionStates().getRegionsOfTable(table);

    // TODO: should we make min number of regions a config param?
    if (tableRegions == null || tableRegions.size() < MIN_REGION_COUNT) {
      int nrRegions = tableRegions == null ? 0 : tableRegions.size();
      LOG.debug(
          "Table "
              + table
              + " has "
              + nrRegions
              + " regions, required min number"
              + " of regions for normalizer to run is "
              + MIN_REGION_COUNT
              + ", not running normalizer");
      return null;
    }

    LOG.debug(
        "Computing normalization plan for table: "
            + table
            + ", number of regions: "
            + tableRegions.size());

    long totalSizeMb = 0;

    for (int i = 0; i < tableRegions.size(); i++) {
      HRegionInfo hri = tableRegions.get(i);
      long regionSize = getRegionSize(hri);
      totalSizeMb += regionSize;
    }

    double avgRegionSize = totalSizeMb / (double) tableRegions.size();

    LOG.debug("Table " + table + ", total aggregated regions size: " + totalSizeMb);
    LOG.debug("Table " + table + ", average region size: " + avgRegionSize);

    int candidateIdx = 0;
    while (candidateIdx < tableRegions.size()) {
      HRegionInfo hri = tableRegions.get(candidateIdx);
      long regionSize = getRegionSize(hri);
      // if the region is > 2 times larger than average, we split it, split
      // is more high priority normalization action than merge.
      if (types.contains(PlanType.SPLIT) && regionSize > 2 * avgRegionSize) {
        LOG.debug(
            "Table "
                + table
                + ", large region "
                + hri.getRegionNameAsString()
                + " has size "
                + regionSize
                + ", more than twice avg size, splitting");
        plans.add(new SplitNormalizationPlan(hri, null));
      } else {
        if (candidateIdx == tableRegions.size() - 1) {
          break;
        }
        HRegionInfo hri2 = tableRegions.get(candidateIdx + 1);
        long regionSize2 = getRegionSize(hri2);
        if (types.contains(PlanType.MERGE) && regionSize + regionSize2 < avgRegionSize) {
          LOG.debug(
              "Table "
                  + table
                  + ", small region size: "
                  + regionSize
                  + " plus its neighbor size: "
                  + regionSize2
                  + ", less than the avg size "
                  + avgRegionSize
                  + ", merging them");
          plans.add(new MergeNormalizationPlan(hri, hri2));
          candidateIdx++;
        }
      }
      candidateIdx++;
    }
    if (plans.isEmpty()) {
      LOG.debug("No normalization needed, regions look good for table: " + table);
      return null;
    }
    Collections.sort(plans, planComparator);
    return plans;
  }

  private long getRegionSize(HRegionInfo hri) {
    ServerName sn =
        masterServices.getAssignmentManager().getRegionStates().getRegionServerOfRegion(hri);
    RegionLoad regionLoad =
        masterServices.getServerManager().getLoad(sn).getRegionsLoad().get(hri.getRegionName());
    return regionLoad.getStorefileSizeMB();
  }
}