package org.checkerframework.checker.formatter.qual;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import org.checkerframework.dataflow.qual.Pure;

/**
 * Elements of this enumeration are used in a {@link Format Format} annotation to indicate the valid
 * types that may be passed as a format parameter. For example:
 *
 * <blockquote>
 *
 * <pre>{@literal @}Format({ConversionCategory.GENERAL, ConversionCategory.INT})
 * String f = "String '%s' has length %d";
 * String.format(f, "Example", 7);</pre>
 *
 * </blockquote>
 *
 * The annotation indicates that the format string requires any Object as the first parameter
 * ({@link ConversionCategory#GENERAL}) and an integer as the second parameter ({@link
 * ConversionCategory#INT}).
 *
 * @see Format
 * @checker_framework.manual #formatter-checker Format String Checker
 */
public enum ConversionCategory {
    /** Use if the parameter can be of any type. Applicable for conversions b, B, h, H, s, S. */
    GENERAL(null /* everything */, "bBhHsS"),

    /**
     * Use if the parameter is of a basic types which represent Unicode characters: char, Character,
     * byte, Byte, short, and Short. This conversion may also be applied to the types int and
     * Integer when Character.isValidCodePoint(int) returns true. Applicable for conversions c, C.
     */
    CHAR(new Class<?>[] {Character.class, Byte.class, Short.class, Integer.class}, "cC"),

    /**
     * Use if the parameter is an integral type: byte, Byte, short, Short, int and Integer, long,
     * Long, and BigInteger. Applicable for conversions d, o, x, X.
     */
    INT(
            new Class<?>[] {Byte.class, Short.class, Integer.class, Long.class, BigInteger.class},
            "doxX"),

    /**
     * Use if the parameter is a floating-point type: float, Float, double, Double, and BigDecimal.
     * Applicable for conversions e, E, f, g, G, a, A.
     */
    FLOAT(new Class<?>[] {Float.class, Double.class, BigDecimal.class}, "eEfgGaA"),

    /**
     * Use if the parameter is a type which is capable of encoding a date or time: long, Long,
     * Calendar, and Date. Applicable for conversions t, T.
     */
    TIME(new Class<?>[] {Long.class, Calendar.class, Date.class}, "tT"),

    /**
     * In a format string, multiple conversions may be applied to the same parameter. This is
     * seldomly needed, but the following is an example of such use:
     *
     * <pre>
     *   format("Test %1$c %1$d", (int)42);
     * </pre>
     *
     * In this example, the first parameter is interpreted as both a character and an int, therefore
     * the parameter must be compatible with both conversion, and can therefore neither be char nor
     * long. This intersection of conversions is called CHAR_AND_INT.
     *
     * <p>One other conversion intersection is interesting, namely the intersection of INT and TIME,
     * resulting in INT_AND_TIME.
     *
     * <p>All other intersection either lead to an already existing type, or NULL, in which case it
     * is illegal to pass object's of any type as parameter.
     */
    CHAR_AND_INT(new Class<?>[] {Byte.class, Short.class, Integer.class}, null),

    INT_AND_TIME(new Class<?>[] {Long.class}, null),

    /**
     * Use if no object of any type can be passed as parameter. In this case, the only legal value
     * is null. This is seldomly needed, and indicates an error in most cases. For example:
     *
     * <pre>
     *   format("Test %1$f %1$d", null);
     * </pre>
     *
     * Only null can be legally passed, passing a value such as 4 or 4.2 would lead to an exception.
     */
    NULL(new Class<?>[0], null),

    /**
     * Use if a parameter is not used by the formatter. This is seldomly needed, and indicates an
     * error in most cases. For example:
     *
     * <pre>
     *   format("Test %1$s %3$s", "a","unused","b");
     * </pre>
     *
     * Only the first "a" and third "b" parameters are used, the second "unused" parameter is
     * ignored.
     */
    UNUSED(null /* everything */, null);

    /** Create a new conversion category. */
    ConversionCategory(Class<?>[] types, String chars) {
        this.types = types;
        this.chars = chars;
    }

    /** The format types. */
    @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up!
    public final Class<?>[] types;

    /** The format characters. */
    public final String chars;

    /**
     * Use this function to get the category associated with a conversion character. For example:
     *
     * <blockquote>
     *
     * <pre>
     * ConversionCategory.fromConversionChar('d') == ConversionCategory.INT;
     * </pre>
     *
     * </blockquote>
     */
    public static ConversionCategory fromConversionChar(char c) {
        for (ConversionCategory v : new ConversionCategory[] {GENERAL, CHAR, INT, FLOAT, TIME}) {
            if (v.chars.contains(String.valueOf(c))) {
                return v;
            }
        }
        throw new IllegalArgumentException("Bad conversion character " + c);
    }

    private static <E> Set<E> arrayToSet(E[] a) {
        return new HashSet<>(Arrays.asList(a));
    }

    public static boolean isSubsetOf(ConversionCategory a, ConversionCategory b) {
        return intersect(a, b) == a;
    }

    /**
     * Returns the intersection of two categories. This is seldomly needed.
     *
     * <blockquote>
     *
     * <pre>
     * ConversionCategory.intersect(INT, TIME) == INT_AND_TIME;
     * </pre>
     *
     * </blockquote>
     *
     * @param a a category
     * @param b a category
     * @return the intersection of the two categories (their greatest lower bound)
     */
    public static ConversionCategory intersect(ConversionCategory a, ConversionCategory b) {
        if (a == UNUSED) {
            return b;
        }
        if (b == UNUSED) {
            return a;
        }
        if (a == GENERAL) {
            return b;
        }
        if (b == GENERAL) {
            return a;
        }

        Set<Class<? extends Object>> as = arrayToSet(a.types);
        Set<Class<? extends Object>> bs = arrayToSet(b.types);
        as.retainAll(bs); // intersection
        for (ConversionCategory v :
                new ConversionCategory[] {
                    CHAR, INT, FLOAT, TIME, CHAR_AND_INT, INT_AND_TIME, NULL
                }) {
            Set<Class<? extends Object>> vs = arrayToSet(v.types);
            if (vs.equals(as)) {
                return v;
            }
        }
        throw new RuntimeException();
    }

    /**
     * Returns the union of two categories. This is seldomly needed.
     *
     * <blockquote>
     *
     * <pre>
     * ConversionCategory.union(INT, TIME) == GENERAL;
     * </pre>
     *
     * </blockquote>
     *
     * @param a a category
     * @param b a category
     * @return the union of the two categories (their least upper bound)
     */
    public static ConversionCategory union(ConversionCategory a, ConversionCategory b) {
        if (a == UNUSED || b == UNUSED) {
            return UNUSED;
        }
        if (a == GENERAL || b == GENERAL) {
            return GENERAL;
        }
        if ((a == CHAR_AND_INT && b == INT_AND_TIME) || (a == INT_AND_TIME && b == CHAR_AND_INT)) {
            // This is special-cased because the union of a.types and b.types
            // does not include BigInteger.class, whereas the types for INT does.
            // Returning INT here to prevent returning GENERAL below.
            return INT;
        }

        Set<Class<? extends Object>> as = arrayToSet(a.types);
        Set<Class<? extends Object>> bs = arrayToSet(b.types);
        as.addAll(bs); // union
        for (ConversionCategory v :
                new ConversionCategory[] {
                    NULL, CHAR_AND_INT, INT_AND_TIME, CHAR, INT, FLOAT, TIME
                }) {
            Set<Class<? extends Object>> vs = arrayToSet(v.types);
            if (vs.equals(as)) {
                return v;
            }
        }

        return GENERAL;
    }

    private String className(Class<?> cls) {
        if (cls == Boolean.class) {
            return "boolean";
        }
        if (cls == Character.class) {
            return "char";
        }
        if (cls == Byte.class) {
            return "byte";
        }
        if (cls == Short.class) {
            return "short";
        }
        if (cls == Integer.class) {
            return "int";
        }
        if (cls == Long.class) {
            return "long";
        }
        if (cls == Float.class) {
            return "float";
        }
        if (cls == Double.class) {
            return "double";
        }
        return cls.getSimpleName();
    }

    /** Returns a pretty printed {@link ConversionCategory}. */
    @Pure
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(this.name());
        sb.append(" conversion category (one of: ");
        boolean first = true;
        for (Class<? extends Object> cls : this.types) {
            if (!first) {
                sb.append(", ");
            }
            sb.append(className(cls));
            first = false;
        }
        sb.append(")");
        return sb.toString();
    }
}
