package nl.ipo.cds.dao.attributemapping;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import nl.ipo.cds.attributemapping.operations.OperationInput;
import nl.ipo.cds.attributemapping.operations.OperationType;
import nl.ipo.cds.attributemapping.operations.OutputOperationType;
import nl.ipo.cds.dao.ManagerDao;
import nl.ipo.cds.domain.AttributeMapping;
import nl.ipo.cds.domain.AttributeType;
import nl.ipo.cds.domain.Dataset;
import nl.ipo.cds.domain.FeatureTypeAttribute;
import nl.ipo.cds.domain.MappingOperation;
import nl.ipo.cds.domain.MappingOperation.MappingOperationType;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.transaction.annotation.Transactional;

public class AttributeMappingDao {

	private final ManagerDao managerDao;
	private final List<FeatureTypeAttribute> inputAttributes;
	private final List<OperationType> operationTypes;

	public AttributeMappingDao (final ManagerDao managerDao) {
		this (managerDao, new ArrayList<FeatureTypeAttribute> (), new ArrayList<OperationType> ());
	}
	
	public AttributeMappingDao (final ManagerDao managerDao, final Collection<OperationType> operationTypes) {
		this (managerDao, new ArrayList<FeatureTypeAttribute> (), operationTypes);
	}
	
	public AttributeMappingDao (final ManagerDao managerDao, final Collection<FeatureTypeAttribute> inputAttributes, final Collection<OperationType> operationTypes) {
		this.managerDao = managerDao;
		this.inputAttributes = new ArrayList<FeatureTypeAttribute> (inputAttributes);
		this.operationTypes = new ArrayList<OperationType> (operationTypes);
	}

	@Transactional
	public OperationDTO getAttributeMapping (final Dataset dataset, final OutputOperationType attribute) {
		final AttributeMapping attributeMapping = managerDao.getAttributeMapping (dataset, attribute.getName ());
		
		// Attribute mapping hasn't been persisted yet:
		if (attributeMapping == null || attributeMapping.getRootOperation () == null) {
			return null;
		}
		
		
		return unmarshalOperation (attributeMapping.getRootOperation ());
	}

	private OperationDTO unmarshalOperation (final MappingOperation mappingOperation) {
		if (mappingOperation == null) {
			return null;
		} else if (mappingOperation.getOperationType () == MappingOperationType.INPUT_OPERATION) {
			return unmarshalInputOperation (mappingOperation);
		} else if (mappingOperation.getOperationType () == MappingOperationType.TRANSFORM_OPERATION) {
			return unmarshalTransformOperation (mappingOperation);
		} else {
			throw new IllegalArgumentException (String.format ("Mapping operation has an invalid type: %s", mappingOperation.getOperationType ()));
		}
	}
	
	private InputOperationDTO unmarshalInputOperation (final MappingOperation mappingOperation) {
		if (mappingOperation == null) {
			throw new NullPointerException ("mappingOperation cannot be null");
		}
		if (mappingOperation.getOperationName () == null) {
			throw new NullPointerException ("operation name is null");
		}
		if (mappingOperation.getInputAttributeType () == null) {
			throw new NullPointerException ("attribute type is null");
		}
		
		return new InputOperationDTO (
				findFeatureTypeAttribute (mappingOperation.getOperationName (), mappingOperation.getInputAttributeType ()),
				mappingOperation.getOperationName (), 
				mappingOperation.getInputAttributeType ()
			);
	}
	
	private FeatureTypeAttribute findFeatureTypeAttribute (final String name, final AttributeType type) {
		for (final FeatureTypeAttribute attr: inputAttributes) {
			if (attr.getName ().getLocalPart ().equals (name) && attr.getType ().equals (type)) {
				return attr;
			}
		}
		
		return null;
	}
	
	private TransformOperationDTO unmarshalTransformOperation (final MappingOperation mappingOperation) {
		if (mappingOperation == null || mappingOperation.getOperationName () == null) {
			throw new NullPointerException ();
		}
		
		// Unmarshal properties:
		
		final OperationType operationType = findOperationType (mappingOperation.getOperationName ()); 
		return new TransformOperationDTO (
				operationType, 
				unmarshalInputs (mappingOperation.getInputs ()), 
				unmarshalProperties (mappingOperation.getProperties(), operationType)
			);
	}
	
	private List<OperationInputDTO> unmarshalInputs (final List<MappingOperation> inputs) {
		if (inputs == null) {
			throw new NullPointerException ("inputs cannot be null");
		}
		
		final List<OperationInputDTO> result = new ArrayList<OperationInputDTO> ();
		
		for (final MappingOperation input: inputs) {
			result.add (new OperationInputDTO (unmarshalOperation (input)));
		}
		
		return result;
	}
	
	private Object unmarshalProperties (final String properties, final OperationType operationType) {
		if (properties == null || operationType.getPropertyBeanClass () == null) {
			return null;
		}
		
		try {
			return new ObjectMapper ().readValue (properties, operationType.getPropertyBeanClass ());
		} catch (JsonParseException e) {
			throw new IllegalArgumentException (String.format ("Invalid JSON string %s", properties), e);
		} catch (JsonMappingException e) {
			throw new IllegalArgumentException (String.format ("Invalid JSON string %s", properties), e);
		} catch (IOException e) {
			throw new IllegalArgumentException (String.format ("Invalid JSON string %s", properties), e);
		}
	}
	
	private OperationType findOperationType (final String name) {
		for (final OperationType ot: operationTypes) {
			if (ot.getName ().equals (name)) {
				return ot;
			}
		}
		
		throw new IllegalArgumentException (String.format ("No operation named %s could be found", name));
	}
	
	@Transactional
	public void putAttributeMapping (final Dataset dataset, final OutputOperationType attribute, final OperationDTO operation, final boolean isValid) {
		final AttributeMapping attributeMapping = getOrCreateAttributeMapping (dataset, attribute);

		attributeMapping.setValid (isValid);
		
		// Persist the root operation:
		attributeMapping.setRootOperation (persistOperation (operation, attributeMapping.getRootOperation ()));
		managerDao.update (attributeMapping);
	}
	
	private MappingOperation persistOperation (final OperationDTO operation, final MappingOperation existingMappingOperation) {
		if (operation == null) {
			// Delete an existing mapping operation:
			if (existingMappingOperation != null) {
				deleteChild (existingMappingOperation);
			}
			
			return null;
		}
		
		final MappingOperation mappingOperation = existingMappingOperation != null
				? existingMappingOperation
				: new MappingOperation ();
		
		// Set properties of this mapping operation:
		mergeOperation (operation, mappingOperation);
		
		// Set children:
		
		// Persist:
		if (mappingOperation.getId () == null) {
			managerDao.create (mappingOperation);
		} else {
			managerDao.update (mappingOperation);
		}
		
		return mappingOperation;
	}
	
	private void mergeOperation (final OperationDTO operation, final MappingOperation mappingOperation) {
		if (operation instanceof InputOperationDTO) {
			mergeInputOperation ((InputOperationDTO)operation, mappingOperation);
		} else if (operation instanceof TransformOperationDTO) {
			mergeTransformOperation ((TransformOperationDTO)operation, mappingOperation);
		} else {
			throw new IllegalArgumentException (String.format ("Invalid operation type %s", operation.getClass ().getCanonicalName ()));
		}
	}
	
	private void mergeInputOperation (final InputOperationDTO inputOperation, final MappingOperation operation) {
		// If the operation previously had children, delete them:
		for (final MappingOperation input: operation.getInputs ()) {
			deleteChild (input);
		}

		// Set properties:
		operation.setOperationType (MappingOperationType.INPUT_OPERATION);
		operation.setInputAttributeType (inputOperation.getAttributeType ());
		operation.setOperationName (inputOperation.getAttributeName ());
		operation.setInputs (new ArrayList<MappingOperation> ());
		operation.setProperties (null);
	}
	
	private void mergeTransformOperation (final TransformOperationDTO transformOperation, final MappingOperation operation) {
		// Set properties:
		operation.setOperationType (MappingOperationType.TRANSFORM_OPERATION);
		operation.setOperationName (transformOperation.getOperationType ().getName ());
		operation.setInputAttributeType (null);

		// Serialize configuration properties:
		operation.setProperties (serializeProperties (transformOperation.getOperationProperties ()));
		
		// Update children:
		final List<OperationInput> operationInputs = transformOperation.getInputs ();
		final List<MappingOperation> oldChildren = new ArrayList<MappingOperation> (operation.getInputs ());
		final List<MappingOperation> newChildren = new ArrayList<MappingOperation> ();
		int i;
		
		for (i = 0; i < operationInputs.size (); ++ i) {
			final OperationInputDTO operationInput = (OperationInputDTO)operationInputs.get (i);
			final MappingOperation childOperation = i < oldChildren.size () ? oldChildren.get (i) : null;
			
			newChildren.add (persistOperation ((OperationDTO)operationInput.getOperation (), childOperation));
		}
		
		// Remove unused children:
		for (; i < oldChildren.size (); ++ i) {
			deleteChild (oldChildren.get (i));
		}
		
		operation.setInputs (newChildren);
	}
	
	private void deleteChild (final MappingOperation mappingOperation) {
		if (mappingOperation == null) {
			return;
		}
		
		// Delete children of this operation:
		final List<MappingOperation> children = mappingOperation.getInputs ();
		if (children != null) {
			for (final MappingOperation child: children) {
				deleteChild (child);
			}
		}
		
		// Delete this operation:
		managerDao.delete (mappingOperation);
	}
	
	private String serializeProperties (final Object properties) {
		if (properties == null) {
			return null;
		}
		
		try {
			return new ObjectMapper ().writeValueAsString (properties);
		} catch (JsonGenerationException e) {
			throw new RuntimeException (e);
		} catch (JsonMappingException e) {
			throw new RuntimeException (e);
		} catch (IOException e) {
			throw new RuntimeException (e);
		}
	}
	
	/**
	 * Returns an existing mapping for the given attribute and dataset, or creates and persists a new one if
	 * it hasn't been created yet.
	 * 
	 * @param dataset
	 * @param attribute
	 * @return An attribute mapping for the given dataset and attribute.
	 */
	private AttributeMapping getOrCreateAttributeMapping (final Dataset dataset, final OutputOperationType attribute) {
		final AttributeMapping existingMapping = managerDao.getAttributeMapping (dataset, attribute.getName ());
		
		if (existingMapping == null) {
			final AttributeMapping mapping = new AttributeMapping (dataset, attribute.getName ());
			managerDao.create (mapping);
			return mapping;
		}
		
		return existingMapping;
	}
}
