/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat Middleware LLC or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Middleware LLC.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 *
 */
package org.hibernate.transform;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;

import org.hibernate.type.Type;
import org.hibernate.util.ArrayHelper;

/**
 * A ResultTransformer that is used to transfor tuples to a value(s)
 * that can be cached.
 *
 * @author Gail Badner
 */
public class CacheableResultTransformer implements ResultTransformer {

	// would be nice to be able to have this class extend
	// PassThroughResultTransformer, but the default constructor
	// is private (as it should be for a singleton)
	private final static PassThroughResultTransformer ACTUAL_TRANSFORMER =
			PassThroughResultTransformer.INSTANCE;
	private final int tupleLength;
	private final int tupleSubsetLength;

	// array with the i-th element indicating whether the i-th
	// expression returned by a query is included in the tuple;
	// IMPLLEMENTATION NOTE:
	// "joined" and "fetched" associations may use the same SQL,
	// but result in different tuple and cached values. This is
	// because "fetched" associations are excluded from the tuple.
	//  includeInTuple provides a way to distinguish these 2 cases.
	private final boolean[] includeInTuple;

	// indexes for tuple that are included in the transformation;
	// set to null if all elements in the tuple are included
	private final int[] includeInTransformIndex;

	/**
	 * Returns a CacheableResultTransformer that is used to transform
	 * tuples to a value(s) that can be cached.
	 *
	 * @param transformer - result transformer that will ultimately be
	 *        be used (after caching results)
	 * @param aliases - the aliases that correspond to the tuple;
	 *        if it is non-null, its length must equal the number
	 *        of true elements in includeInTuple[]
	 * @param includeInTuple - array with the i-th element indicating
	 *        whether the i-th expression returned by a query is
	 *        included in the tuple; the number of true values equals
	 *        the length of the tuple that will be transformed;
	 *        must be non-null
	 * @return a CacheableResultTransformer that is used to transform
	 *         tuples to a value(s) that can be cached.
	 */
	public static CacheableResultTransformer create(ResultTransformer transformer,
													String[] aliases,
													boolean[] includeInTuple) {
		return transformer instanceof TupleSubsetResultTransformer ?
				create( ( TupleSubsetResultTransformer ) transformer, aliases, includeInTuple ) :
				create( includeInTuple )
		;
	}

	/**
	 * Returns a CacheableResultTransformer that is used to transform
	 * tuples to a value(s) that can be cached.
	 *
	 * @param transformer - a tuple subset result transformer;
	 *        must be non-null;
	 * @param aliases - the aliases that correspond to the tuple;
	 *        if it is non-null, its length must equal the number
	 *        of true elements in includeInTuple[]
	 * @param includeInTuple - array with the i-th element indicating
	 *        whether the i-th expression returned by a query is
	 *        included in the tuple; the number of true values equals
	 *        the length of the tuple that will be transformed;
	 *        must be non-null
	 * @return a CacheableResultTransformer that is used to transform
	 *         tuples to a value(s) that can be cached.
	 */
	private static CacheableResultTransformer create(TupleSubsetResultTransformer transformer,
													 String[] aliases,
													 boolean[] includeInTuple) {
		if ( transformer == null ) {
			throw new IllegalArgumentException( "transformer cannot be null" );
		}
		int tupleLength = ArrayHelper.countTrue( includeInTuple );
		if ( aliases != null && aliases.length != tupleLength ) {
			throw new IllegalArgumentException(
					"if aliases is not null, then the length of aliases[] must equal the number of true elements in includeInTuple; " +
							"aliases.length=" + aliases.length + "tupleLength=" + tupleLength
			);
		}
		return new CacheableResultTransformer(
				includeInTuple,
				transformer.includeInTransform( aliases, tupleLength )
		);
	}

	/**
	 * Returns a CacheableResultTransformer that is used to transform
	 * tuples to a value(s) that can be cached.
	 *
	 * @param includeInTuple - array with the i-th element indicating
	 *        whether the i-th expression returned by a query is
	 *        included in the tuple; the number of true values equals
	 *        the length of the tuple that will be transformed;
	 *        must be non-null
	 * @return a CacheableResultTransformer that is used to transform
	 *         tuples to a value(s) that can be cached.
	 */
	private static CacheableResultTransformer create(boolean[] includeInTuple) {
		return new CacheableResultTransformer( includeInTuple, null );
	}

	private CacheableResultTransformer(boolean[] includeInTuple, boolean[] includeInTransform) {
		if ( includeInTuple == null ) {
			throw new IllegalArgumentException( "includeInTuple cannot be null" );
		}
		this.includeInTuple = includeInTuple;
		tupleLength = ArrayHelper.countTrue( includeInTuple );
		tupleSubsetLength = (
				includeInTransform == null ?
						tupleLength :
						ArrayHelper.countTrue( includeInTransform )
		);
		if ( tupleSubsetLength == tupleLength ) {
			includeInTransformIndex = null;
		}
		else {
			includeInTransformIndex = new int[tupleSubsetLength];
			for ( int i = 0, j = 0 ; i < includeInTransform.length ; i++ ) {
				if ( includeInTransform[ i ] ) {
					includeInTransformIndex[ j ] =  i;
					j++;
				}
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public Object transformTuple(Object[] tuple, String aliases[]) {
		if ( aliases != null && aliases.length != tupleLength ) {
			throw new IllegalStateException(
					"aliases expected length is " + tupleLength +
					"; actual length is " + aliases.length );
		}
		// really more correct to pass index( aliases.getClass(), aliases )
		// as the 2nd arg to the following statement;
		// passing null instead because it ends up being ignored.
		return ACTUAL_TRANSFORMER.transformTuple( index( tuple.getClass(), tuple ), null );
	}

	/**
	 * Re-transforms, if necessary, a List of values previously
	 * transformed by this (or an equivalent) CacheableResultTransformer.
	 * Each element of the list is re-transformed in place (i.e, List
	 * elements are replaced with re-transformed values) and the original
	 * List is returned.
	 * <p/>
	 * If re-transformation is unnecessary, the original List is returned
	 * unchanged.
	 *
	 * @param transformedResults - results that were previously transformed
	 * @param aliases - the aliases that correspond to the untransformed tuple;
	 * @param transformer - the transformer for the re-transformation
	 * @return transformedResults, with each element re-transformed (if nececessary)
	 */
	public List retransformResults(List transformedResults,
								   String aliases[],
								   ResultTransformer transformer,
								   boolean[] includeInTuple) {
		if ( transformer == null ) {
			throw new IllegalArgumentException( "transformer cannot be null" );
		}
		if ( ! this.equals( create( transformer, aliases, includeInTuple ) ) ) {
			throw new IllegalStateException(
					"this CacheableResultTransformer is inconsistent with specified arguments; cannot re-transform"
			);
		}
		boolean requiresRetransform = true;
		String[] aliasesToUse = aliases == null ? null : index( ( aliases.getClass() ), aliases );
		if ( transformer == ACTUAL_TRANSFORMER ) {
			requiresRetransform = false;
		}
		else if ( transformer instanceof TupleSubsetResultTransformer ) {
			requiresRetransform =  ! ( ( TupleSubsetResultTransformer ) transformer ).isTransformedValueATupleElement(
					aliasesToUse,
					tupleLength
			);
		}
		if ( requiresRetransform ) {
			for ( int i = 0 ; i < transformedResults.size() ; i++ ) {
				Object[] tuple = ACTUAL_TRANSFORMER.untransformToTuple(
									transformedResults.get( i ),
									tupleSubsetLength == 1
				);
				transformedResults.set( i, transformer.transformTuple( tuple, aliasesToUse ) );
			}
		}
		return transformedResults;
	}

	/**
	 * Untransforms, if necessary, a List of values previously
	 * transformed by this (or an equivalent) CacheableResultTransformer.
	 * Each element of the list is untransformed in place (i.e, List
	 * elements are replaced with untransformed values) and the original
	 * List is returned.
	 * <p/>
	 * If not unnecessary, the original List is returned
	 * unchanged.
	 * <p/>
	 * NOTE: If transformed values are a subset of the original
	 *       tuple, then, on return, elements corresponding to
	 *       excluded tuple elements will be null.
	 * @param results - results that were previously transformed
	 * @return results, with each element untransformed (if nececessary)
	 */
	public List untransformToTuples(List results) {
		if ( includeInTransformIndex == null ) {
			results = ACTUAL_TRANSFORMER.untransformToTuples(
					results,
					tupleSubsetLength == 1
			);
		}
		else {
			for ( int i = 0 ; i < results.size() ; i++ ) {
				Object[] tuple = ACTUAL_TRANSFORMER.untransformToTuple(
									results.get( i ),
									tupleSubsetLength == 1
				);
				results.set( i, unindex( tuple.getClass(), tuple ) );
			}

		}
		return results;
	}

	/**
	 * Returns the result types for the transformed value.
	 * @param tupleResultTypes
	 * @return
	 */
	public Type[] getCachedResultTypes(Type[] tupleResultTypes) {
		return tupleLength != tupleSubsetLength ?
				index( tupleResultTypes.getClass(), tupleResultTypes ) :
				tupleResultTypes
		;
	}

	/**
	 * {@inheritDoc}
	 */
	public List transformList(List list) {
		return list;
	}

	private <T> T[] index(Class<? extends T[]> clazz, T[] objects) {
		T[] objectsIndexed = objects;
		if ( objects != null &&
				includeInTransformIndex != null &&
				objects.length != tupleSubsetLength ) {
			objectsIndexed = clazz.cast( Array.newInstance( clazz.getComponentType(), tupleSubsetLength ) );
			for ( int i = 0 ; i < tupleSubsetLength; i++ ) {
				objectsIndexed[ i ] = objects[ includeInTransformIndex[ i ] ];
			}
		}
		return objectsIndexed;
	}

	private <T> T[] unindex(Class<? extends T[]> clazz, T[] objects) {
		T[] objectsUnindexed = objects;
		if ( objects != null &&
				includeInTransformIndex != null &&
				objects.length != tupleLength ) {
			objectsUnindexed = clazz.cast( Array.newInstance( clazz.getComponentType(), tupleLength ) );
			for ( int i = 0 ; i < tupleSubsetLength; i++ ) {
				objectsUnindexed[ includeInTransformIndex[ i ] ] = objects[ i ];
			}
		}
		return objectsUnindexed;
	}

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}

		CacheableResultTransformer that = ( CacheableResultTransformer ) o;

		if ( tupleLength != that.tupleLength ) {
			return false;
		}
		if ( tupleSubsetLength != that.tupleSubsetLength ) {
			return false;
		}
		if ( !Arrays.equals( includeInTuple, that.includeInTuple ) ) {
			return false;
		}
		if ( !Arrays.equals( includeInTransformIndex, that.includeInTransformIndex ) ) {
			return false;
		}

		return true;
	}

	@Override
	public int hashCode() {
		int result = tupleLength;
		result = 31 * result + tupleSubsetLength;
		result = 31 * result + ( includeInTuple != null ? Arrays.hashCode( includeInTuple ) : 0 );
		result = 31 * result + ( includeInTransformIndex != null ? Arrays.hashCode( includeInTransformIndex ) : 0 );
		return result;
	}
}
