package org.checkerframework.javacutil;

import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.nullness.qual.Nullable;

/** A utility class that helps with {@link TypeKind}s. */
public final class TypeKindUtils {

  /** This class cannot be instantiated. */
  private TypeKindUtils() {
    throw new AssertionError("Class TypeKindUtils cannot be instantiated.");
  }

  /**
   * Return true if the argument is one of INT, SHORT, BYTE, CHAR, LONG.
   *
   * @param typeKind the TypeKind to inspect
   * @return true if typeKind is a primitive integral type kind
   */
  public static boolean isIntegral(TypeKind typeKind) {
    switch (typeKind) {
      case INT:
      case SHORT:
      case BYTE:
      case CHAR:
      case LONG:
        return true;
      default:
        return false;
    }
  }

  /**
   * Return true if the argument is one of INT, SHORT, BYTE, LONG.
   *
   * @param typeKind the TypeKind to inspect
   * @return true if typeKind is a primitive integral type kind, excluding CHAR which does not print
   *     as an integer
   */
  public static boolean isIntegralNumeric(TypeKind typeKind) {
    switch (typeKind) {
      case INT:
      case SHORT:
      case BYTE:
      case LONG:
        return true;
      default:
        return false;
    }
  }

  /**
   * Return true if the argument is one of FLOAT, DOUBLE.
   *
   * @param typeKind the TypeKind to inspect
   * @return true if typeKind is a primitive floating point type kind
   */
  public static boolean isFloatingPoint(TypeKind typeKind) {
    switch (typeKind) {
      case FLOAT:
      case DOUBLE:
        return true;
      default:
        return false;
    }
  }

  /**
   * Returns true iff the argument is a primitive numeric type kind.
   *
   * @param typeKind a type kind
   * @return true if the argument is a primitive numeric type kind
   */
  public static boolean isNumeric(TypeKind typeKind) {
    switch (typeKind) {
      case BYTE:
      case CHAR:
      case DOUBLE:
      case FLOAT:
      case INT:
      case LONG:
      case SHORT:
        return true;
      default:
        return false;
    }
  }

  // Cannot create an overload that takes an AnnotatedTypeMirror because the javacutil
  // package must not depend on the framework package.
  /**
   * Given a primitive type, return its kind. Given a boxed primitive type, return the corresponding
   * primitive type kind. Otherwise, return null.
   *
   * @param type a primitive or boxed primitive type
   * @return a primitive type kind, or null
   */
  public static @Nullable TypeKind primitiveOrBoxedToTypeKind(TypeMirror type) {
    TypeKind typeKind = type.getKind();
    if (typeKind.isPrimitive()) {
      return typeKind;
    }

    if (!(type instanceof DeclaredType)) {
      return null;
    }

    String typeString = TypesUtils.getQualifiedName((DeclaredType) type).toString();

    switch (typeString) {
      case "java.lang.Byte":
        return TypeKind.BYTE;
      case "java.lang.Boolean":
        return TypeKind.BOOLEAN;
      case "java.lang.Character":
        return TypeKind.CHAR;
      case "java.lang.Double":
        return TypeKind.DOUBLE;
      case "java.lang.Float":
        return TypeKind.FLOAT;
      case "java.lang.Integer":
        return TypeKind.INT;
      case "java.lang.Long":
        return TypeKind.LONG;
      case "java.lang.Short":
        return TypeKind.SHORT;
      default:
        return null;
    }
  }

  // No overload that takes AnnotatedTypeMirror because javacutil cannot depend on framework.
  /**
   * Returns the widened numeric type for an arithmetic operation performed on a value of the left
   * type and the right type. Defined in JLS 5.6.2. We return a {@link TypeKind} because creating a
   * {@link TypeMirror} requires a {@link javax.lang.model.util.Types} object from the {@link
   * javax.annotation.processing.ProcessingEnvironment}.
   *
   * @param left a type mirror
   * @param right a type mirror
   * @return the result of widening numeric conversion, or NONE when the conversion cannot be
   *     performed
   */
  public static TypeKind widenedNumericType(TypeMirror left, TypeMirror right) {
    return widenedNumericType(left.getKind(), right.getKind());
  }

  /**
   * Given two type kinds, return the type kind they are widened to, when an arithmetic operation is
   * performed on them. Defined in JLS 5.6.2.
   *
   * @param a a type kind
   * @param b a type kind
   * @return the type kind to which they are widened, when an operation is performed on them
   */
  public static TypeKind widenedNumericType(TypeKind a, TypeKind b) {
    if (!isNumeric(a) || !isNumeric(b)) {
      return TypeKind.NONE;
    }

    if (a == TypeKind.DOUBLE || b == TypeKind.DOUBLE) {
      return TypeKind.DOUBLE;
    }

    if (a == TypeKind.FLOAT || b == TypeKind.FLOAT) {
      return TypeKind.FLOAT;
    }

    if (a == TypeKind.LONG || b == TypeKind.LONG) {
      return TypeKind.LONG;
    }

    return TypeKind.INT;
  }

  /** The type of primitive conversion: narrowing, widening, or same. */
  public enum PrimitiveConversionKind {
    /** The two primitive kinds are the same. */
    SAME,
    /**
     * The conversion is a widening primitive conversion.
     *
     * <p>This includes byte to char, even though that is strictly a "widening and narrowing
     * primitive conversion", according to JLS 5.1.4.
     */
    WIDENING,
    /** The conversion is a narrowing primitive conversion. */
    NARROWING
  }

  /**
   * Return the type of primitive conversion between {@code from} and {@code to}.
   *
   * <p>The narrowing conversions include both short to char and char to short.
   *
   * @param from a primitive type
   * @param to a primitive type
   * @return the type of primitive conversion between {@code from} and {@code to}
   */
  public static PrimitiveConversionKind getPrimitiveConversionKind(TypeKind from, TypeKind to) {
    if (from == TypeKind.BOOLEAN && to == TypeKind.BOOLEAN) {
      return PrimitiveConversionKind.SAME;
    }

    assert (isIntegral(from) || isFloatingPoint(from)) && (isIntegral(to) || isFloatingPoint(to))
        : "getPrimitiveConversionKind " + from + " " + to;

    if (from == to) {
      return PrimitiveConversionKind.SAME;
    }

    boolean fromIntegral = isIntegral(from);
    boolean toFloatingPoint = isFloatingPoint(to);
    if (fromIntegral && toFloatingPoint) {
      return PrimitiveConversionKind.WIDENING;
    }

    boolean toIntegral = isIntegral(to);
    boolean fromFloatingPoint = isFloatingPoint(from);
    if (fromFloatingPoint && toIntegral) {
      return PrimitiveConversionKind.NARROWING;
    }

    if (numBits(from) < numBits(to)) {
      return PrimitiveConversionKind.WIDENING;
    } else {
      // If same number of bits (char to short or short to char), it is a narrowing.
      return PrimitiveConversionKind.NARROWING;
    }
  }

  /**
   * Returns the number of bits in the representation of a primitive type. Returns -1 if the type is
   * not a primitive type.
   *
   * @param tk a primitive type kind
   * @return the number of bits in its representation, or -1 if not integral
   */
  private static int numBits(TypeKind tk) {
    switch (tk) {
      case BYTE:
        return 8;
      case SHORT:
        return 16;
      case CHAR:
        return 16;
      case INT:
        return 32;
      case LONG:
        return 64;
      case FLOAT:
        return 32;
      case DOUBLE:
        return 64;
      case BOOLEAN:
      default:
        return -1;
    }
  }

  /**
   * Returns the minimum value representable by the given integral primitive type.
   *
   * @param tk a primitive type kind
   * @return the minimum value representable by the given integral primitive type
   */
  public static long minValue(TypeKind tk) {
    switch (tk) {
      case BYTE:
        return Byte.MIN_VALUE;
      case SHORT:
        return Short.MIN_VALUE;
      case CHAR:
        return Character.MIN_VALUE;
      case INT:
        return Integer.MIN_VALUE;
      case LONG:
        return Long.MIN_VALUE;
      case FLOAT:
      case DOUBLE:
      case BOOLEAN:
      default:
        throw new BugInCF(tk + " does not have a minimum value");
    }
  }

  /**
   * Returns the maximum value representable by the given integral primitive type.
   *
   * @param tk a primitive type kind
   * @return the maximum value representable by the given integral primitive type
   */
  public static long maxValue(TypeKind tk) {
    switch (tk) {
      case BYTE:
        return Byte.MAX_VALUE;
      case SHORT:
        return Short.MAX_VALUE;
      case CHAR:
        return Character.MAX_VALUE;
      case INT:
        return Integer.MAX_VALUE;
      case LONG:
        return Long.MAX_VALUE;
      case FLOAT:
      case DOUBLE:
      case BOOLEAN:
      default:
        throw new BugInCF(tk + " does not have a maximum value");
    }
  }
}
