/*******************************************************************************
 * Copyright (c) 2010 Robert "Unlogic" Olofsson (unlogic@unlogic.se).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v3
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-3.0-standalone.html
 ******************************************************************************/
package se.unlogic.standardutils.xml;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import se.unlogic.standardutils.annotations.NoAnnotatedFieldsFoundException;
import se.unlogic.standardutils.arrays.ArrayUtils;
import se.unlogic.standardutils.date.DateUtils;
import se.unlogic.standardutils.reflection.ReflectionUtils;
import se.unlogic.standardutils.string.DummyStringyfier;
import se.unlogic.standardutils.string.StringUtils;
import se.unlogic.standardutils.string.Stringyfier;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

public class XMLGenerator {

	//TODO Use weak references in this map to prevent memory leaks
	private static ConcurrentHashMap<Class<?>, ClassXMLInfo> FIELD_MAP = new ConcurrentHashMap<Class<?>, ClassXMLInfo>();

	@SuppressWarnings("unchecked")
	public static Element toXML(Object bean, Document doc){

		ClassXMLInfo classInfo = FIELD_MAP.get(bean.getClass());

		if(classInfo == null){

			Class<?> clazz = bean.getClass();

			XMLElement xmlElement = clazz.getAnnotation(XMLElement.class);

			if(xmlElement == null){

				throw new MissingXMLAnnotationException(clazz);
			}

			String elementName = xmlElement.name();

			if(StringUtils.isEmpty(elementName)){

				elementName = clazz.getSimpleName();
			}

			List<FieldXMLInfo> annotatedFields = new ArrayList<FieldXMLInfo>();

			Class<?> currentClazz = clazz;

			while (currentClazz != Object.class) {

				Field[] fields = currentClazz.getDeclaredFields();

				for(Field field : fields){

					XMLElement elementAnnotation = field.getAnnotation(XMLElement.class);

					if(elementAnnotation != null){

						String name = elementAnnotation.name();

						if(StringUtils.isEmpty(name)){

							name = field.getName();
						}

						Stringyfier valueFormatter = null;

						if(elementAnnotation.valueFormatter() != DummyStringyfier.class){

							try {
								valueFormatter = elementAnnotation.valueFormatter().newInstance();

							} catch (InstantiationException e) {

								throw new RuntimeException(e);

							} catch (IllegalAccessException e) {

								throw new RuntimeException(e);
							}
						}

						if(Collection.class.isAssignableFrom(field.getType())){

							boolean elementable = false;

							if(ReflectionUtils.isGenericlyTyped(field) && Elementable.class.isAssignableFrom((Class<?>) ReflectionUtils.getGenericType(field))){

								elementable = true;
							}

							String childName = elementAnnotation.childName();

							if(StringUtils.isEmpty(childName)){

								childName = "value";
							}

							annotatedFields.add(new FieldXMLInfo(name, field, FieldType.Element,elementAnnotation.cdata(),elementable,true,false,childName,elementAnnotation.skipChildParentElement(),valueFormatter));

						}else if(field.getType().isArray()){

							boolean elementable = false;

							if(Elementable.class.isAssignableFrom(field.getType())){

								elementable = true;
							}

							String childName = elementAnnotation.childName();

							if(StringUtils.isEmpty(childName)){

								childName = "value";
							}

							annotatedFields.add(new FieldXMLInfo(name, field, FieldType.Element,elementAnnotation.cdata(),elementable,false,true,childName,elementAnnotation.skipChildParentElement(),valueFormatter));

						}else{

							String childName = null;

							if(!StringUtils.isEmpty(elementAnnotation.childName())){

								childName = elementAnnotation.childName();
							}

							annotatedFields.add(new FieldXMLInfo(name, field, FieldType.Element,elementAnnotation.cdata(),Elementable.class.isAssignableFrom(field.getType()),false,false,childName,false,valueFormatter));
						}

						ReflectionUtils.fixFieldAccess(field);
					}

					XMLAttribute attributeAnnotation = field.getAnnotation(XMLAttribute.class);

					if(attributeAnnotation != null){

						String name = attributeAnnotation.name();

						if(StringUtils.isEmpty(name)){

							name = field.getName();
						}

						Stringyfier valueFormatter = null;

						if(attributeAnnotation.valueFormatter() != DummyStringyfier.class){

							try {
								valueFormatter = attributeAnnotation.valueFormatter().newInstance();

							} catch (InstantiationException e) {

								throw new RuntimeException(e);

							} catch (IllegalAccessException e) {

								throw new RuntimeException(e);
							}
						}

						annotatedFields.add(new FieldXMLInfo(name, field, FieldType.Attribute,false,false,false,false,null,false,valueFormatter));

						ReflectionUtils.fixFieldAccess(field);
					}
				}

				currentClazz = currentClazz.getSuperclass();
			}

			if(annotatedFields.isEmpty()){

				throw new NoAnnotatedFieldsFoundException(clazz,XMLElement.class,XMLAttribute.class);
			}

			classInfo = new ClassXMLInfo(elementName, annotatedFields);

			FIELD_MAP.put(clazz, classInfo);
		}

		Element classElement = doc.createElement(classInfo.getElementName());

		for(FieldXMLInfo fieldInfo : classInfo.getFields()){

			Object fieldValue;
			try {
				fieldValue = fieldInfo.getField().get(bean);
			} catch (IllegalArgumentException e) {

				throw new RuntimeException(e);

			} catch (IllegalAccessException e) {

				throw new RuntimeException(e);
			}

			if(fieldValue == null){

				continue;

			}else if(!fieldInfo.isList() && fieldInfo.getValueFormatter() != null){

				fieldValue = fieldInfo.getValueFormatter().format(fieldValue);

			}else if(fieldValue instanceof Date){

				fieldValue = DateUtils.DATE_TIME_FORMATTER.format(fieldValue);
			}

			if(fieldInfo.getFieldType() == FieldType.Attribute){

				classElement.setAttribute(fieldInfo.getName(), fieldValue.toString());

			}else if(fieldInfo.isList()){

				List<?> fieldValues = (List<?>)fieldValue;

				if(fieldValues.isEmpty()){

					continue;
				}

				Element subElement;
				
				if(fieldInfo.skipSubElement()){
					
					subElement = classElement;
					
				}else{
					
					subElement = doc.createElement(fieldInfo.getName());
				}


				for(Object value : fieldValues){

					if(value != null){

						parseValue(fieldInfo,value,subElement,doc);
					}
				}

				if(!fieldInfo.skipSubElement() && subElement.hasChildNodes()){
					classElement.appendChild(subElement);
				}

			}else if(fieldInfo.isArray()){

				Object[] fieldValues = (Object[])fieldValue;

				if(ArrayUtils.isEmpty(fieldValues)){

					continue;
				}

				Element subElement;
				
				if(fieldInfo.skipSubElement()){
					
					subElement = classElement;
					
				}else{
					
					subElement = doc.createElement(fieldInfo.getName());
				}


				for(Object value : fieldValues){

					if(value != null){

						parseValue(fieldInfo,value,subElement,doc);
					}
				}

				if(!fieldInfo.skipSubElement() && subElement.hasChildNodes()){
					classElement.appendChild(subElement);
				}

			}else if(fieldInfo.isElementable()){

				Element subElement = ((Elementable)fieldValue).toXML(doc);

				if(subElement != null){

					if(fieldInfo.getChildName() != null){

						Element middleElement = doc.createElement(fieldInfo.getChildName());
						classElement.appendChild(middleElement);
						middleElement.appendChild(subElement);

					}else{
						classElement.appendChild(subElement);
					}
				}
			}else if(fieldInfo.isCDATA()){

				classElement.appendChild(XMLUtils.createCDATAElement(fieldInfo.getName(), fieldValue.toString(), doc));

			}else{
				classElement.appendChild(XMLUtils.createElement(fieldInfo.getName(), fieldValue.toString(), doc));
			}
		}

		return classElement;
	}

	private static void parseValue(FieldXMLInfo fieldInfo, Object value, Element subElement, Document doc) {

		if(fieldInfo.getValueFormatter() != null){

			value = fieldInfo.getValueFormatter().format(value);

		}else if(value instanceof Date){

			value = DateUtils.DATE_TIME_FORMATTER.format(value);
		}

		if(fieldInfo.isElementable()){

			Element subSubElement = ((Elementable)value).toXML(doc);

			if(subSubElement != null){

				subElement.appendChild(subSubElement);
			}
		}else{

			if(fieldInfo.isCDATA()){

				subElement.appendChild(XMLUtils.createCDATAElement(fieldInfo.getChildName(), value, doc));

			}else{

				subElement.appendChild(XMLUtils.createElement(fieldInfo.getChildName(), value, doc));
			}
		}
	}
}
