package org.checkerframework.framework.util.typeinference;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNullType;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.framework.type.TypeHierarchy;
import org.checkerframework.framework.util.AnnotationMirrorMap;
import org.checkerframework.framework.util.AnnotationMirrorSet;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.TypesUtils;

/** A class used to determine the greatest lower bounds for a set of AnnotatedTypeMirrors. */
public class GlbUtil {

    /**
     * Note: This method can be improved for wildcards and type variables.
     *
     * @return the greatest lower bound of typeMirrors. If any of the type mirrors are incomparable,
     *     use an AnnotatedNullType that will contain the greatest lower bounds of the primary
     *     annotations of typeMirrors.
     */
    public static AnnotatedTypeMirror glbAll(
            final Map<AnnotatedTypeMirror, AnnotationMirrorSet> typeMirrors,
            final AnnotatedTypeFactory typeFactory) {
        final QualifierHierarchy qualifierHierarchy = typeFactory.getQualifierHierarchy();
        if (typeMirrors.isEmpty()) {
            return null;
        }

        // dtermine the greatest lower bounds for the primary annotations
        AnnotationMirrorMap<AnnotationMirror> glbPrimaries = new AnnotationMirrorMap<>();
        for (Entry<AnnotatedTypeMirror, AnnotationMirrorSet> tmEntry : typeMirrors.entrySet()) {
            final AnnotationMirrorSet typeAnnoHierarchies = tmEntry.getValue();
            final AnnotatedTypeMirror type = tmEntry.getKey();

            for (AnnotationMirror top : typeAnnoHierarchies) {
                // TODO: When all of the typeMirrors are either wildcards or type variables than the
                // greatest lower bound should involve handling the bounds individually rather than
                // using the effective annotation.  We are doing this for expediency.
                final AnnotationMirror typeAnno = type.getEffectiveAnnotationInHierarchy(top);
                final AnnotationMirror currentAnno = glbPrimaries.get(top);
                if (typeAnno != null && currentAnno != null) {
                    glbPrimaries.put(
                            top, qualifierHierarchy.greatestLowerBound(currentAnno, typeAnno));
                } else if (typeAnno != null) {
                    glbPrimaries.put(top, typeAnno);
                }
            }
        }

        final List<AnnotatedTypeMirror> glbTypes = new ArrayList<>();

        // create a copy of all of the types and apply the glb primary annotation
        final AnnotationMirrorSet values = new AnnotationMirrorSet(glbPrimaries.values());
        for (AnnotatedTypeMirror type : typeMirrors.keySet()) {
            if (type.getKind() != TypeKind.TYPEVAR
                    || !qualifierHierarchy.isSubtype(type.getEffectiveAnnotations(), values)) {
                final AnnotatedTypeMirror copy = type.deepCopy();
                copy.replaceAnnotations(values);
                glbTypes.add(copy);

            } else {
                // if the annotations came from the upper bound of this typevar
                // we do NOT want to place them as primary annotations (and destroy the
                // type vars lower bound)
                glbTypes.add(type);
            }
        }

        final TypeHierarchy typeHierarchy = typeFactory.getTypeHierarchy();

        // sort placing supertypes first
        sortForGlb(glbTypes, typeFactory);

        // find the lowest type in the list that is not an AnnotatedNullType
        AnnotatedTypeMirror glbType = glbTypes.get(0);
        int index = 1;
        while (index < glbTypes.size()) {
            // avoid using null if possible, since constraints form the lower bound will often have
            // NULL types
            if (glbType.getKind() != TypeKind.NULL) {
                glbType = glbTypes.get(index);
            }
            index += 1;
        }

        // if the lowest type is a subtype of all glbTypes then it is the GLB, otherwise
        // there are two types  in glbTypes that are incomparable and we need to use bottom
        // (AnnotatedNullType)
        boolean incomparable = false;
        for (final AnnotatedTypeMirror type : glbTypes) {
            if (!incomparable
                    && type.getKind() != TypeKind.NULL
                    && (!TypesUtils.isErasedSubtype(
                                    glbType.getUnderlyingType(),
                                    type.getUnderlyingType(),
                                    typeFactory.getContext().getTypeUtils())
                            || !typeHierarchy.isSubtype(glbType, type))) {
                incomparable = true;
            }
        }

        // we had two incomparable types in glbTypes
        if (incomparable) {
            return createBottom(typeFactory, glbType.getEffectiveAnnotations());
        }

        return glbType;
    }

    /** Returns an AnnotatedNullType with the given annotations as primaries. */
    private static AnnotatedNullType createBottom(
            final AnnotatedTypeFactory typeFactory, final Set<? extends AnnotationMirror> annos) {
        return typeFactory.getAnnotatedNullType(annos);
    }

    /**
     * Sort the list of type mirrors, placing supertypes first and subtypes last.
     *
     * <p>E.g. the list: {@code ArrayList<String>, List<String>, AbstractList<String>} becomes:
     * {@code List<String>, AbstractList<String>, ArrayList<String>}
     */
    public static void sortForGlb(
            final List<? extends AnnotatedTypeMirror> typeMirrors,
            final AnnotatedTypeFactory typeFactory) {
        final QualifierHierarchy qualifierHierarchy = typeFactory.getQualifierHierarchy();
        final Types types = typeFactory.getProcessingEnv().getTypeUtils();

        Collections.sort(
                typeMirrors,
                new Comparator<AnnotatedTypeMirror>() {
                    @Override
                    public int compare(AnnotatedTypeMirror type1, AnnotatedTypeMirror type2) {
                        final TypeMirror underlyingType1 = type1.getUnderlyingType();
                        final TypeMirror underlyingType2 = type2.getUnderlyingType();

                        if (types.isSameType(underlyingType1, underlyingType2)) {
                            return compareAnnotations(qualifierHierarchy, type1, type2);
                        }

                        if (types.isSubtype(underlyingType1, underlyingType2)) {
                            return 1;
                        }

                        // if they're incomparable or type2 is a subtype of type1
                        return -1;
                    }

                    private int compareAnnotations(
                            final QualifierHierarchy qualHierarchy,
                            final AnnotatedTypeMirror type1,
                            final AnnotatedTypeMirror type2) {
                        if (AnnotationUtils.areSame(
                                type1.getAnnotations(), type2.getAnnotations())) {
                            return 0;
                        }

                        if (qualHierarchy.isSubtype(
                                type1.getAnnotations(), type2.getAnnotations())) {
                            return 1;

                        } else {
                            return -1;
                        }
                    }
                });
    }
}
