/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.cfg.annotations;

import java.lang.annotation.Annotation;
import java.util.Map;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import javax.persistence.Lob;

import org.hibernate.AnnotationException;
import org.hibernate.HibernateException;
import org.hibernate.annotations.Generated;
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.NaturalId;
import org.hibernate.annotations.OptimisticLock;
import org.hibernate.annotations.ValueGenerationType;
import org.hibernate.annotations.common.AssertionFailure;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.cfg.AccessType;
import org.hibernate.cfg.AnnotationBinder;
import org.hibernate.cfg.BinderHelper;
import org.hibernate.cfg.Ejb3Column;
import org.hibernate.cfg.InheritanceState;
import org.hibernate.cfg.PropertyHolder;
import org.hibernate.cfg.PropertyPreloadedData;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.mapping.Collection;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.KeyValue;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.RootClass;
import org.hibernate.mapping.SimpleValue;
import org.hibernate.mapping.ToOne;
import org.hibernate.mapping.Value;
import org.hibernate.tuple.AnnotationValueGeneration;
import org.hibernate.tuple.GenerationTiming;
import org.hibernate.tuple.ValueGeneration;
import org.hibernate.tuple.ValueGenerator;

import org.jboss.logging.Logger;

/**
 * @author Emmanuel Bernard
 */
public class PropertyBinder {
    private static final CoreMessageLogger LOG = Logger.getMessageLogger(CoreMessageLogger.class, PropertyBinder.class.getName());

	private MetadataBuildingContext buildingContext;

	private String name;
	private String returnedClassName;
	private boolean lazy;
	private String lazyGroup;
	private AccessType accessType;
	private Ejb3Column[] columns;
	private PropertyHolder holder;
	private Value value;
	private boolean insertable = true;
	private boolean updatable = true;
	private String cascade;
	private SimpleValueBinder simpleValueBinder;
	private XClass declaringClass;
	private boolean declaringClassSet;
	private boolean embedded;
	private EntityBinder entityBinder;
	private boolean isXToMany;
	private String referencedEntityName;

	public void setReferencedEntityName(String referencedEntityName) {
		this.referencedEntityName = referencedEntityName;
	}

	public void setEmbedded(boolean embedded) {
		this.embedded = embedded;
	}

	public void setEntityBinder(EntityBinder entityBinder) {
		this.entityBinder = entityBinder;
	}

	/*
			 * property can be null
			 * prefer propertyName to property.getName() since some are overloaded
			 */
	private XProperty property;
	private XClass returnedClass;
	private boolean isId;
	private Map<XClass, InheritanceState> inheritanceStatePerClass;
	private Property mappingProperty;

	public void setInsertable(boolean insertable) {
		this.insertable = insertable;
	}

	public void setUpdatable(boolean updatable) {
		this.updatable = updatable;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setReturnedClassName(String returnedClassName) {
		this.returnedClassName = returnedClassName;
	}

	public void setLazy(boolean lazy) {
		this.lazy = lazy;
	}

	public void setLazyGroup(String lazyGroup) {
		this.lazyGroup = lazyGroup;
	}

	public void setAccessType(AccessType accessType) {
		this.accessType = accessType;
	}

	public void setColumns(Ejb3Column[] columns) {
		insertable = columns[0].isInsertable();
		updatable = columns[0].isUpdatable();
		//consistency is checked later when we know the property name
		this.columns = columns;
	}

	public void setHolder(PropertyHolder holder) {
		this.holder = holder;
	}

	public void setValue(Value value) {
		this.value = value;
	}

	public void setCascade(String cascadeStrategy) {
		this.cascade = cascadeStrategy;
	}

	public void setBuildingContext(MetadataBuildingContext buildingContext) {
		this.buildingContext = buildingContext;
	}

	public void setDeclaringClass(XClass declaringClass) {
		this.declaringClass = declaringClass;
		this.declaringClassSet = true;
	}

	private void validateBind() {
		if ( property.isAnnotationPresent( Immutable.class ) ) {
			throw new AnnotationException(
					"@Immutable on property not allowed. " +
							"Only allowed on entity level or on a collection."
			);
		}
		if ( !declaringClassSet ) {
			throw new AssertionFailure( "declaringClass has not been set before a bind" );
		}
	}

	private void validateMake() {
		//TODO check necessary params for a make
	}

	private Property makePropertyAndValue() {
		validateBind();

		LOG.debugf( "MetadataSourceProcessor property %s with lazy=%s", name, lazy );
		final String containerClassName = holder.getClassName();
		holder.startingProperty( property );

		simpleValueBinder = new SimpleValueBinder();
		simpleValueBinder.setBuildingContext( buildingContext );
		simpleValueBinder.setPropertyName( name );
		simpleValueBinder.setReturnedClassName( returnedClassName );
		simpleValueBinder.setColumns( columns );
		simpleValueBinder.setPersistentClassName( containerClassName );
		simpleValueBinder.setType(
				property,
				returnedClass,
				containerClassName,
				holder.resolveAttributeConverterDescriptor( property )
		);
		simpleValueBinder.setReferencedEntityName( referencedEntityName );
		simpleValueBinder.setAccessType( accessType );
		SimpleValue propertyValue = simpleValueBinder.make();
		setValue( propertyValue );
		return makeProperty();
	}

	//used when value is provided
	public Property makePropertyAndBind() {
		return bind( makeProperty() );
	}

	//used to build everything from scratch
	public Property makePropertyValueAndBind() {
		return bind( makePropertyAndValue() );
	}

	public void setXToMany(boolean xToMany) {
		this.isXToMany = xToMany;
	}

	private Property bind(Property prop) {
		if (isId) {
			final RootClass rootClass = ( RootClass ) holder.getPersistentClass();
			//if an xToMany, it as to be wrapped today.
			//FIXME this pose a problem as the PK is the class instead of the associated class which is not really compliant with the spec
			if ( isXToMany || entityBinder.wrapIdsInEmbeddedComponents() ) {
				Component identifier = (Component) rootClass.getIdentifier();
				if (identifier == null) {
					identifier = AnnotationBinder.createComponent(
							holder,
							new PropertyPreloadedData(null, null, null),
							true,
							false,
							buildingContext
					);
					rootClass.setIdentifier( identifier );
					identifier.setNullValue( "undefined" );
					rootClass.setEmbeddedIdentifier( true );
					rootClass.setIdentifierMapper( identifier );
				}
				//FIXME is it good enough?
				identifier.addProperty( prop );
			}
			else {
				rootClass.setIdentifier( ( KeyValue ) getValue() );
				if (embedded) {
					rootClass.setEmbeddedIdentifier( true );
				}
				else {
					rootClass.setIdentifierProperty( prop );
					final org.hibernate.mapping.MappedSuperclass superclass = BinderHelper.getMappedSuperclassOrNull(
							declaringClass,
							inheritanceStatePerClass,
							buildingContext
					);
					if (superclass != null) {
						superclass.setDeclaredIdentifierProperty(prop);
					}
					else {
						//we know the property is on the actual entity
						rootClass.setDeclaredIdentifierProperty( prop );
					}
				}
			}
		}
		else {
			holder.addProperty( prop, columns, declaringClass );
		}
		return prop;
	}

	//used when the value is provided and the binding is done elsewhere
	public Property makeProperty() {
		validateMake();
		LOG.debugf( "Building property %s", name );
		Property prop = new Property();
		prop.setName( name );
		prop.setValue( value );
		prop.setLazy( lazy );
		prop.setLazyGroup( lazyGroup );
		prop.setCascade( cascade );
		prop.setPropertyAccessorName( accessType.getType() );

		if ( property != null ) {
			prop.setValueGenerationStrategy( determineValueGenerationStrategy( property ) );
		}

		NaturalId naturalId = property != null ? property.getAnnotation( NaturalId.class ) : null;
		if ( naturalId != null ) {
			if ( ! entityBinder.isRootEntity() ) {
				throw new AnnotationException( "@NaturalId only valid on root entity (or its @MappedSuperclasses)" );
			}
			if ( ! naturalId.mutable() ) {
				updatable = false;
			}
			prop.setNaturalIdentifier( true );
		}

		// HHH-4635 -- needed for dialect-specific property ordering
		Lob lob = property != null ? property.getAnnotation( Lob.class ) : null;
		prop.setLob( lob != null );

		prop.setInsertable( insertable );
		prop.setUpdateable( updatable );

		// this is already handled for collections in CollectionBinder...
		if ( Collection.class.isInstance( value ) ) {
			prop.setOptimisticLocked( ( (Collection) value ).isOptimisticLocked() );
		}
		else {
			final OptimisticLock lockAnn = property != null
					? property.getAnnotation( OptimisticLock.class )
					: null;
			if ( lockAnn != null ) {
				//TODO this should go to the core as a mapping validation checking
				if ( lockAnn.excluded() && (
						property.isAnnotationPresent( javax.persistence.Version.class )
								|| property.isAnnotationPresent( Id.class )
								|| property.isAnnotationPresent( EmbeddedId.class ) ) ) {
					throw new AnnotationException(
							"@OptimisticLock.exclude=true incompatible with @Id, @EmbeddedId and @Version: "
									+ StringHelper.qualify( holder.getPath(), name )
					);
				}
			}
			final boolean isOwnedValue = !isToOneValue( value ) || insertable; // && updatable as well???
			final boolean includeInOptimisticLockChecks = ( lockAnn != null )
					? ! lockAnn.excluded()
					: isOwnedValue;
			prop.setOptimisticLocked( includeInOptimisticLockChecks );
		}

		LOG.tracev( "Cascading {0} with {1}", name, cascade );
		this.mappingProperty = prop;
		return prop;
	}

	private ValueGeneration determineValueGenerationStrategy(XProperty property) {
		ValueGeneration valueGeneration = getValueGenerationFromAnnotations( property );

		if ( valueGeneration == null ) {
			return NoValueGeneration.INSTANCE;
		}

		final GenerationTiming when = valueGeneration.getGenerationTiming();

		if ( valueGeneration.getValueGenerator() == null ) {
			insertable = false;
			if ( when == GenerationTiming.ALWAYS ) {
				updatable = false;
			}
		}

		return valueGeneration;
	}

	/**
	 * Returns the value generation strategy for the given property, if any.
	 */
	private ValueGeneration getValueGenerationFromAnnotations(XProperty property) {
		AnnotationValueGeneration<?> valueGeneration = null;

		for ( Annotation annotation : property.getAnnotations() ) {
			AnnotationValueGeneration<?> candidate = getValueGenerationFromAnnotation( property, annotation );

			if ( candidate != null ) {
				if ( valueGeneration != null ) {
					throw new AnnotationException(
							"Only one generator annotation is allowed:" + StringHelper.qualify(
									holder.getPath(),
									name
							)
					);
				}
				else {
					valueGeneration = candidate;
				}
			}
		}

		return valueGeneration;
	}

	/**
	 * In case the given annotation is a value generator annotation, the corresponding value generation strategy to be
	 * applied to the given property is returned, {@code null} otherwise.
	 */
	private <A extends Annotation> AnnotationValueGeneration<A> getValueGenerationFromAnnotation(
			XProperty property,
			A annotation) {
		ValueGenerationType generatorAnnotation = annotation.annotationType()
				.getAnnotation( ValueGenerationType.class );

		if ( generatorAnnotation == null ) {
			return null;
		}

		Class<? extends AnnotationValueGeneration<?>> generationType = generatorAnnotation.generatedBy();
		AnnotationValueGeneration<A> valueGeneration = instantiateAndInitializeValueGeneration(
				annotation, generationType, property
		);

		if ( annotation.annotationType() == Generated.class &&
				property.isAnnotationPresent( javax.persistence.Version.class ) &&
				valueGeneration.getGenerationTiming() == GenerationTiming.INSERT ) {

			throw new AnnotationException(
					"@Generated(INSERT) on a @Version property not allowed, use ALWAYS (or NEVER): "
							+ StringHelper.qualify( holder.getPath(), name )
			);
		}

		return valueGeneration;
	}

	/**
	 * Instantiates the given generator annotation type, initializing it with the given instance of the corresponding
	 * generator annotation and the property's type.
	 */
	private <A extends Annotation> AnnotationValueGeneration<A> instantiateAndInitializeValueGeneration(
			A annotation,
			Class<? extends AnnotationValueGeneration<?>> generationType,
			XProperty property) {

		try {
			// This will cause a CCE in case the generation type doesn't match the annotation type; As this would be a
			// programming error of the generation type developer and thus should show up during testing, we don't
			// check this explicitly; If required, this could be done e.g. using ClassMate
			@SuppressWarnings( "unchecked" )
			AnnotationValueGeneration<A> valueGeneration = (AnnotationValueGeneration<A>) generationType.newInstance();
			valueGeneration.initialize(
					annotation,
					buildingContext.getBuildingOptions().getReflectionManager().toClass( property.getType() )
			);

			return valueGeneration;
		}
		catch (HibernateException e) {
			throw e;
		}
		catch (Exception e) {
			throw new AnnotationException(
					"Exception occurred during processing of generator annotation:" + StringHelper.qualify(
							holder.getPath(),
							name
					), e
			);
		}
	}

	private static class NoValueGeneration implements ValueGeneration {
		/**
		 * Singleton access
		 */
		public static final NoValueGeneration INSTANCE = new NoValueGeneration();

		@Override
		public GenerationTiming getGenerationTiming() {
			return GenerationTiming.NEVER;
		}

		@Override
		public ValueGenerator<?> getValueGenerator() {
			return null;
		}

		@Override
		public boolean referenceColumnInSql() {
			return true;
		}

		@Override
		public String getDatabaseGeneratedReferencedColumnValue() {
			return null;
		}
	}

	private boolean isToOneValue(Value value) {
		return ToOne.class.isInstance( value );
	}

	public void setProperty(XProperty property) {
		this.property = property;
	}

	public void setReturnedClass(XClass returnedClass) {
		this.returnedClass = returnedClass;
	}

	public SimpleValueBinder getSimpleValueBinder() {
		return simpleValueBinder;
	}

	public Value getValue() {
		return value;
	}

	public void setId(boolean id) {
		this.isId = id;
	}

	public void setInheritanceStatePerClass(Map<XClass, InheritanceState> inheritanceStatePerClass) {
		this.inheritanceStatePerClass = inheritanceStatePerClass;
	}
}
