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

import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Optional;
import java.util.Set;

import org.apache.ignite.cache.CacheAtomicityMode;
import org.apache.ignite.cache.QueryEntity;
import org.apache.ignite.cache.QueryIndex;
import org.apache.ignite.cache.QueryIndexType;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.internal.processors.query.QueryUtils;
import org.hibernate.HibernateException;
import org.hibernate.boot.model.relational.Namespace;
import org.hibernate.boot.model.relational.Sequence;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Index;
import org.hibernate.mapping.Selectable;
import org.hibernate.mapping.SimpleValue;
import org.hibernate.mapping.Table;
import org.hibernate.mapping.Value;
import org.hibernate.ogm.datastore.ignite.logging.impl.Log;
import org.hibernate.ogm.datastore.ignite.logging.impl.LoggerFactory;
import org.hibernate.ogm.datastore.ignite.util.StringHelper;
import org.hibernate.ogm.datastore.spi.BaseSchemaDefiner;
import org.hibernate.ogm.datastore.spi.DatastoreProvider;
import org.hibernate.ogm.model.key.spi.AssociationKeyMetadata;
import org.hibernate.ogm.model.key.spi.AssociationKind;
import org.hibernate.ogm.model.key.spi.EntityKeyMetadata;
import org.hibernate.ogm.model.key.spi.IdSourceKeyMetadata;
import org.hibernate.ogm.type.impl.EnumType;
import org.hibernate.ogm.type.impl.NumericBooleanType;
import org.hibernate.ogm.type.impl.YesNoType;
import org.hibernate.ogm.type.spi.GridType;
import org.hibernate.ogm.type.spi.TypeTranslator;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.ComponentType;
import org.hibernate.type.Type;

/**
 * @author Victor Kadachigov
 */
public class IgniteCacheInitializer extends BaseSchemaDefiner {

	private static final Log log = LoggerFactory.getLogger();
	private static final String STRING_CLASS_NAME = String.class.getName();
	private static final String INTEGER_CLASS_NAME = Integer.class.getName();

	private ServiceRegistry serviceRegistry;

	@Override
	public void initializeSchema(SchemaDefinitionContext context) {
		serviceRegistry = context.getSessionFactory().getServiceRegistry();
		DatastoreProvider provider = serviceRegistry.getService( DatastoreProvider.class );
		if ( provider instanceof IgniteDatastoreProvider ) {
			IgniteDatastoreProvider igniteDatastoreProvider = (IgniteDatastoreProvider) provider;
			initializeEntities( context, igniteDatastoreProvider );
			initializeAssociations( context, igniteDatastoreProvider );
			initializeIdSources( context, igniteDatastoreProvider );
		}
		else {
			throw log.unexpectedDatastoreProvider( provider.getClass(), IgniteDatastoreProvider.class );
		}
	}

	private void initializeEntities(SchemaDefinitionContext context, final IgniteDatastoreProvider igniteDatastoreProvider) {
		for ( EntityKeyMetadata entityKeyMetadata : context.getAllEntityKeyMetadata() ) {
			try {
				try {
					igniteDatastoreProvider.getEntityCache( entityKeyMetadata );
				}
				catch (HibernateException ex) {
					CacheConfiguration config = createEntityCacheConfiguration( entityKeyMetadata, context );
					igniteDatastoreProvider.initializeCache( config );
				}
			}
			catch (Exception ex) {
				// just write error to log
				throw log.unableToInitializeCache( entityKeyMetadata.getTable(), ex );
			}
		}
	}

	private void initializeAssociations(SchemaDefinitionContext context, IgniteDatastoreProvider igniteDatastoreProvider) {
		for ( AssociationKeyMetadata associationKeyMetadata : context.getAllAssociationKeyMetadata() ) {
			log.debugf( "initializeAssociations. associationKeyMetadata: %s", associationKeyMetadata );
			if ( associationKeyMetadata.getAssociationKind() != AssociationKind.EMBEDDED_COLLECTION
					&& IgniteAssociationSnapshot.isThirdTableAssociation( associationKeyMetadata ) ) {
				try {
					try {
						igniteDatastoreProvider.getAssociationCache( associationKeyMetadata );
					}
					catch (HibernateException ex) {
						CacheConfiguration config = createCacheConfiguration( associationKeyMetadata, context );
						if ( config != null ) {
							igniteDatastoreProvider.initializeCache( config );
						}
					}
				}
				catch (Exception ex) {
					// just write error to log
					throw log.unableToInitializeCache( associationKeyMetadata.getTable(), ex );
				}

			}
		}
	}

	private void initializeIdSources(SchemaDefinitionContext context, IgniteDatastoreProvider igniteDatastoreProvider) {
		// generate tables
		for ( IdSourceKeyMetadata idSourceKeyMetadata : context.getAllIdSourceKeyMetadata() ) {
			if ( idSourceKeyMetadata.getType() == IdSourceKeyMetadata.IdSourceType.TABLE ) {
				try {
					try {
						igniteDatastoreProvider.getIdSourceCache( idSourceKeyMetadata );
					}
					catch (HibernateException ex) {
						CacheConfiguration config = createCacheConfiguration( idSourceKeyMetadata );
						igniteDatastoreProvider.initializeCache( config );
					}
				}
				catch (Exception ex) {
					// just write error to log
					throw log.unableToInitializeCache( idSourceKeyMetadata.getName(), ex );
				}
			}
		}
		Set<String> generatedSequences = new HashSet<>();
		// generate sequences
		for ( Namespace namespace : context.getDatabase().getNamespaces() ) {
			for ( Sequence sequence : namespace.getSequences() ) {
				generatedSequences.add( sequence.getName().getSequenceName().getText() );
				igniteDatastoreProvider.atomicSequence( sequence.getName().getSequenceName().getText(), sequence.getInitialValue(), true );
			}
		}
		for ( IdSourceKeyMetadata idSourceKeyMetadata : context.getAllIdSourceKeyMetadata() ) {
			if ( idSourceKeyMetadata.getType() == IdSourceKeyMetadata.IdSourceType.SEQUENCE ) {
				if ( idSourceKeyMetadata.getName() != null && !generatedSequences.contains( idSourceKeyMetadata.getName() ) ) {
					igniteDatastoreProvider.atomicSequence( idSourceKeyMetadata.getName(), 1, true );
				}
			}
		}
	}

	private CacheConfiguration createCacheConfiguration(IdSourceKeyMetadata idSourceKeyMetadata) {
		CacheConfiguration result = new CacheConfiguration();
		result.setName( StringHelper.stringBeforePoint( idSourceKeyMetadata.getName() ) );
		return result;
	}

	private CacheConfiguration createCacheConfiguration(AssociationKeyMetadata associationKeyMetadata, SchemaDefinitionContext context) {
		QueryEntity queryEntity = new QueryEntity();
		queryEntity.setTableName( associationKeyMetadata.getTable() );
		queryEntity.setValueType( StringHelper.stringAfterPoint( associationKeyMetadata.getTable() ) );
		appendIndex( queryEntity, associationKeyMetadata, context );

		CacheConfiguration result = new CacheConfiguration();
		result.setName( StringHelper.stringBeforePoint( associationKeyMetadata.getTable() ) );
		result.setQueryEntities( Arrays.asList( queryEntity ) );
		return result;
	}

	private void appendIndex(QueryEntity queryEntity, AssociationKeyMetadata associationKeyMetadata, SchemaDefinitionContext context) {
		QueryIndex queryIndex = new QueryIndex();
		queryIndex.setIndexType( QueryIndexType.SORTED );
		LinkedHashMap<String, Boolean> fields = new LinkedHashMap<>();
		addTableInfo( queryEntity, context, associationKeyMetadata.getTable() );
		for ( String columnName : associationKeyMetadata.getRowKeyColumnNames() ) {
			String realColumnName = StringHelper.realColumnName( columnName );
			fields.put( realColumnName, true );
		}
		queryIndex.setFields( fields );
		queryIndex.setName( queryEntity.getTableName() + '_' + org.hibernate.ogm.util.impl.StringHelper.join( fields.keySet(), "_" ) );

		Set<QueryIndex> indexes = new HashSet<>( queryEntity.getIndexes() );
		indexes.add( queryIndex );
		queryEntity.setIndexes( indexes );
	}

	private Class getEntityIdClassName( String table, SchemaDefinitionContext context ) {
		Class<?> entityClass = context.getTableEntityTypeMapping().get( table );
		EntityPersister entityPersister = context.getSessionFactory().getEntityPersister( entityClass.getName() );
		return entityPersister.getIdentifierType().getReturnedClass();
	}

	private CacheConfiguration<?,?> createEntityCacheConfiguration(EntityKeyMetadata entityKeyMetadata, SchemaDefinitionContext context) {
		CacheConfiguration<?,?> cacheConfiguration = new CacheConfiguration<>();
		cacheConfiguration.setStoreKeepBinary( true );
		cacheConfiguration.setSqlSchema( QueryUtils.DFLT_SCHEMA );
		cacheConfiguration.setBackups( 1 );
		cacheConfiguration.setName( StringHelper.stringBeforePoint( entityKeyMetadata.getTable() ) );
		cacheConfiguration.setAtomicityMode( CacheAtomicityMode.TRANSACTIONAL );

		QueryEntity queryEntity = new QueryEntity();
		queryEntity.setTableName( entityKeyMetadata.getTable() );
		queryEntity.setKeyType( getEntityIdClassName( entityKeyMetadata.getTable(), context ).getSimpleName() );
		queryEntity.setValueType( StringHelper.stringAfterPoint( entityKeyMetadata.getTable() ) );

		addTableInfo( queryEntity, context, entityKeyMetadata.getTable() );
		for ( AssociationKeyMetadata associationKeyMetadata : context.getAllAssociationKeyMetadata() ) {
			if ( associationKeyMetadata.getAssociationKind() != AssociationKind.EMBEDDED_COLLECTION
					&& associationKeyMetadata.getTable().equals( entityKeyMetadata.getTable() )
					&& !IgniteAssociationSnapshot.isThirdTableAssociation( associationKeyMetadata ) ) {
				appendIndex( queryEntity, associationKeyMetadata, context );
			}
		}
		addUserIndexes( queryEntity, context, entityKeyMetadata.getTable() );

		log.debugf( "queryEntity: %s", queryEntity );
		cacheConfiguration.setQueryEntities( Arrays.asList( queryEntity ) );
		return cacheConfiguration;
	}

	/**
	 * Create indexes for {@code @Index} annotations
	 * @param queryEntity
	 * @param context
	 */
	private void addUserIndexes(QueryEntity queryEntity, SchemaDefinitionContext context, String tableName) {
		Namespace namespace = context.getDatabase().getDefaultNamespace();
		Optional<Table> tableOptional = namespace.getTables().stream().filter( currentTable -> currentTable.getName().equals( tableName ) ).findFirst();
		if ( tableOptional.isPresent() ) {
			Table table = tableOptional.get();
			for ( Iterator<Index> indexIterator = table.getIndexIterator(); indexIterator.hasNext(); ) {
				Index index = indexIterator.next();
				appendIndex( queryEntity, index, context );
			}
		}
	}

	private void appendIndex(QueryEntity queryEntity, Index index, SchemaDefinitionContext context) {
		QueryIndex queryIndex = new QueryIndex();
		queryIndex.setName( index.getName() );
		queryIndex.setIndexType( QueryIndexType.SORTED );
		LinkedHashMap<String, Boolean> fields = new LinkedHashMap<>();
		for ( Iterator<Column> columnIterator = index.getColumnIterator(); columnIterator.hasNext(); ) {
			Column currentColumn = columnIterator.next();
			fields.put( currentColumn.getName(), true );
		}
		queryIndex.setFields( fields );

		Set<QueryIndex> indexes = new HashSet<>( queryEntity.getIndexes() );
		indexes.add( queryIndex );
		queryEntity.setIndexes( indexes );
	}

	@SuppressWarnings("unchecked")
	private void addTableInfo(QueryEntity queryEntity, SchemaDefinitionContext context, String tableName) {
		Namespace namespace = context.getDatabase().getDefaultNamespace();
		Optional<Table> tableOptional = namespace.getTables().stream().filter( currentTable -> currentTable.getName().equals( tableName ) ).findFirst();
		if ( tableOptional.isPresent() ) {
			Table table = tableOptional.get();
			for ( Iterator<Column> columnIterator = table.getColumnIterator(); columnIterator.hasNext(); ) {
				Column currentColumn = columnIterator.next();
				String fieldType = fieldType( currentColumn );
				queryEntity.addQueryField( StringHelper.realColumnName( currentColumn.getName() ), fieldType, null );
			}
		}
	}

	private String fieldType(Column currentColumn) {
		Value value = currentColumn.getValue();
		Type type = value.getType();
		while ( type.isEntityType() || type.isComponentType() ) {
			if ( type.isEntityType() ) {
				type = ( (SimpleValue) value ).getMetadata().getIdentifierType( type.getName() );
			}
			if ( type.isComponentType() ) {
				int i = 0;
				boolean columnFound = false;
				// search which nested property is mapped to the given column
				for ( Iterator<Selectable> ci = value.getColumnIterator(); ci.hasNext(); ++i ) {
					if ( currentColumn.getName().equals( ci.next().getText() ) ) {
						type = ( (ComponentType) type ).getSubtypes()[i];
						columnFound = true;
						break;
					}
				}
				if ( !columnFound ) {
					throw new IllegalArgumentException( "Cannot determine type for column " + currentColumn );
				}
			}
		}
		GridType gridType = serviceRegistry.getService( TypeTranslator.class ).getType( type );
		if ( gridType instanceof EnumType ) {
			return enumFieldType( (EnumType) gridType );
		}
		if ( gridType instanceof YesNoType ) {
			return STRING_CLASS_NAME;
		}
		if ( gridType instanceof NumericBooleanType ) {
			return INTEGER_CLASS_NAME;
		}
		Class<?> returnedClass = type.getReturnedClass();
		if ( Character.class.equals( returnedClass ) ) {
			return STRING_CLASS_NAME;
		}
		return returnedClass.getName();
	}

	private String enumFieldType(EnumType enumType) {
		return enumType.isOrdinal()
				? INTEGER_CLASS_NAME
				: STRING_CLASS_NAME;
	}
}