/*
 * 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;
        }
    }

}
