package org.codefilarete.stalactite.engine.configurer.builder.embeddable;

import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;

import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.ReversibleAccessor;
import org.codefilarete.reflection.ReversibleMutator;
import org.codefilarete.reflection.ValueAccessPoint;
import org.codefilarete.reflection.ValueAccessPointComparator;
import org.codefilarete.reflection.ValueAccessPointMap;
import org.codefilarete.reflection.ValueAccessPointSet;
import org.codefilarete.stalactite.dsl.MappingConfigurationException;
import org.codefilarete.stalactite.dsl.key.CompositeKeyMappingConfiguration;
import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.UniqueConstraintNamingStrategy;
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.SQLStatement.BindingException;
import org.codefilarete.stalactite.sql.statement.binder.ColumnBinderRegistry;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinderRegistry.EnumBindType;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.bean.Objects;
import org.codefilarete.tool.collection.Arrays;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.Maps;
import org.codefilarete.tool.exception.NotImplementedException;
import org.codefilarete.tool.function.Converter;
import org.codefilarete.tool.function.Hanger.Holder;

import static org.codefilarete.reflection.MethodReferences.toMethodReferenceString;
import static org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMappingConfiguration.EmbeddableLinkageSupport;
import static org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMappingConfiguration.fromCompositeKeyMappingConfiguration;
import static org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMappingConfiguration.fromEmbeddableMappingConfiguration;
import static org.codefilarete.tool.Nullable.nullable;
import static org.codefilarete.tool.collection.Iterables.stream;

/**
 * Engine that converts mapping definition of a {@link EmbeddableMappingConfiguration} to a simple {@link Map}.
 * Designed as such as its instances can be reused (no constructor, attributes are set through
 * {@link #build()}
 * therefore they are not thread-safe. This was made to avoid extra instance creation because this class is used in some loops.
 *
 * Whereas it consumes an {@link EmbeddableMappingConfiguration} it doesn't mean that its goal is to manage embedded beans of an entity : as its
 * name says it's aimed at collecting mapping of any beans, without the entity part (understanding identification and inheritance which is targeted
 * by {@link org.codefilarete.stalactite.engine.configurer.builder.DefaultPersisterBuilder})
 *
 * @author Guillaume Mary
 * @see #build()
 */
public class EmbeddableMappingBuilder<C, T extends Table<T>> {
	
	/**
	 * Iterates over configuration to look for any property defining a {@link Column} to use, its table would be the one to be used by builder.
	 * Throws an exception if several tables are found during iteration.
	 *
	 * @param mappingConfiguration the configuration to look up for any overriding {@link Column}
	 * @return null if no {@link Table} was found (meaning that builder is free to create one)
	 */
	public static Table giveTargetTable(org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration<?> mappingConfiguration) {
		Holder<Table> result = new Holder<>();
		
		// algorithm close to the one of includeEmbeddedMapping(..)
		Queue<Inset> stack = new ArrayDeque<>(fromEmbeddableMappingConfiguration(mappingConfiguration).getInsets());
		while (!stack.isEmpty()) {
			Inset<?, ?> inset = stack.poll();
			inset.getOverriddenColumns().forEach((valueAccessPoint, targetColumn) ->
				assertHolderIsFilledWithTargetTable(result, targetColumn, valueAccessPoint)
			);
			org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMappingConfiguration<?> configuration = inset.getConfiguration();
			stack.addAll(configuration.getInsets());
		}
		return result.get();
	}

	public static Table giveTargetTable(org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration<?> mappingConfiguration, Table mainTable) {
		Holder<Table> result = new Holder<>(mainTable);

		// algorithm close to the one of includeEmbeddedMapping(..)
		Queue<Inset> stack = new ArrayDeque<>(fromEmbeddableMappingConfiguration(mappingConfiguration).getInsets());
		while (!stack.isEmpty()) {
			Inset<?, ?> inset = stack.poll();
			inset.getOverriddenColumns().forEach((valueAccessPoint, targetColumn) ->
					assertHolderIsFilledWithTargetTable(result, targetColumn, valueAccessPoint)
			);
			org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMappingConfiguration<?> configuration = inset.getConfiguration();
			stack.addAll(configuration.getInsets());
		}
		return result.get();
	}
	
	private static void assertHolderIsFilledWithTargetTable(Holder<Table> result, Column targetColumn, ValueAccessPoint<?> valueAccessPoint) {
		if (targetColumn != null) {
			if (result.get() != null && result.get() != targetColumn.getTable()) {
				throw new MappingConfigurationException("Property " + valueAccessPoint + " overrides column with " + targetColumn.getAbsoluteName() + " but it is not part of main table " + result.get().getAbsoluteName());
			}
			result.set(targetColumn.getTable());
		}
	}
	
	/**
	 * Composes an {@link AccessorChain} from given {@link ValueAccessPoint}.
	 * Acts as a caster or converter because {@link ValueAccessPoint} is an abstract vision of elements accepted by
	 * {@link AccessorChain}.
	 * Supported {@link ValueAccessPoint}s are {@link Accessor} and {@link ReversibleAccessor}, else an exception will be thrown
	 *
	 * @param rootAccessors first elements to access a property
	 * @param terminalAccessor final accessor that read property of latest element of rootAccessors
	 * @param beanType read property type
	 * @return a new {@link AccessorChain} that access property of terminalAccessor thanks to all accessors of rootAccessors
	 */
	private static <SRC, TRGT> AccessorChain<SRC, TRGT> newAccessorChain(List<? extends ValueAccessPoint<?>> rootAccessors,
																		 ValueAccessPoint<?> terminalAccessor,
																		 Class<TRGT> beanType) {
		// we must clone rootAccessors to prevent from accessor mixing
		List<ValueAccessPoint<?>> accessorsTmp = new ArrayList<>(rootAccessors);
		accessorsTmp.add(terminalAccessor);
		List<Accessor<?, ?>> finalAccessors = new ArrayList<>();
		accessorsTmp.forEach(valueAccessPoint -> {
			if (valueAccessPoint instanceof Accessor) {
				finalAccessors.add((Accessor) valueAccessPoint);
			} else if (valueAccessPoint instanceof ReversibleMutator) {
				finalAccessors.add(((ReversibleMutator) valueAccessPoint).toAccessor());
			} else {
				// Something has change in ValueAccessPoint hierarchy, it should be fixed (grave)
				throw new NotImplementedException(Reflections.toString(valueAccessPoint.getClass()) + " is unknown from chain creation algorithm");
			}
		});
		return AccessorChain.fromAccessorsWithNullSafe(
				finalAccessors,
				// this can look superfluous but fills the gap of instantiating right bean when configuration is a subtype of inset accessor,
				// case which is allowed by signature of embed(..) method : it accepts "? extend T" as parameter type of given configuration
				// (where T is type returned by accessor, or expected as input of mutator)
				(accessor, aClass) -> Reflections.newInstance(beanType));
	}
	
	private final EmbeddableMappingConfiguration<C> mainMappingConfiguration;
	private final T targetTable;
	private final ColumnBinderRegistry columnBinderRegistry;
	private final ColumnNamingStrategy columnNamingStrategy;
	private final UniqueConstraintNamingStrategy uniqueConstraintNamingStrategy;
	
	public EmbeddableMappingBuilder(org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration<C> mappingConfiguration,
									T targetTable,
									ColumnBinderRegistry columnBinderRegistry,
									ColumnNamingStrategy columnNamingStrategy,
									UniqueConstraintNamingStrategy uniqueConstraintNamingStrategy) {
		this(fromEmbeddableMappingConfiguration(mappingConfiguration),
				targetTable,
				columnBinderRegistry,
				columnNamingStrategy,
				uniqueConstraintNamingStrategy);
	}
	
	public EmbeddableMappingBuilder(CompositeKeyMappingConfiguration<C> mappingConfiguration,
									T targetTable,
									ColumnBinderRegistry columnBinderRegistry,
									ColumnNamingStrategy columnNamingStrategy,
									UniqueConstraintNamingStrategy uniqueConstraintNamingStrategy) {
		this(fromCompositeKeyMappingConfiguration(mappingConfiguration),
				targetTable,
				columnBinderRegistry,
				columnNamingStrategy,
				uniqueConstraintNamingStrategy);
	}
	
	@VisibleForTesting
	EmbeddableMappingBuilder(EmbeddableMappingConfiguration<C> mappingConfiguration,
							 T targetTable,
							 ColumnBinderRegistry columnBinderRegistry,
							 ColumnNamingStrategy columnNameStrategy,
							 UniqueConstraintNamingStrategy uniqueConstraintNamingStrategy) {
		this.mainMappingConfiguration = mappingConfiguration;
		this.targetTable = targetTable;
		this.columnNamingStrategy = columnNameStrategy;
		this.columnBinderRegistry = columnBinderRegistry;
		this.uniqueConstraintNamingStrategy = uniqueConstraintNamingStrategy;
	}
	
	/**
	 * Converts mapping definition of a {@link EmbeddableMappingConfiguration} into a simple {@link Map}
	 *
	 * @return a bean that stores some {@link Map}s representing the definition of the mapping declared by the {@link EmbeddableMappingConfiguration}
	 */
	public EmbeddableMapping<C, T> build() {
		return build(false);
	}
	
	public EmbeddableMapping<C, T> build(boolean onlyExtraTableLinkages) {
		return build(onlyExtraTableLinkages, new ValueAccessPointSet<>());
	}
	
	private EmbeddableMapping<C, T> build(boolean onlyExtraTableLinkages, ValueAccessPointSet<C> excludedProperties) {
		InternalProcessor internalProcessor = new InternalProcessor(onlyExtraTableLinkages);
		// converting direct mapping
		internalProcessor.includeDirectMapping(this.mainMappingConfiguration,
				null,
				new ValueAccessPointMap<>(),
				new ValueAccessPointMap<>(),
				new ValueAccessPointMap<>(),
				excludedProperties);
		// adding embeddable (no particular thought about order compared to previous direct mapping)
		internalProcessor.includeEmbeddedMapping();
		return internalProcessor.result;
	}
	
	protected T getTargetTable() {
		return targetTable;
	}
	
	protected <O> String determineColumnName(EmbeddableLinkage<C, O> linkage, @Nullable String overriddenColumName) {
		return nullable(overriddenColumName)
				.elseSet(linkage::getColumnName)
				.elseSet(linkage::getFieldName)
				.elseSet(() -> columnNamingStrategy.giveName(AccessorDefinition.giveDefinition(linkage.getAccessor())))
				.get();
	}
	
	protected <O> Size determineColumnSize(EmbeddableLinkage<C, O> linkage, @Nullable Size overriddenColumSize) {
		return nullable(overriddenColumSize).elseSet(linkage::getColumnSize).get();
	}
	
	/**
	 * Internal engine driven by {@link #build()} method.
	 * Made to store result in another class than main one and decouple configuration from process.
	 *
	 * @author Guillaume Mary
	 */
	protected class InternalProcessor {
		
		private final EmbeddableMapping<C, T> result = new EmbeddableMapping<>();
		
		private final boolean onlyExtraTableLinkages;
		
		protected InternalProcessor(boolean onlyExtraTableLinkages) {
			this.onlyExtraTableLinkages = onlyExtraTableLinkages;
		}
		
		protected void includeDirectMapping(EmbeddableMappingConfiguration<?> mappingConfiguration,
											@Nullable ValueAccessPoint<C> accessorPrefix,
											ValueAccessPointMap<C, String> overriddenColumnNames,
											ValueAccessPointMap<C, Size> overriddenColumnSizes,
											ValueAccessPointMap<C, Column<T, ?>> overriddenColumns,
											ValueAccessPointSet<C> excludedProperties) {
			Stream<EmbeddableLinkage> linkageStream = mappingConfiguration.getPropertiesMapping().stream()
					.filter(linkage -> !excludedProperties.contains(linkage.getAccessor()));
			
			if (!onlyExtraTableLinkages) {
				// this method (and class) doesn't deal with extra table
				linkageStream = linkageStream.filter(linkage -> linkage.getExtraTableName() == null);
			}
			linkageStream.forEach(linkage -> {
				Column<T, ?> overriddenColumn = overriddenColumns.get(linkage.getAccessor());
				String columnName = nullable(overriddenColumn)
						.map(Column::getName)
						.getOr(() -> determineColumnName(linkage, overriddenColumnNames.get(linkage.getAccessor())));
				Size columnSize = nullable(overriddenColumn)
						.map(Column::getSize)
						.getOr(() -> determineColumnSize(linkage, overriddenColumnSizes.get(linkage.getAccessor())));
				assertMappingIsNotAlreadyDefinedByInheritance(linkage, columnName, mappingConfiguration);
				Duo<ReversibleAccessor<C, Object>, Column<T, Object>> mapping = includeMapping(
						linkage,
						accessorPrefix,
						columnName,
						columnSize,
						overriddenColumn,
						mappingConfiguration);
				Converter<Object, Object> readConverter = linkage.getReadConverter();
				result.getReadConverters().put(mapping.getLeft(), readConverter);
				if (!linkage.isReadonly()) {
					result.getMapping().put(mapping.getLeft(), mapping.getRight());
					Converter<Object, Object> writeConverter = linkage.getWriteConverter();
					result.getWriteConverters().put(mapping.getLeft(), writeConverter);
				} else {
					result.getReadonlyMapping().put(mapping.getLeft(), mapping.getRight());
				}
			});
		}
		
		protected <O> Duo<ReversibleAccessor<C, ?>, Column<T, Object>> includeMapping(EmbeddableLinkage<C, O> linkage,
																					  @Nullable ValueAccessPoint<C> accessorPrefix,
																					  String columnName,
																					  @Nullable Size columnSize,
																					  @Nullable Column<T, O> overriddenColumn,
																					  EmbeddableMappingConfiguration<?> beanMappingConfiguration) {
			Column<T, O> column = nullable(overriddenColumn).getOr(() -> addColumnToTable(linkage, columnName, columnSize));
			
			if (linkage.isUnique() && linkage instanceof EmbeddableLinkageSupport) {
				targetTable.addUniqueConstraint(
								Objects.preventNull(beanMappingConfiguration.getUniqueConstraintNamingStrategy(), uniqueConstraintNamingStrategy).giveName(linkage.getAccessor(), column),
								column);
			}
			
			ensureColumnBindingInRegistry(linkage, column);
			ReversibleAccessor<C, ?> accessor;
			if (accessorPrefix != null) {
				accessor = newAccessorChain(Collections.singletonList(accessorPrefix), linkage.getAccessor(), beanMappingConfiguration.getBeanType());
			} else {
				accessor = linkage.getAccessor();
			}
			return new Duo<>(accessor, (Column<T, Object>) column);
		}
		
		protected <O> void assertMappingIsNotAlreadyDefinedByInheritance(EmbeddableLinkage<C, O> linkage, String columnNameToCheck, EmbeddableMappingConfiguration<O> mappingConfiguration) {
			DuplicateDefinitionChecker duplicateDefinitionChecker = new DuplicateDefinitionChecker(columnNameToCheck, linkage.getAccessor(), columnNamingStrategy);
			stream(mappingConfiguration.inheritanceIterable())
					.flatMap(configuration -> (Stream<EmbeddableLinkage>) configuration.getPropertiesMapping().stream())
					// not using equals() is voluntary since we want reference checking here to exclude same instance,
					// since given linkage is one of given mappingConfiguration
					// (doing as such also prevent equals() method override to break this algorithm)
					.filter(pawn -> linkage != pawn
							// only writable properties are concerned by this check : we allow duplicates for readonly properties
							&& !pawn.isReadonly() && !linkage.isReadonly())
					.forEach(duplicateDefinitionChecker);
		}
		
		protected <O> Column<T, O> addColumnToTable(EmbeddableLinkage<C, O> linkage, String columnName, @Nullable Size columnSize) {
			Column<T, O> addedColumn;
			Class<O> columnType;
			if (linkage.getColumnType().isEnum()) {
				if (linkage.getEnumBindType() == null) {
					columnType = (Class<O>) Enum.class;
				} else {
					columnType = linkage.getEnumBindType() == EnumBindType.NAME
							? (Class<O>) String.class
							: (Class<O>) Integer.class;
				}
			} else {
				if (linkage.getParameterBinder() != null) {
					// when a parameter binder is defined, then the column type must be binder one
					columnType = linkage.getParameterBinder().getColumnType();
				} else {
					columnType = linkage.getColumnType();
				}
			}
			// if user ask for nullability, then we follow his demand, else we rely on property type: primitive ones are mandatory
			Boolean isColumnNullable =
					nullable(linkage.isNullable()).getOr(() -> Reflections.isPrimitiveType(linkage.getColumnType()) ? false : null);
			addedColumn = targetTable.addColumn(columnName, columnType, columnSize, isColumnNullable);
			return addedColumn;
		}
		
		protected <O> void ensureColumnBindingInRegistry(EmbeddableLinkage<C, O> linkage, Column<?, O> column) {
			if (linkage.getColumnType().isEnum()) {
				EnumBindType enumBindType = Objects.preventNull(linkage.getEnumBindType(), EnumBindType.ORDINAL);
				columnBinderRegistry.register(column, enumBindType.newParameterBinder((Class<Enum>) linkage.getColumnType()));
			} else if (linkage.getParameterBinder() != null) {
				columnBinderRegistry.register(column, (ParameterBinder<O>) linkage.getParameterBinder());
			} else {
				try {
					// assertion to check that column binder is registered : it will throw en exception if the binder is not found
					columnBinderRegistry.getBinder(column);
				} catch (BindingException e) {
					throw new MappingConfigurationException("No binder found for property " + AccessorDefinition.toString(linkage.getAccessor())
							+ " : neither its column nor its type are registered (" + column.getAbsoluteName() + ", type " + Reflections.toString(column.getJavaType()) + ")", e);
				}
			}
		}
		
		/**
		 * Adds embedded beans mapping to result
		 */
		private void includeEmbeddedMapping() {
			Set<Inset<C, ?>> treatedInsets = new HashSet<>();
			
			Queue<Inset<C, ?>> stack = Collections.asLifoQueue(new ArrayDeque<>());
			stack.addAll(mainMappingConfiguration.getInsets());
			Queue<Accessor<C, ?>> accessorPath = new ArrayDeque<>();
			while (!stack.isEmpty()) {
				Inset<C, ?> inset = stack.poll();
				
				assertNotAlreadyDeclared(inset, treatedInsets);
				
				EmbeddableMappingConfiguration<C> configuration = (EmbeddableMappingConfiguration<C>) inset.getConfiguration();
				ValueAccessPoint<C> mappingPrefix = null;
				if (inset.getAccessor() != null) {
					accessorPath.add(inset.getAccessor());
					// small optimization to avoid creation of an Accessor chain of 1 element
					if (accessorPath.size() == 1) {
						mappingPrefix = accessorPath.peek();
					} else {
						mappingPrefix = AccessorChain.fromAccessorsWithNullSafe(new ArrayList<>(accessorPath));
					}
				}
				EmbeddableMappingConfiguration<?> superClassConfiguration = configuration.getMappedSuperClassConfiguration();
				if (superClassConfiguration != null) {
					includeMappedSuperClassMapping(inset, accessorPath, superClassConfiguration);
				}
				
				includeDirectMapping(configuration,
						mappingPrefix,
						inset.getOverriddenColumnNames(),
						inset.getOverriddenColumnSizes(),
						(ValueAccessPointMap) inset.getOverriddenColumns(),
						inset.getExcludedProperties());
				if (configuration.getInsets().isEmpty()) {
					accessorPath.remove();
				} else {
					stack.addAll(configuration.getInsets());
				}
				treatedInsets.add(inset);
			}
		}
		
		private void includeMappedSuperClassMapping(Inset<C, ?> inset, Collection<Accessor<C, ?>> accessorPath, EmbeddableMappingConfiguration<?> superClassConfiguration) {
			// we include super type mapping by using a new instance of EmbeddableMappingBuilder, this is the simplest (but maybe not the most
			// debuggable) and allows to manage inheritance of several mappedSuperClass 
			ValueAccessPointSet<C> excludedProperties = new ValueAccessPointSet<>();
			// we remove overridden inset columns to avoid their creation by the EmbeddableMappingBuilder
			// to avoid duplicates because they are already in the target table (through their creation) and the builder
			// will create them with their default name
			excludedProperties.addAll(inset.getOverriddenColumns().keySet());
			EmbeddableMappingBuilder<C, T> mappedSuperClassBuilder = new EmbeddableMappingBuilder<C, T>((EmbeddableMappingConfiguration<C>) superClassConfiguration, targetTable,
					columnBinderRegistry, columnNamingStrategy, Objects.preventNull(superClassConfiguration.getUniqueConstraintNamingStrategy(), uniqueConstraintNamingStrategy)) {
				
				@Override
				protected <O> String determineColumnName(EmbeddableLinkage<C, O> linkage, @Nullable String overriddenColumName) {
					return super.determineColumnName(linkage, inset.getOverriddenColumnNames().get(linkage.getAccessor()));
				}
				
				@Override
				protected <O> Size determineColumnSize(EmbeddableLinkage<C, O> linkage, @Nullable Size overriddenColumSize) {
					return super.determineColumnSize(linkage, inset.getOverriddenColumnSizes().get(linkage.getAccessor()));
				}
			};
			
			EmbeddableMapping<C, T> superMapping = mappedSuperClassBuilder.build(false, excludedProperties);
			Class<?> insetBeanType = inset.getConfiguration().getBeanType();
			superMapping.getMapping().forEach((accessor, column) -> {
				AccessorChain prefix;
				List<Accessor<?, ?>> accessors;
				if (accessorPath.size() == 1) {
					accessors = Arrays.asList(Iterables.first(accessorPath), accessor);
				} else {
					accessors = new ArrayList<>(accessorPath);
					accessors.add(accessor);
				}
				prefix = AccessorChain.fromAccessorsWithNullSafe(
						accessors,
						// this can look superfluous but fills the gap with instantiating right bean when configuration is a subtype of inset accessor,
						// case which is allowed by signature of embed(..) method : it accepts "? extend T" as parameter type of given configuration
						// (where T is type returned by accessor, or expected as input of mutator)
						(localAccessor, accessorInputType) -> Reflections.newInstance(insetBeanType));
				
				// Computing definitive column because it may be overridden by inset declaration
				Column<T, ?> finalColumn;
				if (inset.getOverriddenColumns().containsKey(accessor)) {
					finalColumn = inset.getOverriddenColumns().get(accessor);
				} else if (inset.getOverriddenColumnNames().containsKey(accessor)) {
					finalColumn = targetTable.addColumn(inset.getOverriddenColumnNames().get(accessor), column.getJavaType());
				} else {
					finalColumn = targetTable.addColumn(column.getName(), column.getJavaType());
				}
				result.getMapping().put((ReversibleAccessor<C, Object>) prefix, (Column<T, Object>) finalColumn);
			});
		}
		
		
		/**
		 * Ensures that a bean is not already embedded with same accessor, because its columns would conflict with already defined ones, or checks that every property
		 * is overridden.
		 * Throws an exception if that's not the case.
		 *
		 * @param inset current inset to be checked for duplicate
		 * @param treatedInsets already mapped insets : if one of them matches given inset on mapped type, then a fine-graned check is done to look for conflict
		 */
		private void assertNotAlreadyDeclared(Inset<C, ?> inset, Set<Inset<C, ?>> treatedInsets) {
			Optional<Inset<C, ?>> alreadyMappedType = treatedInsets.stream().filter(i -> i.getEmbeddedClass() == inset.getEmbeddedClass()).findFirst();
			if (alreadyMappedType.isPresent()) {
				// accessors are exactly the same ?
				if (alreadyMappedType.get().getInsetAccessor().equals(inset.getInsetAccessor())) {
					Method currentMethod = inset.getInsetAccessor();
					String currentMethodReference = toMethodReferenceString(currentMethod);
					throw new MappingConfigurationException(currentMethodReference + " is already mapped");
				}
				
				Map<String, ValueAccessPoint<?>> columNamePerAccessPoint = new HashMap<>();
				EmbeddableMappingConfiguration<?> insetConfiguration = inset.getConfiguration();
				insetConfiguration.getPropertiesMapping().forEach(linkage -> {
					if (!inset.getExcludedProperties().contains(linkage.getAccessor())) {
						String columnName = determineColumnName(linkage, inset.getOverriddenColumnNames().get(linkage.getAccessor()));
						columNamePerAccessPoint.put(columnName, linkage.getAccessor());
					}
				});
				Inset<?, ?> abstractInset = alreadyMappedType.get();
				Map<String, ValueAccessPoint<?>> columNamePerAccessPoint2 = new HashMap<>();
				insetConfiguration.getPropertiesMapping().forEach(linkage -> {
					if (!abstractInset.getExcludedProperties().contains(linkage.getAccessor())) {
						String columnName = determineColumnName(linkage, abstractInset.getOverriddenColumnNames().get(linkage.getAccessor()));
						columNamePerAccessPoint2.put(columnName, linkage.getAccessor());
					}
				});
				Map<ValueAccessPoint<?>, ValueAccessPoint<?>> join = Maps.innerJoin(columNamePerAccessPoint, columNamePerAccessPoint2);
				if (!join.isEmpty()) {
					String currentMethodReference = toMethodReferenceString(inset.getInsetAccessor());
					String conflictingDeclaration = toMethodReferenceString(abstractInset.getInsetAccessor());
					throw new MappingConfigurationException(
							currentMethodReference + " conflicts with " + conflictingDeclaration + " while embedding a " + Reflections.toString(inset.getEmbeddedClass())
									+ ", column names should be overridden : "
									+ join.keySet()
									.stream().map(AccessorDefinition::toString).collect(Collectors.joining(", ")));
				}
			}
		}
	}
	
	static class DuplicateDefinitionChecker implements Consumer<EmbeddableLinkage> {
		
		private final String columnNameToCheck;
		private final ReversibleAccessor propertyAccessor;
		private final ColumnNamingStrategy columnNameStrategy;
		private static final ValueAccessPointComparator VALUE_ACCESS_POINT_COMPARATOR = new ValueAccessPointComparator();
		
		DuplicateDefinitionChecker(String columnNameToCheck, ReversibleAccessor propertyAccessor, ColumnNamingStrategy columnNameStrategy) {
			this.columnNameToCheck = columnNameToCheck;
			this.propertyAccessor = propertyAccessor;
			this.columnNameStrategy = columnNameStrategy;
		}
		@Override
		public void accept(EmbeddableLinkage pawn) {
			ReversibleAccessor accessor = pawn.getAccessor();
			if (VALUE_ACCESS_POINT_COMPARATOR.compare(accessor, propertyAccessor) == 0) {
				throw new MappingConfigurationException("Mapping is already defined by method " + AccessorDefinition.toString(propertyAccessor));
			} else if (columnNameToCheck.equals(pawn.getColumnName())
						|| columnNameToCheck.equals(columnNameStrategy.giveName(AccessorDefinition.giveDefinition(accessor)))) {
				throw new MappingConfigurationException("Column '" + columnNameToCheck + "' of mapping '" + AccessorDefinition.toString(propertyAccessor)
						+ "' is already targeted by '" + AccessorDefinition.toString(pawn.getAccessor()) + "'");
			}
		}
	}
}
