package com.evolveum.midpoint.schema.util;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.evolveum.midpoint.prism.PrismConstants;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.SerializationContext;
import com.evolveum.midpoint.prism.SerializationOptions;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.prism.xnode.PrimitiveXNode;
import com.evolveum.midpoint.prism.xnode.XNode;
import com.evolveum.midpoint.schema.constants.ObjectTypes;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.xml.ns._public.common.common_3.EntryType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectFactory;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectReferenceType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ParamsType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.UnknownJavaObjectType;
import com.evolveum.prism.xml.ns._public.types_3.RawType;

public class ParamsTypeUtil {

	public static ParamsType toParamsType(Map<String, Serializable> paramsMap){
		Set<Entry<String, Serializable>> params = paramsMap.entrySet();
		if (!params.isEmpty()) {
			ParamsType paramsType = new ParamsType();
			
			for (Entry<String, Serializable> entry : params) {
				paramsType.getEntry().add(createEntryElement(entry.getKey(),entry.getValue()));
			}
			return paramsType;
		}
		return null;
	}
	
	public static ParamsType toParamsType(Map<String, Serializable> paramsMap, PrismContext prismContext) throws SchemaException{
		Set<Entry<String, Serializable>> params = paramsMap.entrySet();
		if (!params.isEmpty()) {
			ParamsType paramsType = new ParamsType();
			
			for (Entry<String, Serializable> entry : params) {
				paramsType.getEntry().add(createEntryElement(entry, prismContext));
			}
			return paramsType;
		}
		return null;
	}

	private static EntryType createEntryElement(Entry<String, Serializable> entry, PrismContext prismContext) throws SchemaException {
		EntryType result = new EntryType();
		result.setKey(entry.getKey());
		if (XmlTypeConverter.canConvert(entry.getValue().getClass())){
			return createEntryElement(entry.getKey(), entry.getValue());
		} else {
			SerializationContext ctx = new SerializationContext(SerializationOptions.createSerializeReferenceNames());
			XNode xnode = prismContext.getXnodeProcessor().serializeAnyData(entry.getValue(), SchemaConstants.C_PARAM_VALUE, ctx);
			return createEntryElement(entry.getKey(), new RawType(xnode, prismContext));
		}
//		result.setEntryValue(new JAXBElement(SchemaConstants.C_PARAM_VALUE, entry.getValue().getClass(),
//				prismContext.serializeAnyData(entry.getValue(), new QName("any"), prismContext.LANG_XML)));
//		return result;
	}

	public static Map<String, Serializable> fromParamsType(ParamsType paramsType, PrismContext prismContext) throws SchemaException{
		if (paramsType != null) {
			Map<String, Serializable> params = new HashMap<String, Serializable>();
			Serializable realValue = null;
			for (EntryType entry : paramsType.getEntry()) {
				if (entry.getEntryValue() != null){
					
					Serializable value = (Serializable) entry.getEntryValue().getValue();
					if (value instanceof RawType){
						XNode xnode = ((RawType) value).getXnode();
						if (xnode instanceof PrimitiveXNode){
							realValue = ((PrimitiveXNode) xnode).getGuessedFormattedValue();
						}
					} else {
						realValue = value;
					}
				}
				params.put(entry.getKey(), (Serializable) (realValue));
			}
			
			return params;
		}
		return null;
	}
	
	public static Map<String, Serializable> fromParamsType(ParamsType paramsType) {
		if (paramsType != null) {
			Map<String, Serializable> params = new HashMap<String, Serializable>();
			Serializable realValue = null;
			for (EntryType entry : paramsType.getEntry()) {
				if (entry.getEntryValue() != null){
					
//					Serializable value = (Serializable) entry.getEntryValue().getValue();
//					if (value instanceof RawType){
//						XNode xnode = ((RawType) value).getXnode();
//						if (xnode instanceof PrimitiveXNode){
//							realValue = ((PrimitiveXNode) xnode).getGuessedFormattedValue();
//						}
//					} else {
//						realValue = value;
//					}
//				}
				params.put(entry.getKey(), (Serializable) entry.getEntryValue());
				}
			}
			
			return params;
		}
		return null;
	}
	
	/**
	 * Temporary workaround, brutally hacked -- so that the conversion 
	 * of OperationResult into OperationResultType 'somehow' works, at least to the point
	 * where when we:
	 * - have OR1
	 * - serialize it into ORT1
	 * - then deserialize into OR2
	 * - serialize again into ORT2
	 * so we get ORT1.equals(ORT2) - at least in our simple test case :)
	 * 
	 * FIXME: this should be definitely reworked
	 * 
	 * @param entry
	 * @return
	 */
	private static EntryType createEntryElement(String key, Serializable value) {
		EntryType entryType = new EntryType();
		entryType.setKey(key);
		if (value != null) {
			Document doc = DOMUtil.getDocument();
			if (value instanceof ObjectType && ((ObjectType)value).getOid() != null) {
				// Store only reference on the OID. This is faster and getObject can be used to retrieve
				// the object if needed. Although is does not provide 100% accuracy, it is a good tradeoff.
				setObjectReferenceEntry(entryType, ((ObjectType)value));
			// these values should be put 'as they are', in order to be deserialized into themselves
			} else if (value instanceof String || value instanceof Integer || value instanceof Long) {
				entryType.setEntryValue(new JAXBElement<Serializable>(SchemaConstants.C_PARAM_VALUE, Serializable.class, value));
			} else if (XmlTypeConverter.canConvert(value.getClass())) {
//				try {
//					entryType.setEntryValue(new JXmlTypeConverter.toXsdElement(value, SchemaConstants.C_PARAM_VALUE, doc, true));
//				} catch (SchemaException e) {
//					LOGGER.error("Cannot convert value {} to XML: {}",value,e.getMessage());
//					setUnknownJavaObjectEntry(entryType, value);
//				}
			} else if (value instanceof Element || value instanceof JAXBElement<?>) {
				entryType.setEntryValue((JAXBElement<?>) value);
			// FIXME: this is really bad code ... it means that 'our' JAXB object should be put as is
			} else if ("com.evolveum.midpoint.xml.ns._public.common.common_2".equals(value.getClass().getPackage().getName())) {
				JAXBElement<Object> o = new JAXBElement<Object>(SchemaConstants.C_PARAM_VALUE, Object.class, value);
				entryType.setEntryValue(o);
			} else {
				setUnknownJavaObjectEntry(entryType, value);
			}
		}
		return entryType;
	}

	private static void setObjectReferenceEntry(EntryType entryType, ObjectType objectType) {
		ObjectReferenceType objRefType = new ObjectReferenceType();
		objRefType.setOid(objectType.getOid());
		ObjectTypes type = ObjectTypes.getObjectType(objectType.getClass());
		if (type != null) {
			objRefType.setType(type.getTypeQName());
		}
		JAXBElement<ObjectReferenceType> element = new JAXBElement<ObjectReferenceType>(
				SchemaConstants.C_OBJECT_REF, ObjectReferenceType.class, objRefType);
//		entryType.setAny(element);
	}

	private static void setUnknownJavaObjectEntry(EntryType entryType, Serializable value) {
		UnknownJavaObjectType ujo = new UnknownJavaObjectType();
		ujo.setClazz(value.getClass().getName());
		ujo.setToString(value.toString());
		entryType.setEntryValue(new ObjectFactory().createUnknownJavaObject(ujo));
	}

}
