package org.checkerframework.framework.util;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import org.checkerframework.dataflow.qual.SideEffectFree;
import org.checkerframework.framework.qual.InvisibleQualifier;
import org.checkerframework.javacutil.BugInCF;

/** A utility for converting AnnotationMirrors to Strings. */
public class DefaultAnnotationFormatter implements AnnotationFormatter {

    /**
     * Returns true if, by default, anno should not be printed.
     *
     * @see org.checkerframework.framework.qual.InvisibleQualifier
     * @return true if anno's declaration was qualified by InvisibleQualifier
     */
    public static boolean isInvisibleQualified(AnnotationMirror anno) {
        TypeElement annoElement = (TypeElement) anno.getAnnotationType().asElement();
        return annoElement.getAnnotation(InvisibleQualifier.class) != null;
    }

    /**
     * Creates a String of each annotation in annos separated by a single space character and
     * terminated by a space character, obeying the printInvisible parameter.
     *
     * @param annos a collection of annotations to print
     * @param printInvisible whether or not to print "invisible" annotation mirrors
     * @return the list of annotations converted to a String
     */
    @Override
    @SideEffectFree
    public String formatAnnotationString(
            Collection<? extends AnnotationMirror> annos, boolean printInvisible) {
        StringBuilder sb = new StringBuilder();
        for (AnnotationMirror obj : annos) {
            if (obj == null) {
                throw new BugInCF(
                        "AnnotatedTypeMirror.formatAnnotationString: found null AnnotationMirror");
            }
            if (isInvisibleQualified(obj) && !printInvisible) {
                continue;
            }
            formatAnnotationMirror(obj, sb);
            sb.append(" ");
        }
        return sb.toString();
    }

    /**
     * @param anno the annotation mirror to convert
     * @return the string representation of a single AnnotationMirror, without showing full package
     *     names
     */
    @Override
    @SideEffectFree
    public String formatAnnotationMirror(AnnotationMirror anno) {
        StringBuilder sb = new StringBuilder();
        formatAnnotationMirror(anno, sb);
        return sb.toString();
    }

    /** A helper method to output a single AnnotationMirror, without showing full package names. */
    protected void formatAnnotationMirror(AnnotationMirror am, StringBuilder sb) {
        sb.append("@");
        sb.append(am.getAnnotationType().asElement().getSimpleName());
        Map<? extends ExecutableElement, ? extends AnnotationValue> args = am.getElementValues();
        if (!args.isEmpty()) {
            sb.append("(");
            boolean oneValue = false;
            if (args.size() == 1) {
                Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> first =
                        args.entrySet().iterator().next();
                if (first.getKey().getSimpleName().contentEquals("value")) {
                    formatAnnotationMirrorArg(first.getValue(), sb);
                    oneValue = true;
                }
            }
            if (!oneValue) {
                boolean notfirst = false;
                for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> arg :
                        args.entrySet()) {
                    if (!"{}".equals(arg.getValue().toString())) {
                        if (notfirst) {
                            sb.append(", ");
                        }
                        notfirst = true;
                        sb.append(arg.getKey().getSimpleName() + "=");
                        formatAnnotationMirrorArg(arg.getValue(), sb);
                    }
                }
            }
            sb.append(")");
        }
    }

    // A helper method to print AnnotationValues (annotation arguments), without showing full
    // package names.
    @SuppressWarnings("unchecked")
    protected void formatAnnotationMirrorArg(AnnotationValue av, StringBuilder sb) {
        Object val = av.getValue();
        if (List.class.isAssignableFrom(val.getClass())) {
            List<AnnotationValue> vallist = (List<AnnotationValue>) val;
            if (vallist.size() == 1) {
                formatAnnotationMirrorArg(vallist.get(0), sb);
            } else {
                sb.append('{');
                boolean notfirst = false;
                for (AnnotationValue nav : vallist) {
                    if (notfirst) {
                        sb.append(", ");
                    }
                    notfirst = true;
                    formatAnnotationMirrorArg(nav, sb);
                }
                sb.append('}');
            }
        } else if (VariableElement.class.isAssignableFrom(val.getClass())) {
            VariableElement ve = (VariableElement) val;
            sb.append(ve.getEnclosingElement().getSimpleName() + "." + ve.getSimpleName());
        } else {
            sb.append(av.toString());
        }
    }
}
