/*
 * Hibernate OGM, Domain model persistence for NoSQL datastores
 *
 * 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.ogm.datastore.ignite.utils;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import javax.persistence.EntityManager;

import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.Ignition;
import org.apache.ignite.binary.BinaryObject;
import org.apache.ignite.cache.CachePeekMode;
import org.apache.ignite.cache.QueryEntity;
import org.apache.ignite.cache.QueryIndex;
import org.apache.ignite.cache.query.SqlFieldsQuery;
import org.apache.ignite.configuration.CacheConfiguration;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.ogm.OgmSessionFactory;
import org.hibernate.ogm.datastore.document.options.AssociationStorageType;
import org.hibernate.ogm.datastore.ignite.IgniteDialect;
import org.hibernate.ogm.datastore.ignite.impl.IgniteDatastoreProvider;
import org.hibernate.ogm.datastore.ignite.impl.IgniteTupleSnapshot;
import org.hibernate.ogm.datastore.ignite.util.StringHelper;
import org.hibernate.ogm.datastore.spi.DatastoreConfiguration;
import org.hibernate.ogm.datastore.spi.DatastoreProvider;
import org.hibernate.ogm.dialect.spi.GridDialect;
import org.hibernate.ogm.model.key.spi.AssociationKeyMetadata;
import org.hibernate.ogm.model.key.spi.AssociationKind;
import org.hibernate.ogm.model.key.spi.EntityKey;
import org.hibernate.ogm.model.key.spi.EntityKeyMetadata;
import org.hibernate.ogm.model.spi.TupleSnapshot;
import org.hibernate.ogm.persister.impl.OgmCollectionPersister;
import org.hibernate.ogm.persister.impl.OgmEntityPersister;
import org.hibernate.ogm.utils.GridDialectTestHelper;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;

/**
 * @author Dmitriy Kozlov
 */
public class IgniteTestHelper implements GridDialectTestHelper {

	@Override
	public long getNumberOfEntities(SessionFactory sessionFactory) {
		int entityCount = 0;
		Set<IgniteCache<?, ?>> processedCaches = Collections.newSetFromMap( new IdentityHashMap<IgniteCache<?, ?>, Boolean>() );
		for ( EntityPersister entityPersister : ( (SessionFactoryImplementor) sessionFactory ).getEntityPersisters().values() ) {
			IgniteCache<?, ?> entityCache = getEntityCache( sessionFactory, ( (OgmEntityPersister) entityPersister ).getEntityKeyMetadata() );
			if ( !processedCaches.contains( entityCache ) ) {
				entityCount += entityCache.size( CachePeekMode.ALL );
				processedCaches.add( entityCache );
			}
		}
		return entityCount;
	}

	public static <K> Map<K, BinaryObject> find(EntityManager em, Class<?> class1, @SuppressWarnings("unchecked") K... ids) {
		SessionFactory sessionFactory = em.getEntityManagerFactory().unwrap( SessionFactory.class );
		return find( sessionFactory, class1, ids );
	}

	public static <K> Map<K, BinaryObject> find(Session session, Class<?> class1, @SuppressWarnings("unchecked") K... ids) {
		SessionFactory sessionFactory = session.getSessionFactory();
		return find( sessionFactory, class1, ids );
	}

	public static <K> Map<K, BinaryObject> find(SessionFactory sessionFactory, Class<?> class1, K... ids) {
		OgmEntityPersister entityPersister = (OgmEntityPersister) ( (SessionFactoryImplementor) sessionFactory ).getEntityPersister( class1.getName() );
		IgniteCache<K, BinaryObject> entityCache = getEntityCache( sessionFactory, entityPersister.getEntityKeyMetadata() );
		Map<K, BinaryObject> missingIds = new HashMap<>();
		for ( K id : ids ) {
			BinaryObject binaryObject = entityCache.get( id );
			if ( binaryObject != null ) {
				missingIds.put( id, binaryObject );
			}
		}
		return missingIds;
	}

	@Override
	public long getNumberOfAssociations(SessionFactory sessionFactory) {
		int associationCount = 0;
		IgniteDatastoreProvider datastoreProvider = getProvider( sessionFactory );
		for ( CollectionPersister collectionPersister : ( (SessionFactoryImplementor) sessionFactory ).getCollectionPersisters().values() ) {
			AssociationKeyMetadata associationKeyMetadata = ( (OgmCollectionPersister) collectionPersister ).getAssociationKeyMetadata();
			if ( associationKeyMetadata.getAssociationKind() == AssociationKind.ASSOCIATION ) {
				IgniteCache<Object, BinaryObject> associationCache = getAssociationCache( sessionFactory, associationKeyMetadata );
				StringBuilder query = new StringBuilder( "SELECT " )
											.append( StringHelper.realColumnName( associationKeyMetadata.getColumnNames()[0] ) )
											.append( " FROM " ).append( associationKeyMetadata.getTable() );
				SqlFieldsQuery sqlQuery = datastoreProvider.createSqlFieldsQueryWithLog( query.toString(), null );
				Iterable<List<?>> queryResult = associationCache.query( sqlQuery );
				Set<Object> uniqs = new HashSet<>();
				for ( List<?> row : queryResult ) {
					Object value = row.get( 0 );
					if ( value != null ) {
						uniqs.add( value );
					}
				}
				associationCount += uniqs.size();
			}
		}
		return associationCount;
	}

	@Override
	public long getNumberOfAssociations(SessionFactory sessionFactory, AssociationStorageType type) {
		int asscociationCount = 0;
		Set<IgniteCache<Object, BinaryObject>> processedCaches = Collections.newSetFromMap( new IdentityHashMap<IgniteCache<Object, BinaryObject>, Boolean>() );

		for ( CollectionPersister collectionPersister : ( (SessionFactoryImplementor) sessionFactory ).getCollectionPersisters().values() ) {
			AssociationKeyMetadata associationKeyMetadata = ( (OgmCollectionPersister) collectionPersister ).getAssociationKeyMetadata();
			IgniteCache<Object, BinaryObject> associationCache = getAssociationCache( sessionFactory, associationKeyMetadata );
			if ( !processedCaches.contains( associationCache ) ) {
				asscociationCount += associationCache.size();
				processedCaches.add( associationCache );
			}
		}

		return asscociationCount;
	}

	@Override
	public Map<String, Object> extractEntityTuple(Session session, EntityKey key) {
		SessionFactoryImplementor sessionFactory = (SessionFactoryImplementor) session.getSessionFactory();
		IgniteCache<Object, BinaryObject> cache = getEntityCache( sessionFactory, key.getMetadata() );
		Object cacheKey = getProvider( sessionFactory ).createKeyObject( key );

		Map<String, Object> result = new HashMap<>();
		BinaryObject po = cache.get( cacheKey );

		TupleSnapshot snapshot = new IgniteTupleSnapshot( cacheKey, po, key.getMetadata() );
		for ( String fieldName : snapshot.getColumnNames() ) {
			result.put( fieldName, snapshot.get( fieldName ) );
		}

		return result;
	}

	@Override
	public boolean backendSupportsTransactions() {
		return true;
	}

	@Override
	public void dropSchemaAndDatabase(SessionFactory sessionFactory) {
		if ( Ignition.allGrids().size() > 1 ) { // some tests doesn't stop DatastareProvider
			String currentGridName = getProvider( sessionFactory ).getGridName();
			for ( Ignite grid : Ignition.allGrids() ) {
				if ( !Objects.equals( currentGridName, grid.name() ) ) {
					grid.close();
				}
			}
		}
	}

	@Override
	public Map<String, String> getAdditionalConfigurationProperties() {
		return Collections.emptyMap();
	}

	@Override
	public GridDialect getGridDialect(DatastoreProvider datastoreProvider) {
		return new IgniteDialect( (IgniteDatastoreProvider) datastoreProvider );
	}

	public static <K> IgniteCache<K, BinaryObject> getEntityCache(SessionFactory sessionFactory, EntityKeyMetadata entityKeyMetadata) {
		IgniteDatastoreProvider castProvider = getProvider( sessionFactory );
		return castProvider.getEntityCache( entityKeyMetadata );
	}

	public static IgniteCache<Object, BinaryObject> getAssociationCache(SessionFactory sessionFactory, AssociationKeyMetadata associationKeyMetadata) {
		IgniteDatastoreProvider castProvider = getProvider( sessionFactory );
		return castProvider.getAssociationCache( associationKeyMetadata );
	}

	private static IgniteDatastoreProvider getProvider(SessionFactory sessionFactory) {
		DatastoreProvider provider = ( (SessionFactoryImplementor) sessionFactory ).getServiceRegistry().getService( DatastoreProvider.class );
		if ( !( provider instanceof IgniteDatastoreProvider ) ) {
			throw new RuntimeException( "Not testing with Ignite, cannot extract underlying cache" );
		}
		return (IgniteDatastoreProvider) provider;
	}

	@Override
	public Class<? extends DatastoreConfiguration<?>> getDatastoreConfigurationType() {
		return org.hibernate.ogm.datastore.ignite.Ignite.class;
	}

	@Override
	public long getNumberOfEntities(Session session) {
		return getNumberOfEntities( session.getSessionFactory() );
	}

	@Override
	public long getNumberOfAssociations(Session session) {
		return getNumberOfAssociations( session.getSessionFactory() );
	}

	@Override
	public void prepareDatabase(SessionFactory arg0) {
	}

	private static CacheConfiguration getCacheConfiguration(OgmSessionFactory sessionFactory, Class<?> entityClass) {
		OgmEntityPersister entityPersister = (OgmEntityPersister) ( (SessionFactoryImplementor) sessionFactory ).locateEntityPersister( entityClass );
		IgniteCache<Object, BinaryObject> cache = getProvider( sessionFactory ).getEntityCache( entityPersister.getEntityKeyMetadata() );
		return cache.getConfiguration( CacheConfiguration.class );
	}

	public static Set<QueryIndex> getIndexes(OgmSessionFactory sessionFactory, Class<?> entityClass) {
		Set<QueryIndex> result = new HashSet<>();
		CacheConfiguration<Object, BinaryObject> cacheConfig = getCacheConfiguration( sessionFactory, entityClass );
		for ( QueryEntity queryEntity : cacheConfig.getQueryEntities() ) {
			result.addAll( queryEntity.getIndexes() );
		}
		return result;
	}

	public static Map<String, String> getFields(OgmSessionFactory sessionFactory, Class<?> entityClass) {
		Map<String, String> result = new LinkedHashMap<>();
		CacheConfiguration<Object, BinaryObject> cacheConfig = getCacheConfiguration( sessionFactory, entityClass );
		for ( QueryEntity queryEntity : cacheConfig.getQueryEntities() ) {
			result.putAll( queryEntity.getFields() );
		}
		return result;
	}

}