package org.codefilarete.stalactite.engine.configurer;

import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorByMethod;
import org.codefilarete.reflection.AccessorByMethodReference;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.reflection.MethodReferenceCapturer;
import org.codefilarete.reflection.MethodReferenceDispatcher;
import org.codefilarete.reflection.Mutator;
import org.codefilarete.reflection.MutatorByMethod;
import org.codefilarete.reflection.MutatorByMethodReference;
import org.codefilarete.reflection.PropertyAccessor;
import org.codefilarete.reflection.ReversibleAccessor;
import org.codefilarete.stalactite.dsl.MappingConfigurationException;
import org.codefilarete.stalactite.dsl.PolymorphismPolicy;
import org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration;
import org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfigurationProvider;
import org.codefilarete.stalactite.dsl.embeddable.FluentEmbeddableMappingBuilder.FluentEmbeddableMappingBuilderEmbeddableMappingConfigurationImportedEmbedOptions;
import org.codefilarete.stalactite.dsl.embeddable.FluentEmbeddableMappingBuilder.FluentEmbeddableMappingBuilderEnumOptions;
import org.codefilarete.stalactite.dsl.embeddable.FluentEmbeddableMappingConfiguration.FluentEmbeddableMappingConfigurationElementCollectionImportEmbedOptions;
import org.codefilarete.stalactite.dsl.embeddable.ImportedEmbedWithColumnOptions;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfigurationProvider;
import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.property.CollectionOptions;
import org.codefilarete.stalactite.dsl.property.ColumnOptions;
import org.codefilarete.stalactite.dsl.property.ElementCollectionOptions;
import org.codefilarete.stalactite.dsl.property.EmbeddableCollectionOptions;
import org.codefilarete.stalactite.dsl.property.EnumOptions;
import org.codefilarete.stalactite.dsl.relation.OneToManyEntityOptions;
import org.codefilarete.stalactite.dsl.relation.OneToOneEntityOptions;
import org.codefilarete.stalactite.dsl.relation.OneToOneOptions;
import org.codefilarete.stalactite.dsl.subentity.FluentSubEntityMappingBuilder;
import org.codefilarete.stalactite.dsl.subentity.FluentSubEntityMappingBuilderOneToManyOptions;
import org.codefilarete.stalactite.dsl.subentity.SubEntityMappingConfiguration;
import org.codefilarete.stalactite.engine.configurer.elementcollection.ElementCollectionRelation;
import org.codefilarete.stalactite.engine.configurer.embeddable.FluentEmbeddableMappingConfigurationSupport;
import org.codefilarete.stalactite.engine.configurer.embeddable.LinkageSupport;
import org.codefilarete.stalactite.engine.configurer.entity.OneToManyEntityOptionsSupport;
import org.codefilarete.stalactite.engine.configurer.manyToOne.ManyToOneRelation;
import org.codefilarete.stalactite.engine.configurer.manytomany.ManyToManyRelation;
import org.codefilarete.stalactite.engine.configurer.map.MapRelation;
import org.codefilarete.stalactite.engine.configurer.onetomany.OneToManyRelation;
import org.codefilarete.stalactite.engine.configurer.onetoone.OneToOneRelation;
import org.codefilarete.stalactite.engine.configurer.property.ColumnLinkageOptionsByColumn;
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.stalactite.sql.statement.binder.ParameterBinder;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.function.Converter;
import org.codefilarete.tool.function.SerializableTriFunction;
import org.codefilarete.tool.reflect.MethodDispatcher;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import org.danekja.java.util.function.serializable.SerializableBiFunction;
import org.danekja.java.util.function.serializable.SerializableFunction;

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

/**
 * A class that stores configuration made through a {@link FluentSubEntityMappingBuilder}
 * 
 * @author Guillaume Mary
 */
public class FluentSubEntityMappingConfigurationSupport<C, I> implements FluentSubEntityMappingBuilder<C, I> {
	
	private final Class<C> classToPersist;
	
	private final MethodReferenceCapturer methodSpy;
	
	private final List<OneToOneRelation<C, ?, ?>> oneToOneRelations = new ArrayList<>();
	
	private final List<OneToManyRelation<C, ?, ?, ? extends Collection<?>>> oneToManyRelations = new ArrayList<>();
	
	private final List<OneToManyRelation<C, ?, ?, ? extends Collection<?>>> manyToOneRelations = new ArrayList<>();
	
	private final List<ElementCollectionRelation<C, ?, ? extends Collection<?>>> elementCollections = new ArrayList<>();
	
	private final SubEntityDecoratedEmbeddableConfigurationSupport<C, I> propertiesMappingConfigurationDelegate;
	
	@Nullable
	private PolymorphismPolicy<C> polymorphismPolicy;
	
	/**
	 * Creates a builder to map the given class for persistence
	 *
	 * @param classToPersist the class to create a mapping for
	 */
	public FluentSubEntityMappingConfigurationSupport(Class<C> classToPersist) {
		this.classToPersist = classToPersist;
		
		// Helper to capture Method behind method reference
		this.methodSpy = new MethodReferenceCapturer();
		
		this.propertiesMappingConfigurationDelegate = new SubEntityDecoratedEmbeddableConfigurationSupport<>(this, classToPersist);
	}
	
	@Override
	public SubEntityMappingConfiguration<C> getConfiguration() {
		return new SubEntityMappingConfiguration<C>() {
			@Override
			public Class<C> getEntityType() {
				return classToPersist;
			}
			
			@Override
			public EmbeddableMappingConfiguration<C> getPropertiesMapping() {
				return propertiesMappingConfigurationDelegate;
			}
			
			@Override
			public <TRGT, TRGTID> List<OneToOneRelation<C, TRGT, TRGTID>> getOneToOnes() {
				return (List) oneToOneRelations;
			}
			
			@Override
			public <TRGT, TRGTID> List<OneToManyRelation<C, TRGT, TRGTID, Collection<TRGT>>> getOneToManys() {
				return (List) oneToManyRelations;
			}
			
			@Override
			public <TRGT, TRGTID> List<ManyToManyRelation<C, TRGT, TRGTID, Collection<TRGT>, Collection<C>>> getManyToManys() {
				return Collections.emptyList();
			}
			
			@Override
			public <TRGT, TRGTID> List<ManyToOneRelation<C, TRGT, TRGTID, Collection<C>>> getManyToOnes() {
				return (List) manyToOneRelations;
			}
			
			@Override
			public <TRGT> List<ElementCollectionRelation<C, TRGT, ? extends Collection<TRGT>>> getElementCollections() {
				return (List) elementCollections;
			}
			
			@Override
			public List<MapRelation<C, ?, ?, ? extends Map>> getMaps() {
				return Collections.emptyList();
			}
			
			@Override
			public PolymorphismPolicy<C> getPolymorphismPolicy() {
				return polymorphismPolicy;
			}
		};
	}
	
	private Method captureMethod(SerializableFunction getter) {
		return this.methodSpy.findMethod(getter);
	}
	
	private Method captureMethod(SerializableBiConsumer setter) {
		return this.methodSpy.findMethod(setter);
	}
	
	@Override
	public <O> FluentSubEntityMappingBuilderPropertyOptions<C, I, O> map(SerializableBiConsumer<C, O> setter) {
		LinkageSupport<C, O> mapping = propertiesMappingConfigurationDelegate.addMapping(setter);
		return this.propertiesMappingConfigurationDelegate.wrapForAdditionalOptions(mapping);
	}
	
	@Override
	public <O> FluentSubEntityMappingBuilderPropertyOptions<C, I, O> map(SerializableFunction<C, O> getter) {
		LinkageSupport<C, O> mapping = propertiesMappingConfigurationDelegate.addMapping(getter);
		return this.propertiesMappingConfigurationDelegate.wrapForAdditionalOptions(mapping);
	}
	
	@Override
	public <E extends Enum<E>> FluentSubEntityMappingConfigurationEnumOptions<C, I, E> mapEnum(SerializableBiConsumer<C, E> setter) {
		LinkageSupport<C, E> linkage = propertiesMappingConfigurationDelegate.addMapping(setter);
		return handleEnumOptions(propertiesMappingConfigurationDelegate.wrapWithEnumOptions(linkage));
	}
	
	@Override
	public <E extends Enum<E>> FluentSubEntityMappingConfigurationEnumOptions<C, I, E> mapEnum(SerializableFunction<C, E> getter) {
		LinkageSupport<C, E> linkage = propertiesMappingConfigurationDelegate.addMapping(getter);
		return handleEnumOptions(propertiesMappingConfigurationDelegate.wrapWithEnumOptions(linkage));
	}
	
	private <E extends Enum<E>> FluentSubEntityMappingConfigurationEnumOptions<C, I, E> handleEnumOptions(FluentEmbeddableMappingBuilderEnumOptions<C, E> enumOptionsHandler) {
		// we redirect all of the EnumOptions method to the instance that can handle them, returning the dispatcher on this methods so one can chain
		// with some other methods, other methods are redirected to this instance because it can handle them.
		return new MethodDispatcher()
				.redirect(EnumOptions.class, enumOptionsHandler, true)
				.fallbackOn(this)
				.build((Class<FluentSubEntityMappingConfigurationEnumOptions<C, I, E>>) (Class) FluentSubEntityMappingConfigurationEnumOptions.class);
	}
	
	@Override
	public <O, S extends Collection<O>> FluentSubEntityMappingBuilderElementCollectionOptions<C, I, O, S> mapCollection(SerializableFunction<C, S> getter,
																														Class<O> componentType) {
		ElementCollectionRelation<C, O, S> elementCollectionRelation = new ElementCollectionRelation<>(getter, componentType,
				propertiesMappingConfigurationDelegate, null);
		elementCollections.add(elementCollectionRelation);
		return new MethodReferenceDispatcher()
				.redirect(ElementCollectionOptions.class, wrapAsOptions(elementCollectionRelation), true)
				.fallbackOn(this)
				.build((Class<FluentSubEntityMappingBuilderElementCollectionOptions<C, I, O, S>>) (Class) FluentSubEntityMappingBuilderElementCollectionOptions.class);
	}
	
	@Override
	public <O, S extends Collection<O>> FluentSubEntityMappingBuilderElementCollectionOptions<C, I, O, S> mapCollection(SerializableBiConsumer<C, S> setter,
																														Class<O> componentType) {
		ElementCollectionRelation<C, O, S> elementCollectionRelation = new ElementCollectionRelation<>(setter, componentType, null);
		elementCollections.add(elementCollectionRelation);
		return new MethodReferenceDispatcher()
				.redirect(ElementCollectionOptions.class, wrapAsOptions(elementCollectionRelation), true)
				.fallbackOn(this)
				.build((Class<FluentSubEntityMappingBuilderElementCollectionOptions<C, I, O, S>>) (Class) FluentSubEntityMappingBuilderElementCollectionOptions.class);
	}
	
	@Override
	public <O, S extends Collection<O>> FluentSubEntityMappingBuilderElementCollectionImportEmbedOptions<C, I, O, S> mapCollection(SerializableFunction<C, S> getter,
																																   Class<O> componentType,
																																   EmbeddableMappingConfigurationProvider<O> embeddableConfiguration) {
		ElementCollectionRelation<C, O, S> elementCollectionRelation = new ElementCollectionRelation<>(getter, componentType,
				propertiesMappingConfigurationDelegate,
				embeddableConfiguration);
		elementCollections.add(elementCollectionRelation);
		return wrapWithElementCollectionImportOptions(elementCollectionRelation);
	}
	
	@Override
	public <O, S extends Collection<O>> FluentSubEntityMappingBuilderElementCollectionImportEmbedOptions<C, I, O, S> mapCollection(SerializableBiConsumer<C, S> setter,
																																   Class<O> componentType,
																																   EmbeddableMappingConfigurationProvider<O> embeddableConfiguration) {
		ElementCollectionRelation<C, O, S> elementCollectionRelation = new ElementCollectionRelation<>(setter, componentType, embeddableConfiguration);
		elementCollections.add(elementCollectionRelation);
		return wrapWithElementCollectionImportOptions(elementCollectionRelation);
	}
	
	private <O, S extends Collection<O>> FluentSubEntityMappingBuilderElementCollectionImportEmbedOptions<C, I, O, S> wrapWithElementCollectionImportOptions(
			ElementCollectionRelation<C, O, S> elementCollectionRelation) {
		return new MethodReferenceDispatcher()
				.redirect(EmbeddableCollectionOptions.class, new EmbeddableCollectionOptions<C, O, S>() {
					
					@Override
					public <IN> EmbeddableCollectionOptions<C, O, S> overrideName(SerializableFunction<O, IN> getter, String columnName) {
						elementCollectionRelation.overrideName(getter, columnName);
						return null;
					}
					
					@Override
					public <IN> EmbeddableCollectionOptions<C, O, S> overrideName(SerializableBiConsumer<O, IN> setter, String columnName) {
						elementCollectionRelation.overrideName(setter, columnName);
						return null;
					}
					
					@Override
					public <IN> EmbeddableCollectionOptions<C, O, S> overrideSize(SerializableFunction<O, IN> getter, Size columnSize) {
						elementCollectionRelation.overrideSize(getter, columnSize);
						return null;
					}
					
					@Override
					public <IN> EmbeddableCollectionOptions<C, O, S> overrideSize(SerializableBiConsumer<O, IN> setter, Size columnSize) {
						elementCollectionRelation.overrideSize(setter, columnSize);
						return null;
					}
					
					@Override
					public EmbeddableCollectionOptions<C, O, S> initializeWith(Supplier<? extends S> collectionFactory) {
						elementCollectionRelation.setCollectionFactory(collectionFactory);
						return null;
					}
					
					@Override
					public EmbeddableCollectionOptions<C, O, S> reverseJoinColumn(String name) {
						elementCollectionRelation.setReverseColumnName(name);
						return null;
					}
					
					@Override
					public EmbeddableCollectionOptions<C, O, S> indexed() {
						elementCollectionRelation.ordered();
						return null;
					}
					
					@Override
					public EmbeddableCollectionOptions<C, O, S> indexedBy(String columnName) {
						elementCollectionRelation.setIndexingColumnName(columnName);
						return null;
					}
					
					@Override
					public EmbeddableCollectionOptions<C, O, S> onTable(Table table) {
						elementCollectionRelation.setTargetTable(table);
						return null;
					}
					
					@Override
					public EmbeddableCollectionOptions<C, O, S> onTable(String tableName) {
						elementCollectionRelation.setTargetTableName(tableName);
						return null;
					}
				}, true)
				.fallbackOn(this)
				.build((Class<FluentSubEntityMappingBuilderElementCollectionImportEmbedOptions<C, I, O, S>>) (Class) FluentSubEntityMappingBuilderElementCollectionImportEmbedOptions.class);
	}
	
	private <O, S extends Collection<O>> ElementCollectionOptions<C, O, S> wrapAsOptions(ElementCollectionRelation<C, O, S> elementCollectionRelation) {
		return new ElementCollectionOptions<C, O, S>() {
			
			@Override
			public ElementCollectionOptions<C, O, S> initializeWith(Supplier<? extends S> collectionFactory) {
				elementCollectionRelation.setCollectionFactory(collectionFactory);
				return null;
			}
			
			@Override
			public ElementCollectionOptions<C, O, S> elementColumnName(String columnName) {
				elementCollectionRelation.setElementColumnName(columnName);
				return null;
			}
			
			@Override
			public ElementCollectionOptions<C, O, S> elementColumnSize(Size columnSize) {
				elementCollectionRelation.setElementColumnSize(columnSize);
				return null;
			}
			
			@Override
			public FluentSubEntityMappingBuilderElementCollectionOptions<C, I, O, S> reverseJoinColumn(String name) {
				elementCollectionRelation.setReverseColumnName(name);
				return null;
			}
			
			@Override
			public FluentSubEntityMappingBuilderElementCollectionOptions<C, I, O, S> indexed() {
				elementCollectionRelation.ordered();
				return null;
			}
			
			@Override
			public FluentSubEntityMappingBuilderElementCollectionOptions<C, I, O, S> indexedBy(String columnName) {
				elementCollectionRelation.setIndexingColumnName(columnName);
				return null;
			}
			
			@Override
			public ElementCollectionOptions<C, O, S> onTable(Table table) {
				elementCollectionRelation.setTargetTable(table);
				return null;
			}
			
			@Override
			public ElementCollectionOptions<C, O, S> onTable(String tableName) {
				elementCollectionRelation.setTargetTableName(tableName);
				return null;
			}
		};
	}
	
	@Override
	public <O, J> FluentMappingBuilderOneToOneOptions<C, I, O> mapOneToOne(
			SerializableBiConsumer<C, O> setter,
			EntityMappingConfigurationProvider<O, J> mappingConfiguration) {
		// we keep close to user demand: we keep its method reference ...
		Mutator<C, O> mutatorByMethodReference = Accessors.mutatorByMethodReference(setter);
		// ... but we can't do it for accessor, so we use the most equivalent manner: an accessor based on setter method (fallback to property if not present)
		Accessor<C, O> accessor = new MutatorByMethod<C, O>(captureMethod(setter)).toAccessor();
		return mapOneToOne(accessor, mutatorByMethodReference, mappingConfiguration);
	}
	
	@Override
	public <O, J> FluentMappingBuilderOneToOneOptions<C, I, O> mapOneToOne(
			SerializableFunction<C, O> getter,
			EntityMappingConfigurationProvider<O, J> mappingConfiguration) {
		// we keep close to user demand: we keep its method reference ...
		AccessorByMethodReference<C, O> accessorByMethodReference = Accessors.accessorByMethodReference(getter);
		// ... but we can't do it for mutator, so we use the most equivalent manner: a mutator based on getter method (fallback to property if not present)
		Mutator<C, O> mutator = new AccessorByMethod<C, O>(captureMethod(getter)).toMutator();
		return mapOneToOne(accessorByMethodReference, mutator, mappingConfiguration);
	}
	
	private <O, J, T extends Table> FluentMappingBuilderOneToOneOptions<C, I, O> mapOneToOne(Accessor<C, O> accessor,
																							 Mutator<C, O> mutator,
																							 EntityMappingConfigurationProvider<O, J> mappingConfiguration) {
		OneToOneRelation<C, O, J> oneToOneRelation = new OneToOneRelation<>(
				new PropertyAccessor<>(accessor, mutator),
				() -> this.polymorphismPolicy instanceof PolymorphismPolicy.TablePerClassPolymorphism,
				mappingConfiguration);
		this.oneToOneRelations.add(oneToOneRelation);
		return wrapForAdditionalOptions(oneToOneRelation);
	}
	
	private <O, J> FluentMappingBuilderOneToOneOptions<C, I, O> wrapForAdditionalOptions(OneToOneRelation<C, O, J> oneToOneRelation) {
		// then we return an object that allows fluent settings over our OneToOne cascade instance
		return new MethodDispatcher()
				.redirect(OneToOneEntityOptions.class, new OneToOneEntityOptions<C, J, O>() {
					@Override
					public OneToOneOptions<C, O> cascading(RelationMode relationMode) {
						oneToOneRelation.setRelationMode(relationMode);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public OneToOneEntityOptions<C, J, O> mandatory() {
						oneToOneRelation.setNullable(false);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public OneToOneEntityOptions<C, J, O> mappedBy(SerializableFunction<? super O, C> reverseLink) {
						oneToOneRelation.setReverseGetter(reverseLink);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public OneToOneEntityOptions<C, J, O> mappedBy(SerializableBiConsumer<? super O, C> reverseLink) {
						oneToOneRelation.setReverseSetter(reverseLink);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public OneToOneEntityOptions<C, J, O> reverseJoinColumn(Column<?, J> reverseLink) {
						oneToOneRelation.setReverseColumn(reverseLink);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public OneToOneEntityOptions<C, J, O> reverseJoinColumn(String reverseColumnName) {
						oneToOneRelation.setReverseColumn(reverseColumnName);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public OneToOneEntityOptions<C, J, O> fetchSeparately() {
						oneToOneRelation.fetchSeparately();
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public OneToOneOptions<C, O> columnName(String columnName) {
						oneToOneRelation.setColumnName(columnName);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public OneToOneEntityOptions<C, J, O> unique() {
						oneToOneRelation.setUnique(true);
						return null;	// we can return null because dispatcher will return proxy
					}
				}, true)	// true to allow "return null" in implemented methods
				.fallbackOn(this)
				.build((Class<FluentMappingBuilderOneToOneOptions<C, I, O>>) (Class) FluentMappingBuilderOneToOneOptions.class);
	}
	
	@Override
	public <O, J, S extends Collection<O>> FluentSubEntityMappingBuilderOneToManyOptions<C, I, O, S> mapOneToMany(
			SerializableFunction<C, S> getter,
			EntityMappingConfigurationProvider<? super O, J> mappingConfiguration) {
		
		AccessorByMethodReference<C, S> getterReference = Accessors.accessorByMethodReference(getter);
		ReversibleAccessor<C, S> propertyAccessor = 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<C, S>(captureMethod(getter)).toMutator());
		return mapOneToMany(propertyAccessor, mappingConfiguration);
	}
	
	@Override
	public <O, J, S extends Collection<O>> FluentSubEntityMappingBuilderOneToManyOptions<C, I, O, S> mapOneToMany(
			SerializableBiConsumer<C, S> setter,
			EntityMappingConfigurationProvider<? super O, J> mappingConfiguration) {
		
		MutatorByMethodReference<C, S> setterReference = Accessors.mutatorByMethodReference(setter);
		PropertyAccessor<C, S> propertyAccessor = new PropertyAccessor<>(
				Accessors.accessor(setterReference.getDeclaringClass(), propertyName(setterReference.getMethodName())),
				setterReference
		);
		return mapOneToMany(propertyAccessor, mappingConfiguration);
	}
	
	private <O, J, S extends Collection<O>> FluentSubEntityMappingBuilderOneToManyOptions<C, I, O, S> mapOneToMany(
			ReversibleAccessor<C, S> propertyAccessor,
			EntityMappingConfigurationProvider<? super O, J> mappingConfiguration) {
		OneToManyRelation<C, O, J, S> oneToManyRelation = new OneToManyRelation<>(
				propertyAccessor,
				() -> polymorphismPolicy instanceof PolymorphismPolicy.TablePerClassPolymorphism,
				mappingConfiguration);
		this.oneToManyRelations.add(oneToManyRelation);
		return new MethodDispatcher()
				.redirect(OneToManyEntityOptions.class, new OneToManyEntityOptionsSupport<>(oneToManyRelation), true)	// true to allow "return null" in implemented methods
				.fallbackOn(this)
				.build((Class<FluentSubEntityMappingBuilderOneToManyOptions<C, I, O, S>>) (Class) FluentSubEntityMappingBuilderOneToManyOptions.class);
	}
	
	@Override
	public <O> FluentMappingBuilderEmbeddableMappingConfigurationImportedEmbedOptions<C, I, O> embed(SerializableFunction<C, O> getter,
																									 EmbeddableMappingConfigurationProvider<? extends O> embeddableMappingBuilder) {
		return embed(propertiesMappingConfigurationDelegate.embed(getter, embeddableMappingBuilder));
	}
	
	@Override
	public <O> FluentMappingBuilderEmbeddableMappingConfigurationImportedEmbedOptions<C, I, O> embed(SerializableBiConsumer<C, O> setter,
																									 EmbeddableMappingConfigurationProvider<? extends O> embeddableMappingBuilder) {
		return embed(propertiesMappingConfigurationDelegate.embed(setter, embeddableMappingBuilder));
	}
	
	@Override
	public FluentSubEntityMappingBuilder<C, I> mapPolymorphism(PolymorphismPolicy<C> polymorphismPolicy) {
		this.polymorphismPolicy = polymorphismPolicy;
		return this;
	}
	
	private <O> FluentMappingBuilderEmbeddableMappingConfigurationImportedEmbedOptions<C, I, O> embed(FluentEmbeddableMappingBuilderEmbeddableMappingConfigurationImportedEmbedOptions<C, O> embedSupport) {
		return new MethodDispatcher()
				.redirect(ImportedEmbedWithColumnOptions.class, new ImportedEmbedWithColumnOptions<C>() {
					
					@Override
					public ImportedEmbedWithColumnOptions<C> overrideName(SerializableFunction getter, String columnName) {
						embedSupport.overrideName(getter, columnName);
						return null;	// we can return null because dispatcher will return proxy
					}

					@Override
					public ImportedEmbedWithColumnOptions<C> overrideName(SerializableBiConsumer setter, String columnName) {
						embedSupport.overrideName(setter, columnName);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public ImportedEmbedWithColumnOptions<C> overrideSize(SerializableBiConsumer setter, Size columnSize) {
						embedSupport.overrideSize(setter, columnSize);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public ImportedEmbedWithColumnOptions<C> overrideSize(SerializableFunction getter, Size columnSize) {
						embedSupport.overrideSize(getter, columnSize);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public ImportedEmbedWithColumnOptions override(SerializableBiConsumer setter, Column targetColumn) {
						propertiesMappingConfigurationDelegate.currentInset().override(setter, targetColumn);
						return null;
					}
					
					@Override
					public ImportedEmbedWithColumnOptions override(SerializableFunction getter, Column targetColumn) {
						propertiesMappingConfigurationDelegate.currentInset().override(getter, targetColumn);
						return null;
					}

					@Override
					public ImportedEmbedWithColumnOptions<C> exclude(SerializableBiConsumer setter) {
						embedSupport.exclude(setter);
						return null;	// we can return null because dispatcher will return proxy
					}

					@Override
					public ImportedEmbedWithColumnOptions<C> exclude(SerializableFunction getter) {
						embedSupport.exclude(getter);
						return null;	// we can return null because dispatcher will return proxy
					}
				}, true)
				.fallbackOn(this)
				.build((Class<FluentMappingBuilderEmbeddableMappingConfigurationImportedEmbedOptions<C, I, O>>) (Class) FluentMappingBuilderEmbeddableMappingConfigurationImportedEmbedOptions.class);
	}
	
	@Override
	public FluentSubEntityMappingBuilder<C, I> withColumnNaming(ColumnNamingStrategy columnNamingStrategy) {
		this.propertiesMappingConfigurationDelegate.withColumnNaming(columnNamingStrategy);
		return this;
	}
	
	/**
	 * Class very close to {@link FluentEmbeddableMappingConfigurationSupport}, but with dedicated methods to sub-entity mapping
	 */
	static class SubEntityDecoratedEmbeddableConfigurationSupport<C, I> extends FluentEmbeddableMappingConfigurationSupport<C> {
		
		private final FluentSubEntityMappingConfigurationSupport<C, I> entityConfigurationSupport;
		
		/**
		 * Creates a builder to map the given class for persistence
		 *
		 * @param persistedClass the class to create a mapping for
		 */
		public SubEntityDecoratedEmbeddableConfigurationSupport(FluentSubEntityMappingConfigurationSupport<C, I> entityConfigurationSupport, Class<C> persistedClass) {
			super(persistedClass);
			this.entityConfigurationSupport = entityConfigurationSupport;
		}
		
		<E> LinkageSupport<C, E> addMapping(SerializableBiConsumer<C, E> setter) {
			LinkageSupport<C, E> newLinkage = new LinkageSupport<>(setter);
			mapping.add(newLinkage);
			return newLinkage;
		}
		
		<E> LinkageSupport<C, E> addMapping(SerializableFunction<C, E> getter) {
			LinkageSupport<C, E> newLinkage = new LinkageSupport<>(getter);
			mapping.add(newLinkage);
			return newLinkage;
		}
		
		private <O> FluentSubEntityMappingBuilderPropertyOptions<C, I, O> wrapForAdditionalOptions(LinkageSupport<C, O> newMapping) {
			return new MethodDispatcher()
					.redirect(ColumnOptions.class, new ColumnOptions<O>() {
						@Override
						public ColumnOptions<O> mandatory() {
							newMapping.setNullable(false);
							return null;
						}
						
						@Override
						public ColumnOptions<O> nullable() {
							newMapping.setNullable(false);
							return null;
						}
						
						@Override
						public ColumnOptions<O> unique() {
							newMapping.setUnique(true);
							return null;
						}
						
						@Override
						public ColumnOptions<O> setByConstructor() {
							newMapping.setByConstructor();
							return null;
						}
						
						@Override
						public ColumnOptions<O> readonly() {
							newMapping.readonly();
							return null;
						}
						
						@Override
						public ColumnOptions<O> columnName(String name) {
							newMapping.getColumnOptions().setColumnName(name);
							return null;
						}
						
						@Override
						public ColumnOptions<O> columnSize(Size size) {
							newMapping.getColumnOptions().setColumnSize(size);
							return null;
						}
						
						@Override
						public ColumnOptions<O> column(Column column) {
							newMapping.setColumnOptions(new ColumnLinkageOptionsByColumn(column));
							return null;
						}
						
						@Override
						public ColumnOptions<O> fieldName(String name) {
							newMapping.setField(SubEntityDecoratedEmbeddableConfigurationSupport.this.entityConfigurationSupport.classToPersist, name);
							return null;
						}
						
						@Override
						public <X> ColumnOptions<O> readConverter(Converter<X, O> converter) {
							newMapping.setReadConverter(converter);
							return null;
						}
						
						@Override
						public <X> ColumnOptions<O> writeConverter(Converter<O, X> converter) {
							newMapping.setWriteConverter(converter);
							return null;
						}
						
						@Override
						public <V> ColumnOptions<O> sqlBinder(ParameterBinder<V> parameterBinder) {
							newMapping.setParameterBinder(parameterBinder);
							return null;
						}
					}, true)
					.fallbackOn(entityConfigurationSupport)
					.build((Class<FluentSubEntityMappingBuilderPropertyOptions<C, I, O>>) (Class) FluentSubEntityMappingBuilderPropertyOptions.class);
		}
	}
	
}
