package org.codefilarete.stalactite.engine.runtime.load;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;

import org.apache.commons.collections4.map.LinkedMap;
import org.codefilarete.stalactite.dsl.MappingConfigurationException;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater.NodeVisitor.EntityCreationResult;
import org.codefilarete.stalactite.engine.runtime.load.JoinRowConsumer.ExcludingJoinRowConsumer;
import org.codefilarete.stalactite.engine.runtime.load.JoinRowConsumer.ForkJoinRowConsumer;
import org.codefilarete.stalactite.engine.runtime.load.JoinRowConsumer.RootJoinRowConsumer;
import org.codefilarete.stalactite.engine.runtime.load.MergeJoinNode.MergeJoinRowConsumer;
import org.codefilarete.stalactite.engine.runtime.load.PassiveJoinNode.PassiveJoinRowConsumer;
import org.codefilarete.stalactite.engine.runtime.load.RelationJoinNode.BasicEntityCache;
import org.codefilarete.stalactite.engine.runtime.load.RelationJoinNode.EntityCache;
import org.codefilarete.stalactite.engine.runtime.load.RelationJoinNode.RelationJoinRowConsumer;
import org.codefilarete.stalactite.query.model.Fromable;
import org.codefilarete.stalactite.query.model.Selectable;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.stalactite.sql.result.Row;
import org.codefilarete.tool.Nullable;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.ThreadLocals;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.collection.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.codefilarete.tool.Nullable.nullable;

/**
 * Bean graph creator from database rows. Based on a tree of {@link ConsumerNode}s which wraps some {@link JoinRowConsumer}.
 * 
 * @param <C> main entity type
 * @author Guillaume Mary
 */
public class EntityTreeInflater<C> {
	
	public static final Logger LOGGER = LoggerFactory.getLogger(EntityTreeInflater.class);
	
	@SuppressWarnings("java:S5164" /* remove() is called by ThreadLocals.AutoRemoveThreadLocal */)
	private static final ThreadLocal<EntityTreeInflater.TreeInflationContext> CURRENT_CONTEXT = new ThreadLocal<>();
	
	/**
	 * Gives current {@link TreeInflationContext}. A current one is available during {@link #transform(Iterable, int)} invocation.
	 * 
	 * @return current {@link TreeInflationContext}, null if you're not invoking it during its lifecycle
	 */
	public static EntityTreeInflater.TreeInflationContext currentContext() {
		return CURRENT_CONTEXT.get();
	}
	
	/**
	 * All inflaters, mergers and so on, in a tree structure that reflects {@link EntityJoinTree}.
	 * Made as such to benefit from the possibility to cancel in-depth iteration in {@link #transform(ColumnedRow, TreeInflationContext)} method during relation
	 * building
	 */
	private final ConsumerNode consumerRoot;
	
	/**
	 * Constructor with necessary elements
	 * @param consumerRoot top level row consumer, the one that will compute root instances
	 */
	EntityTreeInflater(ConsumerNode consumerRoot) {
		this.consumerRoot = consumerRoot;
	}
	
	/**
	 *
	 * @param rows rows (coming from database select) to be read to build beans graph
	 * @param resultSize expected result size, only for resulting list optimization
	 * @return a set of root beans, built from given rows by asking internal strategy joins to instantiate and complete them
	 */
	public Set<C> transform(Iterable<? extends ColumnedRow> rows, int resultSize) {
		return ThreadLocals.doWithThreadLocal(CURRENT_CONTEXT, TreeInflationContext::new, (Function<EntityTreeInflater.TreeInflationContext, Set<C>>) context ->
						transform(rows, resultSize, context));
	}
	
	private Set<C> transform(Iterable<? extends ColumnedRow> rows, int resultSize, EntityTreeInflater.TreeInflationContext context) {
		// we use an "IdentitySet" (doesn't exist directly, but can be done through IdentityLinkedMap) to avoid duplicate entity : with a HashSet
		// duplicate can happen if equals/hashCode depends on relation, in particular Collection ones, because they are filled from row to row
		// making hashCode value change. Moreover we need to keep track of the order because query might be sorted through "order by" clause.
		Set<C> result = java.util.Collections.newSetFromMap(new IdentityLinkedMap<>(resultSize));
		for (ColumnedRow row : rows) {
			Nullable<C> newInstance = transform(row, context);
			newInstance.invoke(result::add);
		}
		return result;
	}
	
	Nullable<C> transform(ColumnedRow row, EntityTreeInflater.TreeInflationContext context) {
		context.setCurrentRow(row);
		// Algorithm : we iterate depth by depth the tree structure of the joins
		// We start by hierarchy root.
		// We process entity of current depth, process the direct relations, then add those relations to depth iterator
		LOGGER.debug("Creating instance with {}", this.consumerRoot.consumer);

		ColumnedRow rootRow = currentContext().getDecoder(consumerRoot.consumer.getNode());
		EntityCreationResult rootEntityCreationResult = getRootEntityCreationResult(rootRow, context);

		if (rootEntityCreationResult != null) {
			foreachNode(rootEntityCreationResult.consumers, new NodeVisitor(rootEntityCreationResult.entity) {
				
				@Override
				public EntityCreationResult apply(ConsumerNode join, Object entity) {
					// processing current depth
					JoinRowConsumer consumer = join.getConsumer();
					LOGGER.debug("Consuming {} on object {}", consumer, entity);
					consumer.beforeRowConsumption(context);
					ColumnedRow nodeRow = currentContext().getDecoder(consumer.getNode());
					if (consumer instanceof PassiveJoinNode.PassiveJoinRowConsumer) {
						((PassiveJoinRowConsumer) consumer).consume(entity, nodeRow);
						return new EntityCreationResult(entity, join);
					} else if (consumer instanceof MergeJoinNode.MergeJoinRowConsumer) {
						((MergeJoinRowConsumer) consumer).mergeProperties(entity, nodeRow);
						return new EntityCreationResult(entity, join);
					} else if (consumer instanceof RelationJoinNode.RelationJoinRowConsumer) {
						Object relatedEntity = ((RelationJoinRowConsumer) consumer).applyRelatedEntity(entity, nodeRow, context);
						EntityCreationResult result;
						if (consumer instanceof ForkJoinRowConsumer) {
							// In case of join-table polymorphism we have to provide the tree branch on which id was found
							// in order to let created entity filled with right consumers. "Wrong" branches serve no purpose. 
							JoinRowConsumer nextRowConsumer = ((ForkJoinRowConsumer) consumer).giveNextConsumer();
							if (nextRowConsumer == null) {
								// means no identifier of polymorphic entity
								result = new EntityCreationResult(null, (List<ConsumerNode>) null);
							} else {
								Optional<ConsumerNode> consumerNode = join.consumers.stream().filter(c -> nextRowConsumer == c.consumer).findFirst();
								if (!consumerNode.isPresent()) {
									throw new IllegalStateException("Can't find consumer node for " + nextRowConsumer + " in " + join.consumers);
								} else {
									result = new EntityCreationResult(relatedEntity, Arrays.asList(consumerNode.get()));
								}
							}
						} else {
							result = new EntityCreationResult(relatedEntity, join);
						}
						consumer.afterRowConsumption(context);
						return result;
					} else {
						// Developer made something wrong because other types than MergeJoin and RelationJoin are not expected
						throw new IllegalArgumentException("Unexpected join type, only "
								+ Reflections.toString(PassiveJoinRowConsumer.class)
								+ ", " + Reflections.toString(MergeJoinRowConsumer.class)
								+ " and " + Reflections.toString(RelationJoinRowConsumer.class) + " are handled"
								+ ", not " + (consumer == null ? "null" : Reflections.toString(consumer.getClass())));
					}
				}
			});
		}
		return nullable(rootEntityCreationResult).map(c -> (C) c.entity);
	}
	
	private EntityCreationResult getRootEntityCreationResult(ColumnedRow row, EntityTreeInflater.TreeInflationContext context) {
		C rootInstance = ((RootJoinRowConsumer<C>) this.consumerRoot.consumer).createRootInstance(row, context);
		if (rootInstance != null) {
			if (consumerRoot.consumer instanceof ExcludingJoinRowConsumer) {
				// In case of polymorphism we have to provide the tree branch on which instance was found
				// in order to let created entity filled with right consumers. "Wrong" branches serve no purpose. 
				Set<JoinRowConsumer> deadBranches = ((ExcludingJoinRowConsumer) consumerRoot.consumer).giveExcludedConsumers();
				ArrayList<ConsumerNode> consumerNodes = new ArrayList<>(consumerRoot.consumers);
				consumerNodes.removeIf(consumer -> deadBranches.contains(consumer.consumer));
				return new EntityCreationResult(rootInstance, consumerNodes);
			} else {
				return new EntityCreationResult(rootInstance, consumerRoot.consumers);
			}
		} else {
			return null;
		}
	}
	
	@VisibleForTesting
	void foreachNode(Collection<ConsumerNode> seeds, NodeVisitor nodeVisitor) {
		Queue<ConsumerNode> joinNodeStack = Collections.newLifoQueue();
		joinNodeStack.addAll(seeds);
		// Maintaining entities that will be given to each node : they are entities produced by parent node
		Map<ConsumerNode, Object> entityPerConsumer = new HashMap<>(10);
		joinNodeStack.forEach(node -> entityPerConsumer.put(node, nodeVisitor.entityRoot));

		while (!joinNodeStack.isEmpty()) {
			ConsumerNode joinNode = joinNodeStack.poll();
			EntityCreationResult result = nodeVisitor.apply(joinNode, entityPerConsumer.get(joinNode));
			if (result.getEntity() != null) {
				List<ConsumerNode> nextConsumers = result.nextConsumers();
				joinNodeStack.addAll(nextConsumers);
				nextConsumers.forEach(node -> entityPerConsumer.put(node, result.getEntity()));
			}
		}
	}

	/**
	 * Small structure to store {@link JoinRowConsumer} as a tree that reflects {@link EntityJoinTree} input.
	 */
	static class ConsumerNode {
		
		private final JoinRowConsumer consumer;
		
		private final List<ConsumerNode> consumers = new ArrayList<>();
		
		ConsumerNode(JoinRowConsumer consumer) {
			this.consumer = consumer;
		}
		
		public JoinRowConsumer getConsumer() {
			return consumer;
		}
		
		void addConsumer(ConsumerNode consumer) {
			this.consumers.add(consumer);
		}
	}
	
	static abstract class NodeVisitor {
		
		private final Object entityRoot;
		
		NodeVisitor(Object entityRoot) {
			this.entityRoot = entityRoot;
		}
		
		/**
		 * Asks for parentEntity consumption by {@link ConsumerNode}
		 * @param consumerNode consumer expected to use given entity to constructs, fills, does whatever, with given entity
		 * @param parentEntity entity on which consumer mechanism may apply
		 * @return the optional entity created by consumer (as in one-to-one or one-to-many relation), else given parentEntity (not null)
		 */
		abstract EntityCreationResult apply(ConsumerNode consumerNode, Object parentEntity);
		
		static class EntityCreationResult {
			
			private final Object entity;
			
			private final List<ConsumerNode> consumers;
			
			EntityCreationResult(Object entity, ConsumerNode entityCreator) {
				this(entity, entityCreator.consumers);
			}
			
			EntityCreationResult(Object entity, List<ConsumerNode> consumers) {
				this.entity = entity;
				this.consumers = consumers;
			}
			
			public Object getEntity() {
				return entity;
			}
			
			List<ConsumerNode> nextConsumers() {
				return consumers;
			}
		}
	}
	
	/**
	 * Represents a relation between an entity and another one through its join node 
	 */
	static class RelationIdentifier {
		
		protected final Object rootEntity;
		protected final Class relatedEntityType;
		protected final Object relatedBeanIdentifier;
		protected final RelationJoinRowConsumer joinNode;
		
		RelationIdentifier(Object rootEntity, Class<?> relatedEntityType, Object relatedBeanIdentifier, RelationJoinRowConsumer joinNode) {
			this.rootEntity = rootEntity;
			this.relatedEntityType = relatedEntityType;
			this.relatedBeanIdentifier = relatedBeanIdentifier;
			this.joinNode = joinNode;
		}
		
		/**
		 * Implemented so inflater algorithm can check for already treated relation
		 * @param o another RelationIdentifier
		 * @return true if given instance equals this one
		 */
		@Override
		public boolean equals(Object o) {
			if (this == o) return true;
			if (!(o instanceof EntityTreeInflater.RelationIdentifier)) return false;
			
			RelationIdentifier other = (RelationIdentifier) o;
			
			// WARN : this is finely defined according to :
			// - comparison of root entity is based on instance comparison to avoid being dependent of equals() implementation which may vary during inflation process
			// - comparison with related bean is based on its identifier with Object equality because it is expected to be simple and comparable type
			// - comparison of Join Node which stores kind of "relation name" that links current beans (Object equality could be used but using instance matched more our expectation)
			if (rootEntity != other.rootEntity) return false;
			if (!relatedEntityType.equals(other.relatedEntityType)) return false;
			if (!relatedBeanIdentifier.equals(other.relatedBeanIdentifier)) return false;
			return joinNode == other.joinNode;
		}
		
		@Override
		public int hashCode() {
			int result = rootEntity.hashCode();
			result = 31 * result + relatedEntityType.hashCode();
			result = 31 * result + relatedBeanIdentifier.hashCode();
			result = 31 * result + joinNode.hashCode();
			return result;
		}
	}
	
	/**
	 * Container for information used during {@link Row} transformation as a bean graph.
	 * Accessible from {@link EntityTreeInflater#currentContext()} during its lifecycle:
	 * - instanced at beginning of {@link Row} transformation ({@link EntityTreeInflater#transform(Iterable, int)}
	 * - dropped at the end of the method
	 * 
	 * @implNote made non-static to ease access to the surrounding {@link EntityTreeInflater} instance
	 */
	public static class TreeInflationContext {
		
		/** Entity cache */
		private final EntityCache entityCache;
		
		/** Storage for treated relations */
		private final Set<RelationIdentifier> treatedRelations = new HashSet<>();
		
		/**
		 * Currently (Thread-linked) row read by inflater.
		 * This row is not linked to any node and can be used has a base to construct some other {@link ColumnedRow}
		 * based on some {@link JoinNode}.
		 * @see #getDecoder
		 */
		private ColumnedRow currentRow;
		
		private TreeInflationContext() {
			this(new BasicEntityCache());
		}
		
		public TreeInflationContext(EntityCache entityCache) {
			this.entityCache = entityCache;
		}
		
		private TreeInflationContext setCurrentRow(ColumnedRow currentRow) {
			this.currentRow = currentRow;
			return this;
		}
		
		/**
		 * Gives the value provider of current {@link java.sql.ResultSet} row for given {@link JoinNode}.
		 * 
		 *
		 * @param joinNode relation node identifier 
		 * @return null if data is null
		 * @see EntityTreeQueryBuilder#cloneTable(JoinNode)  table clone mechanism.
		 */
		 public ColumnedRow getDecoder(JoinNode<?, ?> joinNode) {
			Fromable table = joinNode.getTable();
			return new ColumnedRow() {
				@Override
				public <E> E get(Selectable<E> key) {
					Selectable<E> column = table.findColumn(key.getExpression());
					if (column == null) {
						// This is more for debugging purpose than for a real production goal, may be removed later
						throw new MappingConfigurationException("Can't find column for " + key.getExpression() + " in table : " + joinNode.getTable().getName());
					}
					return currentRow.get(column);
				}
			};
		}
		
		public boolean isTreatedOrAppend(RelationIdentifier relationIdentifier) {
			boolean alreadyTreated = treatedRelations.contains(relationIdentifier);
			if (!alreadyTreated) {
				treatedRelations.add(relationIdentifier);
			}
			return !alreadyTreated;
		}
		
		/**
		 * Expected to retrieve an entity by its class and identifier from cache or instantiates it and put it into the cache
		 *
		 * @param clazz the type of the entity
		 * @param identifier the identifier of the entity (Long, String, ...)
		 * @param fallbackFactory the "method" that will be called to create the entity when the entity is not in the cache
		 * @return the existing instance in the cache or a new object
		 */
		public <E> E giveEntityFromCache(Class<E> clazz, Object identifier, Supplier<E> fallbackFactory) {
			return this.entityCache.computeIfAbsent(clazz, identifier, fallbackFactory);
		}
	}
	
	/**
	 * A {@link LinkedMap} basing its hash on {@link System#identityHashCode(Object)} one, and comparing key on their reference.
	 * Made to have a {@link Map} that hashes on identity while keeping insertion order.
	 *
	 * @param <K>
	 * @param <V>
	 * @author Guillaume Mary
	 */
	private static class IdentityLinkedMap<K, V> extends LinkedMap<K, V> {
		
		public IdentityLinkedMap(int initialCapacity) {
			super(initialCapacity);
		}
		
		@Override
		protected int hash(Object key) {
			return System.identityHashCode(key);
		}
		
		@Override
		protected boolean isEqualKey(Object key1, Object key2) {
			return key1 == key2;
		}
	}
}
