package org.jgroups.util;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.jgroups.annotations.Experimental;
import org.jgroups.annotations.Property;
import org.jgroups.annotations.Unsupported;
import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;
import org.jgroups.stack.Protocol;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Iterates over all concrete Protocol classes and creates tables with Protocol's properties.
 * These tables are in turn then merged into docbook. 
 * 
 * Iterates over unsupported and experimental classes and creates tables listing those classes.
 * These tables are in turn then merged into docbook.  
 * 
 * @author Vladimir Blagojevic
 * 
 */
public class PropertiesToXML {

    protected static final Log log = LogFactory.getLog(PropertiesToXML.class);

    public static void main(String[] args) {
        String input = "doc/manual/en/modules/protocols.xml";
        String input2 = "doc/manual/en/modules/installation.xml";     

        if (args.length != 1) {
            help();
            return;
        }
        input = args[0];
        String temp_file = input + ".tmp";       
        String temp_file2 = input2 + ".tmp";

        try {
            // first copy protocols.xml file into protocols-temp.xml
            File f = new File(temp_file);
            copy(new FileReader(new File(input)), new FileWriter(f));
            String s = fileToString(f);

            Set<Class<?>> classes = findClassesAssignableFrom("org.jgroups.protocols", Protocol.class);
            classes.addAll(findClassesAssignableFrom("org.jgroups.protocols.pbcast", Protocol.class));
            Properties props = new Properties();
            for (Class<?> clazz : classes) {
                convertToDocbookTable(props, clazz);
            }

            String result = Util.replaceProperties(s, props);
            FileWriter fw = new FileWriter(f, false);
            fw.write(result);
            fw.flush();
            fw.close();
            
            
            
            // copy installation.xml file into installation-temp.xml
            f = new File(temp_file2);
            copy(new FileReader(new File(input2)), new FileWriter(f));
            s = fileToString(f);
            
            props = new Properties();
            List<Class<?>> unsupportedClasses = findClassesAnnotatedWith("org.jgroups",Unsupported.class);
            convertToDocbookTable(props, unsupportedClasses, "Unsupported");
            List<Class<?>> experimentalClasses = findClassesAnnotatedWith("org.jgroups",Experimental.class);            
            convertToDocbookTable(props, experimentalClasses, "Experimental");
            
            result = Util.replaceProperties(s, props);
            fw = new FileWriter(f, false);
            fw.write(result);
            fw.flush();
            fw.close();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void help() {
        System.out.println("PropertiesToXML <input XML file>");
    }

    private static Set<Class<?>> findClassesAssignableFrom(String packageName, Class<?> assignableFrom)
                    throws IOException, ClassNotFoundException {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        Set<Class<?>> classes = new HashSet<Class<?>>();
        String path = packageName.replace('.', '/');
        URL resource = loader.getResource(path);
        if (resource != null) {
            String filePath = resource.getFile();
            if (filePath != null && new File(filePath).isDirectory()) {
                for (String file : new File(filePath).list()) {
                    if (file.endsWith(".class")) {
                        String name = packageName + '.' + file.substring(0, file.indexOf(".class"));
                        Class<?> clazz = Class.forName(name);
                        if (assignableFrom.isAssignableFrom(clazz))
                            classes.add(clazz);
                    }
                }
            }
        }
        return classes;
    }
    
    private static List<Class<?>> findClassesAnnotatedWith(String packageName, Class<? extends Annotation> a) throws IOException, ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<Class<?>>();
        recurse(classes, packageName, a);
        return classes;
    }

    private static void recurse(List<Class<?>> classes, String packageName, Class<? extends Annotation> a) throws ClassNotFoundException {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        String path = packageName.replace('.', '/');
        URL resource = loader.getResource(path);
        if (resource != null) {
            String filePath = resource.getFile();
            if (filePath != null && new File(filePath).isDirectory()) {
                for (String file : new File(filePath).list()) {
                    if (file.endsWith(".class")) {
                        String name = packageName + '.' + file.substring(0, file.indexOf(".class"));
                        Class<?> clazz = Class.forName(name);
                        if (clazz.isAnnotationPresent(a))
                            classes.add(clazz);
                    }
                    else if (new File(filePath,file).isDirectory()) {
                        recurse(classes, packageName + "." + file, a);
                    }
                }
            }
        }
    }
    
    private static void convertToDocbookTable(Properties props, List<Class<?>> clazzes, String title)
                    throws ParserConfigurationException, TransformerException {

        Document xmldoc = null;
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        DOMImplementation impl = builder.getDOMImplementation();
        xmldoc = impl.createDocument(null, "table", null);
        Element tbody = createXMLTable(xmldoc, title);

        for (Class<?> clazz : clazzes) {
            Element row = xmldoc.createElement("row");
            Element entry = xmldoc.createElement("entry");
            entry.setTextContent(clazz.getPackage().getName());
            row.appendChild(entry);

            entry = xmldoc.createElement("entry");
            entry.setTextContent(clazz.getSimpleName());
            row.appendChild(entry);
            tbody.appendChild(row);
        }

        // do we have more than one property (superclass Protocol has only one property (stats))
        if (clazzes != null && clazzes.size() > 1) {
            DOMSource domSource = new DOMSource(xmldoc);
            StringWriter sw = new StringWriter();
            StreamResult streamResult = new StreamResult(sw);
            TransformerFactory tf = TransformerFactory.newInstance();
            Transformer serializer = tf.newTransformer();
            serializer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
            serializer.transform(domSource, streamResult);
            StringBuffer buffer = sw.getBuffer();
            buffer.delete(0, buffer.indexOf("table") - 1);
            props.put(title, buffer.toString());
        }
    }

    private static void convertToDocbookTable(Properties props, Class<?> clazz)
                    throws ParserConfigurationException, TransformerException {
        boolean isConcreteClass = (clazz.getModifiers() & Modifier.ABSTRACT) == 0;
        boolean isExperimental = clazz.isAnnotationPresent(Experimental.class);
        boolean isUnsupported = clazz.isAnnotationPresent(Unsupported.class);
        // if(isConcreteClass && !isExperimental && !isUnsupported) {
        if (isConcreteClass && !isUnsupported) {
            Class<?> protocol = clazz;
            Document xmldoc = null;
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            DOMImplementation impl = builder.getDOMImplementation();
            xmldoc = impl.createDocument(null, "table", null);
            Element tbody = createXMLTree(xmldoc, isExperimental);
            Map<String, String> nameToDescription = new TreeMap<String, String>();

            // iterate fields
            for (Class clazzInLoop = clazz; clazzInLoop != null; clazzInLoop = clazzInLoop
                            .getSuperclass()) {
                Field[] fields = clazzInLoop.getDeclaredFields();
                for (Field field : fields) {
                    if (field.isAnnotationPresent(Property.class)) {
                        String property = field.getName();
                        Property annotation = field.getAnnotation(Property.class);
                        String desc = annotation.description();
                        nameToDescription.put(property, desc);
                    }
                }
            }

            // iterate methods
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(Property.class)
                                && method.getName().startsWith("set")) {

                    Property annotation = method.getAnnotation(Property.class);
                    String desc = annotation.description();

                    if (desc.length() > 0) {

                        String name = annotation.name();
                        if (name.length() < 1) {
                            name = Util.methodNameToAttributeName(method.getName());
                        }
                        nameToDescription.put(name, desc);
                    }
                }
            }

            // and write them out
            for (Map.Entry<String, String> e : nameToDescription.entrySet()) {
                Element row = xmldoc.createElement("row");
                Element entry = xmldoc.createElement("entry");
                entry.setTextContent(e.getKey());
                row.appendChild(entry);

                entry = xmldoc.createElement("entry");
                entry.setTextContent(e.getValue());
                row.appendChild(entry);
                tbody.appendChild(row);
            }

            // do we have more than one property (superclass Protocol has only one property (stats))
            if (nameToDescription.size() > 1) {
                DOMSource domSource = new DOMSource(xmldoc);
                StringWriter sw = new StringWriter();
                StreamResult streamResult = new StreamResult(sw);
                TransformerFactory tf = TransformerFactory.newInstance();
                Transformer serializer = tf.newTransformer();
                serializer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
                serializer.setOutputProperty(OutputKeys.INDENT, "yes");
                serializer.transform(domSource, streamResult);
                StringBuffer buffer = sw.getBuffer();
                buffer.delete(0, buffer.indexOf("table") - 1);
                props.put(protocol.getSimpleName(), buffer.toString());
            }
        }
    }

    private static String fileToString(File f) throws Exception {
        StringWriter output = new StringWriter();
        FileReader input = new FileReader(f);
        char[] buffer = new char[8 * 1024];
        int count = 0;
        int n = 0;
        while (-1 != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
            count += n;
        }
        return output.toString();
    }

    public static int copy(Reader input, Writer output) throws IOException {
        char[] buffer = new char[8 * 1024];
        int count = 0;
        int n = 0;
        try {
            while (-1 != (n = input.read(buffer))) {
                output.write(buffer, 0, n);
                count += n;
            }
        } finally {
            output.flush();
            output.close();
        }
        return count;
    }

    private static Element createXMLTree(Document xmldoc, boolean experimental)
                    throws ParserConfigurationException {

        Element root = xmldoc.getDocumentElement();
        Element title = xmldoc.createElement("title");
        if (experimental)
            title.setTextContent("Properties (experimental)");
        else
            title.setTextContent("Properties");
        root.appendChild(title);

        Element tgroup = xmldoc.createElement("tgroup");
        tgroup.setAttribute("cols", "2");
        root.appendChild(tgroup);

        Element colspec = xmldoc.createElement("colspec");
        colspec.setAttribute("align", "left");
        tgroup.appendChild(colspec);

        Element thead = xmldoc.createElement("thead");
        tgroup.appendChild(thead);

        Element row = xmldoc.createElement("row");
        thead.appendChild(row);

        Element entry = xmldoc.createElement("entry");
        entry.setAttribute("align", "center");
        entry.setTextContent("Name");
        row.appendChild(entry);

        entry = xmldoc.createElement("entry");
        entry.setAttribute("align", "center");
        entry.setTextContent("Description");
        row.appendChild(entry);

        Element tbody = xmldoc.createElement("tbody");
        tgroup.appendChild(tbody);

        return tbody;
    }

    private static Element createXMLTable(Document xmldoc, String titleContent)
                    throws ParserConfigurationException {

        Element root = xmldoc.getDocumentElement();
        Element title = xmldoc.createElement("title");
        title.setTextContent(titleContent);
        root.appendChild(title);

        Element tgroup = xmldoc.createElement("tgroup");
        tgroup.setAttribute("cols", "2");
        root.appendChild(tgroup);

        Element colspec = xmldoc.createElement("colspec");
        colspec.setAttribute("align", "left");
        tgroup.appendChild(colspec);

        Element thead = xmldoc.createElement("thead");
        tgroup.appendChild(thead);

        Element row = xmldoc.createElement("row");
        thead.appendChild(row);

        Element entry = xmldoc.createElement("entry");
        entry.setAttribute("align", "center");
        entry.setTextContent("Package");
        row.appendChild(entry);

        entry = xmldoc.createElement("entry");
        entry.setAttribute("align", "center");
        entry.setTextContent("Class");
        row.appendChild(entry);

        Element tbody = xmldoc.createElement("tbody");
        tgroup.appendChild(tbody);

        return tbody;
    }
}
