package org.codefilarete.stalactite.engine.configurer.elementcollection;

import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Map;
import java.util.function.Supplier;

import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorByMethod;
import org.codefilarete.reflection.AccessorByMethodReference;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.AccessorChain.ValueInitializerOnNullValue;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.reflection.MutatorByMethodReference;
import org.codefilarete.reflection.PropertyAccessor;
import org.codefilarete.reflection.ReversibleAccessor;
import org.codefilarete.reflection.ValueAccessPoint;
import org.codefilarete.reflection.ValueAccessPointMap;
import org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfigurationProvider;
import org.codefilarete.stalactite.engine.configurer.LambdaMethodUnsheller;
import org.codefilarete.stalactite.sql.ddl.Size;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.tool.Reflections;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import org.danekja.java.util.function.serializable.SerializableFunction;

import static org.codefilarete.tool.Reflections.propertyName;

/**
 * Support for element-collection configuration
 * 
 * @author Guillaume Mary
 */
public class ElementCollectionRelation<SRC, TRGT, S extends Collection<TRGT>> {
	
	/** The method that gives the entities from the "root" entity */
	private final ReversibleAccessor<SRC, S> collectionAccessor;
	private final Class<TRGT> componentType;
	/** Indicator to store element indices in the database, retrieve them, and sort the collection while loading the entities */
	private boolean ordered = false;
	/** Optional provider of collection instance to be used if collection value is null */
	private Supplier<S> collectionFactory;
	
	private Table targetTable;
	private String targetTableName;
	private Column<Table, ?> reverseColumn;
	private String reverseColumnName;
	
	/** Element column name override, used in simple case : {@link EmbeddableMappingConfigurationProvider} null, aka not when element is a complex type */
	private String elementColumnName;
	/** Index column name override */
	private String indexingColumnName;
	
	/** Complex type mapping, optional */
	@Nullable
	private final EmbeddableMappingConfigurationProvider<TRGT> embeddableConfigurationProvider;
	
	/** Complex type mapping override, to be used when {@link EmbeddableMappingConfigurationProvider} is not null */
	private final ValueAccessPointMap<SRC, String> overriddenColumnNames = new ValueAccessPointMap<>();
	
	/** Complex type mapping override, to be used when {@link EmbeddableMappingConfigurationProvider} is not null */
	private final ValueAccessPointMap<SRC, Size> overriddenColumnSizes = new ValueAccessPointMap<>();
	
	private Size elementColumnSize;
	
	/**
	 * @param setter collection accessor
	 * @param componentType element type in collection
	 * @param embeddableConfigurationProvider complex type mapping, null when element is a simple type (String, Integer, ...)
	 */
	public ElementCollectionRelation(SerializableBiConsumer<SRC, S> setter,
									 Class<TRGT> componentType,
									 @Nullable EmbeddableMappingConfigurationProvider<TRGT> embeddableConfigurationProvider) {
		MutatorByMethodReference<SRC, S> setterReference = Accessors.mutatorByMethodReference(setter);
		this.collectionAccessor = new PropertyAccessor<>(
				Accessors.accessor(setterReference.getDeclaringClass(), propertyName(setterReference.getMethodName())),
				setterReference
		);
		this.componentType = componentType;
		this.embeddableConfigurationProvider = embeddableConfigurationProvider;
	}
	
	/**
	 * @param getter collection accessor
	 * @param componentType element type in collection
	 * @param lambdaMethodUnsheller engine to capture getter method reference
	 * @param embeddableConfigurationProvider complex type mapping, null when element is a simple type (String, Integer, ...)
	 */
	public ElementCollectionRelation(SerializableFunction<SRC, S> getter,
									 Class<TRGT> componentType,
									 LambdaMethodUnsheller lambdaMethodUnsheller,
									 @Nullable EmbeddableMappingConfigurationProvider<TRGT> embeddableConfigurationProvider) {
		AccessorByMethodReference<SRC, S> getterReference = Accessors.accessorByMethodReference(getter);
		this.collectionAccessor = new PropertyAccessor<>(
				// we keep close to user demand : we keep its method reference ...
				getterReference,
				// ... but we can't do it for mutator, so we use the most equivalent manner : a mutator based on setter method (fallback to property if not present)
				new AccessorByMethod<SRC, S>(lambdaMethodUnsheller.captureLambdaMethod(getter)).toMutator());
		this.componentType = componentType;
		this.embeddableConfigurationProvider = embeddableConfigurationProvider;
	}
	
	public ElementCollectionRelation(ReversibleAccessor<SRC, S> collectionAccessor ,
									 Class<TRGT> componentType,
									 @Nullable EmbeddableMappingConfigurationProvider<TRGT> embeddableConfigurationProvider) {
		this.collectionAccessor = collectionAccessor;
		this.componentType = componentType;
		this.embeddableConfigurationProvider = embeddableConfigurationProvider;
	}
	
	public ReversibleAccessor<SRC, S> getCollectionAccessor() {
		return collectionAccessor;
	}
	
	public boolean isOrdered() {
		return this.ordered;
	}
	
	public void setOrdered(boolean ordered) {
		this.ordered = ordered;
	}
	
	public void ordered() {
		this.ordered = true;
	}
	
	public void setIndexingColumnName(String columnName) {
		ordered();
		this.indexingColumnName = columnName;
	}
	
	public String getIndexingColumnName() {
		return indexingColumnName;
	}
	
	public Supplier<S> getCollectionFactory() {
		return collectionFactory;
	}
	
	public ElementCollectionRelation<SRC, TRGT, S> setCollectionFactory(Supplier<? extends S> collectionFactory) {
		this.collectionFactory = (Supplier<S>) collectionFactory;
		return this;
	}

	public ValueAccessPointMap<SRC, String> getOverriddenColumnNames() {
		return this.overriddenColumnNames;
	}
	
	public void overrideName(SerializableFunction methodRef, String columnName) {
		this.overriddenColumnNames.put(new AccessorByMethodReference(methodRef), columnName);
	}
	
	public void overrideName(SerializableBiConsumer methodRef, String columnName) {
		this.overriddenColumnNames.put(new MutatorByMethodReference(methodRef), columnName);
	}
	
	public void overrideSize(SerializableFunction methodRef, Size columnSize) {
		this.overriddenColumnSizes.put(new AccessorByMethodReference(methodRef), columnSize);
	}
	
	public void overrideSize(SerializableBiConsumer methodRef, Size columnSize) {
		this.overriddenColumnSizes.put(new MutatorByMethodReference(methodRef), columnSize);
	}
	
	public ValueAccessPointMap<SRC, Size> getOverriddenColumnSizes() {
		return this.overriddenColumnSizes;
	}
	
	public Table getTargetTable() {
		return targetTable;
	}
	
	public void setTargetTable(Table targetTable) {
		this.targetTable = targetTable;
	}
	
	public String getTargetTableName() {
		return targetTableName;
	}
	
	public ElementCollectionRelation<SRC, TRGT, S> setTargetTableName(String targetTableName) {
		this.targetTableName = targetTableName;
		return this;
	}
	
	public <I> Column<Table, I> getReverseColumn() {
		return (Column<Table, I>) reverseColumn;
	}
	
	public void setReverseColumn(Column<Table, ?> reverseColumn) {
		this.reverseColumn = reverseColumn;
	}
	
	public String getReverseColumnName() {
		return reverseColumnName;
	}
	
	public ElementCollectionRelation<SRC, TRGT, S> setReverseColumnName(String reverseColumnName) {
		this.reverseColumnName = reverseColumnName;
		return this;
	}
	
	public Class<TRGT> getComponentType() {
		return componentType;
	}
	
	public EmbeddableMappingConfigurationProvider<TRGT> getEmbeddableConfigurationProvider() {
		return embeddableConfigurationProvider;
	}
	
	public void setElementColumnName(String columnName) {
		this.elementColumnName = columnName;
	}
	
	public String getElementColumnName() {
		return elementColumnName;
	}
	
	public void setElementColumnSize(Size elementColumnSize) {
		this.elementColumnSize = elementColumnSize;
	}
	
	public Size getElementColumnSize() {
		return elementColumnSize;
	}
	
	/**
	 * Clones this object to make one with the given accessor as prefix of current one.
	 * Made to "slide" current instance with an accessor prefix. Used for embeddable objects with relation to make the relation being accessible
	 * from the "root" entity.
	 *
	 * @param accessor the prefix of the clone to be created
	 * @param embeddedType the concrete type of the embeddable bean, because accessor may provide an abstraction
	 * @return a clones of this instance prefixed with the given accessor
	 * @param <C> the root entity type that owns the embeddable which has this relation
	 */
	public <C> ElementCollectionRelation<C, TRGT, S> embedInto(Accessor<C, SRC> accessor, Class<SRC> embeddedType) {
		AccessorChain<C, S> shiftedTargetProvider = new AccessorChain<>(accessor, collectionAccessor);
		shiftedTargetProvider.setNullValueHandler(new ValueInitializerOnNullValue() {
			@Override
			protected <T> T newInstance(Accessor<?, T> segmentAccessor, Class<T> valueType) {
				if (segmentAccessor == accessor) {
					return (T) Reflections.newInstance(embeddedType);
				} else if (segmentAccessor == collectionAccessor){
					if (collectionFactory != null) {
						return (T) collectionFactory.get();
					} else {
						return super.newInstance(segmentAccessor, valueType);
					}
				} else {
					return super.newInstance(segmentAccessor, valueType);
				}
			}
		});
		ElementCollectionRelation<C, TRGT, S> result = new ElementCollectionRelation<C, TRGT, S>(shiftedTargetProvider, componentType, embeddableConfigurationProvider);
		
		result.setElementColumnName(this.getElementColumnName());
		result.setElementColumnSize(this.getElementColumnSize());
		result.setTargetTable(this.getTargetTable());
		result.setTargetTableName(this.getTargetTableName());
		result.setReverseColumn(this.getReverseColumn());
		result.setReverseColumnName(this.getReverseColumnName());
		result.getOverriddenColumnNames().putAll((Map<? extends ValueAccessPoint<C>, ? extends String>) this.getOverriddenColumnNames());
		result.getOverriddenColumnSizes().putAll((Map<? extends ValueAccessPoint<C>, ? extends Size>) this.getOverriddenColumnSizes());
		result.setOrdered(this.isOrdered());
		result.setIndexingColumnName(this.getIndexingColumnName());
		result.setCollectionFactory(this.getCollectionFactory());
		return result;
	}
}
