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.List;
import java.util.function.BiConsumer;

import org.codefilarete.reflection.AccessorByMethod;
import org.codefilarete.reflection.AccessorByMethodReference;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.MethodReferenceCapturer;
import org.codefilarete.reflection.MethodReferenceDispatcher;
import org.codefilarete.reflection.MutatorByMethod;
import org.codefilarete.reflection.MutatorByMethodReference;
import org.codefilarete.reflection.PropertyAccessor;
import org.codefilarete.reflection.ReversibleAccessor;
import org.codefilarete.reflection.ValueAccessPointMap;
import org.codefilarete.reflection.ValueAccessPointSet;
import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.key.CompositeKeyMappingConfiguration;
import org.codefilarete.stalactite.dsl.key.CompositeKeyMappingConfigurationProvider;
import org.codefilarete.stalactite.dsl.key.CompositeKeyPropertyOptions;
import org.codefilarete.stalactite.dsl.key.FluentCompositeKeyMappingBuilder;
import org.codefilarete.stalactite.dsl.embeddable.ImportedEmbedOptions;
import org.codefilarete.stalactite.engine.configurer.PropertyAccessorResolver.PropertyMapping;
import org.codefilarete.stalactite.sql.ddl.Size;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinderRegistry.EnumBindType;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.function.SerializableThrowingFunction;
import org.codefilarete.tool.function.SerializableTriFunction;
import org.codefilarete.tool.function.ThreadSafeLazyInitializer;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import org.danekja.java.util.function.serializable.SerializableFunction;

/**
 * @author Guillaume Mary
 */
public class FluentCompositeKeyMappingConfigurationSupport<C> implements FluentCompositeKeyMappingBuilder<C>, LambdaMethodUnsheller,
		CompositeKeyMappingConfiguration<C> {
	
	private CompositeKeyMappingConfigurationProvider<? super C> superMappingBuilder;
	
	/** Owning class of mapped properties */
	private final Class<C> classToPersist;
	
	@Nullable
	private ColumnNamingStrategy columnNamingStrategy;
	
	/** Mapping definitions */
	protected final List<CompositeKeyLinkage> mapping = new ArrayList<>();
	
	/** Collection of embedded elements, even inner ones to help final build process */
	private final Collection<Inset<C, ?>> insets = new ArrayList<>();
	
	/** Last embedded element, introduced to help inner embedding registration (kind of algorithm help). Has no purpose in whole mapping configuration. */
	private Inset<C, ?> currentInset;
	
	/** Helper to unshell method references */
	private final MethodReferenceCapturer methodSpy;
	
	/**
	 * Creates a builder to map the given class for persistence
	 *
	 * @param classToPersist the class to create a mapping for
	 */
	public FluentCompositeKeyMappingConfigurationSupport(Class<C> classToPersist) {
		this.classToPersist = classToPersist;
		
		// Helper to capture Method behind method reference
		this.methodSpy = new MethodReferenceCapturer();
	}
	
	@Override
	public Class<C> getBeanType() {
		return classToPersist;
	}
	
	@Override
	public Collection<Inset<C, Object>> getInsets() {
		return (Collection) insets;
	}
	
	@Override
	public CompositeKeyMappingConfiguration<? super C> getMappedSuperClassConfiguration() {
		return superMappingBuilder == null ? null : superMappingBuilder.getConfiguration();
	}
	
	@Override
	@Nullable
	public ColumnNamingStrategy getColumnNamingStrategy() {
		return columnNamingStrategy;
	}
	
	@Override
	public List<CompositeKeyLinkage> getPropertiesMapping() {
		return mapping;
	}
	
	@Override
	public CompositeKeyMappingConfiguration<C> getConfiguration() {
		return this;
	}
	
	@Override
	public Method captureLambdaMethod(SerializableFunction getter) {
		return this.methodSpy.findMethod(getter);
	}
	
	@Override
	public Method captureLambdaMethod(SerializableBiConsumer setter) {
		return this.methodSpy.findMethod(setter);
	}
	
	@Override
	public FluentCompositeKeyMappingConfigurationSupport<C> withColumnNaming(ColumnNamingStrategy columnNamingStrategy) {
		this.columnNamingStrategy = columnNamingStrategy;
		return this;
	}
	
	/**
	 * Gives access to currently configured {@link Inset}. Made so one can access features of {@link Inset} which are wider than
	 * the one available through {@link FluentCompositeKeyMappingBuilder}.
	 * 
	 * @return the last {@link Inset} built by {@link #newInset(SerializableFunction, CompositeKeyMappingConfigurationProvider)}
	 * or {@link #newInset(SerializableBiConsumer, CompositeKeyMappingConfigurationProvider)}
	 */
	protected Inset<C, ?> currentInset() {
		return currentInset;
	}
	
	protected <O> Inset<C, O> newInset(SerializableFunction<C, O> getter, CompositeKeyMappingConfigurationProvider<? extends O> CompositeKeyMappingBuilder) {
		currentInset = new Inset<>(getter, CompositeKeyMappingBuilder, this);
		return (Inset<C, O>) currentInset;
	}
	
	protected <O> Inset<C, O> newInset(SerializableBiConsumer<C, O> setter, CompositeKeyMappingConfigurationProvider<? extends O> CompositeKeyMappingBuilder) {
		currentInset = new Inset<>(setter, CompositeKeyMappingBuilder, this);
		return (Inset<C, O>) currentInset;
	}
	
	@Override
	public <O> FluentCompositeKeyMappingBuilderPropertyOptions<C> map(SerializableBiConsumer<C, O> setter) {
		return wrapWithPropertyOptions(addMapping(setter));
	}
	
	@Override
	public <O> FluentCompositeKeyMappingBuilderPropertyOptions<C> map(SerializableFunction<C, O> getter) {
		return wrapWithPropertyOptions(addMapping(getter));
	}
	
	<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;
	}
	
	<O> FluentCompositeKeyMappingBuilderPropertyOptions<C> wrapWithPropertyOptions(LinkageSupport<C, O> linkage) {
		return new MethodReferenceDispatcher()
				.redirect(CompositeKeyPropertyOptions.class, new CompositeKeyPropertyOptions() {
					
					@Override
					public CompositeKeyPropertyOptions columnName(String name) {
						linkage.setColumnName(name);
						return null;
					}
					
					@Override
					public CompositeKeyPropertyOptions columnSize(Size size) {
						linkage.setColumnSize(size);
						return null;
					}
					
					@Override
					public CompositeKeyPropertyOptions fieldName(String name) {
						linkage.setField(FluentCompositeKeyMappingConfigurationSupport.this.classToPersist, name);
						return null;
					}
				}, true)
				.fallbackOn(this)
				.build((Class<FluentCompositeKeyMappingBuilderPropertyOptions<C>>) (Class) FluentCompositeKeyMappingBuilderPropertyOptions.class);
	}
	
	@Override
	public <E extends Enum<E>> FluentCompositeKeyMappingBuilderEnumOptions<C> mapEnum(SerializableBiConsumer<C, E> setter) {
		LinkageSupport<C, E> linkage = addMapping(setter);
		return wrapWithEnumOptions(linkage);
	}
	
	@Override
	public <E extends Enum<E>> FluentCompositeKeyMappingBuilderEnumOptions<C> mapEnum(SerializableFunction<C, E> getter) {
		LinkageSupport<C, E> linkage = addMapping(getter);
		return wrapWithEnumOptions(linkage);
	}
	
	<O extends Enum> FluentCompositeKeyMappingBuilderEnumOptions<C> wrapWithEnumOptions(LinkageSupport<C, O> linkage) {
		return new MethodReferenceDispatcher()
				.redirect(CompositeKeyEnumOptions.class, new CompositeKeyEnumOptions() {
					
					@Override
					public CompositeKeyEnumOptions byName() {
						linkage.setEnumBindType(EnumBindType.NAME);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public CompositeKeyEnumOptions byOrdinal() {
						linkage.setEnumBindType(EnumBindType.ORDINAL);
						return null;	// we can return null because dispatcher will return proxy
					}
				}, true)
				.redirect(CompositeKeyPropertyOptions.class, new CompositeKeyPropertyOptions() {
					
					@Override
					public CompositeKeyPropertyOptions columnName(String name) {
						linkage.setColumnName(name);
						return null;
					}
					
					@Override
					public CompositeKeyPropertyOptions columnSize(Size size) {
						linkage.setColumnSize(size);
						return null;
					}
					
					@Override
					public CompositeKeyPropertyOptions fieldName(String name) {
						linkage.setField(FluentCompositeKeyMappingConfigurationSupport.this.classToPersist, name);
						return null;
					}
				}, true)
				.fallbackOn(this)
				.build((Class<FluentCompositeKeyMappingBuilderEnumOptions<C>>) (Class) FluentCompositeKeyMappingBuilderEnumOptions.class);
	}
	
	@Override
	public FluentCompositeKeyMappingBuilder<C> mapSuperClass(CompositeKeyMappingConfigurationProvider<? super C> superMappingConfiguration) {
		this.superMappingBuilder = superMappingConfiguration;
		return this;
	}
	
	@Override
	public <O> FluentCompositeKeyMappingBuilderCompositeKeyMappingConfigurationImportedEmbedOptions<C, O> embed(
			SerializableFunction<C, O> getter,
			CompositeKeyMappingConfigurationProvider<? extends O> compositeKeyMappingBuilder) {
		return addImportedInset(newInset(getter, compositeKeyMappingBuilder));
	}
	
	@Override
	public <O> FluentCompositeKeyMappingBuilderCompositeKeyMappingConfigurationImportedEmbedOptions<C, O> embed(
			SerializableBiConsumer<C, O> setter,
			CompositeKeyMappingConfigurationProvider<? extends O> compositeKeyMappingBuilder) {
		return addImportedInset(newInset(setter, compositeKeyMappingBuilder));
	}
	
	private <O> FluentCompositeKeyMappingBuilderCompositeKeyMappingConfigurationImportedEmbedOptions<C, O> addImportedInset(Inset<C, O> inset) {
		insets.add(inset);
		return new MethodReferenceDispatcher()
				.redirect(ImportedEmbedOptions.class, new ImportedEmbedOptions<C>() {

					@Override
					public <IN> ImportedEmbedOptions<C> overrideName(SerializableFunction<C, IN> getter, String columnName) {
						inset.overrideName(getter, columnName);
						return null;	// we can return null because dispatcher will return proxy
					}

					@Override
					public <IN> ImportedEmbedOptions<C> overrideName(SerializableBiConsumer<C, IN> setter, String columnName) {
						inset.overrideName(setter, columnName);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public <IN> ImportedEmbedOptions<C> overrideSize(SerializableFunction<C, IN> getter, Size columnSize) {
						inset.overrideSize(getter, columnSize);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public <IN> ImportedEmbedOptions<C> overrideSize(SerializableBiConsumer<C, IN> setter, Size columnSize) {
						inset.overrideSize(setter, columnSize);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public ImportedEmbedOptions exclude(SerializableBiConsumer setter) {
						inset.exclude(setter);
						return null;	// we can return null because dispatcher will return proxy
					}
					
					@Override
					public ImportedEmbedOptions exclude(SerializableFunction getter) {
						inset.exclude(getter);
						return null;	// we can return null because dispatcher will return proxy
					}
					
				}, true)
				.fallbackOn(this)
				.build((Class<FluentCompositeKeyMappingBuilderCompositeKeyMappingConfigurationImportedEmbedOptions<C, O>>) (Class) FluentCompositeKeyMappingBuilderCompositeKeyMappingConfigurationImportedEmbedOptions.class);
	}
	
	/**
	 * Small contract for mapping definition storage. See add(..) methods.
	 * 
	 * @param <T> property owner type
	 */
	protected static class LinkageSupport<T, O> implements CompositeKeyLinkage<T, O> {
		
		/**
		 * Optional binder for this mapping.
		 * May be not of column type in cases of converted data with {@link ParameterBinder#preApply(SerializableThrowingFunction)}
		 * or {@link ParameterBinder#thenApply(SerializableThrowingFunction)}.
		 */
		private ParameterBinder<Object> parameterBinder;
		
		@Nullable
		private EnumBindType enumBindType;
		
		@Nullable
		private String columnName;
		
		@Nullable
		private Size columnSize;
		
		private final ValueAccessPointVariantSupport<T, O> accessor;
		
		private String fieldName;
		
		public LinkageSupport(SerializableFunction<T, O> getter) {
			this.accessor = new ValueAccessPointVariantSupport<>(getter);
		}
		
		public LinkageSupport(SerializableBiConsumer<T, O> setter) {
			this.accessor = new ValueAccessPointVariantSupport<>(setter);
		}
		
		@Override
		@Nullable
		public ParameterBinder<Object> getParameterBinder() {
			return parameterBinder;
		}
		
		public void setParameterBinder(@Nullable ParameterBinder<?> parameterBinder) {
			this.parameterBinder = (ParameterBinder<Object>) parameterBinder;
		}
		
		@Nullable
		@Override
		public EnumBindType getEnumBindType() {
			return enumBindType;
		}
		
		public void setEnumBindType(@Nullable EnumBindType enumBindType) {
			this.enumBindType = enumBindType;
		}
		
		@Nullable
		@Override
		public String getColumnName() {
			return this.columnName;
		}
		
		public void setColumnName(String name) {
			this.columnName = name;
		}
		
		@Nullable
		@Override
		public Size getColumnSize() {
			return this.columnSize;
		}
		
		public void setColumnSize(@Nullable Size columnSize) {
			this.columnSize = columnSize;
		}
		
		@Override
		public ReversibleAccessor<T, O> getAccessor() {
			return accessor.getAccessor();
		}
		
		@Nullable
		@Override
		public String getFieldName() {
			return fieldName;
		}
		
		public void setField(Class<T> classToPersist, String fieldName) {
			this.fieldName = fieldName;
			// Note that getField(..) will throw an exception if field is not found, at the opposite of findField(..)
			this.accessor.setField(classToPersist, fieldName);
		}
		
		@Override
		public Class<O> getColumnType() {
			return (Class<O>) AccessorDefinition.giveDefinition(this.accessor.getAccessor()).getMemberType();
		}
		
	}
	
	/**
	 * Information storage of embedded mapping defined externally by an {@link CompositeKeyMappingConfigurationProvider},
	 * see {@link #embed(SerializableFunction, CompositeKeyMappingConfigurationProvider)}
	 *
	 * @param <SRC>
	 * @param <TRGT>
	 * @see #embed(SerializableFunction, CompositeKeyMappingConfigurationProvider)}
	 * @see #embed(SerializableBiConsumer, CompositeKeyMappingConfigurationProvider)}
	 */
	public static class Inset<SRC, TRGT> {
		private final Class<TRGT> embeddedClass;
		private final Method insetAccessor;
		/** Equivalent of {@link #insetAccessor} as a {@link PropertyAccessor}  */
		private final PropertyAccessor<SRC, TRGT> accessor;
		private final ValueAccessPointMap<SRC, String> overriddenColumnNames = new ValueAccessPointMap<>();
		private final ValueAccessPointMap<SRC, Size> overriddenColumnSizes = new ValueAccessPointMap<>();
		private final ValueAccessPointSet<SRC> excludedProperties = new ValueAccessPointSet<>();
		private final CompositeKeyMappingConfigurationProvider<? extends TRGT> configurationProvider;
		private final ValueAccessPointMap<SRC, Column> overriddenColumns = new ValueAccessPointMap<>();
		
		
		Inset(SerializableBiConsumer<SRC, TRGT> targetSetter,
			  CompositeKeyMappingConfigurationProvider<? extends TRGT> configurationProvider,
			  LambdaMethodUnsheller lambdaMethodUnsheller) {
			this.insetAccessor = lambdaMethodUnsheller.captureLambdaMethod(targetSetter);
			this.accessor = new PropertyAccessor<>(
					new MutatorByMethod<SRC, TRGT>(insetAccessor).toAccessor(),
					new MutatorByMethodReference<>(targetSetter));
			// looking for the target type because it's necessary to find its persister (and other objects)
			this.embeddedClass = Reflections.javaBeanTargetType(getInsetAccessor());
			this.configurationProvider = configurationProvider;
		}
		
		Inset(SerializableFunction<SRC, TRGT> targetGetter,
			  CompositeKeyMappingConfigurationProvider<? extends TRGT> configurationProvider,
			  LambdaMethodUnsheller lambdaMethodUnsheller) {
			this.insetAccessor = lambdaMethodUnsheller.captureLambdaMethod(targetGetter);
			this.accessor = new PropertyAccessor<>(
					new AccessorByMethodReference<>(targetGetter),
					new AccessorByMethod<SRC, TRGT>(insetAccessor).toMutator());
			// looking for the target type because it's necessary to find its persister (and other objects)
			this.embeddedClass = Reflections.javaBeanTargetType(getInsetAccessor());
			this.configurationProvider = configurationProvider;
		}
		
		/**
		 * Equivalent of {@link #insetAccessor} as a {@link PropertyAccessor}
		 */
		public PropertyAccessor<SRC, TRGT> getAccessor() {
			return accessor;
		}
		
		/**
		 * Equivalent of given getter or setter at construction time as a {@link Method}
		 */
		public Method getInsetAccessor() {
			return insetAccessor;
		}
		
		public Class<TRGT> getEmbeddedClass() {
			return embeddedClass;
		}
		
		public ValueAccessPointSet<SRC> getExcludedProperties() {
			return this.excludedProperties;
		}
		
		public ValueAccessPointMap<SRC, String> getOverriddenColumnNames() {
			return this.overriddenColumnNames;
		}
		
		public ValueAccessPointMap<SRC, Size> getOverriddenColumnSizes() {
			return overriddenColumnSizes;
		}
		
		public ValueAccessPointMap<SRC, Column> getOverriddenColumns() {
			return overriddenColumns;
		}
		
		public CompositeKeyMappingConfigurationProvider<TRGT> getConfigurationProvider() {
			return (CompositeKeyMappingConfigurationProvider<TRGT>) configurationProvider;
		}
		
		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 overrideName(AccessorChain accessorChain, String columnName) {
			this.overriddenColumnNames.put(accessorChain, 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 void overrideSize(AccessorChain accessorChain, Size columnSize) {
			this.overriddenColumnSizes.put(accessorChain, columnSize);
		}
		
		public void override(SerializableFunction methodRef, Column column) {
			this.overriddenColumns.put(new AccessorByMethodReference(methodRef), column);
		}
		
		public void override(SerializableBiConsumer methodRef, Column column) {
			this.overriddenColumns.put(new MutatorByMethodReference(methodRef), column);
		}
		
		public void exclude(SerializableBiConsumer methodRef) {
			this.excludedProperties.add(new MutatorByMethodReference(methodRef));
		}
		
		public void exclude(SerializableFunction methodRef) {
			this.excludedProperties.add(new AccessorByMethodReference(methodRef));
		}
	}
}
