/*
 * Hibernate Validator, declare and validate application constraints
 *
 * License: Apache License, Version 2.0
 * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
 */
package org.hibernate.validator.internal.metadata.provider;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.AnnotatedParameterizedType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.validation.ParameterNameProvider;
import javax.validation.Valid;

import org.hibernate.validator.internal.metadata.core.AnnotationProcessingOptions;
import org.hibernate.validator.internal.metadata.core.ConstraintHelper;
import org.hibernate.validator.internal.metadata.core.MetaConstraint;
import org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl;
import org.hibernate.validator.internal.metadata.location.ConstraintLocation;
import org.hibernate.validator.internal.util.IgnoreJava6Requirement;
import org.hibernate.validator.internal.util.ReflectionHelper;
import org.hibernate.validator.internal.util.TypeHelper;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;

import static org.hibernate.validator.internal.util.CollectionHelper.newArrayList;
import static org.hibernate.validator.internal.util.CollectionHelper.newHashSet;
import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES;

/**
 * Extends {@code AnnotationMetaDataProvider} by discovering and registering constraints defined via Java 8 type
 * annotations.
 *
 * @author Khalid Alqinyah
 * @author Hardy Ferentschik
 */
@IgnoreJava6Requirement
public class TypeAnnotationAwareMetaDataProvider extends AnnotationMetaDataProvider {

	private static final Log log = LoggerFactory.make();

	public TypeAnnotationAwareMetaDataProvider(ConstraintHelper constraintHelper,
											   ParameterNameProvider parameterNameProvider,
											   AnnotationProcessingOptions annotationProcessingOptions) {
		super( constraintHelper, parameterNameProvider, annotationProcessingOptions );
	}

	@Override
	protected Set<MetaConstraint<?>> findTypeAnnotationConstraintsForMember(Member member) {
		AnnotatedType annotatedType = null;

		if ( member instanceof Field ) {
			annotatedType = ( (Field) member ).getAnnotatedType();
		}

		if ( member instanceof Method ) {
			annotatedType = ( (Method) member ).getAnnotatedReturnType();
		}

		return findTypeArgumentsConstraints(
				member,
				annotatedType,
				( (AccessibleObject) member ).isAnnotationPresent( Valid.class )
		);
	}

	@Override
	protected Set<MetaConstraint<?>> findTypeAnnotationConstraintsForExecutableParameter(Member member, int i) {
		Parameter parameter = ( (Executable) member ).getParameters()[i];
		try {
			return findTypeArgumentsConstraints(
					member,
					parameter.getAnnotatedType(),
					parameter.isAnnotationPresent( Valid.class )
			);
		}
		catch (ArrayIndexOutOfBoundsException ex) {
			log.warn( MESSAGES.constraintOnConstructorOfNonStaticInnerClass(), ex );
			return Collections.emptySet();
		}
	}

	private Set<MetaConstraint<?>> findTypeArgumentsConstraints(Member member, AnnotatedType annotatedType, boolean isCascaded) {
		Optional<AnnotatedType> typeParameter = getTypeParameter( annotatedType );
		if ( !typeParameter.isPresent() ) {
			return Collections.emptySet();
		}

		List<ConstraintDescriptorImpl<?>> constraintDescriptors = findTypeUseConstraints( member, typeParameter.get() );
		if ( constraintDescriptors.isEmpty() ) {
			return Collections.emptySet();
		}

		// HV-925
		// We need to determine the validated type used for constraint validator resolution.
		// Iterables and maps need special treatment at this point, since the validated type is the type of the
		// specified type parameter. In the other cases the validated type is the parameterized type, eg Optional<String>.
		// In the latter case a value unwrapping has to occur
		Type validatedType = annotatedType.getType();
		if ( ReflectionHelper.isIterable( annotatedType.getType() ) || ReflectionHelper.isMap( annotatedType.getType() ) ) {
			if ( !isCascaded ) {
				throw log.getTypeAnnotationConstraintOnIterableRequiresUseOfValidAnnotationException(
						member.getDeclaringClass().getName(),
						member.getName()
				);
			}
			validatedType = typeParameter.get().getType();
		}

		return convertToTypeArgumentMetaConstraints(
				constraintDescriptors,
				member,
				validatedType
		);
	}

	/**
	 * Finds type use annotation constraints defined on the type argument.
	 */
	private List<ConstraintDescriptorImpl<?>> findTypeUseConstraints(Member member, AnnotatedType typeArgument) {
		List<ConstraintDescriptorImpl<?>> metaData = newArrayList();

		for ( Annotation annotation : typeArgument.getAnnotations() ) {
			metaData.addAll( findConstraintAnnotations( member, annotation, ElementType.TYPE_USE ) );
		}

		return metaData;
	}

	/**
	 * Creates meta constraints for type arguments constraints.
	 */
	private Set<MetaConstraint<?>> convertToTypeArgumentMetaConstraints(List<ConstraintDescriptorImpl<?>> constraintDescriptors, Member member, Type type) {
		Set<MetaConstraint<?>> constraints = newHashSet( constraintDescriptors.size() );
		for ( ConstraintDescriptorImpl<?> constraintDescription : constraintDescriptors ) {
			MetaConstraint<?> metaConstraint = createTypeArgumentMetaConstraint( member, constraintDescription, type );
			constraints.add( metaConstraint );
		}
		return constraints;
	}

	/**
	 * Creates a {@code MetaConstraint} for a type argument constraint.
	 */
	private <A extends Annotation> MetaConstraint<?> createTypeArgumentMetaConstraint(Member member, ConstraintDescriptorImpl<A> descriptor, Type type) {
		return new MetaConstraint<A>( descriptor, ConstraintLocation.forTypeArgument( member, type ) );
	}

	/**
	 * Returns the type argument of a parameterized type. If the type is a {@code Map}, the method returns the value
	 * type argument. If the type has more than one type argument and is not a Map, the method returns an empty {@code
	 * Optional}.
	 */
	private Optional<AnnotatedType> getTypeParameter(AnnotatedType annotatedType) {
		if ( annotatedType == null ) {
			return Optional.empty();
		}

		if ( !TypeHelper.isAssignable( AnnotatedParameterizedType.class, annotatedType.getClass() ) ) {
			return Optional.empty();
		}

		AnnotatedType[] annotatedArguments = ( (AnnotatedParameterizedType) annotatedType ).getAnnotatedActualTypeArguments();

		// One type argument, return it
		if ( annotatedArguments.length == 1 ) {
			return Optional.of( annotatedArguments[0] );
		}

		// More than one type argument
		if ( annotatedArguments.length > 1 ) {

			// If it is a Map, return the value type argument
			if ( ReflectionHelper.isMap( annotatedType.getType() ) ) {
				return Optional.of( annotatedArguments[1] );
			}

			// If it is not a Map, log a message and ignore
			log.parameterizedTypeWithMoreThanOneTypeArgumentIsNotSupported( annotatedType.getType().getTypeName() );
		}

		return Optional.empty();
	}
}
