package org.codefilarete.stalactite.engine.configurer.map;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.codefilarete.stalactite.dsl.property.CascadeOptions.RelationMode;
import org.codefilarete.stalactite.engine.EntityPersister;
import org.codefilarete.stalactite.engine.diff.AbstractDiff;
import org.codefilarete.stalactite.engine.runtime.CollectionUpdater;
import org.codefilarete.stalactite.engine.runtime.ConfiguredRelationalPersister;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.collection.StreamSplitter;

/**
 * Class aimed at doing same thing as {@link CollectionUpdater} but for {@link Map} containing entities as keys and values :
 * requires to update {@link Entry} as well as propagate insert / update /delete operation to key-entities and value-entities. 
 *
 * @param <SRC> entity type owning the relation
 * @param <SRCID> entity owning the relation identifier type 
 * @param <K> Map key entity type
 * @param <V> Map value entity type
 * @param <KK> type of {@link KeyValueRecord} key when transforming initial Map entries to {@link KeyValueRecord} to be persisted
 * @param <VV> type of {@link KeyValueRecord} value when transforming initial Map entries to {@link KeyValueRecord} to be persisted
 * @author Guillaume Mary
 */
class MapEntryKeyAndValueEntitiesUpdater<SRC, SRCID, K, V, KK, VV> extends CollectionUpdater<SRC, KeyValueRecord<K, V, SRCID>, Set<KeyValueRecord<K, V, SRCID>>> {
	
	private final ConfiguredRelationalPersister<K, ?> keyEntityPersister;
	private final ConfiguredRelationalPersister<V, ?> valueEntityPersister;
	private final RelationMode keyEntityMaintenanceMode;
	private final RelationMode valueEntityMaintenanceMode;
	private final boolean associationRecordWritable;
	
	public MapEntryKeyAndValueEntitiesUpdater(Function<SRC, Set<KeyValueRecord<K, V, SRCID>>> targetEntityGetter,
											  Function<K, KK> keyMapper,
											  Function<V, VV> valueMapper,
											  ConfiguredRelationalPersister<K, ?> keyEntityPersister,
											  ConfiguredRelationalPersister<V, ?> valueEntityPersister,
											  EntityPersister<KeyValueRecord<KK, VV, SRCID>, RecordId<KK, SRCID>> keyValueRecordPersister,
											  RelationMode keyMaintenanceMode,
											  RelationMode valueMaintenanceMode) {
		super(targetEntityGetter,
				new RelationalPersisterAsEntityWriter<>(keyValueRecordPersister, keyMapper, valueMapper),
				(o, i) -> { /* no reverse setter because we store only raw values */ },
				true,
				// Notice that the way this computation is done as a huge impact on what's done in the updateTargets(..) method
				(KeyValueRecord<K, V, SRCID> entry) -> {
					// We don't take value into account to trigger updateTargets(..) : if it wa taken into account then
					// we would have only insert and delete actions on association records, which leads to not optimal
					// database actions.
					int result = entry.getId().getId().hashCode();
					result = 31 * result + keyEntityPersister.getId(entry.getKey()).hashCode();
					return result;
				});
		this.keyEntityPersister = keyEntityPersister;
		this.valueEntityPersister = valueEntityPersister;
		this.keyEntityMaintenanceMode = keyMaintenanceMode;
		this.valueEntityMaintenanceMode = valueMaintenanceMode;
		this.associationRecordWritable = this.keyEntityMaintenanceMode != RelationMode.READ_ONLY;
	}
	
	@Override
	protected void insertTargets(UpdateContext updateContext) {
		if (keyEntityMaintenanceMode == RelationMode.ALL || keyEntityMaintenanceMode == RelationMode.ALL_ORPHAN_REMOVAL) {
			keyEntityPersister.insert(updateContext.getAddedElements().stream().map(KeyValueRecord::getKey).collect(Collectors.toSet()));
		}
		if (valueEntityMaintenanceMode == RelationMode.ALL || valueEntityMaintenanceMode == RelationMode.ALL_ORPHAN_REMOVAL) {
			valueEntityPersister.insert(updateContext.getAddedElements().stream().map(KeyValueRecord::getValue).collect(Collectors.toSet()));
		}
		// we insert association records after targets to satisfy integrity constraint
		if (associationRecordWritable) {
			super.insertTargets(updateContext);
		}
	}
	
	@Override
	protected void updateTargets(UpdateContext updateContext, boolean allColumnsStatement) {
		// Here we only manage entry values because keys are not modified since they are part of the primary key and overall
		// are in CollectionUpdate identifier (see constructor) so they are already managed through insertTargets(..) and deleteTargets(..)
		// Hence code below focuses only on value removal, update and addition
		if (valueEntityMaintenanceMode != RelationMode.READ_ONLY) {
			Set<V> removedValues = new KeepOrderSet<>();
			Set<V> addedValues = new KeepOrderSet<>();
			Set<AbstractDiff<KeyValueRecord<K, V, SRCID>>> modifiedValues = new KeepOrderSet<>();
			new StreamSplitter<>(updateContext.getHeldElements().stream())
					.dispatch(diff -> diff.getReplacingInstance() == null, entry -> {
						removedValues.add(entry.getSourceInstance().getValue());
					})
					.dispatch(diff -> diff.getReplacingInstance() != null, entry -> {
						if (!valueEntityPersister.getId(entry.getSourceInstance().getValue())
								.equals(valueEntityPersister.getId(entry.getReplacingInstance().getValue()))) {
							modifiedValues.add(entry);
							addedValues.add(entry.getReplacingInstance().getValue());
							removedValues.add(entry.getSourceInstance().getValue());
						}
					})
					.split();
			
			if (valueEntityMaintenanceMode == RelationMode.ALL || valueEntityMaintenanceMode == RelationMode.ALL_ORPHAN_REMOVAL) {
				valueEntityPersister.persist(addedValues);
			}
			
			Set<Duo<KeyValueRecord<K, V, SRCID>, KeyValueRecord<K, V, SRCID>>> recordsToBeUpdated = modifiedValues.stream()
					.map(diff -> new Duo<>(diff.getReplacingInstance(), diff.getSourceInstance())).collect(Collectors.toSet());
			elementPersister.update(recordsToBeUpdated, allColumnsStatement);
			
			if (valueEntityMaintenanceMode == RelationMode.ALL_ORPHAN_REMOVAL) {
				valueEntityPersister.delete(removedValues);
			}
		}
	}
	
	@Override
	protected void deleteTargets(UpdateContext updateContext) {
		// we delete association records before targets to satisfy integrity constraint
		if (associationRecordWritable) {
			super.deleteTargets(updateContext);
		}
		if (keyEntityMaintenanceMode == RelationMode.ALL_ORPHAN_REMOVAL) {
			keyEntityPersister.delete(updateContext.getRemovedElements().stream().map(KeyValueRecord::getKey).collect(Collectors.toSet()));
		}
		if (valueEntityMaintenanceMode == RelationMode.ALL_ORPHAN_REMOVAL) {
			valueEntityPersister.delete(updateContext.getRemovedElements().stream().map(KeyValueRecord::getValue).collect(Collectors.toSet()));
		}
	}
	
	/**
	 * Class that redirects {@link EntityWriter} methods to a given {@link EntityPersister}
	 * @param <K>
	 * @param <V>
	 * @param <KK>
	 * @param <VV>
	 * @param <SRCID>
	 * @author Guillaume Mary
	 */
	private static class RelationalPersisterAsEntityWriter<K, V, KK, VV, SRCID> implements EntityWriter<KeyValueRecord<K, V, SRCID>> {
		
		private final EntityPersister<KeyValueRecord<KK, VV, SRCID>, RecordId<KK, SRCID>> relationEntityPersister;
		private final Function<K, KK> keyMapper;
		private final Function<V, VV> valueMapper;
		
		public RelationalPersisterAsEntityWriter(EntityPersister<KeyValueRecord<KK, VV, SRCID>, RecordId<KK, SRCID>> relationEntityPersister,
												 Function<K, KK> keyMapper,
												 Function<V, VV> valueMapper) {
			this.relationEntityPersister = relationEntityPersister;
			this.keyMapper = keyMapper;
			this.valueMapper = valueMapper;
		}
		
		@Override
		public void update(Iterable<? extends Duo<KeyValueRecord<K, V, SRCID>, KeyValueRecord<K, V, SRCID>>> differencesIterable, boolean allColumnsStatement) {
			relationEntityPersister.update(Iterables.stream(differencesIterable)
					.map(keyValueRecordKeyValueRecordDuo -> {
						KeyValueRecord<K, V, SRCID> left = keyValueRecordKeyValueRecordDuo.getLeft();
						KeyValueRecord<K, V, SRCID> right = keyValueRecordKeyValueRecordDuo.getRight();
						return new Duo<>(
								new KeyValueRecord<>(left.getId().getId(),
										keyMapper.apply(left.getKey()), valueMapper.apply(left.getValue())),
								new KeyValueRecord<>(right.getId().getId(),
										keyMapper.apply(right.getKey()), valueMapper.apply(right.getValue())));
					})
					.collect(Collectors.toSet()), allColumnsStatement);
		}
		
		@Override
		public void delete(Iterable<? extends KeyValueRecord<K, V, SRCID>> entities) {
			relationEntityPersister.delete(Iterables.stream(entities)
					.map(entity -> new KeyValueRecord<>(entity.getId().getId(),
							keyMapper.apply(entity.getKey()), valueMapper.apply(entity.getValue())))
					.collect(Collectors.toSet()));
		}
		
		@Override
		public void insert(Iterable<? extends KeyValueRecord<K, V, SRCID>> entities) {
			relationEntityPersister.insert(Iterables.stream(entities)
					.map(entity -> new KeyValueRecord<>(entity.getId().getId(),
							keyMapper.apply(entity.getKey()), valueMapper.apply(entity.getValue())))
					.collect(Collectors.toSet()));
		}
		
		@Override
		public void persist(Iterable<? extends KeyValueRecord<K, V, SRCID>> entities) {
			relationEntityPersister.persist(Iterables.stream(entities)
					.map(entity -> new KeyValueRecord<>(entity.getId().getId(),
							keyMapper.apply(entity.getKey()), valueMapper.apply(entity.getValue())))
					.collect(Collectors.toSet()));
		}
		
		@Override
		public void updateById(Iterable<? extends KeyValueRecord<K, V, SRCID>> entities) {
			relationEntityPersister.updateById(Iterables.stream(entities)
					.map(entity -> new KeyValueRecord<>(entity.getId().getId(),
							keyMapper.apply(entity.getKey()), valueMapper.apply(entity.getValue())))
					.collect(Collectors.toSet()));
		}
		
		@Override
		public boolean isNew(KeyValueRecord<K, V, SRCID> entity) {
			// all records are persisted (not new) since we've just build them from database above
			return false;
		}
	}
}