/*
 * Copyright 2014 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.crate.core.convert;

import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static org.slf4j.LoggerFactory.getLogger;
import static org.springframework.core.CollectionFactory.createCollection;
import static org.springframework.core.CollectionFactory.createMap;
import static org.springframework.data.crate.core.convert.CrateDocumentPropertyAccessor.INSTANCE;
import static org.springframework.data.crate.core.mapping.CratePersistentProperty.RESERVED_VESRION_FIELD_NAME;
import static org.springframework.data.util.ClassTypeInformation.MAP;
import static org.springframework.data.util.ClassTypeInformation.OBJECT;
import static org.springframework.data.util.ClassTypeInformation.from;
import static org.springframework.util.Assert.notNull;
import static org.springframework.util.ClassUtils.getUserClass;
import static org.springframework.util.CollectionUtils.arrayToList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.convert.EntityInstantiator;
import org.springframework.data.crate.core.mapping.CrateArray;
import org.springframework.data.crate.core.mapping.CrateDocument;
import org.springframework.data.crate.core.mapping.CratePersistentEntity;
import org.springframework.data.crate.core.mapping.CratePersistentProperty;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
import org.springframework.data.mapping.model.PropertyValueProvider;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.util.TypeInformation;

/**
 * {@link CrateConverter} that uses a {@link MappingContext} for complex mapping
 * of domain objects to {@link CrateDocument}.
 * 
 * @author Rizwan Idrees
 * @author Hasnain Javed
 * @since 1.0.0
 */

public class MappingCrateConverter extends AbstractCrateConverter implements ApplicationContextAware {
	
	private final Logger logger = getLogger(getClass());
	
	protected final MappingContext<? extends CratePersistentEntity<?>, CratePersistentProperty> mappingContext;
	
	private final SpELContext spELContext;

	protected ApplicationContext applicationContext;

	protected CrateTypeMapper typeMapper;

	public MappingCrateConverter(MappingContext<? extends CratePersistentEntity<?>, CratePersistentProperty> mappingContext) {
		super(new DefaultConversionService());
		notNull(mappingContext, "Mapping context is required.");
		this.mappingContext = mappingContext;
		this.spELContext = new SpELContext(INSTANCE);
		this.typeMapper = new DefaultCrateTypeMapper();
	}

	@Override
	public MappingContext<? extends CratePersistentEntity<?>, CratePersistentProperty> getMappingContext() {
		return mappingContext;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	@Override
	public <R> R read(Class<R> type, CrateDocument source) {
		return read(from(type), source, null);
	}

	@Override
	public void write(Object source, CrateDocument sink) {

		if(source == null) {
			return;
		}

		TypeInformation<?> type = from(source.getClass());

		if(!conversions.hasCustomWriteTarget(source.getClass(), sink.getClass())) {
			typeMapper.writeType(type, sink);
		}

		writeInternal(source, sink, type);
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Object convertToCrateType(Object obj, TypeInformation<?> typeInformation) {

		if(obj == null) {
			return null;
		}

		if(conversions.isSimpleType(obj.getClass())) {
			return getPotentiallyConvertedSimpleWrite(obj);
		}
		
		Class<?> target = conversions.getCustomWriteTarget(obj.getClass());
		if(target != null) {
			return conversionService.convert(obj, target);
		}
		
		TypeInformation<?> typeHint = typeInformation == null ? OBJECT : typeInformation;
		
		if(obj instanceof CrateArray) {
			return maybeConvertList((CrateArray) obj, typeHint);
		}
		
		if(obj instanceof CrateDocument) {
			
			CrateDocument document = new CrateDocument();
			for (String key : ((CrateDocument) obj).keySet()) {
				Object o = ((CrateDocument) obj).get(key);
				document.put(key, convertToCrateType(o, typeHint));
			}
			return document;
		}
		
		if(obj instanceof Map) {
			
			CrateDocument document = new CrateDocument();
			for(Map.Entry<Object, Object> entry : ((Map<Object, Object>) obj).entrySet()) {
				document.put(entry.getKey().toString(), convertToCrateType(entry.getValue(), typeHint));
			}
			return document;
		}
		
		if(obj.getClass().isArray()) {
			return maybeConvertList(asList((Object[]) obj), typeHint);
		}

		if(obj instanceof Collection) {
			return maybeConvertList((Collection<?>) obj, typeHint);
		}
		
		CrateDocument document = new CrateDocument();
		this.write(obj, document);
		
		if(typeInformation == null) {
			return removeTypeInfoRecursively(document);
		}
		
		return !obj.getClass().equals(typeInformation.getType()) ? document : removeTypeInfoRecursively(document);
	}

	/**
	 * Read an incoming {@link CrateDocument} into the target entity.
	 *
	 * @param type the type information of the target entity.
	 * @param source the document to convert.
	 * @param parent an optional parent object.
	 * @param <R> the entity type.
	 * @return the converted entity.
	 */
	@SuppressWarnings("unchecked")
	protected <R> R read(final TypeInformation<R> type, final CrateDocument source, final Object parent) {
		
	    if(source == null) {
	    	return null;
	    }

	    TypeInformation<? extends R> typeToUse = typeMapper.readType(source, type);
	    Class<? extends R> rawType = typeToUse.getType();

	    if(conversions.hasCustomReadTarget(source.getClass(), rawType)) {
	      return conversionService.convert(source, rawType);
	    }

	    if(typeToUse.isMap()) {
	      return (R) readMap(typeToUse, source, parent);
	    }

	    CratePersistentEntity<R> entity = (CratePersistentEntity<R>) mappingContext.getPersistentEntity(typeToUse);
	    
	    if(entity == null) {
	      throw new MappingException("No mapping metadata found for " + rawType.getName());
	    }
	    
	    return read(entity, source, parent);
	}
	
	/**
	 * Read an incoming {@link CrateDocument} into the target entity.
	 *
	 * @param entity the target entity.
	 * @param source the document to convert.
	 * @param parent an optional parent object.
	 * @param <R> the entity type.
	 * @return the converted entity.
	 */
	@SuppressWarnings("unchecked")
	protected <R> R read(final CratePersistentEntity<R> entity, final CrateDocument source, final Object parent) {
		
		final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext);
		
	    ParameterValueProvider<CratePersistentProperty> provider = getParameterProvider(entity, source, evaluator, parent);
	    
	    EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity);

	    R instance = instantiator.createInstance(entity, provider);
	    final PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(instance);
	    final R result = (R)propertyAccessor.getBean();
	    final CratePersistentProperty idProperty = entity.getIdProperty();
	    final CratePersistentProperty versionProperty = entity.getVersionProperty();
	    
	    if(entity.hasIdProperty()) {
	    	Object idValue = getValueInternal(idProperty, source, result);
	    	propertyAccessor.setProperty(idProperty, idValue);
	    }
	    
	    if(entity.hasVersionProperty()) {
	    	Object versionValue = getValueInternal(versionProperty, source, result);
	    	propertyAccessor.setProperty(versionProperty, versionValue);
	    }
	    
	    for(CratePersistentProperty property : entity.getPersistentProperties()) {
	    	// skip id and version properties as they may have potentially been set above.  
			if((idProperty != null && idProperty.equals(property)) || (versionProperty != null && versionProperty.equals(property))) {
				continue;
			}
			
			if(!source.containsKey(property.getFieldName()) || entity.isConstructorArgument(property)) {
				continue;
			}
			
			propertyAccessor.setProperty(property, getValueInternal(property, source, result));
	    }
	    
	    entity.doWithAssociations(new AssociationHandler<CratePersistentProperty>() {
	    	
	      @Override
	      public void doWithAssociation(final Association<CratePersistentProperty> association) {	    	  
	    	  CratePersistentProperty inverseProp = association.getInverse();
	    	  Object obj = getValueInternal(inverseProp, source, result);
	    	  propertyAccessor.setProperty(inverseProp, obj);
	      }	      
	    });

	    return result;
	  }
	
	/**
	 * Recursively parses the a map from the source document.
	 *
	 * @param type the type information for the document.
	 * @param source the source document.
	 * @param parent the optional parent.
	 * @return the recursively parsed map.
	 */
	protected Map<Object, Object> readMap(final TypeInformation<?> type, final CrateDocument source, final Object parent) {
		
		notNull(source);
		
	    Class<?> mapType = typeMapper.readType(source, type).getType();
	    Map<Object, Object> map = createMap(mapType, source.keySet().size());
	    
	    for(Map.Entry<String, Object> entry : source.entrySet()) {
	    	
	    	Object key = entry.getKey();
	    	Object value = entry.getValue();

	        TypeInformation<?> keyTypeInformation = type.getComponentType();
	        
		    if(keyTypeInformation != null) {
		    	Class<?> keyType = keyTypeInformation.getType();
		        key = conversionService.convert(key, keyType);
		    }
	
		    TypeInformation<?> valueType = type.getMapValueType();
		    
		    if(value instanceof CrateDocument) {
		    	map.put(key, read(valueType, (CrateDocument) value, parent));
		    }else if(value instanceof CrateArray) {
		    	map.put(key, readCollection(valueType, (CrateArray) value, parent));
		    }else {
		    	Class<?> valueClass = valueType == null ? null : valueType.getType();
		        map.put(key, getPotentiallyConvertedSimpleRead(value, valueClass));
		    }
	    }
	    
	    return map;
	}
	
	/**
	 * Convert a source object into a {@link CrateDocument} target.
	 * 
	 * @param source the source object.
	 * @param sink the target document.
	 * @param typeHint the type information for the source.
	 */
	@SuppressWarnings("unchecked")
	protected void writeInternal(final Object source, CrateDocument sink, final TypeInformation<?> typeHint) {
		
		if(source == null) {
			return;
		}

		if(Collection.class.isAssignableFrom(source.getClass())) {
			throw new IllegalArgumentException("Root Document must be either CrateDocument or Map.");
		}
		
		Class<?> customTarget = getCustomWriteHandler(source.getClass(), CrateDocument.class);

		if(customTarget != null) {
			CrateDocument result = conversionService.convert(source, CrateDocument.class);
			sink.putAll(result);
			return;
		}

		if(Map.class.isAssignableFrom(source.getClass())) {
			writeMapInternal((Map<Object, Object>) source, sink, MAP);
			return;
		}

		CratePersistentEntity<?> entity = mappingContext.getPersistentEntity(source.getClass());
		
		writeInternal(source, sink, entity);
		
		addCustomTypeKeyIfNecessary(typeHint, source, sink);
	}
	
	/**
	   * Internal helper method to write the source object into the target document.
	   *
	   * @param source the source object.
	   * @param sink the target document.
	   * @param entity the persistent entity to convert from.
	   */
	  protected void writeInternal(final Object source, final CrateDocument sink, final CratePersistentEntity<?> entity) {
		  
	    if(source == null) {
	      return;
	    }

	    if(entity == null) {
	      throw new MappingException("No mapping metadata found for entity ".concat(source.getClass().getName()));
	    }
	    
		final PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(source);
	    final CratePersistentProperty idProperty = entity.getIdProperty();
	    final CratePersistentProperty versionProperty = entity.getVersionProperty();
	    
	    if(idProperty != null && !sink.containsKey(idProperty.getFieldName())) {
	    	try {
	    		Object id = convertToCrateType(propertyAccessor.getProperty(idProperty), idProperty.getTypeInformation());
	    		sink.put(idProperty.getFieldName(), id);
	    	}catch(ConversionException e) {
	    		logger.warn("Failed to convert id property '{}'. {}", new Object[]{idProperty.getFieldName(),
	    								 										   e.getMessage()});
	    	}
	    }
	    
	    for(CratePersistentProperty property : entity.getPersistentProperties()) {
	    	
	    	if(property.equals(idProperty) || (versionProperty != null && property.equals(versionProperty))) {
		          continue;
	    	}
	    	
	    	Object propertyObj = propertyAccessor.getProperty(property/*, property.getType()*/);
	    	
	        if(propertyObj != null) {
	        	if(!conversions.isSimpleType(propertyObj.getClass()) || isPrimitiveArray(property)) {
	        		writePropertyInternal(propertyObj, sink, property);
	        	}else {
	        		writeSimpleInternal(propertyObj, sink, property.getFieldName());
	        	}
	        }
	    }
	    
	    entity.doWithAssociations(new AssociationHandler<CratePersistentProperty>() {
	    	@Override
	    	public void doWithAssociation(final Association<CratePersistentProperty> association) {
	    		CratePersistentProperty inverse = association.getInverse();
	    		Object propertyObj = propertyAccessor.getProperty(inverse);
	    		if (propertyObj != null) {
	    			writePropertyInternal(propertyObj, sink, inverse);
	    		}
	    	}
	    });
	  }

	/**
	   * Helper method to write a property into the target crate document.
	   *
	   * @param source the source object.
	   * @param sink the target document.
	   * @param property the property information.
	   */
	  @SuppressWarnings("unchecked")
	  private void writePropertyInternal(final Object source, final CrateDocument sink, final CratePersistentProperty property) {
		  
		  if(source == null) {
			  return;
		  }
		  
		  String name = property.getFieldName();
		  TypeInformation<?> valueType = from(source.getClass());
		  TypeInformation<?> type = property.getTypeInformation();
	
		  if(valueType.isCollectionLike()) {
			  CrateArray array = writeCollection(asCollection(source), property);
		      sink.put(name, array);
		      return;
		  }
		  
		  if(valueType.isMap()) {
			  CrateDocument document = writeMap((Map<Object, Object>) source, property);
			  sink.put(name, document);
		      return;
		  }
		  
		  Class<?> basicTargetType = conversions.getCustomWriteTarget(source.getClass(), null);
		  
		  if(basicTargetType != null) {
			  sink.put(name, conversionService.convert(source, basicTargetType));
		      return;
		  }
		  
		  CrateDocument document = new CrateDocument();
		  addCustomTypeKeyIfNecessary(type, source, document);
	
		  CratePersistentEntity<?> entity = isSubtype(property.getType(), source.getClass()) ? mappingContext .getPersistentEntity(source.getClass()) : 
		    																				   mappingContext.getPersistentEntity(type);
		  writeInternal(source, document, entity);
		    
		  sink.put(name, document);
	  }

	/**
	 * Helper method to write the map into the crate document.
	 * 
	 * @param source the source object.
	 * @param sink the target document.
	 * @param type the type information for the document.
	 * @return the written crate document.
	 */
	private CrateDocument writeMapInternal(final Map<Object, Object> source, final CrateDocument sink, final TypeInformation<?> type) {

		for(Map.Entry<Object, Object> entry : source.entrySet()) {
			
			Object key = entry.getKey();
			Object val = entry.getValue();
			
			if(conversions.isSimpleType(key.getClass())) {
				
				String simpleKey = key.toString();
				
				if(val == null || (conversions.isSimpleType(val.getClass()) && !val.getClass().isArray())) {
					writeSimpleInternal(val, sink, simpleKey);
				}else if(val instanceof Collection || val.getClass().isArray()) {
					sink.put(simpleKey, writeCollectionInternal(asCollection(val), new CrateArray(), type.getMapValueType()));
				}else {
					CrateDocument document = new CrateDocument();
					TypeInformation<?> valueTypeInfo = type.isMap() ? type.getMapValueType() : OBJECT;
					writeInternal(val, document, valueTypeInfo);
					sink.put(simpleKey, document);
				}
			} else {
				throw new MappingException("Cannot use a complex object as a key value.");
			}
		}
		
		return sink;
	}
	
	/**
	   * Helper method to write the internal collection.
	   *
	   * @param source the source object.
	   * @param target the target document.
	   * @param type the type information for the document.
	   * @return the created crate list.
	   */
	private CrateArray writeCollectionInternal(final Collection<?> source, final CrateArray target, final TypeInformation<?> type) {
		
		TypeInformation<?> componentType = type == null ? null : type.getComponentType();

	    for(Object element : source) {
	    	
	    	validateCollectionLikeElement(element);
	    	
	    	Class<?> elementType = element == null ? null : element.getClass();
	    	
	    	if(elementType == null || conversions.isSimpleType(elementType)) {
	    		target.add(element);
	    	}else {
	    		CrateDocument document = new CrateDocument();
	    		writeInternal(element, document, componentType);
	    		target.add(document);
	    	}
	    }
	    
	    return target;
	}
	
	/**
	 * Read a collection from the source object.
	 *
	 * @param targetType the target type.
	 * @param source the list as source.
	 * @param parent the optional parent.
	 * @return the converted {@link Collection} or array, will never be {@literal null}.
	 */
	private Object readCollection(final TypeInformation<?> targetType, final CrateArray source, final Object parent) {
		
		notNull(targetType);

	    Class<?> collectionType = targetType.getType();
	    
	    if(source.isEmpty()) {
	      return getPotentiallyConvertedSimpleRead(new HashSet<Object>(), collectionType);
	    }

	    collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType : List.class;
	    
	    Collection<Object> items = targetType.getType().isArray() ? new ArrayList<Object>(source.size()) :
	    															createCollection(collectionType, source.size());
	    
	    TypeInformation<?> componentType = targetType.getComponentType();
	    
	    Class<?> rawComponentType = componentType == null ? null : componentType.getType();

	    for(Object object : source) {
	    	if(object instanceof CrateDocument) {
	    		items.add(read(componentType, (CrateDocument) object, parent));
	    	}else {
	    		items.add(getPotentiallyConvertedSimpleRead(object, rawComponentType));
	    	}
	    }
	    
	    return getPotentiallyConvertedSimpleRead(items, targetType.getType());
	  }
	
	/**
	 * Writes the given simple value to the given {@link CrateDocument}. Will store enum names for enum values.
	 * 
	 * @param value
	 * @param CrateDocument must not be {@literal null}.
	 * @param key must not be {@literal null}.
	 */
	private void writeSimpleInternal(final Object source, final CrateDocument sink, final String key) {
		sink.put(key, getPotentiallyConvertedSimpleWrite(source));
	}
	
	/**
	 * Returns given object as {@link Collection}. Will return the
	 * {@link Collection} as is if the source is a {@link Collection} already,
	 * will convert an array into a {@link Collection} or simply create a single
	 * element collection for everything else.
	 * 
	 * @param source
	 * @return
	 */
	private static Collection<?> asCollection(final Object source) {
		if(source instanceof Collection) {
			return (Collection<?>) source;
		}
		return source.getClass().isArray() ? arrayToList(source) : singleton(source);
	}
	
	/**
	 * Adds custom type information to the given {@link CrateDocument} if necessary.
	 *  
	 * @param type
	 * @param value must not be {@literal null}.
	 * @param document must not be {@literal null}.
	 */
	protected void addCustomTypeKeyIfNecessary(TypeInformation<?> type, Object value, CrateDocument document) {

		TypeInformation<?> actualType = type != null ? type.getActualType() : null;
		Class<?> reference = actualType == null ? Object.class : actualType.getType();
		Class<?> valueType = getUserClass(value.getClass());

		boolean notTheSameClass = !valueType.equals(reference);
		if(notTheSameClass) {
			typeMapper.writeType(valueType, document);
		}
	}
	
	/**
	 * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Crate type.
	 * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is.
	 * 
	 * @param value
	 * @return
	 */
	private Object getPotentiallyConvertedSimpleWrite(Object value) {
		
		if(value == null) {
			return null;
		}
		
		Class<?> customTarget = conversions.getCustomWriteTarget(value.getClass(), null);

		if(customTarget != null) {
			return conversionService.convert(value, customTarget);
		} else {
			return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).toString() : value;
		}
	}
	
	/**
	   * Helper method to read the value based on the value type.
	   *
	   * @param value the value to convert.
	   * @param type the type information.
	   * @param parent the optional parent.
	   * @param <R> the target type.
	   * @return the converted object.
	   */
	  @SuppressWarnings("unchecked")
	  private <R> R readValue(Object value, TypeInformation<?> type, Object parent) {
		  
		  Class<?> rawType = type.getType();
		  
		  if(conversions.hasCustomReadTarget(value.getClass(), rawType)) {
			  return (R) conversionService.convert(value, rawType);			  
		  }else if(value instanceof CrateDocument) {
			  return (R) read(type, (CrateDocument) value, parent);
		  }else if(value instanceof CrateArray) {
			  return (R) readCollection(type, (CrateArray) value, parent);
		  } else {
			  return (R) getPotentiallyConvertedSimpleRead(value, rawType);
		  }
	  }
	
	/**
	 * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies
	 * {@link Enum} handling or returns the value as is.
	 * 
	 * @param value
	 * @param target must not be {@literal null}.
	 * @return
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private Object getPotentiallyConvertedSimpleRead(Object value, Class<?> target) {

		if(value == null || target == null) {
			return value;
		}

		if(conversions.hasCustomReadTarget(value.getClass(), target)) {
			return conversionService.convert(value, target);
		}

		if(Enum.class.isAssignableFrom(target)) {
			return Enum.valueOf((Class<Enum>) target, value.toString());
		}

		return target.isAssignableFrom(value.getClass()) ? value : conversionService.convert(value, target);
	}
	
	private CrateArray maybeConvertList(Iterable<?> source, TypeInformation<?> typeInformation) {
		
		CrateArray array = new CrateArray();
		
		for(Object element : source) {
			validateCollectionLikeElement(element);
			array.add(convertToCrateType(element, typeInformation));
		}
		
		return array;
	}
	
	/**
	 * Loads the property value through the value provider.
	 *
	 * @param property the source property.
	 * @param source the source document.
	 * @param parent the optional parent.
	 * @return the actual property value.
	 */
	protected Object getValueInternal(final CratePersistentProperty property, final CrateDocument source, final Object parent) {
		  return new CratePropertyValueProvider(source, spELContext, parent).getPropertyValue(property);
	  }
	
	/**
	   * Helper method to create the underlying collection/list.
	   *
	   * @param collection the collection to write.
	   * @param property the property information.
	   * @return the created crate array.
	   */
	private CrateArray writeCollection(final Collection<?> collection, final CratePersistentProperty property) {
		return writeCollectionInternal(collection, new CrateArray(), property.getTypeInformation());
	}
	
	/**
	   * Wrapper method to create the underlying map.
	   *
	   * @param map the source map.
	   * @param property the persistent property.
	   * @return the written crate document.
	   */
	private CrateDocument writeMap(final Map<Object, Object> map, final CratePersistentProperty property) {
		notNull(map, "Source map must not be null");
	    notNull(property, "PersistentProperty must not be null");
	    return writeMapInternal(map, new CrateDocument(), property.getTypeInformation());
	}
	
	/**
	   * Check if one class is a subtype of the other.
	   *
	   * @param left the first class.
	   * @param right the second class.
	   * @return true if it is a subtype, false otherwise.
	   */
	private boolean isSubtype(final Class<?> left, final Class<?> right) {
		return left.isAssignableFrom(right) && !left.equals(right);
	}
	
	private boolean isPrimitiveArray(CratePersistentProperty property) {
		return property.isArray() && conversions.isSimpleType(property.getType());
	}
	
	/**
	 * Removes the type information from the conversion result.
	 * 
	 * @param object
	 * @return
	 */
	private Object removeTypeInfoRecursively(Object object) {
		
		if(!(object instanceof CrateDocument)) {
			return object;
		}

		CrateDocument document = (CrateDocument) object;
		
		String keyToRemove = null;
		
		for(String key : document.keySet()) {
			
			if (typeMapper.isTypeKey(key)) {
				keyToRemove = key;
			}

			Object value = document.get(key);
			
			if (value instanceof CrateArray) {
				for (Object element : (CrateArray) value) {
					removeTypeInfoRecursively(element);
				}
			} else {
				removeTypeInfoRecursively(value);
			}
		}
		
		if (keyToRemove != null) {
			document.remove(keyToRemove);
		}
		
		return document;
	}
	
	/**
	   * Creates a new parameter provider.
	   *
	   * @param entity the persistent entity.
	   * @param source the source document.
	   * @param evaluator the SPEL expression evaluator.
	   * @param parent the optional parent.
	   * @return a new parameter value provider.
	   */
	private ParameterValueProvider<CratePersistentProperty> getParameterProvider(final CratePersistentEntity<?> entity, final CrateDocument source, 
																				 final DefaultSpELExpressionEvaluator evaluator, final Object parent) {
		
	    CratePropertyValueProvider provider = new CratePropertyValueProvider(source, evaluator, parent);
	    
	    PersistentEntityParameterValueProvider<CratePersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<>(entity, provider, parent);

	    return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider, parent);
	}
	
	private Class<?> getCustomWriteHandler(Class<?> sourceClass, Class<?> sinkClass) {		
		return conversions.getCustomWriteTarget(sourceClass, sinkClass);
	}
	
	private void validateCollectionLikeElement(Object element) {
		if(element != null && (element instanceof Collection || element.getClass().isArray())) {
			throw new MappingException("Nesting Array or Collection types is not supported by crate");
		}
	}
	
	/**
	 * Property value provider for Crate documents.
	 * @author Hasnain Javed
	 * @since 1.0.0
	 */
	private class CratePropertyValueProvider implements PropertyValueProvider<CratePersistentProperty> {

	    /**
	     * The source document.
	     */
	    private final CrateDocument source;

	    /**
	     * The expression evaluator.
	     */
	    private final SpELExpressionEvaluator evaluator;

	    /**
	     * The optional parent object.
	     */
	    private final Object parent;

	    public CratePropertyValueProvider(final CrateDocument source, final SpELContext factory, final Object parent) {
	      this(source, new DefaultSpELExpressionEvaluator(source, factory), parent);
	    }

	    public CratePropertyValueProvider(final CrateDocument source, final DefaultSpELExpressionEvaluator evaluator, final Object parent) {
	      notNull(source);
	      notNull(evaluator);

	      this.source = source;
	      this.evaluator = evaluator;
	      this.parent = parent;
	    }

	    @Override
	    public <R> R getPropertyValue(final CratePersistentProperty property) {
	    	
	      String expression = property.getSpelExpression();
	      
	      Object value;
	    		  
	      if(expression != null) {
	    	  value = evaluator.evaluate(expression);
	      }else {
	    	  value = property.isVersionProperty() ? source.get(RESERVED_VESRION_FIELD_NAME) : source.get(property.getFieldName());
	      }
	      
	      if(value == null) {
	        return null;
	      }
	      
	      return readValue(value, property.getTypeInformation(), parent);
	    }
	}
	
	/**
	 * An expression parameter value provider.
	 * @author Hasnain Javed
	 * @since 1.0.0
	 */
	private class ConverterAwareSpELExpressionParameterValueProvider extends SpELExpressionParameterValueProvider<CratePersistentProperty> {
		
		private final Object parent;
		
	    public ConverterAwareSpELExpressionParameterValueProvider(final SpELExpressionEvaluator evaluator, final ConversionService conversionService, 
	    														  final ParameterValueProvider<CratePersistentProperty> delegate, final Object parent) {
	      super(evaluator, conversionService, delegate);
	      this.parent = parent;
	    }
	    
	    @Override
	    protected <T> T potentiallyConvertSpelValue(final Object object, final Parameter<T, CratePersistentProperty> parameter) {
	    	return readValue(object, parameter.getType(), parent);
	    }
	}
}