/*
 * 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;

import java.util.Map;

import org.hibernate.AnnotationException;
import org.hibernate.AssertionFailure;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.ColumnTransformer;
import org.hibernate.annotations.ColumnTransformers;
import org.hibernate.annotations.Index;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.ImplicitBasicColumnNameSource;
import org.hibernate.boot.model.naming.ImplicitNamingStrategy;
import org.hibernate.boot.model.naming.ObjectNameNormalizer;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.boot.model.relational.Database;
import org.hibernate.boot.model.source.spi.AttributePath;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.cfg.annotations.Nullability;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Formula;
import org.hibernate.mapping.Join;
import org.hibernate.mapping.SimpleValue;
import org.hibernate.mapping.Table;

import org.jboss.logging.Logger;

/**
 * Wrap state of an EJB3 @Column annotation
 * and build the Hibernate column mapping element
 *
 * @author Emmanuel Bernard
 */
public class Ejb3Column {

    private static final CoreMessageLogger LOG = Logger.getMessageLogger(CoreMessageLogger.class, Ejb3Column.class.getName());

	private MetadataBuildingContext context;

	private Column mappingColumn;
	private boolean insertable = true;
	private boolean updatable = true;
	private String explicitTableName;
	protected Map<String, Join> joins;
	protected PropertyHolder propertyHolder;
	private boolean isImplicit;
	public static final int DEFAULT_COLUMN_LENGTH = 255;
	public String sqlType;
	private int length = DEFAULT_COLUMN_LENGTH;
	private int precision;
	private int scale;
	private String logicalColumnName;
	private String propertyName;
	private boolean unique;
	private boolean nullable = true;
	private String formulaString;
	private Formula formula;
	private Table table;
	private String readExpression;
	private String writeExpression;

	private String defaultValue;

	public void setTable(Table table) {
		this.table = table;
	}

	public String getLogicalColumnName() {
		return logicalColumnName;
	}

	public String getSqlType() {
		return sqlType;
	}

	public int getLength() {
		return length;
	}

	public int getPrecision() {
		return precision;
	}

	public int getScale() {
		return scale;
	}

	public boolean isUnique() {
		return unique;
	}

	public boolean isFormula() {
		return StringHelper.isNotEmpty( formulaString );
	}

	@SuppressWarnings("UnusedDeclaration")
	public String getFormulaString() {
		return formulaString;
	}

	@SuppressWarnings("UnusedDeclaration")
	public String getExplicitTableName() {
		return explicitTableName;
	}

	public void setExplicitTableName(String explicitTableName) {
		if ( "``".equals( explicitTableName ) ) {
			this.explicitTableName = "";
		}
		else {
			this.explicitTableName = explicitTableName;
		}
	}

	public void setFormula(String formula) {
		this.formulaString = formula;
	}

	public boolean isImplicit() {
		return isImplicit;
	}

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

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

	protected MetadataBuildingContext getBuildingContext() {
		return context;
	}

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

	public void setImplicit(boolean implicit) {
		isImplicit = implicit;
	}

	public void setSqlType(String sqlType) {
		this.sqlType = sqlType;
	}

	public void setLength(int length) {
		this.length = length;
	}

	public void setPrecision(int precision) {
		this.precision = precision;
	}

	public void setScale(int scale) {
		this.scale = scale;
	}

	public void setLogicalColumnName(String logicalColumnName) {
		this.logicalColumnName = logicalColumnName;
	}

	public void setPropertyName(String propertyName) {
		this.propertyName = propertyName;
	}

	public String getPropertyName() {
		return propertyName;
	}

	public void setUnique(boolean unique) {
		this.unique = unique;
	}

	public boolean isNullable() {
		return isFormula() ? true : mappingColumn.isNullable();
	}

	public String getDefaultValue() {
		return defaultValue;
	}

	public void setDefaultValue(String defaultValue) {
		this.defaultValue = defaultValue;
	}

	public Ejb3Column() {
	}

	public void bind() {
		if ( StringHelper.isNotEmpty( formulaString ) ) {
			LOG.debugf( "Binding formula %s", formulaString );
			formula = new Formula();
			formula.setFormula( formulaString );
		}
		else {
			initMappingColumn(
					logicalColumnName, propertyName, length, precision, scale, nullable, sqlType, unique, true
			);
			if ( defaultValue != null ) {
				mappingColumn.setDefaultValue( defaultValue );
			}
			if ( LOG.isDebugEnabled() ) {
				LOG.debugf( "Binding column: %s", toString() );
			}
		}
	}

	protected void initMappingColumn(
			String columnName,
			String propertyName,
			int length,
			int precision,
			int scale,
			boolean nullable,
			String sqlType,
			boolean unique,
			boolean applyNamingStrategy) {
		if ( StringHelper.isNotEmpty( formulaString ) ) {
			this.formula = new Formula();
			this.formula.setFormula( formulaString );
		}
		else {
			this.mappingColumn = new Column();
			redefineColumnName( columnName, propertyName, applyNamingStrategy );
			this.mappingColumn.setLength( length );
			if ( precision > 0 ) {  //revelent precision
				this.mappingColumn.setPrecision( precision );
				this.mappingColumn.setScale( scale );
			}
			this.mappingColumn.setNullable( nullable );
			this.mappingColumn.setSqlType( sqlType );
			this.mappingColumn.setUnique( unique );

			if(writeExpression != null && !writeExpression.matches("[^?]*\\?[^?]*")) {
				throw new AnnotationException(
						"@WriteExpression must contain exactly one value placeholder ('?') character: property ["
								+ propertyName + "] and column [" + logicalColumnName + "]"
				);
			}
			if ( readExpression != null) {
				this.mappingColumn.setCustomRead( readExpression );
			}
			if ( writeExpression != null) {
				this.mappingColumn.setCustomWrite( writeExpression );
			}
		}
	}

	public boolean isNameDeferred() {
		return mappingColumn == null || StringHelper.isEmpty( mappingColumn.getName() );
	}

	public void redefineColumnName(String columnName, String propertyName, boolean applyNamingStrategy) {
		final ObjectNameNormalizer normalizer = context.getObjectNameNormalizer();
		final Database database = context.getMetadataCollector().getDatabase();
		final ImplicitNamingStrategy implicitNamingStrategy = context.getBuildingOptions().getImplicitNamingStrategy();
		final PhysicalNamingStrategy physicalNamingStrategy = context.getBuildingOptions().getPhysicalNamingStrategy();

		if ( applyNamingStrategy ) {
			if ( StringHelper.isEmpty( columnName ) ) {
				if ( propertyName != null ) {
					final AttributePath attributePath = AttributePath.parse( propertyName );

					Identifier implicitName = normalizer.normalizeIdentifierQuoting(
							implicitNamingStrategy.determineBasicColumnName(
									new ImplicitBasicColumnNameSource() {
										@Override
										public AttributePath getAttributePath() {
											return attributePath;
										}

										@Override
										public boolean isCollectionElement() {
											// if the propertyHolder is a collection, assume the
											// @Column refers to the element column
											return !propertyHolder.isComponent()
													&& !propertyHolder.isEntity();
										}

										@Override
										public MetadataBuildingContext getBuildingContext() {
											return context;
										}
									}
							)
					);

					// HHH-6005 magic
					if ( implicitName.getText().contains( "_collection&&element_" ) ) {
						implicitName = Identifier.toIdentifier( implicitName.getText().replace( "_collection&&element_", "_" ),
								implicitName.isQuoted() );
					}

					final Identifier physicalName = physicalNamingStrategy.toPhysicalColumnName( implicitName, database.getJdbcEnvironment() );
					mappingColumn.setName( physicalName.render( database.getDialect() ) );
				}
				//Do nothing otherwise
			}
			else {
				final Identifier explicitName = database.toIdentifier( columnName );
				final Identifier physicalName =  physicalNamingStrategy.toPhysicalColumnName( explicitName, database.getJdbcEnvironment() );
				mappingColumn.setName( physicalName.render( database.getDialect() ) );
			}
		}
		else {
			if ( StringHelper.isNotEmpty( columnName ) ) {
				mappingColumn.setName( normalizer.toDatabaseIdentifierText( columnName ) );
			}
		}
	}

	public String getName() {
		return mappingColumn.getName();
	}

	public Column getMappingColumn() {
		return mappingColumn;
	}

	public boolean isInsertable() {
		return insertable;
	}

	public boolean isUpdatable() {
		return updatable;
	}

	public void setNullable(boolean nullable) {
		if ( mappingColumn != null ) {
			mappingColumn.setNullable( nullable );
		}
		else {
			this.nullable = nullable;
		}
	}

	public void setJoins(Map<String, Join> joins) {
		this.joins = joins;
	}

	public PropertyHolder getPropertyHolder() {
		return propertyHolder;
	}

	public void setPropertyHolder(PropertyHolder propertyHolder) {
		this.propertyHolder = propertyHolder;
	}

	protected void setMappingColumn(Column mappingColumn) {
		this.mappingColumn = mappingColumn;
	}

	public void linkWithValue(SimpleValue value) {
		if ( formula != null ) {
			value.addFormula( formula );
		}
		else {
			getMappingColumn().setValue( value );
			value.addColumn( getMappingColumn(), insertable, updatable );
			value.getTable().addColumn( getMappingColumn() );
			addColumnBinding( value );
			table = value.getTable();
		}
	}

	protected void addColumnBinding(SimpleValue value) {
		final String logicalColumnName;
		if ( StringHelper.isNotEmpty( this.logicalColumnName ) ) {
			logicalColumnName = this.logicalColumnName;
		}
		else {
			final ObjectNameNormalizer normalizer = context.getObjectNameNormalizer();
			final Database database = context.getMetadataCollector().getDatabase();
			final ImplicitNamingStrategy implicitNamingStrategy = context.getBuildingOptions()
					.getImplicitNamingStrategy();

			final Identifier implicitName = normalizer.normalizeIdentifierQuoting(
					implicitNamingStrategy.determineBasicColumnName(
							new ImplicitBasicColumnNameSource() {
								@Override
								public AttributePath getAttributePath() {
									return AttributePath.parse( propertyName );
								}

								@Override
								public boolean isCollectionElement() {
									return false;
								}

								@Override
								public MetadataBuildingContext getBuildingContext() {
									return context;
								}
							}
					)
			);
			logicalColumnName = implicitName.render( database.getDialect() );
		}
		context.getMetadataCollector().addColumnNameBinding( value.getTable(), logicalColumnName, getMappingColumn() );
	}

	/**
	 * Find appropriate table of the column.
	 * It can come from a secondary table or from the main table of the persistent class
	 *
	 * @return appropriate table
	 * @throws AnnotationException missing secondary table
	 */
	public Table getTable() {
		if ( table != null ){
			return table;
		}

		if ( isSecondary() ) {
			return getJoin().getTable();
		}
		else {
			return propertyHolder.getTable();
		}
	}

	public boolean isSecondary() {
		if ( propertyHolder == null ) {
			throw new AssertionFailure( "Should not call getTable() on column w/o persistent class defined" );
		}

		return StringHelper.isNotEmpty( explicitTableName )
				&& !propertyHolder.getTable().getName().equals( explicitTableName );
	}

	public Join getJoin() {
		Join join = joins.get( explicitTableName );
		if ( join == null ) {
			// annotation binding seems to use logical and physical naming somewhat inconsistently...
			final String physicalTableName = getBuildingContext().getMetadataCollector().getPhysicalTableName( explicitTableName );
			if ( physicalTableName != null ) {
				join = joins.get( physicalTableName );
			}
		}

		if ( join == null ) {
			throw new AnnotationException(
					"Cannot find the expected secondary table: no "
							+ explicitTableName + " available for " + propertyHolder.getClassName()
			);
		}

		return join;
	}

	public void forceNotNull() {
		if ( mappingColumn == null ) {
			throw new CannotForceNonNullableException(
					"Cannot perform #forceNotNull because internal org.hibernate.mapping.Column reference is null: " +
							"likely a formula"
			);
		}
		mappingColumn.setNullable( false );
	}

	public static Ejb3Column[] buildColumnFromAnnotation(
			javax.persistence.Column[] anns,
			org.hibernate.annotations.Formula formulaAnn,
			Nullability nullability,
			PropertyHolder propertyHolder,
			PropertyData inferredData,
			Map<String, Join> secondaryTables,
			MetadataBuildingContext context) {
		return buildColumnFromAnnotation(
				anns,
				formulaAnn,
				nullability,
				propertyHolder,
				inferredData,
				null,
				secondaryTables,
				context
		);
	}
	public static Ejb3Column[] buildColumnFromAnnotation(
			javax.persistence.Column[] anns,
			org.hibernate.annotations.Formula formulaAnn,
			Nullability nullability,
			PropertyHolder propertyHolder,
			PropertyData inferredData,
			String suffixForDefaultColumnName,
			Map<String, Join> secondaryTables,
			MetadataBuildingContext context) {
		Ejb3Column[] columns;
		if ( formulaAnn != null ) {
			Ejb3Column formulaColumn = new Ejb3Column();
			formulaColumn.setFormula( formulaAnn.value() );
			formulaColumn.setImplicit( false );
			formulaColumn.setBuildingContext( context );
			formulaColumn.setPropertyHolder( propertyHolder );
			formulaColumn.bind();
			columns = new Ejb3Column[] { formulaColumn };
		}
		else {
			javax.persistence.Column[] actualCols = anns;
			javax.persistence.Column[] overriddenCols = propertyHolder.getOverriddenColumn(
					StringHelper.qualify( propertyHolder.getPath(), inferredData.getPropertyName() )
			);
			if ( overriddenCols != null ) {
				//check for overridden first
				if ( anns != null && overriddenCols.length != anns.length ) {
					throw new AnnotationException( "AttributeOverride.column() should override all columns for now" );
				}
				actualCols = overriddenCols.length == 0 ? null : overriddenCols;
				LOG.debugf( "Column(s) overridden for property %s", inferredData.getPropertyName() );
			}
			if ( actualCols == null ) {
				columns = buildImplicitColumn(
						inferredData,
						suffixForDefaultColumnName,
						secondaryTables,
						propertyHolder,
						nullability,
						context
				);
			}
			else {
				final int length = actualCols.length;
				columns = new Ejb3Column[length];
				for (int index = 0; index < length; index++) {

					final ObjectNameNormalizer normalizer = context.getObjectNameNormalizer();
					final Database database = context.getMetadataCollector().getDatabase();
					final ImplicitNamingStrategy implicitNamingStrategy = context.getBuildingOptions().getImplicitNamingStrategy();
					final PhysicalNamingStrategy physicalNamingStrategy = context.getBuildingOptions().getPhysicalNamingStrategy();

					javax.persistence.Column col = actualCols[index];

					final String sqlType;
					if ( col.columnDefinition().equals( "" ) ) {
						sqlType = null;
					}
					else {
						sqlType = normalizer.applyGlobalQuoting( col.columnDefinition() );
					}

					final String tableName;
					if ( StringHelper.isEmpty( col.table() ) ) {
						tableName = "";
					}
					else {
						tableName = database.getJdbcEnvironment()
								.getIdentifierHelper()
								.toIdentifier( col.table() )
								.render();
//						final Identifier logicalName = database.getJdbcEnvironment()
//								.getIdentifierHelper()
//								.toIdentifier( col.table() );
//						final Identifier physicalName = physicalNamingStrategy.toPhysicalTableName( logicalName );
//						tableName = physicalName.render( database.getDialect() );
					}

					final String columnName;
					if ( "".equals( col.name() ) ) {
						columnName = null;
					}
					else {
						// NOTE : this is the logical column name, not the physical!
						columnName = database.getJdbcEnvironment()
								.getIdentifierHelper()
								.toIdentifier( col.name() )
								.render();
					}

					Ejb3Column column = new Ejb3Column();

					if ( length == 1 ) {
						applyColumnDefault( column, inferredData );
					}

					column.setImplicit( false );
					column.setSqlType( sqlType );
					column.setLength( col.length() );
					column.setPrecision( col.precision() );
					column.setScale( col.scale() );
					if ( StringHelper.isEmpty( columnName ) && ! StringHelper.isEmpty( suffixForDefaultColumnName ) ) {
						column.setLogicalColumnName( inferredData.getPropertyName() + suffixForDefaultColumnName );
					}
					else {
						column.setLogicalColumnName( columnName );
					}

					column.setPropertyName(
							BinderHelper.getRelativePath( propertyHolder, inferredData.getPropertyName() )
					);
			 		column.setNullable(
						col.nullable()
					); //TODO force to not null if available? This is a (bad) user choice.
					column.setUnique( col.unique() );
					column.setInsertable( col.insertable() );
					column.setUpdatable( col.updatable() );
					column.setExplicitTableName( tableName );
					column.setPropertyHolder( propertyHolder );
					column.setJoins( secondaryTables );
					column.setBuildingContext( context );
					column.extractDataFromPropertyData(inferredData);
					column.bind();
					columns[index] = column;
				}
			}
		}
		return columns;
	}

	private static void applyColumnDefault(Ejb3Column column, PropertyData inferredData) {
		final XProperty xProperty = inferredData.getProperty();
		if ( xProperty != null ) {
			ColumnDefault columnDefaultAnn = xProperty.getAnnotation( ColumnDefault.class );
			if ( columnDefaultAnn != null ) {
				column.setDefaultValue( columnDefaultAnn.value() );
			}
		}
		else {
			LOG.trace(
					"Could not perform @ColumnDefault lookup as 'PropertyData' did not give access to XProperty"
			);
		}
	}

	//must only be called after all setters are defined and before bind
	private void extractDataFromPropertyData(PropertyData inferredData) {
		if ( inferredData != null ) {
			XProperty property = inferredData.getProperty();
			if ( property != null ) {
				processExpression( property.getAnnotation( ColumnTransformer.class ) );
				ColumnTransformers annotations = property.getAnnotation( ColumnTransformers.class );
				if (annotations != null) {
					for ( ColumnTransformer annotation : annotations.value() ) {
						processExpression( annotation );
					}
				}
			}
		}
	}

	private void processExpression(ColumnTransformer annotation) {
		String nonNullLogicalColumnName = logicalColumnName != null ? logicalColumnName : ""; //use the default for annotations
		if ( annotation != null &&
				( StringHelper.isEmpty( annotation.forColumn() )
						|| annotation.forColumn().equals( nonNullLogicalColumnName ) ) ) {
			readExpression = annotation.read();
			if ( StringHelper.isEmpty( readExpression ) ) {
				readExpression = null;
			}
			writeExpression = annotation.write();
			if ( StringHelper.isEmpty( writeExpression ) ) {
				writeExpression = null;
			}
		}
	}

	private static Ejb3Column[] buildImplicitColumn(
			PropertyData inferredData,
			String suffixForDefaultColumnName,
			Map<String, Join> secondaryTables,
			PropertyHolder propertyHolder,
			Nullability nullability,
			MetadataBuildingContext context) {
		Ejb3Column column = new Ejb3Column();
		Ejb3Column[] columns = new Ejb3Column[1];
		columns[0] = column;

		//not following the spec but more clean
		if ( nullability != Nullability.FORCED_NULL
				&& inferredData.getClassOrElement().isPrimitive()
				&& !inferredData.getProperty().isArray() ) {
			column.setNullable( false );
		}
		column.setLength( DEFAULT_COLUMN_LENGTH );
		final String propertyName = inferredData.getPropertyName();
		column.setPropertyName(
				BinderHelper.getRelativePath( propertyHolder, propertyName )
		);
		column.setPropertyHolder( propertyHolder );
		column.setJoins( secondaryTables );
		column.setBuildingContext( context );

		// property name + suffix is an "explicit" column name
		if ( !StringHelper.isEmpty( suffixForDefaultColumnName ) ) {
			column.setLogicalColumnName( propertyName + suffixForDefaultColumnName );
			column.setImplicit( false );
		}
		else {
			column.setImplicit( true );
		}
		applyColumnDefault( column, inferredData );
		column.extractDataFromPropertyData( inferredData );
		column.bind();
		return columns;
	}

	public static void checkPropertyConsistency(Ejb3Column[] columns, String propertyName) {
		int nbrOfColumns = columns.length;

		if ( nbrOfColumns > 1 ) {
			for (int currentIndex = 1; currentIndex < nbrOfColumns; currentIndex++) {

				if (columns[currentIndex].isFormula() || columns[currentIndex - 1].isFormula()) {
					continue;
				}

				if ( columns[currentIndex].isInsertable() != columns[currentIndex - 1].isInsertable() ) {
					throw new AnnotationException(
							"Mixing insertable and non insertable columns in a property is not allowed: " + propertyName
					);
				}
				if ( columns[currentIndex].isNullable() != columns[currentIndex - 1].isNullable() ) {
					throw new AnnotationException(
							"Mixing nullable and non nullable columns in a property is not allowed: " + propertyName
					);
				}
				if ( columns[currentIndex].isUpdatable() != columns[currentIndex - 1].isUpdatable() ) {
					throw new AnnotationException(
							"Mixing updatable and non updatable columns in a property is not allowed: " + propertyName
					);
				}
				if ( !columns[currentIndex].getTable().equals( columns[currentIndex - 1].getTable() ) ) {
					throw new AnnotationException(
							"Mixing different tables in a property is not allowed: " + propertyName
					);
				}
			}
		}

	}

	public void addIndex(Index index, boolean inSecondPass) {
		if ( index == null ) return;
		String indexName = index.name();
		addIndex( indexName, inSecondPass );
	}

	void addIndex(String indexName, boolean inSecondPass) {
		IndexOrUniqueKeySecondPass secondPass = new IndexOrUniqueKeySecondPass( indexName, this, context, false );
		if ( inSecondPass ) {
			secondPass.doSecondPass( context.getMetadataCollector().getEntityBindingMap() );
		}
		else {
			context.getMetadataCollector().addSecondPass( secondPass );
		}
	}

	void addUniqueKey(String uniqueKeyName, boolean inSecondPass) {
		IndexOrUniqueKeySecondPass secondPass = new IndexOrUniqueKeySecondPass( uniqueKeyName, this, context, true );
		if ( inSecondPass ) {
			secondPass.doSecondPass( context.getMetadataCollector().getEntityBindingMap() );
		}
		else {
			context.getMetadataCollector().addSecondPass( secondPass );
		}
	}

	@Override
	public String toString() {
		return "Ejb3Column" + "{table=" + getTable()
				+ ", mappingColumn=" + mappingColumn.getName()
				+ ", insertable=" + insertable
				+ ", updatable=" + updatable
				+ ", unique=" + unique + '}';
	}
}
