/*
 * Copyright 2015 the original author or authors.
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License v1.0 which
 * accompanies this distribution and is available at
 *
 * http://www.eclipse.org/legal/epl-v10.html
 */

package org.junit.gen5.commons.util;

import static java.util.Arrays.asList;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.*;

/**
 * Collection of utilities for working with {@linkplain Annotation annotations}.
 *
 * @author Sam Brannen
 * @author Stefan Bechtold
 * @since 5.0
 * @see Annotation
 * @see AnnotatedElement
 */
public final class AnnotationUtils {

	private AnnotationUtils() {
		/* no-op */
	}

	/**
	 * Find all annotations of {@code annotationType} that are either
	 * <em>present</em> or <em>meta-present</em> on the supplied {@code element}.
	 */
	public static <A extends Annotation> List<A> findAllAnnotations(AnnotatedElement element, Class<A> annotationType) {
		return findAllAnnotations(element, annotationType, new HashSet<>());
	}

	private static <A extends Annotation> List<A> findAllAnnotations(AnnotatedElement element, Class<A> annotationType,
			Set<Annotation> visited) {
		Preconditions.notNull(annotationType, "annotationType must not be null");

		if (element == null) {
			return Collections.emptyList();
		}

		List<A> collectedAnnotations = new ArrayList<>();

		// Directly present or inherited?
		List<A> annotations = asList(element.getAnnotationsByType(annotationType));
		collectedAnnotations.addAll(annotations);

		// Meta-present on directly present annotations?
		// TODO Isn't this covered by the next block as well?
		for (Annotation candidateAnnotation : element.getDeclaredAnnotations()) {
			if (!isInJavaLangAnnotationPackage(candidateAnnotation) && visited.add(candidateAnnotation)) {
				List<A> metaAnnotations = findAllAnnotations(candidateAnnotation.annotationType(), annotationType,
					visited);
				collectedAnnotations.addAll(metaAnnotations);
			}
		}

		// Meta-present on indirectly present annotations?
		for (Annotation candidateAnnotation : element.getAnnotations()) {
			if (!isInJavaLangAnnotationPackage(candidateAnnotation) && visited.add(candidateAnnotation)) {
				List<A> metaAnnotations = findAllAnnotations(candidateAnnotation.annotationType(), annotationType,
					visited);
				collectedAnnotations.addAll(metaAnnotations);
			}
		}

		return collectedAnnotations;

	}

	/**
	 * Find the first annotation of {@code annotationType} that is either
	 * <em>present</em> or <em>meta-present</em> on the supplied {@code element}.
	 */
	public static <A extends Annotation> Optional<A> findAnnotation(AnnotatedElement element, Class<A> annotationType) {
		//TODO should we delegate to findAllAnnotations?
		return findAnnotation(element, annotationType, new HashSet<Annotation>());
	}

	private static <A extends Annotation> Optional<A> findAnnotation(AnnotatedElement element, Class<A> annotationType,
			Set<Annotation> visited) {

		Preconditions.notNull(annotationType, "annotationType must not be null");

		if (element == null) {
			return Optional.empty();
		}

		// Directly present?
		A annotation = element.getDeclaredAnnotation(annotationType);
		if (annotation != null) {
			return Optional.of(annotation);
		}

		// Meta-present on directly present annotations?
		for (Annotation candidateAnnotation : element.getDeclaredAnnotations()) {
			if (!isInJavaLangAnnotationPackage(candidateAnnotation) && visited.add(candidateAnnotation)) {
				Optional<A> metaAnnotation = findAnnotation(candidateAnnotation.annotationType(), annotationType,
					visited);
				if (metaAnnotation.isPresent()) {
					return metaAnnotation;
				}
			}
		}

		// Indirectly present?
		annotation = element.getAnnotation(annotationType);
		if (annotation != null) {
			return Optional.of(annotation);
		}

		// Meta-present on indirectly present annotations?
		for (Annotation candidateAnnotation : element.getAnnotations()) {
			if (!isInJavaLangAnnotationPackage(candidateAnnotation) && visited.add(candidateAnnotation)) {
				Optional<A> metaAnnotation = findAnnotation(candidateAnnotation.annotationType(), annotationType,
					visited);
				if (metaAnnotation.isPresent()) {
					return metaAnnotation;
				}
			}
		}

		return Optional.empty();
	}

	public static List<Method> findAnnotatedMethods(Class<?> clazz, Class<? extends Annotation> annotationType,
			ReflectionUtils.MethodSortOrder sortOrder) {
		Preconditions.notNull(clazz, "Class must not be null");
		Preconditions.notNull(annotationType, "annotationType must not be null");

		return ReflectionUtils.findMethods(clazz, method -> findAnnotation(method, annotationType).isPresent(),
			sortOrder);
	}

	public static boolean isInJavaLangAnnotationPackage(Annotation annotation) {
		return (annotation != null
				&& annotation.annotationType().getPackage().getName().startsWith("java.lang.annotation"));
	}

}

@@ -1707,64 +1707,8 @@
 ns?%0A
-%09%09// TODO Isn't this covered by the next block as well?%0A
 %09%09fo
@@ -2770,59 +2770,8 @@
 ) %7B%0A
-%09%09//TODO should we delegate to findAllAnnotations?%0A
 %09%09re

Delete addressed TODOs