/*
 * 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.jpa.internal.metamodel;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.IdentifiableType;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.Type;

/**
 * Defines commonality for the JPA {@link IdentifiableType} types.  JPA defines
 * identifiable types as entities or mapped-superclasses.  Basically things to which an
 * identifier can be attached.
 * <p/>
 * NOTE : Currently we only really have support for direct entities in the Hibernate metamodel
 * as the information for them is consumed into the closest actual entity subclass(es) in the
 * internal Hibernate mapping-metamodel.
 *
 * @author Steve Ebersole
 */
public abstract class AbstractIdentifiableType<X>
		extends AbstractManagedType<X>
		implements IdentifiableType<X>, Serializable {

	private final boolean hasIdentifierProperty;
	private final boolean hasIdClass;
	private SingularAttributeImpl<X, ?> id;
	private Set<SingularAttribute<? super X,?>> idClassAttributes;

	private final boolean isVersioned;
	private SingularAttributeImpl<X, ?> version;

	public AbstractIdentifiableType(
			Class<X> javaType,
			String typeName,
			AbstractIdentifiableType<? super X> superType,
			boolean hasIdClass,
			boolean hasIdentifierProperty,
			boolean versioned) {
		super( javaType, typeName, superType );
		this.hasIdClass = hasIdClass;
		this.hasIdentifierProperty = hasIdentifierProperty;
		this.isVersioned = versioned;
	}

	public boolean hasIdClass() {
		return hasIdClass;
	}

	@Override
	public boolean hasSingleIdAttribute() {
		return !hasIdClass() && hasIdentifierProperty;
	}

	@Override
	@SuppressWarnings("unchecked")
	public AbstractIdentifiableType<? super X> getSupertype() {
		// overridden simply to perform the cast
		return (AbstractIdentifiableType<? super X>) super.getSupertype();
	}

	@Override
	@SuppressWarnings({ "unchecked" })
	public <Y> SingularAttribute<? super X, Y> getId(Class<Y> javaType) {
		ensureNoIdClass();
		SingularAttributeImpl id = locateIdAttribute();
		if ( id != null ) {
			checkType( id, javaType );
		}
		return ( SingularAttribute<? super X, Y> ) id;
	}

	private void ensureNoIdClass() {
		if ( hasIdClass() ) {
			throw new IllegalArgumentException(
					"Illegal call to IdentifiableType#getId for class [" + getTypeName() + "] defined with @IdClass"
			);
		}
	}

	private SingularAttributeImpl locateIdAttribute() {
		if ( id != null ) {
			return id;
		}
		else {
			if ( getSupertype() != null ) {
				SingularAttributeImpl id = getSupertype().internalGetId();
				if ( id != null ) {
					return id;
				}
			}
		}

		return null;
	}

	protected SingularAttributeImpl internalGetId() {
		if ( id != null ) {
			return id;
		}
		else {
			if ( getSupertype() != null ) {
				return getSupertype().internalGetId();
			}
		}

		return null;
	}

	@SuppressWarnings("unchecked")
	private void checkType(SingularAttributeImpl attribute, Class javaType) {
		if ( ! javaType.isAssignableFrom( attribute.getType().getJavaType() ) ) {
			throw new IllegalArgumentException(
					String.format(
							"Attribute [%s#%s : %s] not castable to requested type [%s]",
							getTypeName(),
							attribute.getName(),
							attribute.getType().getJavaType().getName(),
							javaType.getName()
					)
			);
		}
	}

	@Override
	@SuppressWarnings({ "unchecked" })
	public <Y> SingularAttribute<X, Y> getDeclaredId(Class<Y> javaType) {
		ensureNoIdClass();
		if ( id == null ) {
			throw new IllegalArgumentException( "The id attribute is not declared on this type [" + getTypeName() + "]" );
		}
		checkType( id, javaType );
		return (SingularAttribute<X, Y>) id;
	}

	@Override
	@SuppressWarnings("unchecked")
	public Type<?> getIdType() {
		final SingularAttributeImpl id = locateIdAttribute();
		if ( id != null ) {
			return id.getType();
		}

		Set<SingularAttribute<? super X, ?>> idClassAttributes = getIdClassAttributesSafely();
		if ( idClassAttributes != null ) {
			if ( idClassAttributes.size() == 1 ) {
				return idClassAttributes.iterator().next().getType();
			}
		}

		return null;
	}

	/**
	 * A form of {@link #getIdClassAttributes} which prefers to return {@code null} rather than throw exceptions
	 *
	 * @return IdClass attributes or {@code null}
	 */
	public Set<SingularAttribute<? super X, ?>> getIdClassAttributesSafely() {
		if ( !hasIdClass() ) {
			return null;
		}
		final Set<SingularAttribute<? super X, ?>> attributes = new HashSet<SingularAttribute<? super X, ?>>();
		internalCollectIdClassAttributes( attributes );

		if ( attributes.isEmpty() ) {
			return null;
		}

		return attributes;
	}

	@Override
	public Set<SingularAttribute<? super X, ?>> getIdClassAttributes() {
		if ( !hasIdClass() ) {
			throw new IllegalArgumentException( "This class [" + getJavaType() + "] does not define an IdClass" );
		}

		final Set<SingularAttribute<? super X, ?>> attributes = new HashSet<SingularAttribute<? super X, ?>>();
		internalCollectIdClassAttributes( attributes );

		if ( attributes.isEmpty() ) {
			throw new IllegalArgumentException( "Unable to locate IdClass attributes [" + getJavaType() + "]" );
		}

		return attributes;
	}

	@SuppressWarnings("unchecked")
	private void internalCollectIdClassAttributes(Set attributes) {
		if ( idClassAttributes != null ) {
			attributes.addAll( idClassAttributes );
		}
		else if ( getSupertype() != null ) {
			getSupertype().internalCollectIdClassAttributes( attributes );
		}
	}

	@Override
	public boolean hasVersionAttribute() {
		return isVersioned;
	}

	public boolean hasDeclaredVersionAttribute() {
		return isVersioned && version != null;
	}

	@Override
	@SuppressWarnings({ "unchecked" })
	public <Y> SingularAttribute<? super X, Y> getVersion(Class<Y> javaType) {
		// todo : is return null allowed?
		if ( ! hasVersionAttribute() ) {
			return null;
		}

		SingularAttributeImpl version = locateVersionAttribute();
		if ( version != null ) {
			checkType( version, javaType );
		}
		return ( SingularAttribute<? super X, Y> ) version;
	}

	private SingularAttributeImpl locateVersionAttribute() {
		if ( version != null ) {
			return version;
		}
		else {
			if ( getSupertype() != null ) {
				SingularAttributeImpl version = getSupertype().internalGetVersion();
				if ( version != null ) {
					return version;
				}
			}
		}

		return null;
	}

	protected SingularAttributeImpl internalGetVersion() {
		if ( version != null ) {
			return version;
		}
		else {
			if ( getSupertype() != null ) {
				return getSupertype().internalGetVersion();
			}
		}

		return null;
	}

	@Override
	@SuppressWarnings({ "unchecked" })
	public <Y> SingularAttribute<X, Y> getDeclaredVersion(Class<Y> javaType) {
		checkDeclaredVersion();
		checkType( version, javaType );
		return ( SingularAttribute<X, Y> ) version;
	}

	private void checkDeclaredVersion() {
		if ( version == null || ( getSupertype() != null && getSupertype().hasVersionAttribute() )) {
			throw new IllegalArgumentException(
					"The version attribute is not declared by this type [" + getJavaType() + "]"
			);
		}
	}


	/**
	 * For used to retrieve the declared version when populating the static metamodel.
	 *
	 * @return The declared
	 */
	public SingularAttribute<X, ?> getDeclaredVersion() {
		checkDeclaredVersion();
		return version;
	}

	public Builder<X> getBuilder() {
		final AbstractManagedType.Builder<X> managedBuilder = super.getBuilder();
		return new Builder<X>() {
			public void applyIdAttribute(SingularAttributeImpl<X, ?> idAttribute) {
				AbstractIdentifiableType.this.id = idAttribute;
				managedBuilder.addAttribute( idAttribute );
			}

			public void applyIdClassAttributes(Set<SingularAttribute<? super X,?>> idClassAttributes) {
				for ( SingularAttribute<? super X,?> idClassAttribute : idClassAttributes ) {
					if ( AbstractIdentifiableType.this == idClassAttribute.getDeclaringType() ) {
						@SuppressWarnings({ "unchecked" })
						SingularAttribute<X,?> declaredAttribute = ( SingularAttribute<X,?> ) idClassAttribute;
						addAttribute( declaredAttribute );
					}
				}
				AbstractIdentifiableType.this.idClassAttributes = idClassAttributes;
			}

			public void applyVersionAttribute(SingularAttributeImpl<X, ?> versionAttribute) {
				AbstractIdentifiableType.this.version = versionAttribute;
				managedBuilder.addAttribute( versionAttribute );
			}

			public void addAttribute(Attribute<X, ?> attribute) {
				managedBuilder.addAttribute( attribute );
			}
		};
	}

	public static interface Builder<X> extends AbstractManagedType.Builder<X> {
		public void applyIdAttribute(SingularAttributeImpl<X,?> idAttribute);
		public void applyIdClassAttributes(Set<SingularAttribute<? super X,?>> idClassAttributes);
		public void applyVersionAttribute(SingularAttributeImpl<X,?> versionAttribute);
	}
}
