/*
 * Copyright (c) 1998-2007 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Emil Ong, Adam Megacz
 */

package com.caucho.jaxb.skeleton;

import org.w3c.dom.Node;

import static javax.xml.XMLConstants.*;

import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.annotation.*;

import javax.xml.namespace.QName;

import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;

import java.io.IOException;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.caucho.jaxb.BinderImpl;
import com.caucho.jaxb.JAXBContextImpl;
import com.caucho.jaxb.JAXBUtil;
import com.caucho.jaxb.NodeIterator;
import com.caucho.jaxb.annotation.XmlLocation;

import com.caucho.jaxb.accessor.Accessor;
import com.caucho.jaxb.accessor.FieldAccessor;
import com.caucho.jaxb.accessor.GetterSetterAccessor;

import com.caucho.jaxb.mapping.AnyAttributeMapping;
import com.caucho.jaxb.mapping.AttributeMapping;
import com.caucho.jaxb.mapping.AnyElementMapping;
import com.caucho.jaxb.mapping.ElementMapping;
import com.caucho.jaxb.mapping.ElementRefMapping;
import com.caucho.jaxb.mapping.ElementsMapping;
import com.caucho.jaxb.mapping.Namer;
import com.caucho.jaxb.mapping.XmlMapping;
import com.caucho.jaxb.mapping.XmlValueMapping;

import com.caucho.util.L10N;

import com.caucho.xml.stream.StaxUtil;

public class ClassSkeleton<C> {
  public static final String XML_SCHEMA_NS = "http://www.w3.org/2001/XMLSchema";
  public static final String XML_SCHEMA_PREFIX = "xsd";

  private static final L10N L = new L10N(ClassSkeleton.class);
  private static final Logger log = 
    Logger.getLogger(ClassSkeleton.class.getName());

  private static final Class[] NO_PARAMS = new Class[0];
  private static final Object[] NO_ARGS = new Object[0];

  protected JAXBContextImpl _context;
  protected QName _elementName;
  private Class<C> _class;
  private ClassSkeleton _parent;
  private Package _package;
  private Method _createMethod;
  private Object _factory;
  private QName _typeName;

  private HashMap<QName,XmlMapping> _attributeQNameToMappingMap
    = new HashMap<QName,XmlMapping>();

  private HashMap<QName,XmlMapping> _elementQNameToMappingMap
    = new HashMap<QName,XmlMapping>();

  private ArrayList<XmlMapping> _attributeMappings
    = new ArrayList<XmlMapping>();

  private ArrayList<XmlMapping> _elementMappings
    = new ArrayList<XmlMapping>();

  private AnyElementMapping _anyElementMapping;
  private AnyAttributeMapping _anyAttributeMapping;

  private Method _beforeUnmarshal;
  private Method _afterUnmarshal;
  private Method _beforeMarshal;
  private Method _afterMarshal;

  private Constructor _constructor;

  /** Special hook to allow injecting the location where an instance was
      unmarshalled from. */
  private Accessor _locationAccessor;

  /**
   * The value @XmlValue.
   * 
   **/
  protected XmlValueMapping _value;

  public Class<C> getType()
  {
    return _class;
  }

  public String toString()
  {
    return "ClassSkeleton[" + _class + "]";
  }

  protected ClassSkeleton(JAXBContextImpl context)
  {
    _context = context;
  }

  public ClassSkeleton(JAXBContextImpl context, Class<C> c)
  {
    this(context);
    _class = c;
  }

  public void init() 
    throws JAXBException
  {
    try {
      _package = _class.getPackage();

      // check for special before and after methods
      try {
        _beforeUnmarshal =
          _class.getMethod("beforeUnmarshal", Unmarshaller.class, Object.class);
      } catch (NoSuchMethodException _) {
        // deliberate
      }

      try {
        _afterUnmarshal =
          _class.getMethod("afterUnmarshal", Unmarshaller.class, Object.class);
      } catch (NoSuchMethodException _) {
        // deliberate
      }

      try {
        _beforeMarshal = _class.getMethod("beforeMarshal", Marshaller.class);
      } catch (NoSuchMethodException _) {
        // deliberate
      }

      try {
        _afterMarshal = _class.getMethod("afterMarshal", Marshaller.class);
      } catch (NoSuchMethodException _) {
        // deliberate
      }

      if (Set.class.isAssignableFrom(_class)) {
        // XXX:
      }
      
      // XXX: @XmlJavaTypeAdapter
      
      // Find the zero-parameter constructor
      try {
        _constructor = _class.getConstructor(NO_PARAMS);
        _constructor.setAccessible(true);
      }
      catch (Exception e1) {
        try {
          _constructor = _class.getDeclaredConstructor(NO_PARAMS);
          _constructor.setAccessible(true);
        }
        catch (Exception e2) {
          throw new JAXBException(L.l("{0}: Zero-arg constructor not found", 
                                      _class.getName()), e2);
        }
      }

      _typeName = JAXBUtil.getXmlSchemaDatatype(_class, _context);

      // Special case: when name="", this is an "anonymous" type, bound
      // exclusively to a particular element name
      if (! "".equals(_typeName.getLocalPart()))
        _context.addXmlType(_typeName, this);

      // Check for the complete name of the element...
      String namespace = _context.getTargetNamespace();

      // look at package defaults first...
      XmlSchema schema = (XmlSchema) _package.getAnnotation(XmlSchema.class);

      if (schema != null && ! "".equals(schema.namespace()))
        namespace = schema.namespace();
      
      // then look at class specific overrides.
      XmlRootElement xre = _class.getAnnotation(XmlRootElement.class);

      if (xre != null) { 
        String localName = null;
        
        if ("##default".equals(xre.name()))
          localName = JAXBUtil.identifierToXmlName(_class);
        else
          localName = xre.name();

        if (! "##default".equals(xre.namespace()))
          namespace = xre.namespace();

        if (namespace == null)
          _elementName = new QName(localName);
        else
          _elementName = new QName(namespace, localName);

        _context.addRootElement(this);
      }

      // order the elements, if specified
      
      XmlAccessOrder accessOrder = XmlAccessOrder.UNDEFINED;

      XmlAccessorOrder packageOrder = 
        _package.getAnnotation(XmlAccessorOrder.class);

      XmlAccessorOrder classOrder = 
        _class.getAnnotation(XmlAccessorOrder.class);

      if (packageOrder != null)
        accessOrder = packageOrder.value();

      if (classOrder != null)
        accessOrder = classOrder.value();

      // try property orders too
     
      XmlType xmlType = (XmlType) _class.getAnnotation(XmlType.class);
      HashMap<String,Integer> orderMap = null;

      if (xmlType != null &&
          (xmlType.propOrder().length != 1 ||
           ! "".equals(xmlType.propOrder()[0]))) {
        // non-default propOrder
        orderMap = new HashMap<String,Integer>();

        for (int i = 0; i < xmlType.propOrder().length; i++)
          orderMap.put(xmlType.propOrder()[i], i);
      }

      // Collect the fields/properties of the class
      if (orderMap != null) {
        for (int i = 0; i < orderMap.size(); i++)
          _elementMappings.add(null);
      }

      XmlAccessorType accessorType = 
        _class.getAnnotation(XmlAccessorType.class);

      XmlAccessType accessType = (accessorType == null ? 
                                  XmlAccessType.PUBLIC_MEMBER :
                                  accessorType.value());

      if (accessType != XmlAccessType.FIELD) {
        // getter/setter
        TreeSet<Method> methodSet = new TreeSet<Method>(methodComparator);

        Method[] declared = _class.getDeclaredMethods();

        for (Method m : declared)
          methodSet.add(m);

        Method[] methods = new Method[methodSet.size()];
        methodSet.toArray(methods);

        AccessibleObject.setAccessible(methods, true);

        while (methodSet.size() > 0) {
          Method m = methodSet.first();
          methodSet.remove(m);

          String name = null;
          Method get = null;
          Method set = null;

          if (m.getName().startsWith("get")) {
            get = m;

            if (Void.TYPE.equals(get.getReturnType()))
              continue;

            name = get.getName().substring(3); // 3 == "get".length());

            Class cl = get.getDeclaringClass();

            try {
              set = cl.getDeclaredMethod("set" + name, get.getReturnType());
            }
            catch (NoSuchMethodException e) {
              continue;
            }

            if (! methodSet.remove(set))
              continue;
          } 
          else if (m.getName().startsWith("set")) {
            set = m;

            Class[] parameterTypes = set.getParameterTypes();

            if (parameterTypes.length != 1)
              continue;

            name = set.getName().substring(3); // 3 == "set".length());

            Class cl = set.getDeclaringClass();

            try {
              get = cl.getDeclaredMethod("get" + name);
            }
            catch (NoSuchMethodException e) {
              continue;
            }

            if (! parameterTypes[0].equals(get.getReturnType()))
              continue;

            if (! methodSet.remove(get))
              continue;
          }
          else
            continue;

          name = Character.toLowerCase(name.charAt(0)) + name.substring(1);

          // JAXB specifies that a "class" property must be specified as "clazz"
          // because of Object.getClass()
          if ("class".equals(name))
            continue;

          // XXX special cases for Throwable specified in JAX-WS
          // Should it be in the general JAXB?
          if (Throwable.class.isAssignableFrom(_class) && 
              ("stackTrace".equals(name) ||
               "cause".equals(name) ||
               "localizedMessage".equals(name)))
            continue;

          // XXX PUBLIC_MEMBER

          if (accessType == XmlAccessType.NONE &&
              ! JAXBUtil.isJAXBAnnotated(get) &&
              ! JAXBUtil.isJAXBAnnotated(set))
            continue;

          // jaxb/0456
          if (Modifier.isStatic(get.getModifiers()) &&
              JAXBUtil.isJAXBAnnotated(get))
            throw new JAXBException(L.l("JAXB annotations cannot be applied to static methods: {0}", get));

          // jaxb/0457
          if (Modifier.isStatic(set.getModifiers()) &&
              JAXBUtil.isJAXBAnnotated(set))
            throw new JAXBException(L.l("JAXB annotations cannot be applied to static methods: {0}", set));

          // jaxb/0374
          if (Modifier.isStatic(set.getModifiers()) ||
              Modifier.isStatic(get.getModifiers()))
            continue;

          if (get != null && get.isAnnotationPresent(XmlTransient.class))
            continue;
          if (set != null && set.isAnnotationPresent(XmlTransient.class))
            continue;

          get.setAccessible(true);
          set.setAccessible(true);

          Accessor a = new GetterSetterAccessor(name, get, set); 

          if (orderMap != null) {
            Integer i = orderMap.remove(name);

            if (i != null)
              a.setOrder(i.intValue());
            // XXX else throw something?
          }

          processAccessor(a);
        }
      }

      if (accessType != XmlAccessType.PROPERTY) {
        // XXX Don't overwrite property accessors
        HashSet<Field> fieldSet = new HashSet<Field>();

        Field[] declared = _class.getDeclaredFields();

        for (Field f : declared)
          fieldSet.add(f);

        Field[] fields = new Field[fieldSet.size()];
        fieldSet.toArray(fields);

        AccessibleObject.setAccessible(fields, true);

        for (Field f : fields) {
          if (f.isAnnotationPresent(XmlLocation.class)) 
          {
            if (! f.getType().equals(Location.class))
              throw new JAXBException(L.l("Fields annotated by @Location must have type javax.xml.stream.Location"));

            _locationAccessor = new FieldAccessor(f);
          }

          // special case: jaxb/0250
          // fields which are static are skipped _unless_ they are also
          // both final and attributes
          if (Modifier.isStatic(f.getModifiers()) &&
              ! (Modifier.isFinal(f.getModifiers()) &&
                 f.isAnnotationPresent(XmlAttribute.class)))
            continue;

          if (f.isAnnotationPresent(XmlTransient.class))
            continue;
          // jaxb/0176: transient modifier ignored

          if (accessType == XmlAccessType.PUBLIC_MEMBER && 
              ! Modifier.isPublic(f.getModifiers()) &&
              ! JAXBUtil.isJAXBAnnotated(f))
            continue;

          if (accessType == XmlAccessType.NONE && ! JAXBUtil.isJAXBAnnotated(f))
            continue;

          Accessor a = new FieldAccessor(f);

          if (orderMap != null) {
            Integer i = orderMap.remove(f.getName());

            if (i != null)
              a.setOrder(i.intValue());
            // XXX else throw something?
          }

          processAccessor(a);
        }
      }

      // do ordering if necessary
      if (orderMap == null && accessOrder == XmlAccessOrder.ALPHABETICAL)
        Collections.sort(_elementMappings, XmlMapping.nameComparator);
    }
    catch (JAXBException e) {
      throw e;
    }
    catch (Exception e) {
      throw new JAXBException(L.l("{0}: Initialization error", 
                                  _class.getName()), e);
    }

    if (! Object.class.equals(_class.getSuperclass()))
      _parent = _context.getSkeleton(_class.getSuperclass());
  }

  /**
   * Handles any processing that needs to happen after all ClassSkeletons
   * have been created and all classes have been discovered.
   **/
  public void postProcess()
    throws JAXBException
  {
    if (log.isLoggable(Level.FINEST))
      log.finest("JAXB: " + _class.getName() + " has children: ");

    for (int i = 0; i < _elementMappings.size(); i++)
      _elementMappings.get(i).putQNames(_elementQNameToMappingMap);
  }

  /**
   * Create an XmlMapping for this accessor and insert it in the correct
   * mapping or field.
   **/
  private void processAccessor(Accessor accessor)
    throws JAXBException
  {
    XmlMapping mapping = XmlMapping.newInstance(_context, accessor);

    if (mapping instanceof XmlValueMapping) {
      if (_value != null)
        throw new JAXBException(L.l("Cannot have two @XmlValue annotated fields or properties"));

      if (_elementMappings.size() > 0) {
        // in case of propOrder & XmlValue
        if (_elementMappings.size() != 1 || _elementMappings.get(0) != null)
          throw new JAXBException(L.l("Cannot have both @XmlValue and elements in a JAXB element (e.g. {0})", _elementMappings.get(0)));

        _elementMappings.clear();
      }

      _value = (XmlValueMapping) mapping;
    }
    else if (mapping instanceof AttributeMapping) {
      mapping.putQNames(_attributeQNameToMappingMap);
      _attributeMappings.add((AttributeMapping) mapping);
    }
    else if (mapping instanceof AnyAttributeMapping) {
      if (_anyAttributeMapping != null)
        throw new JAXBException(L.l("Cannot have two fields or properties with @XmlAnyAttribute annotation"));

      _anyAttributeMapping = (AnyAttributeMapping) mapping;
      _attributeMappings.add(mapping);
    }
    else if ((mapping instanceof ElementMapping) ||
             (mapping instanceof ElementRefMapping) ||
             (mapping instanceof ElementsMapping)) {
      if (_value != null)
        throw new JAXBException(L.l("{0}: Cannot have both @XmlValue and elements in a JAXB element", _class.getName()));

      if (mapping.getAccessor().getOrder() >= 0)
        _elementMappings.set(mapping.getAccessor().getOrder(), mapping);
      else
        _elementMappings.add(mapping);
    }
    else if (mapping instanceof AnyElementMapping) {
      if (_anyElementMapping != null)
        throw new JAXBException(L.l("{0}: Cannot have two @XmlAnyElement annotations in a single class", _class.getName()));

      _anyElementMapping = (AnyElementMapping) mapping;
    }
    else {
      throw new RuntimeException(L.l("Unknown mapping type {0}", mapping.getClass()));
    }
  }

  private XmlMapping getElementMapping(QName q)
    throws JAXBException
  {
    XmlMapping mapping = _elementQNameToMappingMap.get(q);

    if (mapping != null)
      return mapping;

    if (_anyElementMapping != null)
      return _anyElementMapping;

    if (_parent != null)
      return _parent.getElementMapping(q);

    return null;
  }

  public XmlMapping getAttributeMapping(QName q)
    throws JAXBException
  {
    XmlMapping mapping = _attributeQNameToMappingMap.get(q);

    if (mapping != null)
      return mapping;

    if (_anyAttributeMapping != null)
      return _anyAttributeMapping;

    if (_parent != null)
      return _parent.getAttributeMapping(q);

    return null;
  }

  public QName getElementName()
  {
    if (_elementName != null)
      return _elementName;
    else
      return _typeName;
  }

  public void setElementName(QName elementName)
  {
    _elementName = elementName;
  }

  public QName getTypeName()
  {
    return _typeName;
  }

  public void setCreateMethod(Method createMethod, Object factory)
  {
    _createMethod = createMethod;
    _factory = factory;
  }



  public C newInstance()
    throws JAXBException
  {
    try {
      if (_createMethod != null && _factory != null) {
        return (C) _createMethod.invoke(_factory);
      }
      else {
        // XXX move into constructor
        XmlType xmlType = getXmlType();

        if (xmlType != null) {
          Class factoryClass = xmlType.factoryClass();

          if (xmlType.factoryClass() == XmlType.DEFAULT.class)
            factoryClass = _class;

          if (! "".equals(xmlType.factoryMethod())) {
            Method m = 
              factoryClass.getMethod(xmlType.factoryMethod(), NO_PARAMS);

            if (! Modifier.isStatic(m.getModifiers()))
              throw new JAXBException(L.l("Factory method not static"));

            return (C) m.invoke(null);
          }
        }

        Constructor con = _class.getConstructor(NO_PARAMS);

        return (C)con.newInstance(NO_ARGS);
      }
    }
    catch (JAXBException e) {
      throw e;
    }
    catch (Exception e) {
      throw new JAXBException(e);
    }
  }

  public XmlType getXmlType()
  {
    return (XmlType)_class.getAnnotation(XmlType.class);
  }

  public Object read(Unmarshaller u, XMLStreamReader in)
    throws IOException, XMLStreamException, JAXBException
  {
    try {
      C ret = null;

      String nil = in.getAttributeValue(W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil");

      if (! "true".equals(nil))
        ret = newInstance();

      if (_locationAccessor != null)
        _locationAccessor.set(ret, in.getLocation());

      if (_beforeUnmarshal != null)
        _beforeUnmarshal.invoke(ret, u, null);

      if (u.getListener() != null)
        u.getListener().beforeUnmarshal(ret, null);

      if (_value != null) {
        _value.read(u, in, ret, this);
      }
      else {
        // process the attributes
        for (int i = 0; i < in.getAttributeCount(); i++) {
          QName attributeName = in.getAttributeName(i);
          XmlMapping mapping = getAttributeMapping(attributeName);

          if (mapping == null)
            throw new UnmarshalException(L.l("Attribute {0} not found in {1}", 
                                             attributeName, getType()));

          mapping.readAttribute(in, i, ret);
        }

        int i = 0;
        in.nextTag();

        while (in.getEventType() == in.START_ELEMENT) {
          XmlMapping mapping = getElementMapping(in.getName());

          if (mapping == null) {
            throw new UnmarshalException(L.l("Child <{0}> not found in {1}", 
                                             in.getName(), getType()));
          }

          if (! mapping.getAccessor().checkOrder(i++, u.getEventHandler())) {
            throw new UnmarshalException(L.l("Child <{0}> misordered in {1}", 
                                             in.getName(), getType()));
          }

          mapping.read(u, in, ret);
        }

        // essentially a nextTag() that handles end of document gracefully
        while (in.hasNext()) {
          in.next();

          if (in.getEventType() == in.START_ELEMENT ||
              in.getEventType() == in.END_ELEMENT)
            break;
        }
      }

      if (_afterUnmarshal != null)
        _afterUnmarshal.invoke(ret, u, null);

      if (u.getListener() != null)
        u.getListener().afterUnmarshal(ret, null);

      return ret;
    }
    catch (InvocationTargetException e) {
      if (e.getTargetException() != null)
        throw new UnmarshalException(e.getTargetException());

      throw new UnmarshalException(e);
    }
    catch (IllegalAccessException e) {
      throw new UnmarshalException(e);
    }
  }

  public Object bindFrom(BinderImpl binder, Object existing, NodeIterator node)
    throws IOException, JAXBException
  {
    Node root = node.getNode();
    C ret = (C) existing;
    
    if (ret == null)
      ret = newInstance();

    if (_value != null) { 
      _value.bindFrom(binder, node, ret);
    }
    else {
      int i = 0;
      Node child = node.firstChild();

      while (child != null) {
        if (child.getNodeType() == Node.ELEMENT_NODE) {
          QName name = JAXBUtil.qnameFromNode(child);

          XmlMapping mapping = getElementMapping(name);

          if (mapping == null)
            throw new UnmarshalException(L.l("Child <{0}> not found", name));

          if (! mapping.getAccessor().checkOrder(i++, binder.getEventHandler()))
            throw new UnmarshalException(L.l("Child <{0}> misordered", name));

          mapping.bindFrom(binder, node, ret);
        }

        child = node.nextSibling();
      }
    }
    
    node.setNode(root);
    binder.bind(ret, root);

    return ret;
  }
  
  public void write(Marshaller m, XMLStreamWriter out,
                    Object obj, Namer namer, 
                    ArrayList<XmlMapping> attributes)
    throws IOException, XMLStreamException, JAXBException
  {
    if (obj == null)
      return;

    try {
      if (_beforeMarshal != null)
        _beforeMarshal.invoke(obj, m);

      if (m.getListener() != null)
        m.getListener().beforeMarshal(obj);

      QName tagName = null;
      
      if (namer != null)
        tagName = namer.getQName(obj);

      if (tagName == null)
        tagName = _elementName;

      if (_value != null) {
        _value.setQName(tagName);
        _value.write(m, out, obj, _attributeMappings);
      }
      else {
        if (tagName.getNamespaceURI() == null ||
            "".equals(tagName.getNamespaceURI()))
          out.writeStartElement(tagName.getLocalPart());
        else if (tagName.getPrefix() == null ||
                 "".equals(tagName.getPrefix()))
          out.writeStartElement(tagName.getNamespaceURI(),
                                tagName.getLocalPart());
        else
          out.writeStartElement(tagName.getPrefix(),
                                tagName.getLocalPart(),
                                tagName.getNamespaceURI());

        if (attributes != null) {
          for (int i = 0; i < attributes.size(); i++)
            attributes.get(i).write(m, out, obj);
        }

        for (XmlMapping mapping : _attributeMappings)
          mapping.write(m, out, obj);

        for (XmlMapping mapping : _elementMappings)
          mapping.write(m, out, obj);

        if (_anyElementMapping != null) // XXX ordering!
          _anyElementMapping.write(m, out, obj);
        
        out.writeEndElement();
      }
      
      if (_afterMarshal != null)
        _afterMarshal.invoke(obj, m);

      if (m.getListener() != null)
        m.getListener().afterMarshal(obj);
    }
    catch (InvocationTargetException e) {
      throw new JAXBException(e);
    }
    catch (IllegalAccessException e) {
      throw new JAXBException(e);
    }
  }

  public Node bindTo(BinderImpl binder, Node node, 
                     Object obj, Namer namer, 
                     ArrayList<XmlMapping> attributes)
    throws IOException, JAXBException
  {
    if (obj == null)
      return null;

    QName tagName = null;

    if (namer != null)
      tagName = namer.getQName(obj);

    if (tagName == null)
      tagName = _elementName;

    if (_value != null) {
      Node newNode = _value.bindTo(binder, node, obj);

      if (newNode != node) {
        binder.invalidate(node);
        node = newNode;
      }
    }
    else {
      QName nodeName = JAXBUtil.qnameFromNode(node);

      if (tagName.equals(nodeName)) {
        Node child = node.getFirstChild();

        child = JAXBUtil.skipIgnorableNodes(child);

        if (attributes != null) {
          // XXX
        }

        for (XmlMapping mapping : _elementMappings) {
          if (child != null) {
            // try to reuse as many of the child nodes as possible
            Node newNode = mapping.bindTo(binder, child, obj);

            if (newNode != child) {
              node.replaceChild(newNode, child);
              binder.invalidate(child);
              child = newNode;
            }

            child = child.getNextSibling();
            child = JAXBUtil.skipIgnorableNodes(child);
          }
          else {
            Node newNode = 
              JAXBUtil.elementFromQName(mapping.getQName(obj), node);
            node.appendChild(mapping.bindTo(binder, newNode, obj));
          }
        }
      }
      else {
        binder.invalidate(node);

        node = JAXBUtil.elementFromQName(tagName, node);

        for (XmlMapping mapping : _elementMappings) {
          Node child = JAXBUtil.elementFromQName(mapping.getQName(obj), node);
          node.appendChild(mapping.bindTo(binder, child, obj));
        }
      }
    }

    binder.bind(obj, node);

    return node;
  }

  public boolean isRootElement()
  {
    return _elementName != null;
  }

  public void generateSchema(XMLStreamWriter out)
    throws JAXBException, XMLStreamException
  {
    if (_elementName != null) {

      if ("".equals(_typeName.getLocalPart()))
        out.writeStartElement(XML_SCHEMA_PREFIX, "element", XML_SCHEMA_NS);

      else {
        out.writeEmptyElement(XML_SCHEMA_PREFIX, "element", XML_SCHEMA_NS);
        out.writeAttribute("type", _typeName.getLocalPart());
      }

      out.writeAttribute("name", _elementName.getLocalPart());
    }

    generateSchemaType(out);

    if (_elementName != null && "".equals(_typeName.getLocalPart()))
      out.writeEndElement(); // element
  }

  public void generateSchemaType(XMLStreamWriter out)
    throws JAXBException, XMLStreamException
  {
    if (_value != null) {
      out.writeStartElement(XML_SCHEMA_PREFIX, "simpleType", XML_SCHEMA_NS);

      if (! "".equals(_typeName.getLocalPart()))
        out.writeAttribute("name", _typeName.getLocalPart());

      if (Collection.class.isAssignableFrom(_value.getAccessor().getType())) {
        out.writeEmptyElement(XML_SCHEMA_PREFIX, "list", XML_SCHEMA_NS);

        String itemType = StaxUtil.qnameToString(out, _value.getSchemaType());

        out.writeAttribute("itemType", itemType);
      }
      else {
        out.writeEmptyElement(XML_SCHEMA_PREFIX, "restriction", XML_SCHEMA_NS);

        String base = StaxUtil.qnameToString(out, _value.getSchemaType());

        out.writeAttribute("base", base);
      }

      for (XmlMapping mapping : _attributeMappings)
        mapping.generateSchema(out);

      out.writeEndElement(); // simpleType
    }
    else {
      out.writeStartElement(XML_SCHEMA_PREFIX, "complexType", XML_SCHEMA_NS);

      if (Modifier.isAbstract(_class.getModifiers()))
        out.writeAttribute("abstract", "true");

      if (! "".equals(_typeName.getLocalPart()))
        out.writeAttribute("name", _typeName.getLocalPart());

      out.writeStartElement(XML_SCHEMA_PREFIX, "sequence", XML_SCHEMA_NS);

      for (XmlMapping mapping : _elementMappings)
        mapping.generateSchema(out);

      if (_anyElementMapping != null)
        _anyElementMapping.generateSchema(out);

      out.writeEndElement(); // sequence

      for (XmlMapping mapping : _attributeMappings)
        mapping.generateSchema(out);

      out.writeEndElement(); // complexType
    }
  }

  //XXX The TreeSet needs this for some reason
  private static final Comparator methodComparator
    = new java.util.Comparator<Method>() {
      public int compare(Method m1, Method m2)
      {
        return m1.toGenericString().compareTo(m2.toGenericString());
      }

      public boolean equals(Object obj)
      {
        return obj == this;
      }
    };
}
