package org.codefilarete.stalactite.engine.runtime;

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

import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.reflection.ValueAccessPoint;
import org.codefilarete.stalactite.engine.EntityCriteria;
import org.codefilarete.stalactite.engine.EntityPersister;
import org.codefilarete.stalactite.engine.ExecutableQuery;
import org.codefilarete.stalactite.engine.runtime.load.EntityJoinTree;
import org.codefilarete.stalactite.query.RelationalEntityCriteria;
import org.codefilarete.stalactite.query.model.ConditionalOperator;
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.collection.Arrays;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import org.danekja.java.util.function.serializable.SerializableFunction;

/**
 * Contract to allow joining a persister with another for entities with relation.
 * 
 * @author Guillaume Mary
 */
public interface RelationalEntityPersister<C, I> extends EntityPersister<C, I> {
	
	/**
	 * Called to join this instance with given persister. For this method, current instance is considered as the "right part" of the relation.
	 * Made as such because polymorphic cases (which are instance of this interface) are the only one who knows how to join themselves with another persister.
	 * 
	 * @param <SRC> source entity type
	 * @param <T1> left table type
	 * @param <T2> right table type
	 * @param sourcePersister source that needs this instance joins
	 * @param propertyAccessor accessor to the property of this persister's entity from the source entity type
	 * @param leftColumn left part of the join, expected to be one of source table
	 * @param rightColumn right part of the join, expected to be one of current instance table
	 * @param rightTableAlias optional alias for right table, if null table name will be used
	 * @param beanRelationFixer setter that fix relation of this instance onto source persister instance
	 * @param optional true for optional relation, makes an outer join, else should create a inner join
	 * @param loadSeparately indicator to make the target entities loaded in a separate query
	 * @return the created join name, then it could be found in sourcePersister#getEntityJoinTree
	 */
	<SRC, T1 extends Table<T1>, T2 extends Table<T2>, SRCID, JOINID> String joinAsOne(RelationalEntityPersister<SRC, SRCID> sourcePersister,
																					  Accessor<SRC, C> propertyAccessor,
																					  Key<T1, JOINID> leftColumn,
																					  Key<T2, JOINID> rightColumn,
																					  @Nullable String rightTableAlias,
																					  BeanRelationFixer<SRC, C> beanRelationFixer,
																					  boolean optional,
																					  boolean loadSeparately);
	
	/**
	 * Called to join this instance with given persister. For this method, current instance is considered as the "right part" of the relation.
	 * Made as such because polymorphic cases (which are instance of this interface) are the only one who knows how to join themselves with another persister.
	 *
	 * @param <SRC> source entity type
	 * @param <T1> left table type
	 * @param <T2> right table type
	 * @param sourcePersister source that needs this instance joins
	 * @param propertyAccessor accessor to the property of this persister's entity from the source entity type
	 * @param leftColumn left part of the join, expected to be one of source table
	 * @param rightColumn right part of the join, expected to be one of current instance table
	 * @param beanRelationFixer setter that fix relation of this instance onto source persister instance, expected to manage collection instantiation
	 * @param duplicateIdentifierProvider a function that computes the relation identifier
	 * @param joinName parent join node name on which join must be added,
	 * not always {@link EntityJoinTree#ROOT_JOIN_NAME} in particular in one-to-many with association table
	 * @param optional true for optional relation, makes an outer join, else should create a inner join
	 * @param loadSeparately indicator to make the target entities loaded in a separate query
	 */
	default <SRC, T1 extends Table<T1>, T2 extends Table<T2>, SRCID, JOINID> String joinAsMany(String joinName,
																							   RelationalEntityPersister<SRC, SRCID> sourcePersister,
																							   Accessor<SRC, ?> propertyAccessor,
																							   Key<T1, JOINID> leftColumn,
																							   Key<T2, JOINID> rightColumn,
																							   BeanRelationFixer<SRC, C> beanRelationFixer,
																							   @Nullable Function<ColumnedRow, Object> duplicateIdentifierProvider,
																							   boolean optional,
																							   boolean loadSeparately) {
		return joinAsMany(joinName, sourcePersister, propertyAccessor, leftColumn, rightColumn, beanRelationFixer,
				duplicateIdentifierProvider, Collections.emptySet(), optional, loadSeparately);
	}
	
	/**
	 * Called to join this instance with given persister. For this method, current instance is considered as the "right part" of the relation.
	 * Made as such because polymorphic cases (which are instance of this interface) are the only one who knows how to join themselves with another persister.
	 *
	 * @param <SRC> source entity type
	 * @param <T1> left table type
	 * @param <T2> right table type
	 * @param sourcePersister source that needs this instance joins
	 * @param propertyAccessor accessor to the property of this persister's entity from the source entity type
	 * @param leftColumn left part of the join, expected to be one of source table
	 * @param rightColumn right part of the join, expected to be one of current instance table
	 * @param beanRelationFixer setter that fix relation of this instance onto source persister instance, expected to manage collection instantiation
	 * @param duplicateIdentifierProvider a function that computes the relation identifier
	 * @param joinName parent join node name on which join must be added,
	 * not always {@link EntityJoinTree#ROOT_JOIN_NAME} in particular in one-to-many with association table
	 * @param selectableColumns columns to be added to SQL select clause
	 * @param optional true for optional relation, makes an outer join, else should create a inner join
	 * @param loadSeparately indicator to make the target entities loaded in a separate query
	 */
	<SRC, T1 extends Table<T1>, T2 extends Table<T2>, SRCID, JOINID> String joinAsMany(String joinName,
																					   RelationalEntityPersister<SRC, SRCID> sourcePersister,
																					   Accessor<SRC, ?> propertyAccessor,
																					   Key<T1, JOINID> leftColumn,
																					   Key<T2, JOINID> rightColumn,
																					   BeanRelationFixer<SRC, C> beanRelationFixer,
																					   @Nullable Function<ColumnedRow, Object> duplicateIdentifierProvider,
																					   Set<? extends Column<T2, ?>> selectableColumns,
																					   boolean optional,
																					   boolean loadSeparately);
	
	EntityJoinTree<C, I> getEntityJoinTree();
	
	/**
	 * Copies current instance joins root to given select
	 * 
	 * @param entityJoinTree target of the copy
	 * @param joinName name of target select join on which joins of this instance must be copied
	 * @param <E> target select entity type
	 * @param <ID> identifier type
	 */
	<E, ID> void copyRootJoinsTo(EntityJoinTree<E, ID> entityJoinTree, String joinName);
	
	/**
	 * Overridden for a more accurate return type.
	 * {@inheritDoc}
	 */
	default <O> ExecutableEntityQueryCriteria<C, ?> selectWhere(SerializableFunction<C, O> getter, ConditionalOperator<O, ?> operator) {
		return selectWhere(AccessorChain.fromMethodReference(getter), operator);
	}
	
	/**
	 * Overridden for a more accurate return type.
	 * {@inheritDoc}
	 */
	default <O> ExecutableEntityQueryCriteria<C, ?> selectWhere(SerializableBiConsumer<C, O> setter, ConditionalOperator<O, ?> operator) {
		return selectWhere(Arrays.asList(Accessors.mutatorByMethodReference(setter)), operator);
	}
	
	/**
	 * Overridden for a more accurate return type.
	 * {@inheritDoc}
	 */
	default <O, A> ExecutableEntityQueryCriteria<C, ?> selectWhere(SerializableFunction<C, A> getter1, SerializableFunction<A, O> getter2, ConditionalOperator<O, ?> operator) {
		return selectWhere(AccessorChain.fromMethodReferences(getter1, getter2), operator);
	}
	
	/**
	 * Overridden for a more accurate return type.
	 * {@inheritDoc}
	 */
	default <O> ExecutableEntityQueryCriteria<C, ?> selectWhere(List<? extends ValueAccessPoint<?>> accessorChain, ConditionalOperator<O, ?> operator) {
		return selectWhere().and(accessorChain, operator);
	}
	
	/**
	 * Overridden for a more accurate return type.
	 * {@inheritDoc}
	 */
	default <O> ExecutableEntityQueryCriteria<C, ?> selectWhere(AccessorChain<C, ?> accessorChain, ConditionalOperator<O, ?> operator) {
		return selectWhere().and(accessorChain, operator);
	}
	
	/**
	 * Overridden for a more accurate return type.
	 * {@inheritDoc}
	 */
	ExecutableEntityQueryCriteria<C, ?> selectWhere();
	
	/**
	 * Mashup between {@link EntityCriteria} and {@link ExecutableQuery} to make an {@link EntityCriteria} executable
	 * @param <C> type of object returned by query execution
	 */
	interface ExecutableEntityQueryCriteria<C, SELF extends ExecutableEntityQueryCriteria<C, SELF>>
			extends ExecutableEntityQuery<C, SELF>, RelationalEntityCriteria<C, SELF>, EntityCriteria.OrderByChain<C, SELF> {
		
	}
}
