/*
 * Copyright 2008-2011 Jose Luis Martin Garcia
 *
 * 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.jdal.hibernate;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.EntityMode;
import org.hibernate.Hibernate;
import org.hibernate.LockOptions;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.collection.AbstractPersistentCollection;
import org.hibernate.criterion.Example;
import org.hibernate.engine.PersistenceContext;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.impl.CriteriaImpl.Subcriteria;
import org.hibernate.impl.SessionImpl;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.collection.CollectionPersister;
import org.jdal.beans.PropertyUtils;
import org.jdal.util.BeanUtils;
import org.springframework.util.StringUtils;

/**
 * Hibernate Utility library
 *
 * @author Jose Luis Martin - ([email protected]info)
 */
@SuppressWarnings("rawtypes")
public abstract class HibernateUtils {
	
	public static final int DEFAULT_DEPTH = 2;
	private static final Log log = LogFactory.getLog(HibernateUtils.class);
	private static final String EXISTS_QUERY = "SELECT 1 from %s x WHERE %s = ?";
		
	/** 
	 * Initialize a Object for use whith closed session. 
	 * Will recurse on properties at maximum of n depth.
	 * 
	 * @param sessionFactory the Hibernate SessionFactory to use
	 * @param obj Object to initialize
	 * @param depth max depth in recursion
	 */
	public static void initialize(SessionFactory sessionFactory, Object obj, 
			int depth) {
		initialize(sessionFactory, obj, new ArrayList<Object>(), depth);
	}
	
	
	/**
	 * Initialize a Object for use with closed sessions, 
	 * Use with care, will recurse on all properties.
	 * 
	 * @param sessionFactory the hibernate SessionFactory
	 * @param obj persistent object to initialize
	 */
	public static void initialize(SessionFactory sessionFactory, Object obj) {
		initialize(sessionFactory, obj, new ArrayList<Object>(), DEFAULT_DEPTH);
	}
	
	
	/** 
	 * Initialize Object for use with closed Session. 
	 * 
	 * @param sessionFactory max depth in recursion
	 * @param obj Object to initialize
	 * @param initializedObjects list with already initialized Objects
	 * @param depth max depth in recursion
	 */
	private static void initialize(SessionFactory sessionFactory, Object obj, 
			List<Object> initializedObjects, int depth) {
		
		// return on nulls, depth = 0 or already initialized objects
		if (obj == null || depth == 0) { 
			return; 
		}
		
		if (!Hibernate.isInitialized(obj)) {
			// if collection, initialize objects in collection too. Hibernate don't do it.
			if (obj instanceof Collection) {
				initializeCollection(sessionFactory, obj, initializedObjects,
						depth);
				return;
			}
			
			sessionFactory.getCurrentSession().buildLockRequest(LockOptions.NONE).lock(obj);
			Hibernate.initialize(obj);
		}
	
		// now we can call equals safely. If object are already initializated, return
		if (initializedObjects.contains(obj))
			return;
		
		initializedObjects.add(obj);
		
		// initialize all persistent associaciations.
		ClassMetadata classMetadata = getClassMetadata(sessionFactory, obj);
		
		if (classMetadata == null) {
			return; // Not persistent object
		}
		
		Object[] pvs = classMetadata.getPropertyValues(obj, EntityMode.POJO);
		
		for (Object pv : pvs) {
			initialize(sessionFactory, pv, initializedObjects, depth - 1);
		}
	}

	/**
	 * Initalize Collection and recurse on elements 
	 * 
	 * @param sessionFactory the hibernate SessionFactory
	 * @param obj the obj to initilize
	 * @param initializedObjects list with already initialized objects
	 * @param depth max depth in recursion
	 */
	private static void initializeCollection(SessionFactory sessionFactory,
			Object obj, List<Object> initializedObjects, int depth) {

		Collection<?> collection = (Collection<?>) obj;
		initializeCollection(collection, sessionFactory.getCurrentSession());
		
		// Initialize elements
		for (Object o : collection) {
			initialize(sessionFactory, o, initializedObjects, depth - 1);
		}
	}
	
	/**
	 * Initialize Collection (detached or not)
	 * @param collection collection to initialize
	 * @param session Session to use for initialization
	 */
	public static void initializeCollection(Collection collection, Session session) {
		if (collection instanceof AbstractPersistentCollection) {
			AbstractPersistentCollection ps = (AbstractPersistentCollection) collection;
			log.debug("Initalizing PersistentCollection of role: " + ps.getRole());	
			if (!ps.wasInitialized()) {
				SessionImpl source = (SessionImpl) session;
				PersistenceContext context = source.getPersistenceContext();
				CollectionPersister cp = source.getFactory().getCollectionPersister(ps.getRole());
				
				if (context.getCollectionEntry(ps) == null) {  // detached
					context.addUninitializedDetachedCollection(cp, ps);
				}
				
				ps.setCurrentSession(context.getSession());
				Hibernate.initialize(collection);
			}
		}
	}

	/**
	 * Get ClassMetadata for persistent object
	 *  
	 * @param sessionFactory the hibernate SessionFactory
	 * @param obj Object to initilize
	 * @return ClassMetadata the class metadata
	 */
	private static ClassMetadata getClassMetadata(
			SessionFactory sessionFactory, Object obj) {
		return sessionFactory.getClassMetadata(Hibernate.getClass(obj));	
	}

	/**
	 * Gets the identifier property name of persistent object
	 * 
	 * @param sessionFactory the hibernate SessionFactory
	 * @param obj the persistent object
	 * @return the identifier property name
	 */
	public static String getIdentifierPropertyName(
			SessionFactory sessionFactory, Object obj) {
		
		ClassMetadata cm = getClassMetadata(sessionFactory, obj);

		return cm == null ? null : cm.getIdentifierPropertyName();
		
	}
	
	/**
	 * Get all name attributes from a object that are of the given class 
	 * @param obj Object to get fields
	 * @param type type of the class to find.
	 * @return 	name attributes with the class type specified
	 */
	public static Set<String> getFieldNamesByType(Object obj, 
			Class <?> type) {
		Set<String> fieldNames = new HashSet<String> (0);
		Field [] fields = obj.getClass().getDeclaredFields();
		for (Field field : fields) {
			if (type.equals(field.getType())) {
				fieldNames.add(field.getName());
			}
		}
		return fieldNames;
	}
	
	/**
	 * Get a hibernate Example object that excludes zeroes values and excludes
	 * all boolean -primitive and wrapper class- attributes of a given object.  
	 * @param instance given object for a build a Example object.
	 * @return a hibernate Example object.
	 */
	public static Example excludeBooleanFields (Object instance) {
		Example result = Example.create(instance).excludeZeroes();
		Set<String> fieldNames = getFieldNamesByType(instance, Boolean.class);
		fieldNames.addAll(getFieldNamesByType(instance, Boolean.TYPE));
		for (String fieldName : fieldNames) {
			result.excludeProperty(fieldName);
		}
		return result;
	}
	
	/**
	 * Return a existing alias for propertyPath on Criteria or null if none
	 * @param criteria Hibernate Criteria
	 * @param propertyPath the property path
	 * @return alias or null if none
	 */
	public static String findAliasForPropertyPath(Criteria criteria, String propertyPath) {
		CriteriaImpl c = (CriteriaImpl) criteria;
		Iterator iter = c.iterateSubcriteria();
		while (iter.hasNext()) {
			Subcriteria subCriteria = (Subcriteria) iter.next();
			if (propertyPath.equals(subCriteria.getPath()));
				return subCriteria.getAlias();
		}
		// not found
		return null; 
	}
	
	/**
	 * Create an alias for a property path
	 * @return the alias
	 */
	public static String  createAlias(Criteria criteria, String propertyPath) {
		String[] paths = PropertyUtils.split(propertyPath);
		String alias = "";
		
		for (String name : paths) {
			alias += StringUtils.isEmpty(alias) ? name : "." + name;
			criteria.createAlias(alias, name);
		}
		
		
		return PropertyUtils.getPropertyName(alias);
	}
	
	/**
	 * Test if a entity already exists.
	 * @param entity entity to test
	 * @param session hibernate session
	 * @return true if exists, false otherwise
	 */
	public static boolean exists(Object entity, Session session) {
 		String propertyId = getIdentifierPropertyName(session.getSessionFactory(), entity);
		if (propertyId == null)
			return false;
		
		Object id = BeanUtils.getProperty(entity, propertyId);
		if (id == null)
			return false;
		
		return session.createQuery(String.format(EXISTS_QUERY, entity.getClass().getSimpleName(), propertyId))
			.setParameter(0, id)
			.list()
			.size() > 0;
	}
}