/*
* JBoss, Home of Professional Open Source
* Copyright 2009, Red Hat, Inc. and/or its affiliates, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hibernate.validator.internal.metadata.aggregated;

import java.lang.annotation.ElementType;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.validation.groups.Default;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.PropertyDescriptor;

import org.hibernate.validator.internal.engine.groups.Sequence;
import org.hibernate.validator.internal.engine.groups.ValidationOrder;
import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator;
import org.hibernate.validator.internal.metadata.aggregated.ConstraintMetaData.ConstraintMetaDataKind;
import org.hibernate.validator.internal.metadata.core.ConstraintHelper;
import org.hibernate.validator.internal.metadata.core.MetaConstraint;
import org.hibernate.validator.internal.metadata.descriptor.BeanDescriptorImpl;
import org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl;
import org.hibernate.validator.internal.metadata.raw.BeanConfiguration;
import org.hibernate.validator.internal.metadata.raw.ConfigurationSource;
import org.hibernate.validator.internal.metadata.raw.ConstrainedElement;
import org.hibernate.validator.internal.metadata.raw.ConstrainedElement.ConstrainedElementKind;
import org.hibernate.validator.internal.metadata.raw.ConstrainedField;
import org.hibernate.validator.internal.metadata.raw.ConstrainedMethod;
import org.hibernate.validator.internal.metadata.raw.ConstrainedType;
import org.hibernate.validator.internal.util.CollectionHelper.Partitioner;
import org.hibernate.validator.internal.util.ReflectionHelper;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
import org.hibernate.validator.method.metadata.MethodDescriptor;
import org.hibernate.validator.method.metadata.TypeDescriptor;
import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider;

import static org.hibernate.validator.internal.util.CollectionHelper.newArrayList;
import static org.hibernate.validator.internal.util.CollectionHelper.newHashMap;
import static org.hibernate.validator.internal.util.CollectionHelper.newHashSet;
import static org.hibernate.validator.internal.util.CollectionHelper.partition;
import static org.hibernate.validator.internal.util.ReflectionHelper.computeAllImplementedInterfaces;

/**
 * This class encapsulates all meta data needed for validation. Implementations of {@code Validator} interface can
 * instantiate an instance of this class and delegate the metadata extraction to it.
 *
 * @author Hardy Ferentschik
 * @author Gunnar Morling
 * @author Kevin Pollet <kevin.pollet@serli.com> (C) 2011 SERLI
 */
@SuppressWarnings("deprecation")
public final class BeanMetaDataImpl<T> implements BeanMetaData<T> {

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

	/**
	 * Represents the "sequence" of just Default.class.
	 */
	private static final List<Class<?>> DEFAULT_GROUP_SEQUENCE = Collections.<Class<?>>singletonList( Default.class );

	private final ValidationOrderGenerator validationOrderGenerator;

	/**
	 * The root bean class for this meta data.
	 */
	private final Class<T> beanClass;

	/**
	 * Set of all constraints for this bean type (defined on any implemented interfaces or super types)
	 */
	private final Set<MetaConstraint<?>> allMetaConstraints;

	/**
	 * Set of all constraints which are directly defined on the bean or any of the directly implemented interfaces
	 */
	private final Set<MetaConstraint<?>> directMetaConstraints;

	/**
	 * Contains constrained related meta data for all methods of the type
	 * represented by this bean meta data. Keyed by method, values are an
	 * aggregated view on each method together with all the methods from the
	 * inheritance hierarchy with the same signature.
	 */
	private final Map<String, MethodMetaData> methodMetaData;

	private final Map<String, PropertyMetaData> propertyMetaData;

	/**
	 * List of cascaded members.
	 */
	private final Set<Member> cascadedMembers;

	/**
	 * The default groups sequence for this bean class.
	 */
	private List<Class<?>> defaultGroupSequence = newArrayList();

	/**
	 * The default group sequence provider.
	 *
	 * @see org.hibernate.validator.group.GroupSequenceProvider
	 * @see DefaultGroupSequenceProvider
	 */
	private DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider;

	private ValidationOrder validationOrder;
	/**
	 * The class hierarchy for this class starting with the class itself going up the inheritance chain. Interfaces
	 * are not included.
	 */
	private final List<Class<?>> classHierarchyWithoutInterfaces;

	/**
	 * Creates a new {@link BeanMetaDataImpl}
	 *
	 * @param beanClass The Java type represented by this meta data object.
	 * @param defaultGroupSequence The default group sequence.
	 * @param defaultGroupSequenceProvider The default group sequence provider if set.
	 * @param constraintMetaData All constraint meta data relating to the represented type.
	 */
	public BeanMetaDataImpl(Class<T> beanClass,
							List<Class<?>> defaultGroupSequence,
							DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider,
							Set<ConstraintMetaData> constraintMetaDataSet,
							ValidationOrderGenerator validationOrderGenerator) {

		this.validationOrderGenerator = validationOrderGenerator;
		this.beanClass = beanClass;
		this.propertyMetaData = newHashMap();

		Set<PropertyMetaData> propertyMetaDataSet = newHashSet();
		Set<MethodMetaData> methodMetaDataSet = newHashSet();

		for ( ConstraintMetaData oneElement : constraintMetaDataSet ) {
			if ( oneElement.getKind() == ConstraintMetaDataKind.PROPERTY ) {
				propertyMetaDataSet.add( (PropertyMetaData) oneElement );
			}
			else {
				methodMetaDataSet.add( (MethodMetaData) oneElement );
			}
		}

		Set<Member> cascadedMembers = newHashSet();
		Set<MetaConstraint<?>> allMetaConstraints = newHashSet();

		for ( PropertyMetaData oneProperty : propertyMetaDataSet ) {

			propertyMetaData.put( oneProperty.getName(), oneProperty );

			if ( oneProperty.isCascading() ) {
				cascadedMembers.addAll( oneProperty.getCascadingMembers() );
			}

			allMetaConstraints.addAll( oneProperty.getConstraints() );
		}

		this.cascadedMembers = Collections.unmodifiableSet( cascadedMembers );
		this.allMetaConstraints = Collections.unmodifiableSet( allMetaConstraints );

		classHierarchyWithoutInterfaces = ReflectionHelper.computeClassHierarchy( beanClass, false );

		setDefaultGroupSequenceOrProvider( defaultGroupSequence, defaultGroupSequenceProvider, validationOrderGenerator );

		directMetaConstraints = buildDirectConstraintSets();

		this.methodMetaData = Collections.unmodifiableMap( buildMethodMetaData( methodMetaDataSet ) );
	}

	@Override
	public Class<T> getBeanClass() {
		return beanClass;
	}

	@Override
	public BeanDescriptor getBeanDescriptor() {
		return getBeanDescriptorInternal();
	}

	@Override
	public TypeDescriptor getTypeDescriptor() {
		return getBeanDescriptorInternal();
	}

	@Override
	public Set<Member> getCascadedMembers() {
		return cascadedMembers;
	}

	@Override
	public Set<MetaConstraint<?>> getMetaConstraints() {
		return allMetaConstraints;
	}

	@Override
	public Set<MetaConstraint<?>> getDirectMetaConstraints() {
		return directMetaConstraints;
	}

	@Override
	public MethodMetaData getMetaDataFor(Method method) {
		return methodMetaData.get( method.getName() + Arrays.toString( method.getParameterTypes() ) );
	}

	@Override
	public Set<MethodMetaData> getAllMethodMetaData() {
		return new HashSet<MethodMetaData>( methodMetaData.values() );
	}

	@Override
	public PropertyMetaData getMetaDataFor(String propertyName) {
		return propertyMetaData.get( propertyName );
	}

	@Override
	public boolean isPropertyPresent(String name) {
		return propertyMetaData.containsKey( name );
	}

	@Override
	public List<Class<?>> getDefaultGroupSequence(T beanState) {
		if ( hasDefaultGroupSequenceProvider() ) {
			List<Class<?>> providerDefaultGroupSequence = defaultGroupSequenceProvider.getValidationGroups( beanState );
			return getValidDefaultGroupSequence( providerDefaultGroupSequence );
		}

		return Collections.unmodifiableList( defaultGroupSequence );
	}

	@Override
	public Iterator<Sequence> getDefaultValidationSequence(T beanState) {
		if ( hasDefaultGroupSequenceProvider() ) {
			List<Class<?>> providerDefaultGroupSequence = defaultGroupSequenceProvider.getValidationGroups( beanState );
			return validationOrderGenerator.getDefaultValidationOrder(
						beanClass,
						getValidDefaultGroupSequence( providerDefaultGroupSequence )
					)
					.getSequenceIterator();
		}
		else {
			return validationOrder.getSequenceIterator();
		}
	}

	@Override
	public boolean defaultGroupSequenceIsRedefined() {
		return defaultGroupSequence.size() > 1 || hasDefaultGroupSequenceProvider();
	}

	@Override
	public Set<PropertyMetaData> getAllPropertyMetaData() {
		return Collections.unmodifiableSet( new HashSet<PropertyMetaData>( propertyMetaData.values() ) );
	}

	@Override
	public List<Class<?>> getClassHierarchy() {
		return classHierarchyWithoutInterfaces;
	}

	/**
	 * Returns a bean descriptor representing this meta data object. A new
	 * descriptor instance is created with each invocation. The descriptor might
	 * be cached internally in the future should that need arise.
	 *
	 * @return A bean descriptor for this meta data object.
	 */
	private BeanDescriptorImpl<T> getBeanDescriptorInternal() {

		return new BeanDescriptorImpl<T>(
				beanClass,
				getClassLevelConstraintsAsDescriptors(),
				getConstrainedPropertiesAsDescriptors(),
				getMethodsAsDescriptors(),
				defaultGroupSequenceIsRedefined(),
				getDefaultGroupSequence( null )
		);
	}

	private Set<ConstraintDescriptorImpl<?>> getClassLevelConstraintsAsDescriptors() {

		Set<MetaConstraint<?>> classLevelConstraints = getClassLevelConstraints( allMetaConstraints );

		Set<ConstraintDescriptorImpl<?>> theValue = newHashSet();

		for ( MetaConstraint<?> oneConstraint : classLevelConstraints ) {
			theValue.add( oneConstraint.getDescriptor() );
		}

		return theValue;
	}

	private Map<String, PropertyDescriptor> getConstrainedPropertiesAsDescriptors() {
		Map<String, PropertyDescriptor> theValue = newHashMap();

		for ( Entry<String, PropertyMetaData> oneProperty : propertyMetaData.entrySet() ) {
			if ( oneProperty.getValue().isConstrained() ) {
				theValue.put(
						oneProperty.getKey(),
						oneProperty.getValue()
								.asDescriptor( defaultGroupSequenceIsRedefined(), getDefaultGroupSequence( null ) )
				);
			}
		}

		return theValue;
	}

	private Map<String, MethodDescriptor> getMethodsAsDescriptors() {
		Map<String, MethodDescriptor> theValue = newHashMap();

		for ( Entry<String, MethodMetaData> oneMethod : methodMetaData.entrySet() ) {
			theValue.put(
					oneMethod.getKey(), oneMethod.getValue().asDescriptor(
					defaultGroupSequenceIsRedefined(),
					getDefaultGroupSequence( null )
			)
			);
		}

		return theValue;
	}

	private void setDefaultGroupSequenceOrProvider(List<Class<?>> defaultGroupSequence, DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider, ValidationOrderGenerator validationOrderGenerator) {
		if ( defaultGroupSequence != null && defaultGroupSequenceProvider != null ) {
			throw log.getInvalidDefaultGroupSequenceDefinitionException();
		}

		if ( defaultGroupSequenceProvider != null ) {
			this.defaultGroupSequenceProvider = defaultGroupSequenceProvider;
			this.validationOrder = null;
		}
		else if ( defaultGroupSequence != null && !defaultGroupSequence.isEmpty() ) {
			setDefaultGroupSequence( defaultGroupSequence );
			this.validationOrder = validationOrderGenerator.getDefaultValidationOrder( beanClass, this.defaultGroupSequence );
		}
		else {
			this.defaultGroupSequence = DEFAULT_GROUP_SEQUENCE;
			this.validationOrder = ValidationOrder.DEFAULT_SEQUENCE;
		}
	}

	private Set<MetaConstraint<?>> getClassLevelConstraints(Set<MetaConstraint<?>> constraints) {

		Set<MetaConstraint<?>> classLevelConstraints = partition(
				constraints,
				byElementType()
		).get( ElementType.TYPE );

		return classLevelConstraints != null ? classLevelConstraints : Collections.<MetaConstraint<?>>emptySet();
	}

	private Set<MetaConstraint<?>> buildDirectConstraintSets() {

		Set<MetaConstraint<?>> constraints = newHashSet();

		Set<Class<?>> classAndInterfaces = computeAllImplementedInterfaces( beanClass );
		classAndInterfaces.add( beanClass );

		for ( Class<?> clazz : classAndInterfaces ) {
			for ( MetaConstraint<?> oneConstraint : allMetaConstraints ) {
				if ( oneConstraint.getLocation().getBeanClass().equals( clazz ) ) {
					constraints.add( oneConstraint );
				}
			}
		}

		return Collections.unmodifiableSet( constraints );
	}

	/**
	 * Builds up the method meta data for this type
	 */
	private Map<String, MethodMetaData> buildMethodMetaData(Set<MethodMetaData> allMethodMetaData) {

		Map<String, MethodMetaData> theValue = newHashMap();

		for ( MethodMetaData oneAggregatedMethodMetaData : allMethodMetaData ) {
			theValue.put(
					oneAggregatedMethodMetaData.getName() + Arrays.toString( oneAggregatedMethodMetaData.getParameterTypes() ),
					oneAggregatedMethodMetaData
			);
		}

		return theValue;
	}

	private void setDefaultGroupSequence(List<Class<?>> groupSequence) {
		defaultGroupSequence = getValidDefaultGroupSequence( groupSequence );
	}

	private List<Class<?>> getValidDefaultGroupSequence(List<Class<?>> groupSequence) {
		List<Class<?>> validDefaultGroupSequence = new ArrayList<Class<?>>();

		boolean groupSequenceContainsDefault = false;
		if ( groupSequence != null ) {
			for ( Class<?> group : groupSequence ) {
				if ( group.getName().equals( beanClass.getName() ) ) {
					validDefaultGroupSequence.add( Default.class );
					groupSequenceContainsDefault = true;
				}
				else if ( group.getName().equals( Default.class.getName() ) ) {
					throw log.getNoDefaultGroupInGroupSequenceException();
				}
				else {
					validDefaultGroupSequence.add( group );
				}
			}
		}
		if ( !groupSequenceContainsDefault ) {
			throw log.getBeanClassMustBePartOfRedefinedDefaultGroupSequenceException( beanClass.getName() );
		}
		if ( log.isTraceEnabled() ) {
			log.tracef(
					"Members of the default group sequence for bean %s are: %s.",
					beanClass.getName(),
					validDefaultGroupSequence
			);
		}

		return validDefaultGroupSequence;
	}

	private boolean hasDefaultGroupSequenceProvider() {
		return defaultGroupSequenceProvider != null;
	}

	private Partitioner<ElementType, MetaConstraint<?>> byElementType() {
		return new Partitioner<ElementType, MetaConstraint<?>>() {
			@Override
			public ElementType getPartition(MetaConstraint<?> constraint) {
				return constraint.getElementType();
			}
		};
	}

	@Override
	public String toString() {
		final StringBuilder sb = new StringBuilder();
		sb.append( "BeanMetaDataImpl" );
		sb.append( "{beanClass=" ).append( beanClass.getSimpleName() );
		sb.append( ", constraintCount=" ).append( getMetaConstraints().size() );
		sb.append( ", cascadedMemberCount=" ).append( cascadedMembers.size() );
		sb.append( ", defaultGroupSequence=" ).append( getDefaultGroupSequence( null ) );
		sb.append( '}' );
		return sb.toString();
	}

	public static class BeanMetaDataBuilder<T> {

		private final ConstraintHelper constraintHelper;

		private final ValidationOrderGenerator validationOrderGenerator;

		private final Class<T> beanClass;

		private final Set<BuilderDelegate> builders = newHashSet();

		private ConfigurationSource sequenceSource;

		private ConfigurationSource providerSource;

		private List<Class<?>> defaultGroupSequence;

		private DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider;

		private BeanMetaDataBuilder(ConstraintHelper constraintHelper, ValidationOrderGenerator validationOrderGenerator, Class<T> beanClass) {
			this.beanClass = beanClass;
			this.constraintHelper = constraintHelper;
			this.validationOrderGenerator = validationOrderGenerator;
		}

		public static <T> BeanMetaDataBuilder<T> getInstance(ConstraintHelper constraintHelper, ValidationOrderGenerator validationOrderGenerator, Class<T> beanClass) {
			return new BeanMetaDataBuilder<T>( constraintHelper, validationOrderGenerator, beanClass );
		}

		public void add(BeanConfiguration<? super T> configuration) {
			if ( configuration.getBeanClass().equals( beanClass ) ) {
				if ( configuration.getDefaultGroupSequence() != null
						&& ( sequenceSource == null || configuration.getSource()
						.getPriority() >= sequenceSource.getPriority() ) ) {

					sequenceSource = configuration.getSource();
					defaultGroupSequence = configuration.getDefaultGroupSequence();
				}

				if ( configuration.getDefaultGroupSequenceProvider() != null
						&& ( providerSource == null || configuration.getSource()
						.getPriority() >= providerSource.getPriority() ) ) {

					providerSource = configuration.getSource();
					defaultGroupSequenceProvider = configuration.getDefaultGroupSequenceProvider();
				}
			}

			for ( ConstrainedElement oneConstrainedElement : configuration.getConstrainedElements() ) {
				addMetaDataToBuilder( oneConstrainedElement, builders );
			}
		}

		private void addMetaDataToBuilder(ConstrainedElement constrainableElement, Set<BuilderDelegate> builders) {

			for ( BuilderDelegate oneBuilder : builders ) {
				boolean foundBuilder = oneBuilder.add( constrainableElement );

				if ( foundBuilder ) {
					return;
				}
			}

			builders.add(
					new BuilderDelegate(
							constrainableElement,
							constraintHelper
					)
			);
		}

		public BeanMetaDataImpl<T> build() {

			Set<ConstraintMetaData> aggregatedElements = newHashSet();

			for ( BuilderDelegate oneBuilder : builders ) {
				aggregatedElements.addAll(
						oneBuilder.build(
								( defaultGroupSequence != null && defaultGroupSequence.size() > 1 ) || defaultGroupSequenceProvider != null,
								defaultGroupSequence
						)
				);
			}

			return new BeanMetaDataImpl<T>(
					beanClass,
					defaultGroupSequence,
					defaultGroupSequenceProvider,
					aggregatedElements,
					validationOrderGenerator
			);
		}
	}

	private static class BuilderDelegate {
		private final ConstraintHelper constraintHelper;
		private MetaDataBuilder propertyBuilder;
		private MethodMetaData.Builder methodBuilder;

		public BuilderDelegate(ConstrainedElement constrainedElement, ConstraintHelper constraintHelper) {
			this.constraintHelper = constraintHelper;

			switch ( constrainedElement.getKind() ) {
				case FIELD:
					ConstrainedField constrainedField = (ConstrainedField) constrainedElement;
					propertyBuilder = new PropertyMetaData.Builder(
							constrainedField,
							constraintHelper
					);
					break;
				case METHOD:
					ConstrainedMethod constrainedMethod = (ConstrainedMethod) constrainedElement;
					methodBuilder = new MethodMetaData.Builder(
							constrainedMethod,
							constraintHelper
					);

					if ( constrainedMethod.isGetterMethod() ) {
						propertyBuilder = new PropertyMetaData.Builder(
								constrainedMethod,
								constraintHelper
						);
					}
					break;
				case TYPE:
					ConstrainedType constrainedType = (ConstrainedType) constrainedElement;
					propertyBuilder = new PropertyMetaData.Builder(
							constrainedType,
							constraintHelper
					);
					break;
			}
		}

		public boolean add(ConstrainedElement constrainedElement) {
			boolean added = false;

			if ( methodBuilder != null && methodBuilder.accepts( constrainedElement ) ) {
				methodBuilder.add( constrainedElement );
				added = true;
			}

			if ( propertyBuilder != null && propertyBuilder.accepts( constrainedElement ) ) {
				propertyBuilder.add( constrainedElement );

				if ( added == false && constrainedElement.getKind() == ConstrainedElementKind.METHOD && methodBuilder == null ) {
					ConstrainedMethod constrainedMethod = (ConstrainedMethod) constrainedElement;
					methodBuilder = new MethodMetaData.Builder(
							constrainedMethod,
							constraintHelper
					);
				}

				added = true;
			}

			return added;
		}

		public Set<ConstraintMetaData> build(boolean defaultGroupSequenceRedefined, List<Class<?>> defaultGroupSequence) {

			Set<ConstraintMetaData> theValue = newHashSet();

			if ( propertyBuilder != null ) {
				theValue.add( propertyBuilder.build() );
			}

			if ( methodBuilder != null ) {
				theValue.add( methodBuilder.build() );
			}

			return theValue;
		}

	}
}
