/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.myfaces.tobago.apt;

import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.apt.Filer;
import com.sun.mirror.declaration.ClassDeclaration;
import com.sun.mirror.declaration.InterfaceDeclaration;
import com.sun.mirror.declaration.MethodDeclaration;
import com.sun.mirror.declaration.TypeDeclaration;
import com.sun.mirror.type.InterfaceType;
import org.apache.commons.io.IOUtils;
import org.apache.myfaces.tobago.apt.annotation.Converter;
import org.apache.myfaces.tobago.apt.annotation.Facet;
import org.apache.myfaces.tobago.apt.annotation.TagAttribute;
import org.apache.myfaces.tobago.apt.annotation.UIComponentTag;
import org.apache.myfaces.tobago.apt.annotation.UIComponentTagAttribute;
import org.apache.myfaces.tobago.apt.annotation.Validator;
import org.codehaus.plexus.util.FileUtils;
import org.jdom.Attribute;
import org.jdom.Comment;
import org.jdom.DocType;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.filter.ContentFilter;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/*
 * Date: Sep 25, 2006
 * Time: 9:31:09 PM
 */
public class FacesConfigAnnotationVisitor extends AbstractAnnotationVisitor {
  public static final String SOURCE_FACES_CONFIG_KEY = "sourceFacesConfig";
  public static final String TARGET_FACES_CONFIG_KEY = "targetFacesConfig";

  private String jsfVersion = "1.1";

  private static final String SEPARATOR = System.getProperty("line.separator");
  private static final String COMPONENT = "component";
  private static final String COMPONENT_TYPE = "component-type";
  private static final String COMPONENT_CLASS = "component-class";
  private static final String COMPONENT_EXTENSION = "component-extension";
  private static final String ALLOWED_CHILD_COMPONENTS = "allowed-child-components";
  private static final String CATEGORY = "category";
  private static final String DEPRECATED = "deprecated";
  private static final String HIDDEN = "hidden";
  private static final String FACET = "facet";
  private static final String DISPLAY_NAME = "display-name";
  private static final String DESCRIPTION = "description";
  private static final String FACET_NAME = "facet-name";
  private static final String FACET_EXTENSION = "facet-extension";
  private static final String PROPERTY = "property";
  private static final String PROPERTY_NAME = "property-name";
  private static final String PROPERTY_CLASS = "property-class";
  private static final String PROPERTY_EXTENSION = "property-extension";
  private static final String VALUE_EXPRESSION = "value-expression"; //UIComponentTagAttribute.valueExpression()
  private static final String PROPERTY_VALUES = "property-values"; //UIComponentTagAttribute.allowedValues()
  private static final String READONLY = "read-only";
  private static final String REQUIRED = "required"; //UITagAttribute.required()
  private static final String DEFAULT_VALUE = "default-value";
  private static final String ATTRIBUTE = "attribute";
  private static final String ATTRIBUTE_NAME = "attribute-name";
  private static final String ATTRIBUTE_CLASS = "attribute-class";
  private static final String ATTRIBUTE_EXTENSION = "attribute-extension";
  private static final String APPLICATION = "application";
  private static final String FACTORY = "factory";
  private static final String CONVERTER = "converter";
  private static final String CONVERTER_ID = "converter-id";
  private static final String CONVERTER_FOR_CLASS = "converter-for-class";
  private static final String CONVERTER_CLASS = "converter-class";
  private static final String VALIDATOR = "validator";
  private static final String VALIDATOR_ID = "validator-id";
  private static final String VALIDATOR_FOR_CLASS = "validator-for-class";
  private static final String VALIDATOR_CLASS = "validator-class";
  private static final String RENDERER = "renderer";
  private static final String COMPONENT_FAMILY = "component-family";
  private static final String RENDER_KIT = "render-kit";
  private static final String RENDER_KIT_ID = "render-kit-id";
  private static final String RENDER_KIT_CLASS = "render-kit-class";
  private static final String RENDERER_TYPE = "renderer-type";
  private static final String RENDERER_CLASS = "renderer-class";

  public FacesConfigAnnotationVisitor(AnnotationProcessorEnvironment env) {
    super(env);
  }

  public void process() throws ParserConfigurationException, IOException {
    String sourceFacesConfigFile = null;
    String targetFacesConfigFile = null;
    for (Map.Entry<String, String> entry : getEnv().getOptions().entrySet()) {
      if (entry.getKey().startsWith("-A" + SOURCE_FACES_CONFIG_KEY + "=")) {
        sourceFacesConfigFile = entry.getKey().substring(SOURCE_FACES_CONFIG_KEY.length() + 3);
      }
      if (entry.getKey().startsWith("-A" + TARGET_FACES_CONFIG_KEY + "=")) {
        targetFacesConfigFile = entry.getKey().substring(TARGET_FACES_CONFIG_KEY.length() + 3);
      }
      if (entry.getKey().startsWith("-Ajsf-version=")) {
        String version = entry.getKey().substring("-Ajsf-version=".length());
        if ("1.2".equals(version)) {
          jsfVersion = "1.2";
        }
        if ("2.0".equals(version)) {
          jsfVersion = "2.0";
        }
      }
    }
    Document document;
    Writer writer = null;
    try {
      String content = FileUtils.fileRead(sourceFacesConfigFile);
      SAXBuilder builder = new SAXBuilder();
      builder.setEntityResolver(new EntityResolver() {
        public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
          if ("-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN".equals(publicId)) {
            InputStream stream = FacesConfigAnnotationVisitor.class.getResourceAsStream(
                "/org/apache/myfaces/tobago/dtd/web-facesconfig_1_1.dtd");
            return new InputSource(stream);
          }
          return null;
        }
      });
      document = builder.build(new StringReader(content));

      // Normalise line endings. For some reason, JDOM replaces \r\n inside a comment with \n.
      normaliseLineEndings(document);

      // rewrite DOM as a string to find differences, since text outside the root element is not tracked

      Element rootElement = document.getRootElement();
      if (is12()) {
        rootElement.setNamespace(Namespace.getNamespace("http://java.sun.com/xml/ns/javaee"));
        Namespace xsi = Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
        rootElement.addNamespaceDeclaration(Namespace.getNamespace("xi", "http://www.w3.org/2001/XInclude"));
        rootElement.setAttribute(new Attribute("schemaLocation",
            "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd", xsi));
        rootElement.setAttribute("version", "1.2");
      } else if (is20()) {
        rootElement.setNamespace(Namespace.getNamespace("http://java.sun.com/xml/ns/javaee"));
        Namespace xsi = Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
        rootElement.addNamespaceDeclaration(Namespace.getNamespace("xi", "http://www.w3.org/2001/XInclude"));
        rootElement.setAttribute(new Attribute("schemaLocation",
            "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd", xsi));
        rootElement.setAttribute("version", "2.0");
      }
      Namespace namespace = rootElement.getNamespace();
      if (is12()||is20()) {
        applyNamespace(rootElement, namespace);
      }
      List<Element> components = rootElement.getChildren(COMPONENT, namespace);

      List<Element> newComponents = new ArrayList<Element>();
      List<Element> newRenderer = new ArrayList<Element>();
      List<Element> newConverters = new ArrayList<Element>();
      List<Element> newValidators = new ArrayList<Element>();

      for (ClassDeclaration decl : getCollectedClassDeclarations()) {
        if (decl.getAnnotation(UIComponentTag.class) != null) {
          addElement(decl, newComponents, newRenderer, namespace);
        } else if (decl.getAnnotation(Converter.class) != null) {
          addConverter(decl, newConverters, namespace);
        } else if (decl.getAnnotation(Validator.class) != null) {
          addValidator(decl, newValidators, namespace);
        }
      }

      for (InterfaceDeclaration decl : getCollectedInterfaceDeclarations()) {
        if (decl.getAnnotation(UIComponentTag.class) != null) {
          addElement(decl, newComponents, newRenderer, namespace);
        }
      }
      List<Element> elementsToAdd = new ArrayList<Element>();
      // sort out duplicates
      for (Element newElement : newComponents) {
        boolean found = containsElement(components, newElement);
        if (!found) {
          elementsToAdd.add(newElement);
        }
      }
      if (!elementsToAdd.isEmpty()) {
        // if facesconfig contains no component section add the components after factory or application
        int lastIndex = getIndexAfter(rootElement, COMPONENT, FACTORY, APPLICATION);
        rootElement.addContent(lastIndex, elementsToAdd);
      }
      if (!newRenderer.isEmpty()) {
        Element renderKit = new Element(RENDER_KIT, namespace);
        Element renderKitId = new Element(RENDER_KIT_ID, namespace);
        renderKitId.setText("tobago");
        renderKit.addContent(renderKitId);
        Element renderKitClass = new Element(RENDER_KIT_CLASS, namespace);
        renderKitClass.setText("org.apache.myfaces.tobago.renderkit.TobagoRenderKit");
        renderKit.addContent(renderKitClass);
        renderKit.addContent(newRenderer);
        int lastIndex = getIndexAfter(rootElement, CONVERTER, COMPONENT, FACTORY, APPLICATION);
        rootElement.addContent(lastIndex, renderKit);
      }
      if (!newConverters.isEmpty()) {
        int lastIndex = getIndexAfter(rootElement, RENDER_KIT, CONVERTER, COMPONENT, FACTORY, APPLICATION);
        rootElement.addContent(lastIndex, newConverters);
      }
      if (!newValidators.isEmpty()) {
        rootElement.addContent(newValidators);
      }
      if (is11()) {
        document.setDocType(new DocType("faces-config",
            "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN",
           "http://java.sun.com/dtd/web-facesconfig_1_1.dtd"));
      }
      writer =
          getEnv().getFiler().createTextFile(Filer.Location.SOURCE_TREE, "", new File(targetFacesConfigFile), null);

      StringWriter facesConfig = new StringWriter(1024);
      Format format = Format.getPrettyFormat();
      format.setLineSeparator(SEPARATOR);
      XMLOutputter out = new XMLOutputter(format);
      out.output(document, facesConfig);
      if (is11()) {
        // TODO: is this replace really necessary?
        String facesConfigStr =
            facesConfig.toString().replaceFirst(" xmlns=\"http://java.sun.com/JSF/Configuration\"", "");
        // TODO: Find a better way
        facesConfigStr = facesConfigStr.replaceFirst("\"http://java.sun.com/dtd/web-facesconfig_1_1.dtd\"",
            "\"http://java.sun.com/dtd/web-facesconfig_1_1.dtd\"[\n"
                + "<!ELEMENT allowed-child-components (#PCDATA)>\n"
                + "<!ELEMENT category (#PCDATA)>\n"
                + "<!ELEMENT deprecated (#PCDATA)>\n"
                + "<!ELEMENT hidden (#PCDATA)>\n"
                + "<!ELEMENT preferred (#PCDATA)>\n"
                + "<!ELEMENT read-only (#PCDATA)>\n"
                + "<!ELEMENT value-expression (#PCDATA)>\n"
                + "<!ELEMENT property-values (#PCDATA)>\n"
                + "<!ELEMENT required (#PCDATA)>\n"
                + "]");
        writer.append(facesConfigStr);
      } else {
        writer.append(facesConfig.toString());
      }

    } catch (JDOMException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      IOUtils.closeQuietly(writer);
    }
  }

  private void applyNamespace(Element parent, Namespace namespace) {
    for (Element element : (List<Element>) parent.getChildren()) {
      element.setNamespace(namespace);
      applyNamespace(element, namespace);
    }
  }

  private void addConverter(ClassDeclaration decl, List<Element> newConverters, Namespace namespace) {
    Converter converterAnn = decl.getAnnotation(Converter.class);
    Element converter = new Element(CONVERTER, namespace);
    if (converterAnn.id().length() > 0) {
      Element converterId = new Element(CONVERTER_ID, namespace);
      converterId.setText(converterAnn.id());
      converter.addContent(converterId);
    } else if (converterAnn.forClass().length() > 0) {
      Element converterForClass = new Element(CONVERTER_FOR_CLASS, namespace);
      converterForClass.setText(converterAnn.forClass());
      converter.addContent(converterForClass);
    }

    Element converterClass = new Element(CONVERTER_CLASS, namespace);
    converterClass.setText(decl.getQualifiedName());
    converter.addContent(converterClass);
    newConverters.add(converter);
  }

  private void addValidator(ClassDeclaration decl, List<Element> newValidators, Namespace namespace) {
    Validator validatorAnn = decl.getAnnotation(Validator.class);
    Element validator = new Element(VALIDATOR, namespace);
    if (validatorAnn.id().length() > 0) {
      Element validatorId = new Element(VALIDATOR_ID, namespace);
      validatorId.setText(validatorAnn.id());
      validator.addContent(validatorId);
    } else if (validatorAnn.forClass().length() > 0) {
      Element validatorForClass = new Element(VALIDATOR_FOR_CLASS, namespace);
      validatorForClass.setText(validatorAnn.forClass());
      validator.addContent(validatorForClass);
    }

    Element validatorClass = new Element(VALIDATOR_CLASS, namespace);
    validatorClass.setText(decl.getQualifiedName());
    validator.addContent(validatorClass);
    newValidators.add(validator);
  }

  private boolean containsElement(List<Element> components, Element newElement) {
    return getEqualElement(components, newElement) != null;
  }

  private Element getEqualElement(List<Element> components, Element newElement) {
    for (Element element : components) {
      if (equals(element, newElement)) {
        return element;
      }
    }
    return null;
  }

  private int getIndexAfter(Element rootElement, String... tagNames) {
    for (String tagName : tagNames) {
      int index = getIndexAfter(rootElement, tagName);
      if (index != 0) {
        return index;
      }
    }
    return 0;
  }

  private int getIndexAfter(Element rootElement, String tagName) {
    List<Element> components = rootElement.getChildren(tagName, rootElement.getNamespace());
    if (!components.isEmpty()) {
      return rootElement.indexOf(components.get(components.size() - 1)) + 1;
    }
    return 0;
  }

  public boolean equals(Element element1, Element element2) {
    Namespace namespace = element1.getNamespace();
    if (element1.getName().equals(element2.getName()) && element1.getNamespace().equals(element2.getNamespace())) {
      if (element1.getChildText(COMPONENT_CLASS, namespace).equals(element2.getChildText(COMPONENT_CLASS, namespace))) {
        if (element1.getChildText(COMPONENT_TYPE, namespace).equals(element2.getChildText(COMPONENT_TYPE, namespace))) {
          return true;
        }
      }
    }
    return false;
  }

  protected Element createComponentElement(TypeDeclaration decl, UIComponentTag componentTag,
      Class uiComponentClass, Namespace namespace) throws IOException, NoSuchFieldException, IllegalAccessException {
    Field componentField = uiComponentClass.getField("COMPONENT_TYPE");
    String componentType = (String) componentField.get(null);
    Element element = new Element(COMPONENT, namespace);
    String displayName = componentTag.displayName();
    if (displayName.equals("")) {
      displayName = uiComponentClass.getName().substring(uiComponentClass.getName().lastIndexOf(".") + 1);
    }
    Element elementDisplayName = new Element(DISPLAY_NAME, namespace);
    elementDisplayName.setText(displayName);
    element.addContent(elementDisplayName);
    Element elementType = new Element(COMPONENT_TYPE, namespace);
    elementType.setText(componentType);
    element.addContent(elementType);
    Element elementClass = new Element(COMPONENT_CLASS, namespace);
    elementClass.setText(componentTag.uiComponent());
    element.addContent(elementClass);

    return element;
  }

  protected void addRendererElement(TypeDeclaration decl, UIComponentTag componentTag,
      Class uiComponentClass, List<Element> renderer, Namespace namespace)
      throws IOException, NoSuchFieldException, IllegalAccessException {
    String rendererType = componentTag.rendererType();
    if (rendererType != null && rendererType.length() > 0) {
      Field componentField = uiComponentClass.getField("COMPONENT_FAMILY");
      String componentFamily = (String) componentField.get(null);
      Element element = new Element(RENDERER, namespace);
      String displayName = componentTag.displayName();
      if (displayName.equals("")) {
        displayName = uiComponentClass.getName().substring(uiComponentClass.getName().lastIndexOf(".") + 1);
      }
      Element elementDisplayName = new Element(DISPLAY_NAME, namespace);
      elementDisplayName.setText(displayName);
      element.addContent(elementDisplayName);
      Element elementComponentFamily = new Element(COMPONENT_FAMILY, namespace);
      elementComponentFamily.addContent(componentFamily);
      element.addContent(elementComponentFamily);
      Element elementType = new Element(RENDERER_TYPE, namespace);
      elementType.setText(rendererType);
      element.addContent(elementType);
      Element elementClass = new Element(RENDERER_CLASS, namespace);
      String className = "org.apache.myfaces.tobago.renderkit." + rendererType + "Renderer";
      elementClass.setText(className);
      element.addContent(elementClass);
      renderer.add(element);
    }
  }


  private Element createElementExtension(TypeDeclaration decl, UIComponentTag uiComponentTag,
      Namespace namespace) {
    Element elementExtension = new Element(COMPONENT_EXTENSION, namespace);
    Element elementAllowedChildComponents = new Element(ALLOWED_CHILD_COMPONENTS, namespace);
    String[] allowedChildComponents = uiComponentTag.allowedChildComponenents();
    String allowedComponentTypes = "";
    for (String componentType : allowedChildComponents) {
      allowedComponentTypes += componentType + " ";
    }
    elementAllowedChildComponents.setText(allowedComponentTypes);
    elementExtension.addContent(elementAllowedChildComponents);
    Element elementCategory = new Element(CATEGORY, namespace);
    elementCategory.setText(uiComponentTag.category().toString());
    elementExtension.addContent(elementCategory);
    Deprecated deprecated = decl.getAnnotation(Deprecated.class);
    if (deprecated != null) {
      Element elementDeprecated = new Element(DEPRECATED, namespace);
      elementDeprecated.setText("Warning: This component is deprecated!");
      elementExtension.addContent(elementDeprecated);
    }
    Element elementHidden = new Element(HIDDEN, namespace);
    elementHidden.setText(Boolean.toString(uiComponentTag.isHidden()));
    elementExtension.addContent(elementHidden);

    return elementExtension;
  }

  protected void addAttribute(MethodDeclaration d, Class uiComponentClass, List properties, List attributes,
      Namespace namespace) {
    UIComponentTagAttribute componentAttribute = d.getAnnotation(UIComponentTagAttribute.class);
    if (componentAttribute != null) {
      String simpleName = d.getSimpleName();
      if (simpleName.startsWith("set")) {
        String attributeStr = simpleName.substring(3, 4).toLowerCase(Locale.ENGLISH) + simpleName.substring(4);
        String methodStr;
        if (componentAttribute.type().length == 1
            && (componentAttribute.type()[0].equals(Boolean.class.getName())
            || componentAttribute.type()[0].equals("boolean"))) {
          methodStr = "is" + simpleName.substring(3);
        } else {
          methodStr = "get" + simpleName.substring(3);
        }
        try {
          uiComponentClass.getMethod(methodStr, new Class[0]);
          Element property = new Element(PROPERTY, namespace);
          Element propertyName = new Element(PROPERTY_NAME, namespace);
          Element propertyClass = new Element(PROPERTY_CLASS, namespace);

          propertyName.setText(attributeStr);
          addClass(componentAttribute, propertyClass);

          addDescription(d, property, namespace);

          property.addContent(propertyName);
          property.addContent(propertyClass);
          if (componentAttribute.defaultValue().length() > 0) {
            Element defaultValue = new Element(DEFAULT_VALUE, namespace);
            defaultValue.setText(componentAttribute.defaultValue());
            property.addContent(defaultValue);
          }

          property.addContent(createPropertyOrAttributeExtension(PROPERTY_EXTENSION, d, componentAttribute, namespace));
          properties.add(property);
        } catch (NoSuchMethodException e) {
          // if property not found should be attribute
          Element attribute = new Element(ATTRIBUTE, namespace);
          Element attributeName = new Element(ATTRIBUTE_NAME, namespace);
          Element attributeClass = new Element(ATTRIBUTE_CLASS, namespace);

          attributeName.setText(attributeStr);
          addClass(componentAttribute, attributeClass);

          addDescription(d, attribute, namespace);

          attribute.addContent(attributeName);
          attribute.addContent(attributeClass);
          if (componentAttribute.defaultValue().length() > 0) {
            Element defaultValue = new Element(DEFAULT_VALUE, namespace);
            defaultValue.setText(componentAttribute.defaultValue());
            attribute.addContent(defaultValue);
          }

          attribute.addContent(createPropertyOrAttributeExtension(ATTRIBUTE_EXTENSION, d,
              componentAttribute, namespace));

          attributes.add(attribute);
        }
      } else {
        throw new IllegalArgumentException("Only setter allowed found: " + simpleName);
      }
    }
  }

  private void addClass(UIComponentTagAttribute componentAttribute, Element attributeClass) {
    if (componentAttribute.type().length > 1) {
      attributeClass.setText(Object.class.getName());
    } else if (componentAttribute.type().length == 1) {
      String className = componentAttribute.type()[0];
      if (componentAttribute.expression().isMethodExpression() && isUnifiedEL()) {
        className = "javax.el.MethodExpression";
      }
      attributeClass.setText((className.equals(Boolean.class.getName()) && !isUnifiedEL()) ? "boolean" : className);
    } else {
      if (componentAttribute.expression().isMethodExpression()) {
        String className = "";
        if (isUnifiedEL()) {
          className = "javax.el.MethodExpression";
        } else {
          className = "javax.faces.el.MethodBinding";
        }
        attributeClass.setText(className);
      }
    }
  }

  private void addDescription(MethodDeclaration d, Element attribute, Namespace namespace) {
    String comment = d.getDocComment();
    if (comment != null) {
      int index = comment.indexOf('@');
      if (index != -1) {
        comment = comment.substring(0, index);
      }
      comment = comment.trim();
      if (comment.length() > 0) {
        Element description = new Element(DESCRIPTION, namespace);
        description.setText(comment);
        attribute.addContent(description);
      }
    }
  }

  private Element createPropertyOrAttributeExtension(String extensionType, MethodDeclaration methodDeclaration,
      UIComponentTagAttribute uiComponentTagAttribute, Namespace namespace) throws IllegalArgumentException {
    Element extensionElement = new Element(extensionType, namespace);
    Element valueExpression = new Element(VALUE_EXPRESSION, namespace);
    valueExpression.setText(uiComponentTagAttribute.expression().toMetaDataString());
    extensionElement.addContent(valueExpression);
    String[] allowedValues = uiComponentTagAttribute.allowedValues();
    if (allowedValues.length > 0) {
      Element propertyValues = new Element(PROPERTY_VALUES, namespace);
      String values = "";
      for (String value : allowedValues) {
        values += value + " ";
      }
      propertyValues.setText(values);
      extensionElement.addContent(propertyValues);
    }
    Deprecated deprecated = methodDeclaration.getAnnotation(Deprecated.class);
    if (deprecated != null) {
      Element elementDeprecated = new Element(DEPRECATED, namespace);
      elementDeprecated.setText("Warning: This property is deprecated!");
      extensionElement.addContent(elementDeprecated);
    }
    Element hidden = new Element(HIDDEN, namespace);
    hidden.setText(Boolean.toString(uiComponentTagAttribute.isHidden()));
    extensionElement.addContent(hidden);
    Element readOnly = new Element(READONLY, namespace);
    readOnly.setText(Boolean.toString(uiComponentTagAttribute.isReadOnly()));
    extensionElement.addContent(readOnly);
    TagAttribute tagAttribute = methodDeclaration.getAnnotation(TagAttribute.class);
    if (tagAttribute != null) {
      Element required = new Element(REQUIRED, namespace);
      required.setText(Boolean.toString(tagAttribute.required()));
      extensionElement.addContent(required);
    }

    return extensionElement;
  }

  protected void addAttributes(InterfaceDeclaration type, Class uiComponentClass, List properties, List attributes,
      Namespace namespace) {
    addAttributes(type.getSuperinterfaces(), uiComponentClass, properties, attributes, namespace);
    for (MethodDeclaration decl : getCollectedMethodDeclarations()) {
      if (decl.getDeclaringType().equals(type)) {
        addAttribute(decl, uiComponentClass, properties, attributes, namespace);
      }
    }
  }

  protected void addAttributes(Collection<InterfaceType> interfaces, Class uiComponentClass, List properties,
      List attributes, Namespace namespace) {
    for (InterfaceType type : interfaces) {
      addAttributes(type.getDeclaration(), uiComponentClass, properties, attributes, namespace);
    }
  }

  protected void addAttributes(ClassDeclaration d, Class uiComponentClass, List properties, List attributes,
      Namespace namespace) {
    for (MethodDeclaration decl : getCollectedMethodDeclarations()) {
      if (d.getQualifiedName().
          equals(decl.getDeclaringType().getQualifiedName())) {
        addAttribute(decl, uiComponentClass, properties, attributes, namespace);
      }
    }
    addAttributes(d.getSuperinterfaces(), uiComponentClass, properties, attributes, namespace);
    if (d.getSuperclass() != null) {
      addAttributes(d.getSuperclass().getDeclaration(), uiComponentClass, properties, attributes, namespace);
    }
  }


  private void addFacets(UIComponentTag componentTag, Namespace namespace, Element element) {
    Facet[] facets = componentTag.facets();
    for (Facet facet : facets) {
      Element facetElement = new Element(FACET, namespace);
      String description = facet.description();
      if (description != null && description.length() > 0) {
        Element facetDescription = new Element(DESCRIPTION, namespace);
        facetDescription.setText(description);
        facetElement.addContent(facetDescription);
      }
      Element facetName = new Element(FACET_NAME, namespace);
      facetName.setText(facet.name());
      facetElement.addContent(facetName);
      Element facetExtension = new Element(FACET_EXTENSION, namespace);
      Element elementAllowedChildComponents = new Element(ALLOWED_CHILD_COMPONENTS, namespace);
      String[] allowedChildComponents = facet.allowedChildComponenents();
      String allowedComponentTypes = "";
      for (String componentType : allowedChildComponents) {
        allowedComponentTypes += componentType + " ";
      }
      elementAllowedChildComponents.setText(allowedComponentTypes);
      facetExtension.addContent(elementAllowedChildComponents);
      facetElement.addContent(facetExtension);
      element.addContent(facetElement);
    }
  }

  protected void addElement(ClassDeclaration decl, List<Element> components, List<Element> renderer,
      Namespace namespace) throws IOException {
    UIComponentTag componentTag = decl.getAnnotation(UIComponentTag.class);
    if (componentTag != null) {
      try {
        Class<?> uiComponentClass = Class.forName(componentTag.uiComponent());
        if (!componentTag.isComponentAlreadyDefined()) {
          Element element = createComponentElement(decl, componentTag, uiComponentClass, namespace);
          if (element != null) {
            if (!containsElement(components, element)) {
              addFacets(componentTag, namespace, element);
              List attributes = new ArrayList();
              List properties = new ArrayList();
              addAttributes(decl, uiComponentClass, attributes, properties, namespace);
              if (!attributes.isEmpty()) {
                element.addContent(attributes);
              }
              if (!properties.isEmpty()) {
                element.addContent(properties);
              }
              element.addContent(createElementExtension(decl, componentTag, namespace));
              components.add(element);
            } else {
              // TODO add facet and attributes
            }
          }
        }
        addRendererElement(decl, componentTag, uiComponentClass, renderer, namespace);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  protected void addElement(InterfaceDeclaration decl, List<Element> components, List<Element> renderer,
      final Namespace namespace) throws IOException {
    UIComponentTag componentTag = decl.getAnnotation(UIComponentTag.class);
    if (componentTag != null) {
      try {
        Class<?> uiComponentClass = Class.forName(componentTag.uiComponent());
        if (!componentTag.isComponentAlreadyDefined()) {
          Element element = createComponentElement(decl, componentTag, uiComponentClass, namespace);
          if (element != null) {
            if (!containsElement(components, element)) {
              addFacets(componentTag, namespace, element);
              List attributes = new ArrayList();
              List properties = new ArrayList();
              addAttributes(decl, uiComponentClass, properties, attributes, namespace);
              if (!attributes.isEmpty()) {
                element.addContent(attributes);
                Collections.sort(attributes, new Comparator<Element>() {
                  public int compare(org.jdom.Element d1, org.jdom.Element d2) {
                    return d1.getChildText(ATTRIBUTE_NAME, namespace).compareTo(
                        d2.getChildText(ATTRIBUTE_NAME, namespace));
                  }
                });
              }
              if (!properties.isEmpty()) {
                Collections.sort(properties, new Comparator<Element>() {
                  public int compare(org.jdom.Element d1, org.jdom.Element d2) {
                    return d1.getChildText(PROPERTY_NAME, namespace).compareTo(
                        d2.getChildText(PROPERTY_NAME, namespace));
                  }
                });
                element.addContent(properties);
              }
              element.addContent(createElementExtension(decl, componentTag, namespace));
              components.add(element);
            } else {
              // TODO add facet and attributes
            }
          }
        }
        addRendererElement(decl, componentTag, uiComponentClass, renderer, namespace);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  private void normaliseLineEndings(Document document) {
    for (Iterator i = document.getDescendants(new ContentFilter(ContentFilter.COMMENT)); i.hasNext();) {
      Comment c = (Comment) i.next();
      c.setText(c.getText().replaceAll("\n", SEPARATOR));
    }
  }

  private boolean is12() {
    return "1.2".equals(jsfVersion);
  }

  private boolean is11() {
    return "1.1".equals(jsfVersion);
  }

  private boolean is20() {
    return "2.0".equals(jsfVersion);
  }

  private boolean isUnifiedEL() {
    return !"1.1".equals(jsfVersion);
  }
}
