/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.util;

import alluxio.Constants;
import alluxio.security.authorization.Mode;

import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Utility methods to parse specific formats, print according to specific formats or transform among
 * different formats. They are not only bound to string format but also include parsing objects to a
 * specific string format, parsing a specific string format back to objects and printing or logging
 * according to a specific format.
 */
@ThreadSafe
public final class FormatUtils {
  /**
   * Parses a list of {@code Objects} into a {@code String}.
   *
   * @param objs a list of Objects to convert to a String
   * @return comma-separated concatenation of the string representation returned by Object#toString
   *         of the individual objects
   */
  public static String parametersToString(Object... objs) {
    StringBuilder sb = new StringBuilder("(");
    if (objs != null) {
      for (int k = 0; k < objs.length; k++) {
        if (k != 0) {
          sb.append(", ");
        }
        if (objs[k] == null) {
          sb.append("null");
        } else {
          sb.append(objs[k].toString());
        }
      }
    }
    sb.append(")");
    return sb.toString();
  }

  /**
   * Parses a {@link ByteBuffer} into a {@link String}. In particular, the function prints the
   * content of the buffer in 4-byte increments as space separated integers.
   *
   * @param buf buffer to use
   * @return the String representation of the {@link ByteBuffer}
   */
  public static String byteBufferToString(ByteBuffer buf) {
    StringBuilder sb = new StringBuilder();
    for (int k = 0; k < buf.limit() / 4; k++) {
      if (k != 0) {
        sb.append(" ");
      }
      sb.append(buf.getInt());
    }
    return sb.toString();
  }

  /**
   * Parses a byte array into a space separated hex string where each byte is represented in the
   * format {@code 0x%02x}.
   *
   * @param bytes the byte array to be transformed
   * @return the string representation of the byte array
   */
  public static String byteArrayToHexString(byte[] bytes) {
    return byteArrayToHexString(bytes, "0x", " ");
  }

  /**
   * Parses a byte array into a hex string where each byte is represented in the
   * format {@code %02x}.
   *
   * @param bytes the byte array to be transformed
   * @param prefix the prefix to use
   * @param separator the separator to use
   * @return the string representation of the byte array
   */
  public static String byteArrayToHexString(byte[] bytes, String prefix, String separator) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
      sb.append(String.format("%s%02x", prefix, bytes[i]));
      if (i != bytes.length - 1) {
        sb.append(separator);
      }
    }
    return sb.toString();
  }

  /**
   * Formats time elapsed since the given start time (in milliseconds).
   *
   * @param startTimeMs start time in milliseconds
   * @param message prefix for the message to be printed
   * @return formatted string with the elapsed time (in milliseconds)
   */
  public static String formatTimeTakenMs(long startTimeMs, String message) {
    return message + " took " + (CommonUtils.getCurrentMs() - startTimeMs) + " ms.";
  }

  /**
   * Formats time elapsed since the given start time (in nanoseconds).
   *
   * @param startTimeNs start time in nanoseconds
   * @param message prefix for the message to be printed
   * @return formatted string with the elapsed time (in nanoseconds)
   */
  public static String formatTimeTakenNs(long startTimeNs, String message) {
    return message + " took " + (System.nanoTime() - startTimeNs) + " ns.";
  }

  /**
   * Returns a human-readable version of bytes 10GB 2048KB etc.
   *
   * @param bytes the number of bytes
   * @return human readable version
   */
  public static String getSizeFromBytes(long bytes) {
    double ret = bytes;
    if (ret <= 1024 * 5) {
      return String.format(Locale.ENGLISH, "%dB", bytes);
    }
    ret /= 1024;
    if (ret <= 1024 * 5) {
      return String.format(Locale.ENGLISH, "%.2fKB", ret);
    }
    ret /= 1024;
    if (ret <= 1024 * 5) {
      return String.format(Locale.ENGLISH, "%.2fMB", ret);
    }
    ret /= 1024;
    if (ret <= 1024 * 5) {
      return String.format(Locale.ENGLISH, "%.2fGB", ret);
    }
    ret /= 1024;
    if (ret <= 1024 * 5) {
      return String.format(Locale.ENGLISH, "%.2fTB", ret);
    }
    ret /= 1024;
    if (ret <= 1024 * 5) {
      return String.format(Locale.ENGLISH, "%.2fPB", ret);
    }
    ret /= 1024;
    //Long.MAX_VALUE bytes approximately equals to 8EB.
    return String.format(Locale.ENGLISH, "%.2fEB", ret);
  }

  /**
   * Parses a String size to Bytes.
   *
   * @param spaceSize the size of a space, e.g. 10GB, 5TB, 1024
   * @return the space size in bytes
   */
  public static long parseSpaceSize(String spaceSize) {
    double alpha = 0.0001;
    String ori = spaceSize;
    String end = "";
    int index = spaceSize.length() - 1;
    while (index >= 0) {
      if (spaceSize.charAt(index) > '9' || spaceSize.charAt(index) < '0') {
        end = spaceSize.charAt(index) + end;
      } else {
        break;
      }
      index--;
    }
    spaceSize = spaceSize.substring(0, index + 1);
    double ret;
    try {
      ret = Double.parseDouble(spaceSize);
    } catch (NumberFormatException e) {
      throw new IllegalArgumentException("Fail to parse " + ori + " to bytes");
    }
    end = end.toLowerCase();
    if (end.isEmpty() || end.equals("b")) {
      return (long) (ret + alpha);
    } else if (end.equals("kb") || end.equals("k") || end.equals("kib") || end.equals("ki")) {
      return (long) (ret * Constants.KB + alpha);
    } else if (end.equals("mb") || end.equals("m") || end.equals("mib") || end.equals("mi")) {
      return (long) (ret * Constants.MB + alpha);
    } else if (end.equals("gb") || end.equals("g") || end.equals("gib") || end.equals("gi")) {
      return (long) (ret * Constants.GB + alpha);
    } else if (end.equals("tb") || end.equals("t") || end.equals("tib") || end.equals("ti")) {
      return (long) (ret * Constants.TB + alpha);
    } else if (end.equals("pb") || end.equals("p") || end.equals("pib") || end.equals("pi")) {
      // When parsing petabyte values, we can't multiply with doubles and longs, since that will
      // lose presicion with such high numbers. Therefore we use a BigDecimal.
      BigDecimal pBDecimal = new BigDecimal(Constants.PB);
      return pBDecimal.multiply(BigDecimal.valueOf(ret)).longValue();
    } else {
      throw new IllegalArgumentException("Fail to parse " + ori + " to bytes");
    }
  }

  /**
   * Regular expression pattern to separate digits (negative sign allowed) and letters in a string.
   */
  private static final Pattern SEP_DIGIT_LETTER = Pattern.compile("([-]?[0-9]*)([a-zA-Z]*)");

  /**
   * Parses a String size to Milliseconds. Supports negative numbers.
   *
   * @param timeSize the size of a time, e.g. 1M, 5H, 10D, -1
   * @return the time size in milliseconds
   */
  public static long parseTimeSize(String timeSize) {
    double alpha = 0.0001;
    String time = "";
    String size = "";
    Matcher m = SEP_DIGIT_LETTER.matcher(timeSize);
    if (m.matches()) {
      time = m.group(1);
      size = m.group(2);
    }
    double douTime;
    try {
      douTime = Double.parseDouble(time);
    } catch (NumberFormatException e) {
      throw new IllegalArgumentException("Fail to parse " + timeSize + " to milliseconds");
    }
    long sign = 1;
    if (douTime < 0) {
      sign = -1;
      douTime = -douTime;
    }
    size = size.toLowerCase();
    if (size.isEmpty() || size.equalsIgnoreCase("ms")
        || size.equalsIgnoreCase("millisecond")) {
      return sign * (long) (douTime + alpha);
    } else if (size.equalsIgnoreCase("s") || size.equalsIgnoreCase("sec")
        || size.equalsIgnoreCase("second")) {
      return sign * (long) (douTime * Constants.SECOND + alpha);
    } else if (size.equalsIgnoreCase("m") || size.equalsIgnoreCase("min")
        || size.equalsIgnoreCase("minute")) {
      return sign * (long) (douTime * Constants.MINUTE + alpha);
    } else if (size.equalsIgnoreCase("h") || size.equalsIgnoreCase("hr")
        || size.equalsIgnoreCase("hour")) {
      return sign * (long) (douTime * Constants.HOUR + alpha);
    } else if (size.equalsIgnoreCase("d") || size.equalsIgnoreCase("day")) {
      return sign * (long) (douTime * Constants.DAY + alpha);
    } else {
      throw new IllegalArgumentException("Fail to parse " + timeSize + " to milliseconds");
    }
  }

  /**
   * Formats digital representation of a model as a human-readable string.
   *
   * @param mode file mode
   * @param directory if the mode corresponds to a directory
   * @param hasExtended true if extended acls exist
   * @return human-readable version of the given mode
   */
  public static String formatMode(short mode, boolean directory, boolean hasExtended) {
    StringBuilder str = new StringBuilder();
    if (directory) {
      str.append("d");
    } else {
      str.append("-");
    }
    str.append(new Mode(mode).toString());
    if (hasExtended) {
      str.append("+");
    }
    return str.toString();
  }

  private FormatUtils() {} // prevent instantiation
}
