package org.openmhealth.reference.util;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;

import name.jenkins.paul.john.concordia.Concordia;

import org.openmhealth.reference.concordia.OmhValidationController;

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.BeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter.SerializeExceptFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

/**
 * <p>
 * A custom ObjectMapper for Open mHealth that adds functionality like
 * filtering fields on HTTP-level serialization.
 * </p>
 *
 * @author John Jenkins
 */
public class OmhObjectMapper extends ObjectMapper {
	/**
	 * <p>
	 * A custom annotation for fields that should not be serialized by Jackson
	 * when their encapsulating object is being serialized by Spring.
	 * </p> 
	 *
	 * @author John Jenkins
	 */
	@Documented
	@Target(ElementType.FIELD)
	@Retention(RetentionPolicy.RUNTIME)
	public static @interface JacksonFieldFilter {		
		/**
		 * The filter group to which the field belongs. All fields that belong
		 * to the same class should have the same filter group name and that
		 * class should be annotated with {@link JsonFilter}, which specifies
		 * the same group name. 
		 * 
		 * @return The filter group to which the field belongs.
		 */
		String value();
	}
	
	/**
	 * <p>
	 * The custom filter that allows for the addition of new field names.
	 * </p>
	 *
	 * @author John Jenkins
	 */
	private static class ExtendableSerializeExceptFilter
		extends SerializeExceptFilter {
		
		/**
		 * Creates a new filter with the field name.
		 * 
		 * @param fieldName
		 *        The first field for this filter.
		 */
		public ExtendableSerializeExceptFilter(final String fieldName) {
			super(new HashSet<String>(Arrays.asList(fieldName)));
		}
		
		/**
		 * Adds another field to this filter.
		 * 
		 * @param fieldName
		 *        The new field.
		 */
		public void addField(final String fieldName) {
			_propertiesToExclude.add(fieldName);
		}
	}
    
	/**
     * A default version UID to use when serializing an instance of this class.
     */
    private static final long serialVersionUID = 1L;
    
    /**
     * The set of private fields that should never be serialized.
     */
    private static final SimpleFilterProvider FILTER_PROVIDER =
    	new SimpleFilterProvider();

	/**
	 * Creates the object mapper and initializes the filters. This is private
	 * as it should only ever be called by Spring via reflection.
	 */
	private OmhObjectMapper() {
		// Ensure that unknown fields are ignored.
		FILTER_PROVIDER.setFailOnUnknownId(false);
		
		// Save the FilterProvider in this ObjectMapper.
	    setFilters(FILTER_PROVIDER);
	    
	    // Add our Concordia validation controller.
	    InjectableValues.Std injectableValues = new InjectableValues.Std();
	    injectableValues
	    	.addValue(
	    		Concordia.JACKSON_INJECTABLE_VALIDATION_CONTROLLER,
	    		OmhValidationController.VALIDATION_CONTROLLER);
	    setInjectableValues(injectableValues);
	}
    
    /**
	 * Registers a new field filter and ensures that its desired fields are
	 * never serialized.
	 * 
	 * @param filterClass
	 *        The class to whose filters should be registered to this
	 *        ObjectMapper.
	 */
    public static synchronized void register(final Class<?> filterClass) {
    	// Sanitize the input.
    	if(filterClass == null) {
    		throw new IllegalArgumentException("The filter class is null.");
    	}
    	
    	// Cycle through each of the class's fields.
    	for(Field field : filterClass.getFields()) {
    		// Determine if that field has a JacksonFieldFilter annotation.
    		JacksonFieldFilter filter =
    			field.getAnnotation(JacksonFieldFilter.class);
    		
    		// If it does have the annotation.
    		if(filter != null) {
    			// Get the serialized field name.
    			String fieldName;
    			JsonProperty jsonProperty = 
    				field.getAnnotation(JsonProperty.class);
    			if(jsonProperty == null) {
    				fieldName = field.getName();
    			}
    			else {
    				fieldName = jsonProperty.value();
    			}
    			
    			// Get the given group name.
    			String filterGroup = filter.value();
    			
    			// Get the existing filter, if one exists.
    			BeanPropertyFilter propertyFilter =
    				FILTER_PROVIDER.findFilter(filterGroup);
    			
    			// If no such filter exists, update the filter provider with a
    			// new one.
    			if(propertyFilter == null) {
    				FILTER_PROVIDER
    					.addFilter(
    						filterGroup,
    						new ExtendableSerializeExceptFilter(fieldName));
    			}
    			else {
    				// Safely cast the filter as we are the only maintainers of
    				// it, and this is the only type of filter we use.
    				ExtendableSerializeExceptFilter amenableFilter =
    					(ExtendableSerializeExceptFilter) propertyFilter;
    				
    				// Add the field.
    				amenableFilter.addField(fieldName);
    			}
    		}
    	}
    }
}