/*
 * Copyright 2017 PingCAP, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.tikv.common.util;

import static org.tikv.common.key.Key.toRawKey;

import com.google.common.collect.BoundType;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;
import com.google.protobuf.ByteString;
import java.util.List;
import org.tikv.common.exception.TiClientInternalException;
import org.tikv.common.key.Key;
import org.tikv.kvproto.Coprocessor.KeyRange;

public class KeyRangeUtils {
  public static Range<Key> makeRange(ByteString startKey, ByteString endKey) {
    return Range.closedOpen(toRawKey(startKey, true), toRawKey(endKey));
  }

  /**
   * Build a Coprocessor Range with CLOSED_OPEN endpoints
   *
   * @param startKey startKey
   * @param endKey endKey
   * @return a CLOSED_OPEN range for coprocessor
   */
  public static KeyRange makeCoprocRange(ByteString startKey, ByteString endKey) {
    return KeyRange.newBuilder().setStart(startKey).setEnd(endKey).build();
  }

  /**
   * Build a Coprocessor Range
   *
   * @param range Range with Comparable endpoints
   * @return a CLOSED_OPEN range for coprocessor
   */
  public static KeyRange makeCoprocRange(Range<Key> range) {
    if (!range.hasLowerBound() || !range.hasUpperBound()) {
      throw new TiClientInternalException("range is not bounded");
    }
    if (range.lowerBoundType().equals(BoundType.OPEN)
        || range.upperBoundType().equals(BoundType.CLOSED)) {
      throw new TiClientInternalException("range must be CLOSED_OPEN");
    }
    return makeCoprocRange(
        range.lowerEndpoint().toByteString(), range.upperEndpoint().toByteString());
  }

  /**
   * Merge potential discrete ranges into one large range.
   *
   * @param ranges the range list to merge
   * @return the minimal range which encloses all ranges in this range list.
   */
  public static List<KeyRange> mergeRanges(List<KeyRange> ranges) {
    if (ranges == null || ranges.isEmpty() || ranges.size() == 1) {
      return ranges;
    }

    KeyRange first = ranges.get(0);
    Key lowMin = toRawKey(first.getStart(), true);
    Key upperMax = toRawKey(first.getEnd(), false);

    for (int i = 1; i < ranges.size(); i++) {
      KeyRange keyRange = ranges.get(i);
      Key start = toRawKey(keyRange.getStart(), true);
      Key end = toRawKey(keyRange.getEnd(), false);
      if (start.compareTo(lowMin) < 0) {
        lowMin = start;
      }
      if (end.compareTo(upperMax) > 0) {
        upperMax = end;
      }
    }

    ImmutableList.Builder<KeyRange> rangeBuilder = ImmutableList.builder();
    rangeBuilder.add(makeCoprocRange(lowMin.toByteString(), upperMax.toByteString()));
    return rangeBuilder.build();
  }

  /**
   * Merge SORTED potential discrete ranges into one large range.
   *
   * @param ranges the sorted range list to merge
   * @return the minimal range which encloses all ranges in this range list.
   */
  public static List<KeyRange> mergeSortedRanges(List<KeyRange> ranges) {
    return mergeSortedRanges(ranges, 1);
  }

  /**
   * Merge SORTED potential discrete ranges into no more than {@code splitNum} large range.
   *
   * @param ranges the sorted range list to merge
   * @param splitNum upper bound of number of ranges to merge into
   * @return the minimal range which encloses all ranges in this range list.
   */
  public static List<KeyRange> mergeSortedRanges(List<KeyRange> ranges, int splitNum) {
    if (splitNum <= 0) {
      throw new RuntimeException("Cannot split ranges by non-positive integer");
    }
    if (ranges == null || ranges.isEmpty() || ranges.size() <= splitNum) {
      return ranges;
    }
    // use ceil for split step
    int step = (ranges.size() + splitNum - 1) / splitNum;
    ImmutableList.Builder<KeyRange> rangeBuilder = ImmutableList.builder();
    for (int i = 0, nowPos = 0; i < splitNum; i++) {
      int nextPos = Math.min(nowPos + step - 1, ranges.size() - 1);
      KeyRange first = ranges.get(nowPos);
      KeyRange last = ranges.get(nextPos);

      Key lowerMin = toRawKey(first.getStart(), true);
      Key upperMax = toRawKey(last.getEnd(), false);

      rangeBuilder.add(makeCoprocRange(lowerMin.toByteString(), upperMax.toByteString()));
      nowPos = nowPos + step;
    }
    return rangeBuilder.build();
  }

  static String formatByteString(ByteString key) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < key.size(); i++) {
      sb.append(key.byteAt(i) & 0xff);
      if (i < key.size() - 1) {
        sb.append(",");
      }
    }
    return sb.toString();
  }
}