package org.checkerframework.framework.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.lang.model.element.AnnotationMirror;
import org.checkerframework.framework.source.Result;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.javacutil.BugInCF;

/**
 * Represents field invariants, which the user states by writing {@code @FieldInvariant}. Think of
 * this as a set of (field, qualifier) pairs.
 *
 * <p>A FieldInvariants object may be malformed (inconsistent number of fields and qualifiers). In
 * this case, the BaseTypeVisitor will issue an error.
 */
public class FieldInvariants {

    /**
     * A list of simple field names. A field may appear more than once in this list. In a
     * well-formed FieldInvariants, has the same length as {@code qualifiers}.
     */
    private final List<String> fields;

    /**
     * A list of qualifiers that apply to the field at the same index in {@code fields}. In a
     * well-formed FieldInvariants, has the same length as {@code fields}.
     */
    private final List<AnnotationMirror> qualifiers;

    /**
     * Creates a new FieldInvariants object. The result is well-formed if length of qualifiers is
     * either 1 or equal to length of {@code fields}.
     *
     * @param fields list of fields
     * @param qualifiers list of qualifiers
     */
    public FieldInvariants(List<String> fields, List<AnnotationMirror> qualifiers) {
        this(null, fields, qualifiers);
    }

    /**
     * Creates a new object with all the invariants in {@code other}, plus those specified by {@code
     * fields} and {@code qualifiers}. The result is well-formed if length of qualifiers is either 1
     * or equal to length of {@code fields}.
     *
     * @param other other invariant object, may be null
     * @param fields list of fields
     * @param qualifiers list of qualifiers
     */
    public FieldInvariants(
            FieldInvariants other, List<String> fields, List<AnnotationMirror> qualifiers) {
        if (qualifiers.size() == 1) {
            while (fields.size() > qualifiers.size()) {
                qualifiers.add(qualifiers.get(0));
            }
        }
        if (other != null) {
            fields.addAll(other.fields);
            qualifiers.addAll(other.qualifiers);
        }

        this.fields = Collections.unmodifiableList(fields);
        this.qualifiers = qualifiers;
    }

    /** The simple names of the fields that have a qualifier. May contain duplicates. */
    public List<String> getFields() {
        return fields;
    }

    /**
     * Returns a list of qualifiers for {@code field}. If {@code field} has no qualifiers, returns
     * an empty list.
     *
     * @param field simple field name
     * @return a list of qualifiers for {@code field}, possibly empty
     */
    public List<AnnotationMirror> getQualifiersFor(CharSequence field) {
        if (!isWellFormed()) {
            throw new BugInCF("malformed FieldInvariants");
        }
        String fieldString = field.toString();
        int index = fields.indexOf(fieldString);
        if (index == -1) {
            return Collections.emptyList();
        }
        List<AnnotationMirror> list = new ArrayList<>();
        for (int i = 0; i < fields.size(); i++) {
            if (fields.get(i).equals(fieldString)) {
                list.add(qualifiers.get(i));
            }
        }
        return list;
    }

    /** @return true if there is a qualifier for each field in {@code fields} */
    public boolean isWellFormed() {
        return qualifiers.size() == fields.size();
    }

    /**
     * @return null if {@code superInvar} is a super invariant, otherwise returns a Result with the
     *     error message
     */
    public Result isSuperInvariant(FieldInvariants superInvar, AnnotatedTypeFactory factory) {
        QualifierHierarchy qualifierHierarchy = factory.getQualifierHierarchy();
        if (!this.fields.containsAll(superInvar.fields)) {
            List<String> missingFields = new ArrayList<>(superInvar.fields);
            missingFields.removeAll(fields);
            return Result.failure(
                    "field.invariant.not.found.superclass", String.join(", ", missingFields));
        }

        for (String field : superInvar.fields) {
            List<AnnotationMirror> superQualifiers = superInvar.getQualifiersFor(field);
            List<AnnotationMirror> subQualifiers = this.getQualifiersFor(field);
            for (AnnotationMirror superA : superQualifiers) {
                AnnotationMirror sub =
                        qualifierHierarchy.findAnnotationInSameHierarchy(subQualifiers, superA);
                if (sub == null || !qualifierHierarchy.isSubtype(sub, superA)) {
                    return Result.failure(
                            "field.invariant.not.subtype.superclass", field, sub, superA);
                }
            }
        }
        return null;
    }
}
