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

import javax.annotation.Nullable;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;

import org.codefilarete.reflection.Accessor;
import org.codefilarete.stalactite.engine.runtime.load.EntityJoinTree.JoinType;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater.RelationIdentifier;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater.TreeInflationContext;
import org.codefilarete.stalactite.engine.runtime.load.JoinRowConsumer.ForkJoinRowConsumer;
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.Reflections;

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

/**
 * Particular {@link JoinNode} made to handle relation from an entity to a collection of some another polymorphic one. Actually relation doesn't
 * make the complexity of that class: polymorphic entity instantiation is the core focus of it. Here are the hot spots:
 * identifier is given by the subclass which find its id in the row (see {@link JoinTablePolymorphicRelationJoinRowConsumer#giveIdentifier}),
 * and while doing it, it remembers which consumer made it. Then while instantiating entity we invoke it to get right entity type (parent mapping
 * would only give parent entity, which, out of being a wrong approach, can be an abstract type). Then instance is filled up with parent properties
 * by calling merging method (see line 98).
 * Finally, {@link JoinTablePolymorphicRelationJoinRowConsumer} must extend {@link ForkJoinRowConsumer} to give next branch to be consumed by
 * {@link EntityTreeInflater} to avoid "dead" branch to be read : we give it according to the consumer which found the identifier. 
 * 
 * @author Guillaume Mary
 */
public class JoinTablePolymorphicRelationJoinNode<C, T1 extends Table, T2 extends Table, JOINTYPE, I> extends RelationJoinNode<C, T1, T2, JOINTYPE, I> {
	
	private final Set<SubPersister<? extends C>> subPersisters = new HashSet<>();
	
	public JoinTablePolymorphicRelationJoinNode(JoinNode<?, T1> parent,
												Accessor<?, ?> propertyAccessor,
												Key<T1, JOINTYPE> leftJoinColumn,
												Key<T2, JOINTYPE> rightJoinColumn,
												JoinType joinType,
												Set<Column<T2, ?>> columnsToSelect,
												@Nullable String tableAlias,
												EntityInflater<C, I> entityInflater,
												BeanRelationFixer<Object, C> beanRelationFixer,
												@Nullable Function<ColumnedRow, ?> relationIdentifierProvider) {
		super(parent, propertyAccessor, leftJoinColumn, rightJoinColumn, joinType, columnsToSelect, tableAlias, entityInflater, beanRelationFixer, relationIdentifierProvider);
	}
	
	@Override
	public JoinTablePolymorphicRelationJoinRowConsumer toConsumer(JoinNode<C, T2> joinNode) {
		DefaultRelationJoinRowConsumer<C, I> parentRowConsumer = (DefaultRelationJoinRowConsumer<C, I>) super.toConsumer(joinNode);
		return new JoinTablePolymorphicRelationJoinRowConsumer(joinNode, parentRowConsumer, getConsumptionListener());
	}
	
	public <D extends C> void addSubPersisterJoin(PolymorphicMergeJoinRowConsumer<D, I> subPersisterJoin) {
		this.subPersisters.add(new SubPersister<>(subPersisterJoin));
	}
	
	private class SubPersister<D extends C> {
		
		private final PolymorphicMergeJoinRowConsumer<D, I> subPersisterMergeConsumer;
		
		public SubPersister(PolymorphicMergeJoinRowConsumer<D, I> subPersisterMergeConsumer) {
			this.subPersisterMergeConsumer = subPersisterMergeConsumer;
		}
	}
	
	public class JoinTablePolymorphicRelationJoinRowConsumer implements RelationJoinRowConsumer<C, I>, ForkJoinRowConsumer {
		
		private final ThreadLocal<RowIdentifier<? extends C>> currentlyFoundConsumer = new ThreadLocal<>();
		
		private final JoinNode<C, ?> joinNode;
		
		private final DefaultRelationJoinRowConsumer<C, I> parentRowConsumer;
		
		/** Optional listener of ResultSet decoding */
		@Nullable
		private final EntityTreeJoinNodeConsumptionListener<C> consumptionListener;
		
		private JoinTablePolymorphicRelationJoinRowConsumer(JoinNode<C, ?> joinNode,
                                                            DefaultRelationJoinRowConsumer<C, I> parentRowConsumer,
                                                            @Nullable EntityTreeJoinNodeConsumptionListener<C> consumptionListener) {
			this.joinNode = joinNode;
			this.parentRowConsumer = parentRowConsumer;
			this.consumptionListener = consumptionListener;
		}

		@Override
		public JoinNode<C, ?> getNode() {
			return joinNode;
		}

		@Override
		public C applyRelatedEntity(Object parentJoinEntity, ColumnedRow row, TreeInflationContext context) {
			RowIdentifier<? extends C> rowIdentifier = giveIdentifier();
			currentlyFoundConsumer.set(rowIdentifier);
			if (rowIdentifier != null) {
				I rightIdentifier = rowIdentifier.entityIdentifier;
				// we avoid treating twice same relation, overall to avoid adding twice same instance to a collection (one-to-many list cases)
				// in case of multiple collections in ResultSet because it creates similar data (through cross join) which are treated as many as
				// collections cross with each other. This also works for one-to-one relations but produces no bugs. It can also be seen as a performance
				// enhancement even if it hasn't been measured.
				RelationIdentifier eventuallyApplied = new RelationIdentifier(parentJoinEntity, getEntityType(), rightIdentifier, this);
				// primary key null means no entity => nothing to do
				if (rightIdentifier != null && context.isTreatedOrAppend(eventuallyApplied)) {
					C rightEntity = (C) context.giveEntityFromCache(getEntityType(), rightIdentifier, () -> {
						ColumnedRow subInflaterRow = EntityTreeInflater.currentContext().getDecoder(rowIdentifier.rowConsumer.getNode());
						C entity = rowIdentifier.rowConsumer.transform(subInflaterRow);
						// We have to apply parent properties on created bean by subclass, because sub-transformer doesn't contain them
						parentRowConsumer.getRowTransformer().applyRowToBean(row, entity);
						return entity;
					});
					getBeanRelationFixer().apply(parentJoinEntity, rightEntity);
					if (this.consumptionListener != null) {
						this.consumptionListener.onNodeConsumption(rightEntity, row);
					}
					return rightEntity;
				}
			}
			return null;
		}
		
		@Nullable
		/* Optimized, from 530 000 nanos to 65 000 nanos at 1st exec, from 40 000 nanos to 12 000 nanos on usual run */
		private RowIdentifier<? extends C> giveIdentifier() {
			// @Optimized : use for & return instead of stream().map().filter(notNull).findFirst()
			for (SubPersister<?> pawn : subPersisters) {
				ColumnedRow subInflaterRow = EntityTreeInflater.currentContext().getDecoder(pawn.subPersisterMergeConsumer.getNode());
				I assemble = pawn.subPersisterMergeConsumer.giveIdentifier(subInflaterRow);
				if (assemble != null) {
					return new RowIdentifier<>(assemble, pawn.subPersisterMergeConsumer);
				}
			}
			return null;
		}
		
		@Override
		public JoinRowConsumer giveNextConsumer() {
			return nullable(currentlyFoundConsumer.get()).map(rowIdentifier -> rowIdentifier.rowConsumer).get();
		}
		
		@Override
		public void afterRowConsumption(TreeInflationContext context) {
			currentlyFoundConsumer.remove();
		}
		
		private class RowIdentifier<D extends C> {
			
			private final I entityIdentifier;
			private final PolymorphicMergeJoinRowConsumer<D, I> rowConsumer;
			
			private RowIdentifier(I entityIdentifier, PolymorphicMergeJoinRowConsumer<D, I> rowConsumer) {
				this.entityIdentifier = entityIdentifier;
				this.rowConsumer = rowConsumer;
			}
		}
		
		/**
		 * Implemented for debug. DO NOT RELY ON IT for anything else.
		 */
		@Override
		public String toString() {
			return Reflections.toString(this.getClass());
		}
	}
}
