/*
 * Created on Jun 26, 2005
 */
package org.flexdock.test.xml;

import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import javax.swing.tree.DefaultMutableTreeNode;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.flexdock.util.Utilities;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;

/**
 * @author Christopher Butler
 */
public class XMLDebugger {
	
	public static void println(Object obj) {
		if(obj==null) {
			System.out.println("null");
			return;
		}
		
		XMLDebugger debugger = new XMLDebugger();
		String xml = debugger.getXML(obj);
		System.out.println(xml);
	}
	
	public String getXML(Object obj) {
		if(obj==null)
			return null;

		Document document = createDocument();
		Element rootElem = document.getDocumentElement();
		buildXML(obj, document, rootElem);

		try {
			TransformerFactory factory = TransformerFactory.newInstance();
	        if(Utilities.JAVA_1_5) {
	        	factory.setAttribute("indent-number", new Integer(4));
	        }
			Transformer transformer = factory.newTransformer();
			transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			
			StringWriter writer = new StringWriter();
			StreamResult result = new StreamResult(writer);
			DOMSource src = new DOMSource(document);
			transformer.transform(src, result);
			return writer.toString(); 
			
		} catch(Exception e) {
			e.printStackTrace();
			return null;
		}


	}
	
	private void buildXML(Object obj, Document document, Element parentElem) {
		String elemName = getName(obj);
		Element objElem = document.createElement(elemName);
		
		if(isBasic(obj)) {
			objElem.setAttribute("value", obj.toString());
			parentElem.appendChild(objElem);
			return;
		}
		
		
		Field[] fields = getFields(obj);
		for(int i=0; i<fields.length; i++) {
			String modifiers = Modifier.toString(fields[i].getModifiers());
			if(modifiers.indexOf("static")!=-1 || modifiers.indexOf("transient")!=-1)
				continue;
			
			Object fieldValue = getValue(fields[i], obj);
			String fieldName = fields[i].getName();
			
			if(fieldValue==null) {
				objElem.setAttribute(fieldName, "null");
			}
			else if(isBasic(fieldValue)) {
				objElem.setAttribute(fieldName, fieldValue.toString());
			}
			else if(fieldValue instanceof Collection) {
				buildXML((Collection)fieldValue, document, objElem, fieldName);
			}
			else if(fieldValue instanceof Object[]) {
				buildXML((Object[])fieldValue, document, objElem, fieldName);
			}
			else if(fieldValue instanceof Map) {
				buildXML((Map)fieldValue, document, objElem, fieldName);
			}
			else {
				buildXML(fieldValue, document, objElem);
			}
		}
		try {
			parentElem.appendChild(objElem);
		} catch(NullPointerException e) {
			System.out.println();
			throw e;
		}
	}
	
	
	
	private void buildXML(Collection collection, Document document, Element parentElem, String fieldName) {
		Element objElem = document.createElement(fieldName);
		objElem.setAttribute("type", "collection");

		for(Iterator it=collection.iterator(); it.hasNext();) {
			Object obj = it.next();
			buildXML(obj, document, objElem);
		}

		parentElem.appendChild(objElem);
	}
	
	private void buildXML(Map map, Document document, Element parentElem, String fieldName) {
		Element objElem = document.createElement(fieldName);
		objElem.setAttribute("type", "map");

		for(Iterator it=map.keySet().iterator(); it.hasNext();) {
			Object key = it.next();
			Object value = map.get(key);
			Object obj = new MapEntry(key, value);
			buildXML(obj, document, objElem);
		}

		parentElem.appendChild(objElem);		
	}
	
	private void buildXML(Object[] objects, Document document, Element parentElem, String fieldName) {
		Element objElem = document.createElement(fieldName);
		objElem.setAttribute("type", "array");

		for(int i=0; i<objects.length; i++) {
			buildXML(objects[i], document, objElem);
		}

		parentElem.appendChild(objElem);
	}
	
	
	
	private static Field[] getFields(Object obj) {
		Class c = obj.getClass();
		Field[] fields = c.getDeclaredFields();
		if(obj instanceof DefaultMutableTreeNode) {
			Field children = getField(obj, "children");
			if(children!=null) {
				Field[] tmp = new Field[fields.length+1];
				System.arraycopy(fields, 0, tmp, 0, fields.length);
				tmp[tmp.length-1] = children;
				fields = tmp;
			}
		}
		return fields;
	}
	
	

	private static Field getField(Object obj, String fieldName) {
		Class c = obj.getClass();

		while(true) {
			try {
				return c.getDeclaredField(fieldName);
			} catch(Exception e) {
				if(c==Object.class)
					return null;
				c = c.getSuperclass();
			}
		}
	}
	
	private static Object getValue(Field field, Object owner) {
		boolean toggle = !field.isAccessible();
		
		if(toggle)
			field.setAccessible(true);
		
		Object obj = null;
		try {
			obj = field.get(owner);
		} catch(Throwable t) {
			t.printStackTrace();
		}
		
		if(toggle)
			field.setAccessible(false);
		
		return obj;
	}
	
	private static String getName(Object obj) {
		Class clazz = obj.getClass();
		
		String name = clazz.getName();
		int indx = name.lastIndexOf('.');
		if(indx!=-1)
			name = name.substring(indx+1, name.length());
		
		indx = name.lastIndexOf('$');
		if(indx!=-1)
			name = name.substring(indx+1, name.length());
		return name;
	}
	
	private static boolean isBasic(Object obj) {
		return obj.getClass().isPrimitive() || obj.getClass().getName().startsWith("java.lang");
	}
	
	private static Document createDocument() {
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			
			StringReader reader = new StringReader("<?xml version=\"1.0\"?><ObjectXML></ObjectXML>");
			InputSource src = new InputSource(reader);
			return builder.parse(src);
		} catch(Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	
	public static class MapEntry {
		private Object key;
		private Object value;
		public MapEntry(Object key, Object value) {
			this.key = key;
			this.value = value;
		}
	}
	
	public static class DataObject {
		private Object data;
		
		public DataObject(Object obj) {
			data = obj;
		}
	}

}
