////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2020 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle.utils;

import java.util.List;
import java.util.function.Predicate;

import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;

/**
 * Contains utility methods designed to work with annotations.
 *
 */
public final class AnnotationUtil {

    /**
     * Common message.
     */
    private static final String THE_AST_IS_NULL = "the ast is null";

    /**
     * Private utility constructor.
     * @throws UnsupportedOperationException if called
     */
    private AnnotationUtil() {
        throw new UnsupportedOperationException("do not instantiate.");
    }

    /**
     * Checks if the AST is annotated with the passed in annotation.
     *
     * <p>
     * This method will not look for imports or package
     * statements to detect the passed in annotation.
     * </p>
     *
     * <p>
     * To check if an AST contains a passed in annotation
     * taking into account fully-qualified names
     * (ex: java.lang.Override, Override)
     * this method will need to be called twice. Once for each
     * name given.
     * </p>
     *
     * @param ast the current node
     * @param annotation the annotation name to check for
     * @return true if contains the annotation
     */
    public static boolean containsAnnotation(final DetailAST ast,
        String annotation) {
        return getAnnotation(ast, annotation) != null;
    }

    /**
     * Checks if the AST is annotated with any annotation.
     *
     * @param ast the current node
     * @return {@code true} if the AST contains at least one annotation
     * @throws IllegalArgumentException when ast is null
     */
    public static boolean containsAnnotation(final DetailAST ast) {
        if (ast == null) {
            throw new IllegalArgumentException(THE_AST_IS_NULL);
        }
        final DetailAST holder = getAnnotationHolder(ast);
        return holder != null && holder.findFirstToken(TokenTypes.ANNOTATION) != null;
    }

    /**
     * Checks if the given AST element is annotated with any of the specified annotations.
     *
     * <p>
     * This method accepts both simple and fully-qualified names,
     * e.g. "Override" will match both java.lang.Override and Override.
     * </p>
     *
     * @param ast The type or method definition.
     * @param annotations A collection of annotations to look for.
     * @return {@code true} if the given AST element is annotated with
     *                      at least one of the specified annotations;
     *                      {@code false} otherwise.
     * @throws IllegalArgumentException when ast or annotations are null
     */
    public static boolean containsAnnotation(DetailAST ast, List<String> annotations) {
        if (ast == null) {
            throw new IllegalArgumentException(THE_AST_IS_NULL);
        }

        if (annotations == null) {
            throw new IllegalArgumentException("annotations cannot be null");
        }

        boolean result = false;

        if (!annotations.isEmpty()) {
            final DetailAST firstMatchingAnnotation = findFirstAnnotation(ast, annotationNode -> {
                DetailAST identNode = annotationNode.findFirstToken(TokenTypes.IDENT);
                if (identNode == null) {
                    identNode = annotationNode.findFirstToken(TokenTypes.DOT)
                            .findFirstToken(TokenTypes.IDENT);
                }

                return annotations.contains(identNode.getText());
            });
            result = firstMatchingAnnotation != null;
        }

        return result;
    }

    /**
     * Gets the AST that holds a series of annotations for the
     * potentially annotated AST.  Returns {@code null}
     * if the passed in AST does not have an Annotation Holder.
     *
     * @param ast the current node
     * @return the Annotation Holder
     * @throws IllegalArgumentException when ast is null
     */
    public static DetailAST getAnnotationHolder(DetailAST ast) {
        if (ast == null) {
            throw new IllegalArgumentException(THE_AST_IS_NULL);
        }

        final DetailAST annotationHolder;

        if (ast.getType() == TokenTypes.ENUM_CONSTANT_DEF
            || ast.getType() == TokenTypes.PACKAGE_DEF) {
            annotationHolder = ast.findFirstToken(TokenTypes.ANNOTATIONS);
        }
        else {
            annotationHolder = ast.findFirstToken(TokenTypes.MODIFIERS);
        }

        return annotationHolder;
    }

    /**
     * Checks if the AST is annotated with the passed in annotation
     * and returns the AST representing that annotation.
     *
     * <p>
     * This method will not look for imports or package
     * statements to detect the passed in annotation.
     * </p>
     *
     * <p>
     * To check if an AST contains a passed in annotation
     * taking into account fully-qualified names
     * (ex: java.lang.Override, Override)
     * this method will need to be called twice. Once for each
     * name given.
     * </p>
     *
     * @param ast the current node
     * @param annotation the annotation name to check for
     * @return the AST representing that annotation
     * @throws IllegalArgumentException when ast or annotations are null; when annotation is blank
     */
    public static DetailAST getAnnotation(final DetailAST ast,
        String annotation) {
        if (ast == null) {
            throw new IllegalArgumentException(THE_AST_IS_NULL);
        }

        if (annotation == null) {
            throw new IllegalArgumentException("the annotation is null");
        }

        if (CommonUtil.isBlank(annotation)) {
            throw new IllegalArgumentException(
                    "the annotation is empty or spaces");
        }

        return findFirstAnnotation(ast, annotationNode -> {
            final DetailAST firstChild = annotationNode.findFirstToken(TokenTypes.AT);
            final String name =
                    FullIdent.createFullIdent(firstChild.getNextSibling()).getText();
            return annotation.equals(name);
        });
    }

    /**
     * Checks if the given AST is annotated with at least one annotation that
     * matches the given predicate and returns the AST representing the first
     * matching annotation.
     *
     * <p>
     * This method will not look for imports or package
     * statements to detect the passed in annotation.
     * </p>
     *
     * @param ast the current node
     * @param predicate The predicate which decides if an annotation matches
     * @return the AST representing that annotation
     */
    private static DetailAST findFirstAnnotation(final DetailAST ast,
                                                 Predicate<DetailAST> predicate) {
        final DetailAST holder = getAnnotationHolder(ast);
        DetailAST result = null;
        for (DetailAST child = holder.getFirstChild();
            child != null; child = child.getNextSibling()) {
            if (child.getType() == TokenTypes.ANNOTATION && predicate.test(child)) {
                result = child;
                break;
            }
        }

        return result;
    }

}
