package org.codefilarete.stalactite.engine.runtime;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;

import org.codefilarete.stalactite.engine.EntityPersister;
import org.codefilarete.stalactite.engine.diff.AbstractDiff;
import org.codefilarete.stalactite.engine.diff.CollectionDiffer;
import org.codefilarete.stalactite.engine.diff.Diff;
import org.codefilarete.stalactite.engine.diff.State;
import org.codefilarete.stalactite.engine.listener.UpdateListener.UpdatePayload;
import org.codefilarete.stalactite.mapping.IdAccessor;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.collection.Iterables;

/**
 * Class aimed at making the difference of entities of an {@link UpdatePayload} and updating, inserting or deleting them according to difference
 * found between the two collections.
 * Gives entry points to add some more behavior for each of those actions. 
 * 
 * @param <I> source entity type 
 * @param <O> collection item type
 * @param <C> collection type
 * @author Guillaume Mary
 */
public class CollectionUpdater<I, O, C extends Collection<O>> implements BiConsumer<Duo<I, I>, Boolean> {
	
	private final CollectionDiffer<O> differ;
	
	private final Function<I, C> collectionGetter;
	private final BiConsumer<O, I> reverseSetter;
	protected final EntityWriter<O> elementPersister;
	private final boolean shouldDeleteRemoved;
	
	/**
	 * Default and simple use case constructor.
	 * See {@link #CollectionUpdater(Function, EntityPersister, BiConsumer, boolean, Function)} for particular case about id policy
	 * 
	 * @param collectionGetter getter for collection from source entity
	 * @param elementPersister target entities persister
	 * @param reverseSetter setter for applying source entity to target objects, give null if no reverse mapping exists
	 * @param shouldDeleteRemoved true to delete orphans
	 */
	public CollectionUpdater(Function<I, C> collectionGetter,
							 ConfiguredPersister<O, ?> elementPersister,
							 @Nullable BiConsumer<O, I> reverseSetter,
							 boolean shouldDeleteRemoved) {
		this(collectionGetter, elementPersister, reverseSetter, shouldDeleteRemoved, elementPersister.getMapping()::getId);
	}
	
	/**
	 * Constructor that lets one defines id policy : in some cases default policy (based on
	 * {@link IdAccessor#getId(Object)}) is not sufficient, such as when {@link Collection} contains "value type".
	 * 
	 * @param collectionGetter getter for collection from source entity
	 * @param elementPersister target entities persister
	 * @param reverseSetter setter for applying source entity to target entities (used by {@link #onRemovedElements(UpdateContext, AbstractDiff)} to 
	 * 						nullify relation), null accepted if no reverse mapping exists
	 * @param shouldDeleteRemoved true to delete orphans
	 * @param idProvider expected to provide identifier of entities, used to store them per their id (in HashMap) to avoid usage of entity equals/hashcode
	 */
	public CollectionUpdater(Function<I, C> collectionGetter,
							 EntityPersister<O, ?> elementPersister,
							 @Nullable BiConsumer<O, I> reverseSetter,
							 boolean shouldDeleteRemoved,
							 Function<O, ?> idProvider) {
		this.collectionGetter = collectionGetter;
		this.reverseSetter = reverseSetter;
		this.elementPersister = new EntityPersisterEntityWriterAdaptor<>(elementPersister);
		this.shouldDeleteRemoved = shouldDeleteRemoved;
		this.differ = new CollectionDiffer<>(idProvider);
	}
	
	public CollectionUpdater(Function<I, C> collectionGetter,
							 EntityWriter<O> elementPersister,
							 @Nullable BiConsumer<O, I> reverseSetter,
							 boolean shouldDeleteRemoved,
							 Function<O, ?> idProvider) {
		this.collectionGetter = collectionGetter;
		this.reverseSetter = reverseSetter;
		this.elementPersister = elementPersister;
		this.shouldDeleteRemoved = shouldDeleteRemoved;
		this.differ = new CollectionDiffer<>(idProvider);
	}
	
	public CollectionDiffer<O> getDiffer() {
		return differ;
	}
	
	@Override
	public void accept(Duo<I, I> entry, Boolean allColumnsStatement) {
		C modified = collectionGetter.apply(entry.getLeft());
		C unmodified = collectionGetter.apply(entry.getRight());
		UpdateContext updateContext = newUpdateContext(entry);
		if (modified == null && unmodified == null) {
			// nothing to do since reference hasn't changed : still null
		} else if (modified != null && unmodified == null) {
			modified.forEach(o -> onAddedElements(updateContext, new Diff<>(State.ADDED, null, o)));
		} else if (modified == null && unmodified != null) {
			unmodified.forEach(o -> onRemovedElements(updateContext, new Diff<>(State.REMOVED, o, null)));
		} else {
			Set<? extends AbstractDiff<O>> diffSet = diff(modified, unmodified);
			for (AbstractDiff<O> diff : diffSet) {
				switch (diff.getState()) {
					case ADDED:
						onAddedElements(updateContext, diff);
						break;
					case HELD:
						onHeldElements(updateContext, diff);
						break;
					case REMOVED:
						onRemovedElements(updateContext, diff);
						break;
				}
			}
		}
		// is there any better order for these statements ?
		updateTargets(updateContext, allColumnsStatement);
		deleteTargets(updateContext);
		insertTargets(updateContext);
	}
	
	/**
	 * Updates collection entities
	 * @param updateContext context created by {@link #newUpdateContext(Duo)}
	 * @param allColumnsStatement indicates if all (mapped) columns of entities must be in statement, else only modified ones will be updated
	 */
	protected void updateTargets(UpdateContext updateContext, boolean allColumnsStatement) {
		List<Duo<O, O>> updateInput = Iterables.collectToList(updateContext.getHeldElements(),
				(AbstractDiff diff) -> new Duo<>((O) diff.getReplacingInstance(), (O) diff.getSourceInstance()));
		// NB: update will only be done if necessary by target persister
		elementPersister.update(updateInput, allColumnsStatement);
	}
	
	/**
	 * Deletes entities removed from collection (only when orphan removal is asked)
	 * @param updateContext context created by {@link #newUpdateContext(Duo)}
	 */
	protected void deleteTargets(UpdateContext updateContext) {
		elementPersister.delete(updateContext.getRemovedElements());
	}
	
	/**
	 * Insert entities added to collection
	 * @param updateContext context created by {@link #newUpdateContext(Duo)}
	 */
	protected void insertTargets(UpdateContext updateContext) {
		// added entities may be to be inserted or updated, not only inserted if they were already in another Collection
		elementPersister.persist(updateContext.getAddedElements());
	}
	
	/**
	 * Method in charge of making the differences between 2 collections of entities (many side type)
	 */
	protected Set<? extends AbstractDiff<O>> diff(Collection<O> modified, Collection<O> unmodified) {
		// Casting to Set is a bit weird here but we should not be in another since List is taken into account upstream through mapping and redirected
		// to dedicated case (ListCollectionUpdater)
		return differ.diff(unmodified, modified);
	}
	
	/**
	 * Methods asked to give a new {@link UpdateContext}. The returned instance will be passed to methods
	 * {@link #onAddedElements(UpdateContext, AbstractDiff)}, {@link #onHeldElements(UpdateContext, AbstractDiff)} and {@link #onRemovedElements(UpdateContext, AbstractDiff)}.
	 * Can be overridden to return a subtype and richer {@link UpdateContext}.
	 * 
	 * @param updatePayload instance given to {@link #accept(Duo, Boolean)}
	 * @return a new {@link UpdateContext} with given payload
	 */
	protected UpdateContext newUpdateContext(Duo<I, I> updatePayload) {
		return new UpdateContext(updatePayload);
	}
	
	protected void onAddedElements(UpdateContext updateContext, AbstractDiff<O> diff) {
		updateContext.getAddedElements().add(diff.getReplacingInstance());
	}
	
	protected void onHeldElements(UpdateContext updateContext, AbstractDiff<O> diff) {
		updateContext.getHeldElements().add(diff);
	}
	
	protected void onRemovedElements(UpdateContext updateContext, AbstractDiff<O> diff) {
		// we delete only persisted entity to prevent from a not found record
		if (shouldDeleteRemoved) {
			if (!elementPersister.isNew(diff.getSourceInstance())) {
				updateContext.getRemovedElements().add(diff.getSourceInstance());
			}
		} else // entity shouldn't be deleted, so we may have to update it
			if (reverseSetter != null) {
				// we cut the link between target and source
				// NB : we don't take versioning into account overall because we can't : how to do it since we miss the unmodified version ?
				reverseSetter.accept(diff.getSourceInstance(), null);
				elementPersister.updateById(Collections.singleton(diff.getSourceInstance()));
			}
	}
	
	protected class UpdateContext {
		
		private final Duo<I, I> payload;
		/** List of many-side entities to be inserted (for massive SQL orders and better debug) */
		private final List<O> addedElements = new ArrayList<>();
		/** List of many-side entities to be update (for massive SQL orders and better debug) */
		private final List<AbstractDiff<O>> heldElements = new ArrayList<>();
		/** List of many-side entities to be deleted (for massive SQL orders and better debug) */
		private final List<O> removedElements = new ArrayList<>();
		
		public UpdateContext(Duo<I, I> updatePayload) {
			this.payload = updatePayload;
		}
		
		public Duo<I, I> getPayload() {
			return payload;
		}
		
		public List<O> getAddedElements() {
			return addedElements;
		}
		
		public List<AbstractDiff<O>> getHeldElements() {
			return heldElements;
		}
		
		public List<O> getRemovedElements() {
			return removedElements;
		}
	}
	
	/**
	 * A dedicated interface for this class use case. Avoid to implements too many method coming from
	 * {@link EntityPersister} in particular.
	 * 
	 * @param <C> entity type to be managed
	 * @author Guillaume Mary
	 */
	public interface EntityWriter<C> {
		
		void update(Iterable<? extends Duo<C, C>> differencesIterable, boolean allColumnsStatement);
		
		void delete(Iterable<? extends C> entities);
		
		void insert(Iterable<? extends C> entities);
		
		void persist(Iterable<? extends C> entities);
		
		boolean isNew(C entity);
		
		void updateById(Iterable<? extends C> entities);
		
	}
	
	/**
	 * Internal adaptor that makes an {@link EntityPersister} become an {@link EntityWriter}.
	 * Just some methods redirecting to delegate ones.
	 * 
	 * @param <C> entity type to be managed
	 * @author Guillaume Mary
	 */
	private static class EntityPersisterEntityWriterAdaptor<C> implements EntityWriter<C> {
		
		private final EntityPersister<C, ?> delegate;
		
		private EntityPersisterEntityWriterAdaptor(EntityPersister<C, ?> delegate) {
			this.delegate = delegate;
		}
		
		@Override
		public void update(Iterable<? extends Duo<C, C>> differencesIterable, boolean allColumnsStatement) {
			delegate.update(differencesIterable, allColumnsStatement);
		}
		
		@Override
		public void delete(Iterable<? extends C> entities) {
			delegate.delete(entities);
		}
		
		@Override
		public void insert(Iterable<? extends C> entities) {
			delegate.insert(entities);
		}
		
		@Override
		public void persist(Iterable<? extends C> entities) {
			delegate.persist(entities);
		}
		
		@Override
		public boolean isNew(C entity) {
			return delegate.isNew(entity);
		}
		
		@Override
		public void updateById(Iterable<? extends C> entities) {
			delegate.updateById(entities);
		}
	}
}
