/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat, Inc. and/or its affiliates 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, Inc.
 *
 * 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.search.query.hibernate.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.annotations.common.AssertionFailure;
import org.hibernate.search.engine.spi.EntityIndexBinder;
import org.hibernate.search.engine.spi.SearchFactoryImplementor;
import org.hibernate.search.query.engine.spi.EntityInfo;
import org.hibernate.search.query.engine.spi.TimeoutManager;

/**
 * A loader which loads objects of multiple types.
 *
 * @author Emmanuel Bernard
 */
public class MultiClassesQueryLoader extends AbstractLoader {
	private Session session;
	private SearchFactoryImplementor searchFactoryImplementor;
	private List<RootEntityMetadata> entityMatadata;
	private TimeoutManager timeoutManager;
	private ObjectsInitializer objectsInitializer;

	public void init(Session session,
					SearchFactoryImplementor searchFactoryImplementor,
					ObjectsInitializer objectsInitializer,
					TimeoutManager timeoutManager) {
		super.init( session, searchFactoryImplementor );
		this.session = session;
		this.searchFactoryImplementor = searchFactoryImplementor;
		this.timeoutManager = timeoutManager;
		this.objectsInitializer = objectsInitializer;
	}

	@Override
	public boolean isSizeSafe() {
		return true; //no user provided criteria
	}

	public void setEntityTypes(Set<Class<?>> entityTypes) {
		List<Class<?>> safeEntityTypes = new ArrayList<Class<?>>();
		//TODO should we go find the root entity for a given class rather than just checking for it's root status?
		//     root entity could lead to quite inefficient queries in Hibernate when using table per class
		if ( entityTypes.size() == 0 ) {
			//support all classes
			for ( Map.Entry<Class<?>, EntityIndexBinder> entry : searchFactoryImplementor.getIndexBindingForEntity().entrySet() ) {
				//get only root entities to limit queries
				if ( entry.getValue().getDocumentBuilder().isRoot() ) {
					safeEntityTypes.add( entry.getKey() );
				}
			}
		}
		else {
			safeEntityTypes.addAll( entityTypes );
		}
		entityMatadata = new ArrayList<RootEntityMetadata>( safeEntityTypes.size() );
		for (Class clazz :  safeEntityTypes) {
			entityMatadata.add( new RootEntityMetadata( clazz, searchFactoryImplementor ) );
		}
	}

	public Object executeLoad(EntityInfo entityInfo) {
		final Object result = ObjectLoaderHelper.load( entityInfo, session );
		timeoutManager.isTimedOut();
		return result;
	}

	public List executeLoad(EntityInfo... entityInfos) {
		if ( entityInfos.length == 0 ) return Collections.EMPTY_LIST;
		if ( entityInfos.length == 1 ) {
			final Object entity = load( entityInfos[0] );
			if ( entity == null ) {
				return Collections.EMPTY_LIST;
			}
			else {
				final List<Object> list = Collections.singletonList( entity );
				return list;
			}
		}

		//split EntityInfo per root entity
		Map<RootEntityMetadata, List<EntityInfo>> entityinfoBuckets =
				new HashMap<RootEntityMetadata, List<EntityInfo>>( entityMatadata.size());
		for (EntityInfo entityInfo : entityInfos) {
			boolean found = false;
			final Class<?> clazz = entityInfo.getClazz();
			for (RootEntityMetadata rootEntityInfo : entityMatadata) {
				if ( rootEntityInfo.rootEntity == clazz || rootEntityInfo.mappedSubclasses.contains( clazz ) ) {
					List<EntityInfo> bucket = entityinfoBuckets.get( rootEntityInfo );
					if ( bucket == null ) {
						bucket = new ArrayList<EntityInfo>();
						entityinfoBuckets.put( rootEntityInfo, bucket );
					}
					bucket.add( entityInfo );
					found = true;
					break; //we stop looping for the right bucket
				}
			}
			if (!found) throw new AssertionFailure( "Could not find root entity for " + clazz );
		}

		//initialize objects by bucket
		for ( Map.Entry<RootEntityMetadata, List<EntityInfo>> entry : entityinfoBuckets.entrySet() ) {
			final RootEntityMetadata key = entry.getKey();
			final List<EntityInfo> value = entry.getValue();
			final EntityInfo[] bucketEntityInfos = value.toArray( new EntityInfo[value.size()] );

			objectsInitializer.initializeObjects(
					bucketEntityInfos,
					key.criteria,
					key.rootEntity,
					searchFactoryImplementor,
					timeoutManager,
					session);
			timeoutManager.isTimedOut();

		}
		return ObjectLoaderHelper.returnAlreadyLoadedObjectsInCorrectOrder( entityInfos, session );
	}

	private static class RootEntityMetadata {
		public final Class<?> rootEntity;
		public final Set<Class<?>> mappedSubclasses;
		private final Criteria criteria;

		RootEntityMetadata(Class<?> rootEntity, SearchFactoryImplementor searchFactoryImplementor) {
			this.rootEntity = rootEntity;
			EntityIndexBinder provider = searchFactoryImplementor.getIndexBindingForEntity( rootEntity );
			if ( provider == null) throw new AssertionFailure("Provider not found for class: " + rootEntity);
			this.mappedSubclasses = provider.getDocumentBuilder().getMappedSubclasses();
			this.criteria = null; //default
		}
	}
}
