package org.checkerframework.checker.units;

import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.util.Elements;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.units.qual.Prefix;
import org.checkerframework.checker.units.qual.UnknownUnits;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationUtils;

/**
 * A helper class for UnitsRelations, providing numerous methods which help process Annotations and
 * Annotated Types representing various units.
 */
public class UnitsRelationsTools {

    /**
     * Creates an AnnotationMirror representing a unit defined by annoClass, with the specific
     * Prefix p.
     *
     * @param env the Checker Processing Environment, provided as a parameter in init() of a
     *     UnitsRelations implementation
     * @param annoClass the fully-qualified name of an Annotation representing a Unit (eg m.class
     *     for meters)
     * @param p a Prefix value
     * @return an AnnotationMirror of the Unit with the Prefix p, or null if it cannot be
     *     constructed
     */
    public static @Nullable AnnotationMirror buildAnnoMirrorWithSpecificPrefix(
            final ProcessingEnvironment env, final CharSequence annoClass, final Prefix p) {
        AnnotationBuilder builder = new AnnotationBuilder(env, annoClass);
        builder.setValue("value", p);
        return builder.build();
    }

    /**
     * Creates an AnnotationMirror representing a unit defined by annoClass, with no prefix.
     *
     * @param env checker Processing Environment, provided as a parameter in init() of a
     *     UnitsRelations implementation
     * @param annoClass the getElementValueClassname of an Annotation representing a Unit (eg
     *     m.class for meters)
     * @return an AnnotationMirror of the Unit with no prefix, or null if it cannot be constructed
     */
    public static @Nullable AnnotationMirror buildAnnoMirrorWithNoPrefix(
            final ProcessingEnvironment env, final CharSequence annoClass) {
        return AnnotationBuilder.fromName(env.getElementUtils(), annoClass);
    }

    /**
     * Retrieves the SI Prefix of an Annotated Type.
     *
     * @param annoType an AnnotatedTypeMirror representing a Units Annotated Type
     * @return a Prefix value (including Prefix.one), or null if it has none
     */
    public static @Nullable Prefix getPrefix(final AnnotatedTypeMirror annoType) {
        Prefix result = null;

        // go through each Annotation of an Annotated Type, find the prefix and return it
        for (AnnotationMirror mirror : annoType.getAnnotations()) {
            // try to get a prefix
            result = getPrefix(mirror);
            // if it is not null, then return the retrieved prefix immediately
            if (result != null) {
                return result;
            }
        }

        // if it can't find any prefix at all, then return null
        return result;
    }

    /**
     * Retrieves the SI Prefix of an Annotation.
     *
     * @param unitsAnnotation an AnnotationMirror representing a Units Annotation
     * @return a Prefix value (including Prefix.one), or null if it has none
     */
    public static @Nullable Prefix getPrefix(final AnnotationMirror unitsAnnotation) {
        AnnotationValue annotationValue = getAnnotationMirrorPrefix(unitsAnnotation);

        // if this Annotation has no prefix, return null
        if (hasNoPrefix(annotationValue)) {
            return null;
        }

        // if the Annotation has a value, then detect and match the string name of the prefix, and
        // return the matching Prefix
        String prefixString = annotationValue.getValue().toString();
        for (Prefix prefix : Prefix.values()) {
            if (prefixString.equals(prefix.toString())) {
                return prefix;
            }
        }

        // if none of the strings match, then return null
        return null;
    }

    /**
     * Checks to see if an Annotated Type has no prefix.
     *
     * @param annoType an AnnotatedTypeMirror representing a Units Annotated Type
     * @return true if it has no prefix, false otherwise
     */
    public static boolean hasNoPrefix(final AnnotatedTypeMirror annoType) {
        for (AnnotationMirror mirror : annoType.getAnnotations()) {
            // if any Annotation has a prefix, return false
            if (!hasNoPrefix(mirror)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Checks to see if an Annotation has no prefix (ie, no value element).
     *
     * @param unitsAnnotation an AnnotationMirror representing a Units Annotation
     * @return true if it has no prefix, false otherwise
     */
    public static boolean hasNoPrefix(final AnnotationMirror unitsAnnotation) {
        AnnotationValue annotationValue = getAnnotationMirrorPrefix(unitsAnnotation);
        return hasNoPrefix(annotationValue);
    }

    private static boolean hasNoPrefix(final AnnotationValue annotationValue) {
        // Annotation has no element value (ie no SI prefix)
        if (annotationValue == null) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Given an Annotation, returns the prefix (eg kilo) as an AnnotationValue if there is any,
     * otherwise returns null.
     */
    private static @Nullable AnnotationValue getAnnotationMirrorPrefix(
            final AnnotationMirror unitsAnnotation) {
        Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues =
                unitsAnnotation.getElementValues();

        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry :
                elementValues.entrySet()) {
            if (entry.getKey().getSimpleName().contentEquals("value")) {
                return entry.getValue();
            }
        }

        return null;
    }

    /**
     * Removes the prefix value from an Annotation, by constructing and returning a copy of its base
     * SI unit's Annotation.
     *
     * @param elements the Element Utilities from a checker's processing environment, typically
     *     obtained by calling env.getElementUtils() in init() of a Units Relations implementation
     * @param unitsAnnotation an AnnotationMirror representing a Units Annotation
     * @return the base SI Unit's AnnotationMirror, or null if the base SI Unit cannot be
     *     constructed
     */
    public static @Nullable AnnotationMirror removePrefix(
            final Elements elements, final AnnotationMirror unitsAnnotation) {
        if (hasNoPrefix(unitsAnnotation)) {
            // Optimization, though the else case would also work.
            return unitsAnnotation;
        } else {
            // In the Units Checker, the only annotation value is the prefix value.  Therefore,
            // fromName (which creates an annotation with no values) is acceptable.
            // TODO: refine sensitivity of removal for extension units, in case extension
            // Annotations have more than just Prefix in its values.
            return AnnotationBuilder.fromName(
                    elements, unitsAnnotation.getAnnotationType().toString());
        }
    }

    /**
     * Removes the Prefix value from an Annotated Type, by constructing and returning a copy of the
     * Annotated Type without the prefix.
     *
     * @param elements the Element Utilities from a checker's processing environment, typically
     *     obtained by calling env.getElementUtils() in init() of a Units Relations implementation
     * @param annoType an AnnotatedTypeMirror representing a Units Annotated Type
     * @return a copy of the Annotated Type without the prefix
     */
    public static AnnotatedTypeMirror removePrefix(
            final Elements elements, final AnnotatedTypeMirror annoType) {
        // deep copy the Annotated Type Mirror without any of the Annotations
        AnnotatedTypeMirror result = annoType.deepCopy(false);

        // get all of the original Annotations in the Annotated Type
        Set<AnnotationMirror> annos = annoType.getAnnotations();

        // loop through all the Annotations to see if they use Prefix.one, remove Prefix.one if it
        // does
        for (AnnotationMirror anno : annos) {
            // try to clean the Annotation Mirror of the Prefix
            AnnotationMirror cleanedMirror = removePrefix(elements, anno);
            // if successful, add the cleaned annotation to the deep copy
            if (cleanedMirror != null) {
                result.addAnnotation(cleanedMirror);
            }
            // if unsuccessful, add the original annotation
            else {
                result.addAnnotation(anno);
            }
        }

        return result;
    }

    /**
     * Checks to see if a particular Annotated Type has no units, such as scalar constants in
     * calculations.
     *
     * <p>Any number that isn't assigned a unit will automatically get the Annotation UnknownUnits.
     * eg: int x = 5; // x has @UnknownUnits
     *
     * @param annoType an AnnotatedTypeMirror representing a Units Annotated Type
     * @return true if the Type has no units, false otherwise
     */
    public static boolean hasNoUnits(final AnnotatedTypeMirror annoType) {
        return (annoType.getAnnotation(UnknownUnits.class) != null);
    }

    /**
     * Checks to see if a particular Annotated Type has a specific unit (represented by its
     * Annotation).
     *
     * @param annoType an AnnotatedTypeMirror representing a Units Annotated Type
     * @param unitsAnnotation an AnnotationMirror representing a Units Annotation of a specific unit
     * @return true if the Type has the specific unit, false otherwise
     */
    public static boolean hasSpecificUnit(
            final AnnotatedTypeMirror annoType, final AnnotationMirror unitsAnnotation) {
        return AnnotationUtils.containsSame(annoType.getAnnotations(), unitsAnnotation);
    }

    /**
     * Checks to see if a particular Annotated Type has a particular base unit (represented by its
     * Annotation).
     *
     * @param annoType an AnnotatedTypeMirror representing a Units Annotated Type
     * @param unitsAnnotation an AnnotationMirror representing a Units Annotation of the base unit
     * @return true if the Type has the specific unit, false otherwise
     */
    public static boolean hasSpecificUnitIgnoringPrefix(
            final AnnotatedTypeMirror annoType, final AnnotationMirror unitsAnnotation) {
        return AnnotationUtils.containsSameByName(annoType.getAnnotations(), unitsAnnotation);
    }

    /**
     * Creates an AnnotationMirror representing a unit defined by annoClass, with the specific
     * Prefix p.
     *
     * <p>This interface is intended only for subclasses of UnitsRelations; other clients should use
     * {@link #buildAnnoMirrorWithSpecificPrefix(ProcessingEnvironment, CharSequence, Prefix)}
     *
     * @param env the Checker Processing Environment, provided as a parameter in init() of a
     *     UnitsRelations implementation
     * @param annoClass the Class of an Annotation representing a Unit (eg m.class for meters)
     * @param p a Prefix value
     * @return an AnnotationMirror of the Unit with the Prefix p, or null if it cannot be
     *     constructed
     */
    public static @Nullable AnnotationMirror buildAnnoMirrorWithSpecificPrefix(
            final ProcessingEnvironment env,
            final Class<? extends Annotation> annoClass,
            final Prefix p) {
        AnnotationBuilder builder = new AnnotationBuilder(env, annoClass.getCanonicalName());
        builder.setValue("value", p);
        return builder.build();
    }

    /**
     * Creates an AnnotationMirror representing a unit defined by annoClass, with the default Prefix
     * of {@code Prefix.one}.
     *
     * <p>This interface is intended only for subclasses of UnitsRelations; other clients should not
     * use it.
     *
     * @param env the Checker Processing Environment, provided as a parameter in init() of a
     *     UnitsRelations implementation
     * @param annoClass the Class of an Annotation representing a Unit (eg m.class for meters)
     * @return an AnnotationMirror of the Unit with Prefix.one, or null if it cannot be constructed
     */
    public static @Nullable AnnotationMirror buildAnnoMirrorWithDefaultPrefix(
            final ProcessingEnvironment env, final Class<? extends Annotation> annoClass) {
        return buildAnnoMirrorWithSpecificPrefix(env, annoClass, Prefix.one);
    }

    /**
     * Creates an AnnotationMirror representing a unit defined by annoClass, with no prefix.
     *
     * <p>This interface is intended only for subclasses of UnitsRelations; other clients should use
     * {@link #buildAnnoMirrorWithNoPrefix(ProcessingEnvironment, CharSequence, Prefix)}
     *
     * @param env checker Processing Environment, provided as a parameter in init() of a
     *     UnitsRelations implementation
     * @param annoClass the Class of an Annotation representing a Unit (eg m.class for meters)
     * @return an AnnotationMirror of the Unit with no prefix, or null if it cannot be constructed
     */
    static @Nullable AnnotationMirror buildAnnoMirrorWithNoPrefix(
            final ProcessingEnvironment env, final Class<? extends Annotation> annoClass) {
        return AnnotationBuilder.fromClass(env.getElementUtils(), annoClass);
    }
}
