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

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

import org.codefilarete.reflection.AccessorByMethodReference;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.reflection.MutatorByMethodReference;
import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
import org.codefilarete.reflection.SerializableAccessor;
import org.codefilarete.reflection.SerializableMutator;
import org.codefilarete.reflection.SerializablePropertyAccessor;
import org.codefilarete.reflection.SerializablePropertyMutator;
import org.codefilarete.reflection.ValueAccessPoint;
import org.codefilarete.reflection.ValueAccessPointMap;
import org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfigurationProvider;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfigurationProvider;
import org.codefilarete.stalactite.dsl.property.CascadeOptions.RelationMode;
import org.codefilarete.stalactite.sql.ddl.Size;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;

/**
 * Storage of configuration of a {@link Map} relation (kind of "element collection" but with a {@link Map}).
 * 
 * @param <SRC> entity type owning relation
 * @param <K> Map entry key type
 * @param <V> Map entry value key type
 * @param <M> Map type
 * @author Guillaume Mary
 */
public class MapRelation<SRC, K, V, M extends Map<K, V>> {
	
	/** The method that gives the entities from the "root" entity */
	private final ReadWritePropertyAccessPoint<SRC, M> mapProvider;
	private final Class<K> keyType;
	private final Class<V> valueType;
	/** Optional provider of {@link Map} instance to be used if collection value is null */
	private Supplier<M> mapFactory;
	
	private Table targetTable;
	private String targetTableName;
	private Column<Table, ?> reverseColumn;
	private String reverseColumnName;
	
	/** key column name override */
	@Nullable
	private String keyColumnName;
	
	/** key column size override */
	@Nullable
	private Size keyColumnSize;
	
	/** value column name override */
	@Nullable
	private String valueColumnName;
	
	/** value column size override */
	@Nullable
	private Size valueColumnSize;
	
	/** Key complex type mapping override, to be used when {@link #keyEmbeddableConfigurationProvider} is not null */
	private final ValueAccessPointMap<K, String, ValueAccessPoint<K>> overriddenKeyColumnNames = new ValueAccessPointMap<>();
	
	/** Key complex type mapping override, to be used when {@link #valueEmbeddableConfigurationProvider} is not null */
	private final ValueAccessPointMap<K, Size, ValueAccessPoint<K>> overriddenKeyColumnSizes = new ValueAccessPointMap<>();
	
	/** Value complex type mapping override, to be used when {@link #valueEmbeddableConfigurationProvider} is not null */
	private final ValueAccessPointMap<V, String, ValueAccessPoint<V>> overriddenValueColumnNames = new ValueAccessPointMap<>();
	
	/** Value complex type mapping override, to be used when {@link #valueEmbeddableConfigurationProvider} is not null */
	private final ValueAccessPointMap<V, Size, ValueAccessPoint<V>> overriddenValueColumnSizes = new ValueAccessPointMap<>();
	
	
	/** Complex type mapping, optional */
	@Nullable
	private EntityMappingConfigurationProvider<K, ?> keyEntityConfigurationProvider;
	@Nullable
	private EmbeddableMappingConfigurationProvider<K> keyEmbeddableConfigurationProvider;
	
	@Nullable
	private EntityMappingConfigurationProvider<V, ?> valueEntityConfigurationProvider;
	@Nullable
	private EmbeddableMappingConfigurationProvider<V> valueEmbeddableConfigurationProvider;
	
	/** Default relation mode is {@link RelationMode#ALL} */
	private RelationMode keyEntityRelationMode = RelationMode.ALL;
	
	/** Default relation mode is {@link RelationMode#ALL} */
	private RelationMode valueEntityRelationMode = RelationMode.ALL;
	
	private boolean fetchSeparately;
	
	public MapRelation(SerializablePropertyMutator<SRC, M> setter,
					   Class<K> keyType,
					   Class<V> valueType) {
		this(Accessors.readWriteAccessPoint(setter), keyType, valueType);
	}
	
	public MapRelation(SerializablePropertyAccessor<SRC, M> getter,
					   Class<K> keyType,
					   Class<V> valueType) {
		this(Accessors.readWriteAccessPoint(getter), keyType, valueType);
	}
	
	public MapRelation(ReadWritePropertyAccessPoint<SRC, M> mapProvider,
					   Class<K> keyType,
					   Class<V> valueType) {
		this.keyType = keyType;
		this.valueType = valueType;
		this.mapProvider = mapProvider;
	}
	
	public ReadWritePropertyAccessPoint<SRC, M> getMapProvider() {
		return mapProvider;
	}
	
	public Class<K> getKeyType() {
		return keyType;
	}
	
	public Class<V> getValueType() {
		return valueType;
	}
	
	public Supplier<M> getMapFactory() {
		return mapFactory;
	}
	
	public void setMapFactory(Supplier<? extends M> mapFactory) {
		this.mapFactory = (Supplier<M>) mapFactory;
	}
	
	public Table getTargetTable() {
		return targetTable;
	}
	
	public void setTargetTable(Table targetTable) {
		this.targetTable = targetTable;
	}
	
	public String getTargetTableName() {
		return targetTableName;
	}
	
	public void setTargetTableName(String targetTableName) {
		this.targetTableName = targetTableName;
	}
	
	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 void setReverseColumnName(String reverseColumnName) {
		this.reverseColumnName = reverseColumnName;
	}
	
	public void setKeyConfigurationProvider(@Nullable EntityMappingConfigurationProvider<K, ?> keyConfigurationProvider) {
		this.keyEntityConfigurationProvider = keyConfigurationProvider;
	}
	
	@Nullable
	public EmbeddableMappingConfigurationProvider<K> getKeyEmbeddableConfigurationProvider() {
		return keyEmbeddableConfigurationProvider;
	}
	
	public void setKeyConfigurationProvider(@Nullable EmbeddableMappingConfigurationProvider<K> keyConfigurationProvider) {
		this.keyEmbeddableConfigurationProvider = keyConfigurationProvider;
	}
	
	@Nullable
	public EntityMappingConfigurationProvider<K, ?> getKeyEntityConfigurationProvider() {
		return keyEntityConfigurationProvider;
	}
	
	public void setValueConfigurationProvider(@Nullable EntityMappingConfigurationProvider<V, ?> valueConfigurationProvider) {
		this.valueEntityConfigurationProvider = valueConfigurationProvider;
	}
	
	@Nullable
	public EntityMappingConfigurationProvider<V, ?> getValueEntityConfigurationProvider() {
		return valueEntityConfigurationProvider;
	}
	
	public void setValueConfigurationProvider(@Nullable EmbeddableMappingConfigurationProvider<V> valueConfigurationProvider) {
		this.valueEmbeddableConfigurationProvider = valueConfigurationProvider;
	}
	
	@Nullable
	public EmbeddableMappingConfigurationProvider<V> getValueEmbeddableConfigurationProvider() {
		return valueEmbeddableConfigurationProvider;
	}
	
	public void setKeyColumnName(String keyColumnName) {
		this.keyColumnName = keyColumnName;
	}
	
	@Nullable
	public String getKeyColumnName() {
		return keyColumnName;
	}
	
	public void setKeyColumnSize(Size keyColumnSize) {
		this.keyColumnSize = keyColumnSize;
	}
	
	@Nullable
	public Size getKeyColumnSize() {
		return keyColumnSize;
	}
	
	public void setValueColumnName(String valueColumnName) {
		this.valueColumnName = valueColumnName;
	}
	
	@Nullable
	public String getValueColumnName() {
		return valueColumnName;
	}
	
	public void setValueColumnSize(Size valueColumnSize) {
		this.valueColumnSize = valueColumnSize;
	}
	
	@Nullable
	public Size getValueColumnSize() {
		return valueColumnSize;
	}
	
	public ValueAccessPointMap<K, String, ValueAccessPoint<K>> getOverriddenKeyColumnNames() {
		return this.overriddenKeyColumnNames;
	}
	
	public void overrideKeyName(SerializableAccessor<K, ?> methodRef, String columnName) {
		this.overriddenKeyColumnNames.put(new AccessorByMethodReference<>(methodRef), columnName);
	}
	
	public void overrideKeyName(SerializableMutator<K, ?> methodRef, String columnName) {
		this.overriddenKeyColumnNames.put(new MutatorByMethodReference<>(methodRef), columnName);
	}
	
	public ValueAccessPointMap<V, String, ValueAccessPoint<V>> getOverriddenValueColumnNames() {
		return this.overriddenValueColumnNames;
	}
	
	
	public void overrideKeySize(SerializableAccessor<K, ?> methodRef, Size columnSize) {
		this.overriddenKeyColumnSizes.put(new AccessorByMethodReference<>(methodRef), columnSize);
	}
	
	public void overrideKeySize(SerializableMutator<K, ?> methodRef, Size columnSize) {
		this.overriddenKeyColumnSizes.put(new MutatorByMethodReference<>(methodRef), columnSize);
	}
	
	public ValueAccessPointMap<K, Size, ValueAccessPoint<K>> getOverriddenKeyColumnSizes() {
		return this.overriddenKeyColumnSizes;
	}
	
	public void overrideValueName(SerializableAccessor<V, ?> methodRef, String columnName) {
		this.overriddenValueColumnNames.put(new AccessorByMethodReference<>(methodRef), columnName);
	}
	
	public void overrideValueName(SerializableMutator<V, ?> methodRef, String columnName) {
		this.overriddenValueColumnNames.put(new MutatorByMethodReference<>(methodRef), columnName);
	}
	
	public void overrideValueSize(SerializableAccessor<V, ?> methodRef, Size columnSize) {
		this.overriddenValueColumnSizes.put(new AccessorByMethodReference<>(methodRef), columnSize);
	}
	
	public void overrideValueSize(SerializableMutator<V, ?> methodRef, Size columnSize) {
		this.overriddenValueColumnSizes.put(new MutatorByMethodReference<>(methodRef), columnSize);
	}
	
	public ValueAccessPointMap<V, Size, ValueAccessPoint<V>> getOverriddenValueColumnSizes() {
		return this.overriddenValueColumnSizes;
	}
	
	public RelationMode getKeyEntityRelationMode() {
		return keyEntityRelationMode;
	}
	
	public void setKeyEntityRelationMode(RelationMode keyEntityRelationMode) {
		this.keyEntityRelationMode = keyEntityRelationMode;
	}
	
	public RelationMode getValueEntityRelationMode() {
		return valueEntityRelationMode;
	}
	
	public void setValueEntityRelationMode(RelationMode valueEntityRelationMode) {
		this.valueEntityRelationMode = valueEntityRelationMode;
	}
	
	public boolean isFetchSeparately() {
		return fetchSeparately;
	}
	
	public void fetchSeparately() {
		this.fetchSeparately = true;
	}
}
