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

import javax.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
import org.codefilarete.reflection.Accessor;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater.TreeInflationContext;
import org.codefilarete.stalactite.engine.runtime.load.MergeJoinNode.MergeJoinRowConsumer;
import org.codefilarete.stalactite.mapping.AbstractTransformer;
import org.codefilarete.stalactite.mapping.EntityMapping;
import org.codefilarete.stalactite.mapping.RowTransformer;
import org.codefilarete.stalactite.query.model.Fromable;
import org.codefilarete.stalactite.query.model.JoinLink;
import org.codefilarete.stalactite.query.model.Query;
import org.codefilarete.stalactite.query.model.QueryStatement.PseudoColumn;
import org.codefilarete.stalactite.query.model.QueryStatement.PseudoTable;
import org.codefilarete.stalactite.query.model.Selectable;
import org.codefilarete.stalactite.query.model.Union;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Key;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.BeanRelationFixer;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.bean.Randomizer;
import org.codefilarete.tool.bean.Randomizer.LinearRandomGenerator;
import org.codefilarete.tool.collection.Arrays;
import org.codefilarete.tool.collection.Collections;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.collection.Maps;
import org.codefilarete.tool.collection.ReadOnlyList;

import static org.codefilarete.stalactite.sql.ddl.structure.Table.COMPARATOR_ON_SCHEMA_AND_NAME;

/**
 * Tree representing joins of a from clause, nodes are {@link JoinNode}.
 * It maintains an index of its joins based on an unique name for each, so they can be referenced outside {@link EntityJoinTree} without
 * depending on classes of this package (since the reference is a {@link String}).
 *
 * @author Guillaume Mary
 */
public class EntityJoinTree<C, I> {
	
	/**
	 * Key of the very first {@link EntityJoinTree} added to the join structure (the one generated by constructor), see {@link #getRoot()}
	 */
	public static final String ROOT_JOIN_NAME = "ROOT";
	
	private final JoinRoot<C, I, ?> root;
	
	/**
	 * A mapping between a name and a join to find them when we want to join one with another new one
	 * 
	 * @see #addRelationJoin(String, EntityInflater, Accessor, Key, Key, String, JoinType, BeanRelationFixer, Set)
	 * @see #indexKeyGenerator
	 */
	// Implemented as a LinkedHashMap to keep order only for debugging purpose
	private final BidiMap<String, JoinNode<?, ?>> joinIndex = new DualLinkedHashBidiMap<>();
	
	/**
	 * The objet that will help to give node names / keys into the index (no impact on the generated SQL)
	 * 
	 * @see #joinIndex
	 */
	private final NodeKeyGenerator indexKeyGenerator = new NodeKeyGenerator();
	
	// because Table doesn't implement an hashCode and because we may have clone in JoinNodes, we use a smart TreeSet to avoid duplicates
	private final Set<Table<?>> tablesToExcludeFromDDL = new TreeSet<>(COMPARATOR_ON_SCHEMA_AND_NAME);
	
	private final Set<Table<?>> tablesToIncludeToDDL = new TreeSet<>(COMPARATOR_ON_SCHEMA_AND_NAME);
	
	public EntityJoinTree(EntityMapping<C, I, ?> entityMapping) {
		this(new EntityInflater.EntityMappingAdapter<>(entityMapping), entityMapping.getTargetTable());
	}
	
	public EntityJoinTree(EntityInflater<C, I> rootEntityInflater, Fromable table) {
		this.root = new JoinRoot<>(this, rootEntityInflater, table);
		this.joinIndex.put(ROOT_JOIN_NAME, root);
	}
	
	public EntityJoinTree(Function<EntityJoinTree<C, I>, JoinRoot<C, I, ?>> joinRootCreator) {
		this.root = joinRootCreator.apply(this);
		this.joinIndex.put(ROOT_JOIN_NAME, root);
	}
	
	public JoinRoot<C, I, ?> getRoot() {
		return root;
	}
	
	/**
	 * Returns mapping between {@link JoinNode} and their internal name.
	 *
	 * @return an unmodifiable version of the internal mapping (because its maintenance responsibility falls to current class)
	 */
	@VisibleForTesting
	public BidiMap<String, JoinNode> getJoinIndex() {
		return UnmodifiableBidiMap.unmodifiableBidiMap(joinIndex);
	}
	
	/**
	 * Declares a {@link Table} to be included in DDL generation.
	 * To be used for particular use cases because {@link #giveTables()} collects this tree tables.
	 * 
	 * @param table any {@link Table}
	 */
	public void addTableToIncludeToDDL(Table<?> table) {
		this.tablesToIncludeToDDL.add(table);
	}
	
	/**
	 * Adds a join to this select.
	 * Use for one-to-one or one-to-many cases when join is used to describe a related bean.
	 *
	 * @param <U> type of bean mapped by the given strategy
	 * @param <T1> joined left table
	 * @param <T2> joined right table
	 * @param <ID> type of joined values
	 * @param leftStrategyName the name of a (previously) registered join. {@code leftJoinColumn} must be a {@link Column} of its left {@link Table}
	 * @param inflater the strategy of the mapped bean. Used to give {@link Column}s and {@link RowTransformer}
	 * @param propertyAccessor accessor to the property of this persister's entity from the source entity type
	 * @param leftJoinColumn the {@link Column} (of a previously registered join) to be joined with {@code rightJoinColumn}
	 * @param rightJoinColumn the {@link Column} to be joined with {@code leftJoinColumn}
	 * @param rightTableAlias optional alias for right table, if null table name will be used
	 * @param joinType says whether the join must be open
	 * @param beanRelationFixer a function to fulfill relation between beans
	 * @param additionalSelectableColumns columns to be added to SQL select clause out of ones took from given inflater, necessary for indexed relations
	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
	 */
	public <U, T1 extends Table<T1>, T2 extends Table<T2>, ID> String addRelationJoin(String leftStrategyName,
																					  EntityInflater<U, ID> inflater,
																					  Accessor<?, ?> propertyAccessor,
																					  Key<T1, ID> leftJoinColumn,
																					  Key<T2, ID> rightJoinColumn,
																					  @Nullable String rightTableAlias,
																					  JoinType joinType,
																					  BeanRelationFixer<C, U> beanRelationFixer,
																					  Set<? extends Column<T2, ?>> additionalSelectableColumns) {
		return addRelationJoin(leftStrategyName, inflater, propertyAccessor, leftJoinColumn, rightJoinColumn, rightTableAlias, joinType, beanRelationFixer, additionalSelectableColumns, null);
	}
	
	/**
	 * Adds a join to this select.
	 * Use for one-to-one or one-to-many cases when join is used to describe a related bean.
	 * Difference with {@link #addRelationJoin(String, EntityInflater, Accessor, Key, Key, String, JoinType, BeanRelationFixer, Set)} is last
	 * parameter : an optional function which computes an identifier of a relation between 2 entities, this is required to prevent from fulfilling
	 * twice the relation when SQL returns several times same identifier (when at least 2 collections are implied). By default this function is
	 * made of parentEntityId + childEntityId and can be overwritten here (in particular when relation is a List, entity index is added to computation).
	 * See {@link RelationJoinNode.RelationJoinRowConsumer#applyRelatedEntity(Object, ColumnedRow, TreeInflationContext)} for usage.
	 *
	 * @param <U> type of bean mapped by the given strategy
	 * @param <T1> joined left table
	 * @param <T2> joined right table
	 * @param <ID> type of joined values
	 * @param leftStrategyName the name of a (previously) registered join. {@code leftJoinColumn} must be a {@link Column} of its left {@link Table}
	 * @param inflater the strategy of the mapped bean. Used to give {@link Column}s and {@link RowTransformer}
	 * @param propertyAccessor accessor to the property of this persister's entity from the source entity type   
	 * @param leftJoinColumn the {@link Column} (of a previously registered join) to be joined with {@code rightJoinColumn}
	 * @param rightJoinColumn the {@link Column} to be joined with {@code leftJoinColumn}
	 * @param rightTableAlias optional alias for right table, if null table name will be used
	 * @param joinType says whether the join must be open
	 * @param beanRelationFixer a function to fulfill relation between beans
	 * @param additionalSelectableColumns columns to be added to SQL select clause out of ones took from given inflater, necessary for indexed relations
	 * @param relationIdentifierProvider relation identifier provider, not null for List cases : necessary because List may contain duplicate
	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
	 * @see RelationJoinNode.RelationJoinRowConsumer#applyRelatedEntity(Object, ColumnedRow, TreeInflationContext)
	 */
	public <U, T1 extends Table<T1>, T2 extends Table<T2>, ID, JOINTYPE> String addRelationJoin(String leftStrategyName,
																								EntityInflater<U, ID> inflater,
																								Accessor<?, ?> propertyAccessor,
																								Key<T1, JOINTYPE> leftJoinColumn,
																								Key<T2, JOINTYPE> rightJoinColumn,
																								@Nullable String rightTableAlias,
																								JoinType joinType,
																								BeanRelationFixer<?, U> beanRelationFixer,
																								Set<? extends Column<T2, ?>> additionalSelectableColumns,
																								@Nullable Function<ColumnedRow, Object> relationIdentifierProvider) {
		return this.addJoin(leftStrategyName, parent -> {
			Duo<T2, IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>>> tableClone = cloneTable(rightJoinColumn.getTable());
			// Build a new Key using the cloned table and the corresponding cloned columns
			Key.KeyBuilder<T2, JOINTYPE> rightJoinLinkBuilder = Key.from(tableClone.getLeft());
			Set<? extends JoinLink<?, ?>> columns = rightJoinColumn.getColumns();
			for (JoinLink<?, ?> column : columns) {
				// Note that we can cast to JoinLink because we're already dealing with JoinLink since we are in JoinNode
				JoinLink<T2, Object> clonedColumn = (JoinLink<T2, Object>) tableClone.getRight().get(column);
				rightJoinLinkBuilder.addColumn(clonedColumn);
			}
			
			// We create the column mapping from the original node column to the cloned columns, not from the table clone ones.
			// This allows keeping the original columns in the map (user's one), which is necessary for caller to decode the result set 
			IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> originalColumnsToClones = tableClone.getRight();
			
			return new RelationJoinNode<U, T1, T2, JOINTYPE, ID>(
					(JoinNode) parent,
					propertyAccessor,
					leftJoinColumn,
					rightJoinLinkBuilder.build(),
					joinType,
					new KeepOrderSet<>(Collections.cat(inflater.getSelectableColumns(), additionalSelectableColumns)),
					rightTableAlias,
					inflater,
					beanRelationFixer,
					relationIdentifierProvider,
					originalColumnsToClones);
		});
	}
	
	
	/**
	 * Adds a join to this select.
	 * Use for inheritance cases when joined data are used to complete an existing bean.
	 *
	 * @param <U> type of bean mapped by the given strategy
	 * @param <T1> left table type
	 * @param <T2> right table type
	 * @param <ID> type of joined values
	 * @param leftStrategyName the name of a (previously) registered join. {@code leftJoinColumn} must be a {@link Column} of its left {@link Table}.
	 * 						Right table data will be merged with this "root".
	 * @param inflater the strategy of the mapped bean. Used to give {@link Column}s and {@link RowTransformer}
	 * @param leftJoinColumn the {@link Column} (of previous strategy left table) to be joined with {@code rightJoinColumn}
	 * @param rightJoinColumn the {@link Column} (of the strategy table) to be joined with {@code leftJoinColumn}
	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
	 */
	public <U, T1 extends Fromable, T2 extends Fromable, ID> String addMergeJoin(String leftStrategyName,
																				 EntityMerger<U> inflater,
																				 Key<T1, ID> leftJoinColumn,
																				 Key<T2, ID> rightJoinColumn) {
		return this.addJoin(leftStrategyName, parent -> new MergeJoinNode<>((JoinNode<?, T1>) parent,
				leftJoinColumn, rightJoinColumn, JoinType.INNER,
				null, inflater));
	}
	
	/**
	 * Adds a merge join to this select : no bean will be created by given {@link EntityInflater}, only its
	 * {@link AbstractTransformer#applyRowToBean(ColumnedRow, Object)} will be used during bean graph loading process.
	 *
	 * @param <T1> left table type
	 * @param <T2> right table type
	 * @param <ID> type of joined values
	 * @param leftStrategyName join name on which join must be created
	 * @param entityMerger strategy to be used to load bean
	 * @param leftJoinColumn left join column, expected to be one of left strategy table
	 * @param rightJoinColumn right join column
	 * @param joinType type of join to create
	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
	 */
	public <U, T1 extends Fromable, T2 extends Fromable, ID> String addMergeJoin(String leftStrategyName,
																				 EntityMerger<U> entityMerger,
																				 Key<T1, ID> leftJoinColumn,
																				 Key<T2, ID> rightJoinColumn,
																				 JoinType joinType) {
		return this.addJoin(leftStrategyName, parent -> new MergeJoinNode<>((JoinNode<?, T1>) parent,
				leftJoinColumn, rightJoinColumn, joinType,
				null, entityMerger));
	}
	
	public <U, T1 extends Fromable, T2 extends Fromable, ID> String addMergeJoin(String leftStrategyName,
																				 EntityMerger<U> entityMerger,
																				 Key<T1, ID> leftJoinColumn,
																				 Key<T2, ID> rightJoinColumn,
																				 JoinType joinType,
																				 Function<JoinNode<U, T2>, MergeJoinRowConsumer<U>> columnedRowConsumer) {
		return this.addJoin(leftStrategyName, parent -> new MergeJoinNode<U, T1, T2, ID>((JoinNode<?, T1>) parent,
				leftJoinColumn, rightJoinColumn, joinType,
				null, entityMerger) {
			@Override
			public MergeJoinRowConsumer<U> toConsumer(JoinNode<U, T2> joinNode) {
				return columnedRowConsumer.apply(joinNode);
			}
		});
	}
	
	/**
	 * Adds a passive join to this select : this kind of join doesn't take part to bean construction, it aims only at adding an SQL join to
	 * bean graph loading.
	 *
	 * @param leftStrategyName join name on which join must be created
	 * @param leftJoinColumn left join column, expected to be one of left strategy table
	 * @param rightJoinColumn right join column
	 * @param joinType type of join to create
	 * @param columnsToSelect columns that must be added to final select
	 * @param <T1> left table type
	 * @param <T2> right table type
	 * @param <JOINTYPE> type of joined values
	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
	 */
	public <T1 extends Table<T1>, T2 extends Table<T2>, JOINTYPE> String addPassiveJoin(String leftStrategyName,
																						Key<T1, JOINTYPE> leftJoinColumn,
																						Key<T2, JOINTYPE> rightJoinColumn,
																						JoinType joinType,
																						Set<? extends JoinLink<T2, ?>> columnsToSelect) {
		return this.addJoin(leftStrategyName, parent -> new PassiveJoinNode<C, T1, T2, JOINTYPE>((JoinNode<?, T1>) (JoinNode) parent,
				leftJoinColumn, rightJoinColumn, joinType,
				columnsToSelect, null));
	}
	
	public <T1 extends Table<T1>, T2 extends Table<T2>, JOINTYPE> String addPassiveJoin(String leftStrategyName,
																						Key<T1, JOINTYPE> leftJoinColumn,
																						Key<T2, JOINTYPE> rightJoinColumn,
																						JoinType joinType,
																						Set<? extends Selectable<?>> columnsToSelect,
																						EntityTreeJoinNodeConsumptionListener<C> consumptionListener) {
		return this.addJoin(leftStrategyName, parent -> new PassiveJoinNode<C, T1, T2, JOINTYPE>((JoinNode<?, T1>) (JoinNode) parent,
				leftJoinColumn, rightJoinColumn, joinType,
				columnsToSelect, null).setConsumptionListener(consumptionListener));
	}
	
	public <T1 extends Table<T1>, T2 extends Table<T2>, JOINTYPE> String addPassiveJoin(String leftStrategyName,
																						Key<T1, JOINTYPE> leftJoinColumn,
																						Key<T2, JOINTYPE> rightJoinColumn,
																						String tableAlias,
																						JoinType joinType,
																						Set<? extends Selectable<?>> columnsToSelect,
																						EntityTreeJoinNodeConsumptionListener<C> consumptionListener,
																						boolean rightTableParticipatesToDDL) {
		if (!rightTableParticipatesToDDL) {
			tablesToExcludeFromDDL.add(rightJoinColumn.getTable());
		}
		return this.addJoin(leftStrategyName, parent -> new PassiveJoinNode<C, T1, T2, JOINTYPE>((JoinNode<?, T1>) (JoinNode) parent,
				leftJoinColumn, rightJoinColumn, joinType,
				columnsToSelect, tableAlias).setConsumptionListener(consumptionListener));
	}
	
	public String addJoin(String leftStrategyName, Function<? super JoinNode<?, Fromable> /* parent node */, ? extends AbstractJoinNode<?, ?, ?, ?>> joinNodeSupplier) {
		JoinNode<?, Fromable> owningJoin = getJoin(leftStrategyName);
		if (owningJoin == null) {
			throw new IllegalArgumentException("No join named " + leftStrategyName + " exists to add a new join on");
		}
		AbstractJoinNode joinNode = joinNodeSupplier.apply(owningJoin);
		String joinName = this.indexKeyGenerator.generateKey(joinNode);
		this.joinIndex.put(joinName, joinNode);
		return joinName;
	}
	
	/**
	 * Gives a particular node of the joins graph by its name. Joins graph name are given in return of
	 * {@link #addRelationJoin(String, EntityInflater, Accessor, Key, Key, String, JoinType, BeanRelationFixer, Set)}.
	 * When {@link #ROOT_JOIN_NAME} is given, {@link #getRoot()} will be used, meanwhile, be aware that using this method to retreive root node
	 * is not the recommended way : prefer usage of {@link #getRoot()} to prevent exposure of {@link #ROOT_JOIN_NAME}
	 *
	 * @param leftStrategyName join node name to be given
	 * @return null if the node doesn't exist
	 * @see #getRoot()
	 */
	@Nullable
	public JoinNode<?, Fromable> getJoin(String leftStrategyName) {
		return (JoinNode<?, Fromable>) this.joinIndex.get(leftStrategyName);
	}
	
	/**
	 * Gives all tables used by this tree
	 *
	 * @return all joins tables of this tree
	 */
	public Set<Table<?>> giveTables() {
		// because Table doesn't implement an hashCode and because we may have clone in JoinNodes, we use a smart TreeSet to avoid duplicates
		Set<Table<?>> result = new TreeSet<>(COMPARATOR_ON_SCHEMA_AND_NAME);
		extractTable(getRoot(), result);
		foreachJoin(node -> {
			// for now, we skip direct extraction of table-per-class nodes, because their originalColumnsToLocalOnes contains both join link values and
			// columns to select, and the former is the one of the abstract entity that doesn't match a concrete table (because of table-per-class model);
			// but sub-tables are addressed because the node contains the joins to the sub-tables.
			// For now, we skip direct extraction of table-per-class nodes, because their originalColumnsToLocalOnes contains PseudoColumns
			// to select which are not part of a concrete Table. Note that the sub-tables are still addressed because the node contains the joins
			// to the sub-tables.
			if (!(node instanceof TablePerClassPolymorphicRelationJoinNode)) {
				extractTable(node, result);
			}
		});
		tablesToIncludeToDDL.forEach(result::add);
		return result;
	}
	
	private void extractTable(JoinNode<?, ?> node, Set<Table<?>> result) {
		// we must take the original table because that's where user add the foreign keys, because the table cloning mechanism doesn't propagate them
		node.getOriginalColumnsToLocalOnes().keySet().forEach(column -> {
			if (column instanceof Column) {
				Fromable table = ((Column) column).getTable();
				if (!tablesToExcludeFromDDL.contains(table)) {
					result.add((Table<?>) table);
				}
			}
		});
	}
	
	/**
	 * Shortcut for {@code joinIterator().forEachRemaining()}.
	 * Goes down this tree by breadth first. Made to avoid everyone implements node iteration.
	 * Consumer is invoked foreach node <strong>except root</strong> because it usually has a special treatment.
	 * Traversal is made in pre-order : node is consumed first then its children.
	 * 
	 * @param consumer a {@link AbstractJoinNode} consumer
	 */
	public void foreachJoin(Consumer<AbstractJoinNode<?, ?, ?, ?>> consumer) {
		joinIterator().forEachRemaining(consumer);
	}
	
	/**
	 * Copies this tree onto given one under given join node name
	 * 
	 * @param target tree receiving copies of this tree nodes
	 * @param joinNodeName node name under which this tree must be copied, 
	 * @param <E> main entity type of target tree
	 * @param <ID> main entity identifier type of target tree
	 */
	public <E, ID> void projectTo(EntityJoinTree<E, ID> target, String joinNodeName) {
		projectTo(target.getJoin(joinNodeName));
	}
	
	public void projectTo(JoinNode<?, Fromable> joinNode) {
		EntityJoinTree<?, ?> tree = joinNode.getTree();
		foreachJoinWithDepth(joinNode, (targetOwner, currentNode) -> {
			// cloning each node, the only difference lays on left column : target gets its matching column
			currentNode.getLeftJoinLink().getColumns().forEach(leftColumn -> {
				Selectable<?> projectedLeftColumn = targetOwner.getTable().findColumn(leftColumn.getExpression());
				if (projectedLeftColumn == null) {
					throw new IllegalArgumentException("Expected column "
							+ leftColumn.getExpression() + " to exist in target table " + targetOwner.getTable().getName()
							+ " but couldn't be found among " + Iterables.collect(targetOwner.getTable().getColumns(), Selectable::getExpression, ArrayList::new));
				}
			});
			AbstractJoinNode nodeClone = cloneNodeForParent(currentNode, targetOwner, currentNode.getLeftJoinLink());
			// maintaining join names through trees : we add current node name to target one. Then nodes can be found across trees
			Set<Entry<String, JoinNode<?, ?>>> set = this.joinIndex.entrySet();
			Entry<String, JoinNode<?, ?>> nodeName = Iterables.find(set, entry -> entry.getValue() == currentNode);
			tree.joinIndex.put(nodeName.getKey(), nodeClone);
			
			return nodeClone;
		});
	}
	
	/**
	 * Creates an {@link Iterator} that goes down this tree by breadth first. Made to avoid everyone implements node iteration.
	 * Consumer is invoked foreach node <strong>except root</strong> because it usually has a special treatment.
	 * Traversal is made in pre-order : node is consumed first then its children.
	 */
	public Iterator<AbstractJoinNode<?, ?, ?, ?>> joinIterator() {
		return new NodeIterator();
	}
	
	/**
	 * Goes down this tree by breadth first.
	 * Consumer is invoked foreach node <strong>except root</strong> because it usually has a special treatment.
	 * Used to create an equivalent tree of this instance with another type of node. This generally requires knowing current parent to allow child
	 * addition : consumer gets current parent as a first argument
	 * 
	 * @param initialNode very first parent given as first argument to consumer
	 * @param consumer producer of target tree node, gets previous created node (to add created node to it) and node of this tree 
	 * @param <S> type of node of the equivalent tree
	 */
	<S> void foreachJoinWithDepth(S initialNode, BiFunction<S, AbstractJoinNode<?, ?, ?, ?>, S> consumer) {
		Queue<S> targetPath = new ArrayDeque<>();
		targetPath.add(initialNode);
		NodeIteratorWithDepth<S> nodeIterator = new NodeIteratorWithDepth<>(targetPath, consumer);
		// We simply iterate all over the iterator to consume all elements
		// Please note that forEachRemaining can't be used because it is unsupported by NodeIteratorWithDepth 
		while (nodeIterator.hasNext()) {
			nodeIterator.next();
		}
		
	}
	
	/**
	 * Internal class that focuses on nodes. Iteration node is made breadth-first.
	 */
	private class NodeIterator implements Iterator<AbstractJoinNode<?, ?, ?, ?>> {
		
		protected final Queue<AbstractJoinNode> joinStack;
		protected AbstractJoinNode currentNode;
		protected boolean nextDepth = false;
		
		public NodeIterator() {
			joinStack = new ArrayDeque<>(root.getJoins());
		}
		
		@Override
		public boolean hasNext() {
			return !joinStack.isEmpty();
		}
		
		@Override
		@SuppressWarnings("java:S2272")    // NoSuchElementException is manged by Queue#remove()
		public AbstractJoinNode next() {
			// we prefer remove() to poll() because it manages NoSuchElementException which is also in next() contract
			currentNode = joinStack.remove();
			ReadOnlyList<AbstractJoinNode> nextJoins = currentNode.getJoins();
			joinStack.addAll(nextJoins);
			nextDepth = !nextJoins.isEmpty();
			return currentNode;
		}
	}
	
	private class NodeIteratorWithDepth<S> extends NodeIterator {
		
		private final Queue<S> targetPath;
		private final BiFunction<S, AbstractJoinNode<Object, Fromable, Fromable, Object>, S> consumer;
		
		public NodeIteratorWithDepth(Queue<S> targetPath, BiFunction<S, AbstractJoinNode<?, ?, ?, ?>, S> consumer) {
			this.targetPath = targetPath;
			this.consumer = (BiFunction<S, AbstractJoinNode<Object, Fromable, Fromable, Object>, S>) (BiFunction) consumer;
		}
		
		@Override
		public AbstractJoinNode next() {
			super.next();
			S targetOwner = targetPath.peek();
			S nodeClone = (S) consumer.apply(targetOwner, currentNode);
			if (nextDepth) {
				targetPath.add(nodeClone);
			}
			
			// if depth changes, we must remove target depth
			AbstractJoinNode nextIterationNode = joinStack.peek();
			if (nextIterationNode != null && nextIterationNode.getParent() != currentNode.getParent()) {
				targetPath.remove();
			}
			
			return currentNode;
		}
		
		@Override
		public void forEachRemaining(Consumer<? super AbstractJoinNode<?, ?, ?, ?>> action) {
			// this is not supported since a consumer is already given to constructor
			throw new UnsupportedOperationException();
		}
	}
	
	/**
	 * Clones table of given join (only on its columns, no need for its foreign key clones nor indexes)
	 * 
	 * @param fromable the table to clone
	 * @return a copy (on name and columns) of given join table
	 */
	static <T extends Fromable> Duo<T, IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>>> cloneTable(T fromable) {
		if (fromable instanceof Table) {
			Table<?> table = (Table<?>) fromable;
			Table tableClone = new Table(fromable.getName());
			IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> columnClones = new IdentityHashMap<>(tableClone.getColumns().size());
			(((Table<?>) fromable).getColumns()).forEach(column -> {
				Column<?, ?> clone = tableClone.addColumn(column.getName(), column.getJavaType(), column.getSize(), column.isNullable());
				columnClones.put(column, clone);
			});
			
			// Propagating primary key because right tables are used to generate schema, and at this stage they lack primary key.
			// Note that foreign keys will be added through the tree building process when appending joins, so we don't need to clone them here
			if (table.getPrimaryKey() != null) {
				Key<?, ?> primaryKey = table.getPrimaryKey();
				primaryKey.getColumns().forEach(column -> {
					// we can cast to JoinLink because we're already dealing with JoinLink since we are in JoinNode
					Column<?, ?> clonedColumn = (Column<?, ?>) columnClones.get(column);
					clonedColumn.primaryKey();
				});
			}
			return new Duo<>((T) tableClone, columnClones);
		} else if (fromable instanceof PseudoTable) {
			PseudoTable pseudoTable = new PseudoTable(((PseudoTable) fromable).getQueryStatement(), fromable.getName());
			IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> columnClones = new IdentityHashMap<>(pseudoTable.getColumns().size());
			(((PseudoTable) fromable).getColumns()).forEach(column -> {
				// we can only have Union in From clause, no sub-query, because of table-per-class polymorphism, so we can cast to Union
				PseudoColumn<?> clone = ((Union) pseudoTable.getQueryStatement()).registerColumn(column.getExpression(), column.getJavaType());
				columnClones.put(column, clone);
			});
			return new Duo<>((T) pseudoTable, columnClones);
		} else {
			throw new UnsupportedOperationException("Cloning " + Reflections.toString(fromable.getClass()) + " is not implemented");
		}
	}

	static <T extends Fromable> Key.KeyBuilder<T, ?> mimicKey(Key<?, ?> leftJoinColumn, T fromable) {
		if (fromable instanceof Table) {
			Table<?> table = (Table<?>) fromable;
			Key.KeyBuilder<Table, ?> leftKeyBuilder = Key.from(table);
			leftJoinColumn.getColumns().forEach(column -> {
				Column column1 = table.addColumn(column.getExpression(), column.getJavaType());
				leftKeyBuilder.addColumn(column1);
			});
			return (Key.KeyBuilder<T, ?>) leftKeyBuilder;
		} else if (fromable instanceof PseudoTable) {
			PseudoTable union = (PseudoTable) fromable;
			Key.KeyBuilder<PseudoTable, ?> leftKeyBuilder = Key.from(union);
			leftJoinColumn.getColumns().forEach(column -> {
				Selectable<?> column1 = union.findColumn(column.getExpression());
				leftKeyBuilder.addColumn((JoinLink<PseudoTable, ?>) column1);
			});
			return (Key.KeyBuilder<T, ?>) leftKeyBuilder;
		} else {
			throw new UnsupportedOperationException("Cloning " + Reflections.toString(fromable.getClass()) + " is not implemented");
		}
	}
	
	/**
	 * Copies given node and set it as a child of given parent.
	 * Could have been implemented by each node class itself but since this behavior is required only by the tree
	 * and a particular algorithm, decision was made to do it outside of them.
	 * 
	 * @param node node to be cloned
	 * @param parent parent node target of the clone
	 * @param leftJoinColumn columns to be used as the left key of the new node
	 * @return a copy of given node, put as child of parent, using leftColumn
	 */
	public static AbstractJoinNode<?, ?, ?, ?> cloneNodeForParent(AbstractJoinNode<?, ?, ?, ?> node, JoinNode parent, Key<?, ?> leftJoinColumn) {
		Duo<Fromable, IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>>> tableClone = cloneTable(node.getTable());
		// Build a new Key using the cloned table and the corresponding cloned columns
		Key.KeyBuilder<Fromable, Object> rightJoinLinkBuilder = Key.from(tableClone.getLeft());
		Set<? extends JoinLink<?, ?>> columns = node.getRightJoinLink().getColumns();
		for (JoinLink<?, ?> column : columns) {
			// Note that we can cast to JoinLink because we're already dealing with JoinLink since we are in JoinNode
			JoinLink<Fromable, Object> clonedColumn = (JoinLink<Fromable, Object>) tableClone.getRight().get(column);
			rightJoinLinkBuilder.addColumn(clonedColumn);
		}
		// We create the column mapping from the original node column to the cloned columns, not from the table clone ones.
		// This allows keeping the original columns in the map (user's one), which is necessary for caller to decode the result set 
		IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> originalColumnsToClones = Maps.innerJoinOnValuesAndKeys(node.getOriginalColumnsToLocalOnes(), tableClone.getRight(), IdentityHashMap::new);
		
		Key.KeyBuilder<Fromable, ?> leftJoinLinkBuilder = mimicKey(leftJoinColumn, parent.getTable());
		
		AbstractJoinNode nodeCopy;
		if (node instanceof RelationJoinNode) {
			nodeCopy = new RelationJoinNode(
					parent,
					((RelationJoinNode<?, ?, ?, ?, ?>) node).getPropertyAccessor(),
					leftJoinLinkBuilder.build(),
					rightJoinLinkBuilder.build(),
					node.getJoinType(),
					node.getColumnsToSelect(),
					node.getTableAlias(),
					((RelationJoinNode) node).getEntityInflater(),
					((RelationJoinNode) node).getBeanRelationFixer(),
					((RelationJoinNode) node).getRelationIdentifierProvider(),
					originalColumnsToClones);
		} else if (node instanceof MergeJoinNode) {
			nodeCopy = new MergeJoinNode(
					parent,
					leftJoinLinkBuilder.build(),
					rightJoinLinkBuilder.build(),
					node.getJoinType(),
					node.getTableAlias(),
					((MergeJoinNode) node).getMerger(),
					node.getColumnsToSelect(),
					originalColumnsToClones);
		} else if (node instanceof PassiveJoinNode) {
			nodeCopy = new PassiveJoinNode(
					parent,
					leftJoinLinkBuilder.build(),
					rightJoinLinkBuilder.build(),
					node.getJoinType(),
					node.getColumnsToSelect(),
					node.getTableAlias(),
					originalColumnsToClones);
		} else {
			throw new UnsupportedOperationException("Unexpected type of join : some algorithm has changed, please implement it here or fix it : "
					+ Reflections.toString(node.getClass()));
		}
		nodeCopy.setConsumptionListener(node.getConsumptionListener());
		
		return nodeCopy;
	}
	
	private class NodeKeyGenerator {
		
		/**
		 * We don't use {@link java.security.SecureRandom} because it consumes too much time while computing random values (more than 500ms
		 * for a 6-digit identifier ! ... for each Node !!), and there's no need for security here.
		 */
		@SuppressWarnings("java:S2245" /* no security issue about Random class : it's only used as identifier generator, using SecureRandom is less performant */)
		private final Randomizer keyGenerator = new Randomizer(new LinearRandomGenerator(new Random()));
		
		private String generateKey(JoinNode node) {
			// We generate a name which is unique across trees so node clones can be found outside this class by their name on different trees.
			// This is necessary for particular case of reading indexing column of indexed collection with an association table : the node that needs
			// to use indexing column is not the owner of the association table clone hence it can't use it (see table clone mechanism at EntityTreeQueryBuilder).
			// Said differently the "needer" is the official owner whereas the indexing column is on another node dedicated to the relation table maintenance.
			// The found way for the official node to access data through the indexing column is to use the identifier of the relation table node,
			// because it has it when it is created (see OneToManyWithIndexedAssociationTableEngine), and because the node is cloned through tree
			// the identifier should be "universal".
			// Note that this naming strategy could be more chaotic (totally random) since names are only here to give a unique identifier to joins
			// but the hereafter algorithm can help for debug
			return node.getTable().getAbsoluteName()
					+ "-" + Integer.toHexString(System.identityHashCode(EntityJoinTree.this)) + "-" + keyGenerator.randomHexString(6);
		}
	}
	
	public enum JoinType {
		INNER,
		OUTER
	}
	
}
