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

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
import org.codefilarete.stalactite.mapping.ComposedIdMapping;
import org.codefilarete.stalactite.mapping.DefaultEntityMapping;
import org.codefilarete.stalactite.mapping.EmbeddedBeanMapping;
import org.codefilarete.stalactite.mapping.IdAccessor;
import org.codefilarete.stalactite.mapping.IdMapping;
import org.codefilarete.stalactite.mapping.id.assembly.ComposedIdentifierAssembler;
import org.codefilarete.stalactite.mapping.id.assembly.IdentifierAssembler;
import org.codefilarete.stalactite.mapping.id.manager.AlreadyAssignedIdentifierManager;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.stalactite.sql.result.Row;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.collection.Maps;

/**
 * Mapping strategy dedicated to {@link KeyValueRecord}. Very close to {@link org.codefilarete.stalactite.engine.configurer.AssociationRecordMapping}
 * in its principle.
 *
 * @param <K> embedded key bean type
 * @param <V> embedded value bean type
 * @param <I> source identifier type
 * @param <T> relation table type
 * @author Guillaume Mary
 */
public class KeyValueRecordMapping<K, V, I, T extends Table<T>> extends DefaultEntityMapping<KeyValueRecord<K, V, I>, RecordId<K, I>, T> {
	
	@VisibleForTesting
	public KeyValueRecordMapping(T targetTable,
						  Map<? extends ReadWritePropertyAccessPoint<KeyValueRecord<K, V, I>, ?>, Column<T, ?>> propertyToColumn,
						  KeyValueRecordIdMapping<K, I, T> idMapping) {
		super((Class) KeyValueRecord.class,
				targetTable,
				propertyToColumn,
				// cast because idMapping has KeyValueRecord<K, ?, I> as generics instead of KeyValueRecord<K, V, I>
				(ComposedIdMapping<KeyValueRecord<K, V, I>, RecordId<K, I>>) (ComposedIdMapping) idMapping);
		
	}
	
	/**
	 * {@link IdMapping} for {@link KeyValueRecord} : a composed id made of
	 * - {@link KeyValueRecord#getId()}
	 * - {@link KeyValueRecord#getKey()}
	 */
	@VisibleForTesting
	public static class KeyValueRecordIdMapping<K, I, T extends Table<T>> extends ComposedIdMapping<KeyValueRecord<K, ?, I>, RecordId<K, I>> {
		
		private KeyValueRecordIdMapping(
				RecordIdAssembler recordIdAssembler) {
			super(new KeyValueRecordIdAccessor<>(),
					new AlreadyAssignedIdentifierManager<>((Class<RecordId<K, I>>) (Class) RecordId.class,
							KeyValueRecord::markAsPersisted,
							KeyValueRecord::isPersisted),
					recordIdAssembler);
		}
		
		public <LEFTTABLE extends Table<LEFTTABLE>> KeyValueRecordIdMapping(
				T targetTable,
				Function<ColumnedRow, K> entryKeyAssembler,
				Function<K, Map<Column<T, ?>, ?>> entryKeyColumnValueProvider,
				IdentifierAssembler<I, LEFTTABLE> sourceIdentifierAssembler,
				Map<Column<LEFTTABLE, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping) {
			this(new RecordIdAssembler<>(targetTable, entryKeyAssembler, entryKeyColumnValueProvider, sourceIdentifierAssembler, primaryKey2ForeignKeyMapping));
		}
		
		public <LEFTTABLE extends Table<LEFTTABLE>> KeyValueRecordIdMapping(
				T targetTable,
				EmbeddedBeanMapping<K, T> entryKeyMapping,
				IdentifierAssembler<I, LEFTTABLE> sourceIdentifierAssembler,
				Map<Column<LEFTTABLE, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping) {
			this(new RecordIdAssembler<>(targetTable, entryKeyMapping, sourceIdentifierAssembler, primaryKey2ForeignKeyMapping));
		}
		
		/**
		 * Override because {@link ComposedIdMapping} is based on null identifier to determine newness, which is always false for {@link KeyValueRecord}
		 * because they always have one. We delegate its computation to the entity.
		 *
		 * @param entity any non-null entity
		 * @return true or false based on {@link KeyValueRecord#isNew()}
		 */
		@Override
		public boolean isNew(KeyValueRecord<K, ?, I> entity) {
			return entity.isNew();
		}
		
		public static class KeyValueRecordIdAccessor<K, I> implements IdAccessor<KeyValueRecord<K, ?, I>, RecordId<K, I>> {
			
			@Override
			public RecordId<K, I> getId(KeyValueRecord<K, ?, I> associationRecord) {
				return associationRecord.getId();
			}
			
			@Override
			public void setId(KeyValueRecord<K, ?, I> associationRecord, RecordId<K, I> identifier) {
				associationRecord.setId(new RecordId<>(identifier.getId(), associationRecord.getKey()));
				associationRecord.setKey(identifier.getKey());
			}
		}
		
		/**
		 * Identifier assembler for {@link RecordId}
		 *
		 * @param <K> embedded key bean type
		 * @param <ID> source identifier type
		 */
		@VisibleForTesting
		static class RecordIdAssembler<K, ID, T extends Table<T>> extends ComposedIdentifierAssembler<RecordId<K, ID>, T> {
			
			private final Function<ColumnedRow, K> entryKeyAssembler;
			private final Function<K, Map<Column<T, ?>, ?>> entryKeyColumnValueProvider;
			private final IdentifierAssembler<ID, ?> sourceIdentifierAssembler;
			private final Map<Column<?, ?>, Column<T, ?>> primaryKey2ForeignKeyMapping;
			
			/**
			 * Constructor mapping given values to fields
			 * 
			 * @param targetTable association table on which value should be inserted an read
			 * @param entryKeyAssembler builder of K instances from {@link Column} found in a {@link Row}
			 * @param entryKeyColumnValueProvider provides {@link Column} values to be inserted / updated from a K instance
			 * @param sourceIdentifierAssembler assemble / disassemble ID instances from / to {@link Column}s
			 * @param primaryKey2ForeignKeyMapping used to remap {@link Column}s of sourceIdentifierAssembler to association table since it gives it for LEFTTABLE
			 * @param <LEFTTABLE> table type of source declaring the relation
			 */
			@VisibleForTesting
			<LEFTTABLE extends Table<LEFTTABLE>> RecordIdAssembler(
					T targetTable,
					Function<ColumnedRow, K> entryKeyAssembler,
					Function<K, Map<Column<T, ?>, ?>> entryKeyColumnValueProvider,
					IdentifierAssembler<ID, LEFTTABLE> sourceIdentifierAssembler,
					Map<? extends Column<LEFTTABLE, ?>, ? extends Column<T, ?>> primaryKey2ForeignKeyMapping
			) {
				super(targetTable);
				this.entryKeyAssembler = entryKeyAssembler;
				this.entryKeyColumnValueProvider = entryKeyColumnValueProvider;
				this.sourceIdentifierAssembler = sourceIdentifierAssembler;
				this.primaryKey2ForeignKeyMapping = (Map) primaryKey2ForeignKeyMapping;
			}
			
			/**
			 * Constructor for cases where K is a simple (embedded) bean.
			 *
			 * @param targetTable association table on which value should be inserted an read
			 * @param entryKeyMapping persistence mapping of K instance
			 * @param sourceIdentifierAssembler assemble / disassemble ID instances from / to {@link Column}s
			 * @param primaryKey2ForeignKeyMapping used to remap {@link Column}s of sourceIdentifierAssembler to association table since it gives it for LEFTTABLE
			 * @param <LEFTTABLE> table type of source declaring the relation
			 */
			@VisibleForTesting
			<LEFTTABLE extends Table<LEFTTABLE>> RecordIdAssembler(
					T targetTable,
					EmbeddedBeanMapping<K, T> entryKeyMapping,
					IdentifierAssembler<ID, LEFTTABLE> sourceIdentifierAssembler,
					Map<? extends Column<LEFTTABLE, ?>, ? extends Column<T, ?>> primaryKey2ForeignKeyMapping
					) {
				this(targetTable, entryKeyMapping::transform, entryKeyMapping::getInsertValues, sourceIdentifierAssembler, primaryKey2ForeignKeyMapping);
			}
			
			/**
			 * Constructor for cases where entry key an entity referencing another table. Then K type becomes its identifier.
			 *
			 * @param targetTable association table on which value should be inserted an read
			 * @param rightTableIdentifierAssembler entity identifier mapping (K type mapping)
			 * @param sourceIdentifierAssembler assemble / disassemble ID instances from / to {@link Column}s
			 * @param primaryKey2ForeignKeyMapping used to remap {@link Column}s of sourceIdentifierAssembler to association table since it gives it for LEFTTABLE
			 * @param <LEFTTABLE> table type of source declaring the relation
			 */
			@VisibleForTesting
			<LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>> RecordIdAssembler(
					T targetTable,
					IdentifierAssembler<K, RIGHTTABLE> rightTableIdentifierAssembler,
					IdentifierAssembler<ID, LEFTTABLE> sourceIdentifierAssembler,
					Map<? extends Column<LEFTTABLE, ?>, ? extends Column<T, ?>> primaryKey2ForeignKeyMapping,
					Map<Column<RIGHTTABLE, ?>, Column<T, ?>> rightTable2EntryKeyMapping
					) {
				this(targetTable, rightTableIdentifierAssembler::assemble, k -> {
					Map<Column<RIGHTTABLE, ?>, ?> keyColumnValues = rightTableIdentifierAssembler.getColumnValues(k);
					return Maps.innerJoin(rightTable2EntryKeyMapping, keyColumnValues);
					
				}, sourceIdentifierAssembler, primaryKey2ForeignKeyMapping);
			}
			
			@Override
			public RecordId<K, ID> assemble(ColumnedRow row) {
				ID id = sourceIdentifierAssembler.assemble(row);
				K entryKey = entryKeyAssembler.apply(row);
				
				return id == null ? null : new RecordId<>(id, entryKey);
			}
			
			@Override
			public Map<Column<T, ?>, ?> getColumnValues(RecordId<K, ID> record) {
				Map<Column<?, ?>, Object> sourceColumnValues = (Map) sourceIdentifierAssembler.getColumnValues(record.getId());
				Map<Column<T, ?>, Object> idColumnValues = Maps.innerJoin(primaryKey2ForeignKeyMapping, sourceColumnValues);
				Map<Column<T, ?>, ?> entryKeyColumnValues = entryKeyColumnValueProvider.apply(record.getKey());
				Map<Column<T, ?>, Object> result = new HashMap<>();
				result.putAll(idColumnValues);
				result.putAll(entryKeyColumnValues);
				return result;
			}
		}
	}
}
