package org.jgroups.util;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

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.jgroups.Version;
import org.jgroups.annotations.Property;
import org.jgroups.stack.Configurator;
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 XML schema used for validation of
 * configuration files.
 * 
 * https://jira.jboss.org/jira/browse/JGRP-448
 * 
 * @author Vladimir Blagojevic
 * 
 */
public class XMLSchemaGenerator {

   public static void main(String[] args) {

      String outputDir = "./";

      for (int i = 0; i < args.length; i++) {
         String arg = args[i];
         if ("-o".equals(arg)) {
            outputDir = args[++i];
            continue;
         } else {
            System.out.println("XMLSchemaGenerator -o <path to newly created xsd schema file>");
            return;
         }
      }

      File f = new File(outputDir, "JGroups-" + Version.major + "." + Version.minor + ".xsd");
      try {
         FileWriter fw = new FileWriter(f, false);
         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
         DocumentBuilder builder = factory.newDocumentBuilder();
         DOMImplementation impl = builder.getDOMImplementation();
         Document xmldoc = impl.createDocument("http://www.w3.org/2001/XMLSchema", "xs:schema",
                  null);
         xmldoc.getDocumentElement().setAttribute("targetNamespace", "urn:org:jgroups");
         xmldoc.getDocumentElement().setAttribute("elementFormDefault", "qualified");

         Element xsElement = xmldoc.createElement("xs:element");
         xsElement.setAttribute("name", "config");
         xmldoc.getDocumentElement().appendChild(xsElement);

         Element complexType = xmldoc.createElement("xs:complexType");
         xsElement.appendChild(complexType);

         Element allType = xmldoc.createElement("xs:choice");
         allType.setAttribute("maxOccurs", "unbounded");
         complexType.appendChild(allType);

         Set<Class<?>> classes = getClasses("org.jgroups.protocols", Protocol.class);
         for (Class<?> clazz : classes) {
            classToXML(xmldoc, allType, clazz, "");
         }
         classes = getClasses("org.jgroups.protocols.pbcast", Protocol.class);
         for (Class<?> clazz : classes) {
            classToXML(xmldoc, allType, clazz, "pbcast.");
         }

         DOMSource domSource = new DOMSource(xmldoc);
         StreamResult streamResult = new StreamResult(fw);
         TransformerFactory tf = TransformerFactory.newInstance();
         Transformer serializer = tf.newTransformer();
         serializer.setOutputProperty(OutputKeys.METHOD, "xml");
         serializer.setOutputProperty(OutputKeys.INDENT, "yes");
         serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "1");
         serializer.transform(domSource, streamResult);

         fw.flush();
         fw.close();
      } catch (Exception e) {
         e.printStackTrace();
      }
   }

   private static Set<Class<?>> getClasses(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 void classToXML(Document xmldoc, Element parent, Class<?> clazz,
            String preAppendToSimpleClassName) throws Exception {

      boolean isConcreteClass = (clazz.getModifiers() & Modifier.ABSTRACT) == 0;
      if (isConcreteClass && !clazz.isAnonymousClass()) {
         parent.appendChild(createXMLTree(xmldoc, clazz, preAppendToSimpleClassName));
      }
   }

   private static Element createXMLTree(Document xmldoc, Class<?> clazz,
            String preAppendToSimpleClassName) throws Exception {

      Element classElement = xmldoc.createElement("xs:element");
      String elementName = preAppendToSimpleClassName + clazz.getSimpleName();
      if(elementName == null || elementName.length()==0) {
          throw new IllegalArgumentException("Cannot create empty attribute name for element xs:element, class is " + clazz);
      }
      classElement.setAttribute("name",elementName);

      Element complexType = xmldoc.createElement("xs:complexType");
      classElement.appendChild(complexType);

      // 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 r = field.getAnnotation(Property.class);
               boolean annotationRedefinesName = r.name().length() > 0
                        && r.deprecatedMessage().length() == 0;
               if (annotationRedefinesName) {
                  property = r.name();
               }
               if(property == null || property.length()==0) {
                   throw new IllegalArgumentException("Cannot create empty attribute name for element xs:attribute, field is " + field);
               }
               Element attributeElement = xmldoc.createElement("xs:attribute");
               attributeElement.setAttribute("name", property);

               // Agreement with Bela Ban on Jan-20-2009 (Go Obama!!!) to treat all types as
               // xs:string since we do not know where users are going to use
               // replacement tokens in configuration files. Therefore, the type becomes
               // indeterminate.
               // attributeElement.setAttribute("type", fieldToXMLSchemaAttributeType(field));
               attributeElement.setAttribute("type", "xs:string");
               complexType.appendChild(attributeElement);

               Element annotationElement = xmldoc.createElement("xs:annotation");
               attributeElement.appendChild(annotationElement);

               Element documentationElement = xmldoc.createElement("xs:documentation");
               documentationElement.setTextContent(r.description());
               annotationElement.appendChild(documentationElement);
            }
         }
      }

      // 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 name = annotation.name();
            if (name.length() < 1) {
               name = Util.methodNameToAttributeName(method.getName());
            }
            Element attributeElement = xmldoc.createElement("xs:attribute");
            attributeElement.setAttribute("name", name);
            attributeElement.setAttribute("type", "xs:string");
            complexType.appendChild(attributeElement);

            String desc = annotation.description();
            if (desc.length() > 0) {
               Element annotationElement = xmldoc.createElement("xs:annotation");
               attributeElement.appendChild(annotationElement);

               Element documentationElement = xmldoc.createElement("xs:documentation");
               documentationElement.setTextContent(annotation.description());
               annotationElement.appendChild(documentationElement);
            }
         }
      }
      return classElement;
   }
}
