/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.ratis.util;

/**
 * The traditional binary prefixes, kilo, mega, ..., exa,
 * which can be represented by a 64-bit integer.
 * {@link TraditionalBinaryPrefix} symbols are case insensitive.
 */
public enum TraditionalBinaryPrefix {
  KILO(10),
  MEGA(KILO.bitShift + 10),
  GIGA(MEGA.bitShift + 10),
  TERA(GIGA.bitShift + 10),
  PETA(TERA.bitShift + 10),
  EXA (PETA.bitShift + 10);

  private final long value;
  private final char symbol;
  private final int bitShift;
  private final long bitMask;

  TraditionalBinaryPrefix(int bitShift) {
    this.bitShift = bitShift;
    this.value = 1L << bitShift;
    this.bitMask = this.value - 1L;
    this.symbol = toString().charAt(0);
  }

  public long getValue() {
    return value;
  }

  public char getSymbol() {
    return symbol;
  }

  public long toLong(long n) {
    final long shifted = n << bitShift;
    if (n != shifted >>> bitShift) {
      throw new ArithmeticException("Long overflow: " + toString(n)
          + " cannot be assigned to a long.");
    }
    return shifted;
  }

  public String toString(long n) {
    return n + String.valueOf(symbol);
  }

  /**
   * @return The object corresponding to the symbol.
   */
  public static TraditionalBinaryPrefix valueOf(char symbol) {
    symbol = Character.toUpperCase(symbol);
    for(TraditionalBinaryPrefix prefix : TraditionalBinaryPrefix.values()) {
      if (symbol == prefix.symbol) {
        return prefix;
      }
    }
    throw new IllegalArgumentException("Unknown symbol '" + symbol + "'");
  }

  /**
   * Convert a string to long.
   * The input string is first be trimmed
   * and then it is parsed with traditional binary prefix.
   *
   * For example,
   * "-1230k" will be converted to -1230 * 1024 = -1259520;
   * "891g" will be converted to 891 * 1024^3 = 956703965184;
   *
   * @param s input string
   * @return a long value represented by the input string.
   */
  public static long string2long(String s) {
    s = s.trim();
    final int lastpos = s.length() - 1;
    final char lastchar = s.charAt(lastpos);
    if (Character.isDigit(lastchar)) {
      return Long.parseLong(s);
    } else {
      long p;
      try {
        p = TraditionalBinaryPrefix.valueOf(lastchar).value;
      } catch (IllegalArgumentException e) {
        throw new IllegalArgumentException("Invalid size prefix '" + lastchar
            + "' in '" + s + "'. Allowed prefixes are k, m, g, t, p, e (case insensitive)");
      }
      long num = Long.parseLong(s.substring(0, lastpos).trim());
      if (num > Long.MAX_VALUE/p || num < Long.MIN_VALUE/p) {
        throw new IllegalArgumentException(s + " does not fit in a Long");
      }
      return num * p;
    }
  }

  /**
   * Convert a long integer to a string with traditional binary prefix.
   *
   * @param n the value to be converted
   * @param unit The unit, e.g. "B" for bytes.
   * @param decimalPlaces The number of decimal places.
   * @return a string with traditional binary prefix.
   */
  public static String long2String(long n, String unit, int decimalPlaces) {
    if (unit == null) {
      unit = "";
    }
    //take care a special case
    if (n == Long.MIN_VALUE) {
      return "-8 " + EXA.symbol + unit;
    }

    final StringBuilder b = new StringBuilder();
    //take care negative numbers
    if (n < 0) {
      b.append('-');
      n = -n;
    }
    if (n < KILO.value) {
      //no prefix
      b.append(n);
      return (unit.isEmpty()? b: b.append(" ").append(unit)).toString();
    } else {
      //find traditional binary prefix
      int i = 0;
      while(i < values().length && n >= values()[i].value) {
        i++;
      }
      TraditionalBinaryPrefix prefix = values()[i - 1];

      if ((n & prefix.bitMask) == 0) {
        //exact division
        b.append(n >> prefix.bitShift);
      } else {
        final String  format = "%." + decimalPlaces + "f";
        String s = StringUtils.format(format, n/(double)prefix.value);
        //check a special rounding up case
        if (s.startsWith("1024")) {
          prefix = values()[i];
          s = StringUtils.format(format, n/(double)prefix.value);
        }
        b.append(s);
      }
      return b.append(' ').append(prefix.symbol).append(unit).toString();
    }
  }
}
