/*
  * JBoss, Home of Professional Open Source
  * Copyright 2005, JBoss Inc., and individual contributors as indicated
  * by the @authors tag. See the copyright.txt in the distribution for a
  * full listing of individual contributors.
  *
  * This is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as
  * published by the Free Software Foundation; either version 2.1 of
  * the License, or (at your option) any later version.
  *
  * This software 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. See the GNU
  * Lesser General Public License for more details.
  *
  * You should have received a copy of the GNU Lesser General Public
  * License along with this software; if not, write to the Free
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
package org.jboss.xb.binding.sunday.unmarshalling;

import java.io.InputStream;
import java.io.Reader;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.ListIterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;

import javax.xml.namespace.QName;

import org.apache.xerces.xs.StringList;
import org.apache.xerces.xs.XSAnnotation;
import org.apache.xerces.xs.XSAttributeDeclaration;
import org.apache.xerces.xs.XSAttributeUse;
import org.apache.xerces.xs.XSComplexTypeDefinition;
import org.apache.xerces.xs.XSConstants;
import org.apache.xerces.xs.XSElementDeclaration;
import org.apache.xerces.xs.XSModel;
import org.apache.xerces.xs.XSModelGroup;
import org.apache.xerces.xs.XSModelGroupDefinition;
import org.apache.xerces.xs.XSNamedMap;
import org.apache.xerces.xs.XSObjectList;
import org.apache.xerces.xs.XSParticle;
import org.apache.xerces.xs.XSSimpleTypeDefinition;
import org.apache.xerces.xs.XSTerm;
import org.apache.xerces.xs.XSTypeDefinition;
import org.apache.xerces.xs.XSWildcard;
import org.jboss.logging.Logger;
import org.jboss.xb.binding.Constants;
import org.jboss.xb.binding.JBossXBRuntimeException;
import org.jboss.xb.binding.Util;
import org.jboss.xb.binding.resolver.MultiClassSchemaResolver;
import org.jboss.xb.binding.sunday.unmarshalling.impl.runtime.RtCharactersHandler;
import org.jboss.xb.binding.sunday.xop.XOPIncludeHandler;
import org.jboss.xb.binding.group.ValueListRepeatableParticleHandler;
import org.jboss.xb.binding.metadata.AddMethodMetaData;
import org.jboss.xb.binding.metadata.CharactersMetaData;
import org.jboss.xb.binding.metadata.ClassMetaData;
import org.jboss.xb.binding.metadata.MapEntryMetaData;
import org.jboss.xb.binding.metadata.PackageMetaData;
import org.jboss.xb.binding.metadata.PropertyMetaData;
import org.jboss.xb.binding.metadata.PutMethodMetaData;
import org.jboss.xb.binding.metadata.SchemaMetaData;
import org.jboss.xb.binding.metadata.ValueMetaData;
import org.jboss.xb.binding.metadata.XsdAnnotation;
import org.jboss.xb.binding.metadata.XsdAppInfo;

/**
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version <tt>$Revision: 3954 $</tt>
 */
public class XsdBinder
{
   static final Logger log = Logger.getLogger(XsdBinder.class);

   /**
    * Creates a new instance of the binder the user can use to tune
    * configuration before parsing the XSD.
    * 
    * @return  a new instance of the XsdBinder
    */
   public static XsdBinder newInstance()
   {
     return new XsdBinder();
   }
   
   /**
    * Create a SchemaBinding from and xsd url/uri.
    *
    * @param xsdUrl
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(String xsdUrl)
   {
      MultiClassSchemaResolver resolver = new MultiClassSchemaResolver();
      resolver.setBaseURI(xsdUrl);
      return bind(xsdUrl, resolver);
   }

   /**
    * Create a SchemaBinding from and xsd url/uri.
    *
    * @param xsdUrl
    * @param resolver the resolver will be used to resolve imported schemas in the schema being loaded
    *                 and also will be set on the returned instance of SchemaBinding
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(String xsdUrl, SchemaBindingResolver resolver)
   {
      XSModel model = Util.loadSchema(xsdUrl, resolver);
      return bind(model, resolver);
   }

   public static SchemaBinding bind(InputStream xsdStream, String encoding)
   {
      return bind(xsdStream, encoding, new MultiClassSchemaResolver());
   }

   /**
    * Create a SchemaBinding from and xsd stream.
    *
    * @param xsdStream - the xsd InputStream
    * @param encoding  - optional stream encoding
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(InputStream xsdStream, String encoding, String baseURI)
   {
      return bind(xsdStream, encoding, baseURI, true);
   }

   /**
    * Create a SchemaBinding from and xsd stream.
    *
    * @param xsdStream - the xsd InputStream
    * @param encoding  - optional stream encoding
    * @param processAnnotations - process annotations
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(InputStream xsdStream, String encoding, String baseURI, boolean processAnnotations)
   {
      MultiClassSchemaResolver resolver = new MultiClassSchemaResolver();
      resolver.setBaseURI(baseURI);
      return bind(xsdStream, encoding, resolver, processAnnotations);
   }

   /**
    * Create a SchemaBinding from and xsd stream.
    *
    * @param xsdStream - the xsd InputStream
    * @param encoding  - optional stream encoding
    * @param resolver  the resolver will be used to resolve imported schemas in the schema being loaded
    *                  and also will be set on the returned instance of SchemaBinding
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(InputStream xsdStream, String encoding, SchemaBindingResolver resolver)
   {
      return bind(xsdStream, encoding, resolver, true);
   }

   /**
    * Create a SchemaBinding from and xsd stream.
    *
    * @param xsdStream - the xsd InputStream
    * @param encoding  - optional stream encoding
    * @param resolver  the resolver will be used to resolve imported schemas in the schema being loaded
    *                  and also will be set on the returned instance of SchemaBinding
    * @param processAnnotations whether to process annotations
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(InputStream xsdStream, String encoding, SchemaBindingResolver resolver, boolean processAnnotations)
   {
      XSModel model = Util.loadSchema(xsdStream, encoding, resolver);
      return bind(model, resolver, processAnnotations);
   }

   public static SchemaBinding bind(Reader xsdReader, String encoding)
   {
      return bind(xsdReader, encoding, new MultiClassSchemaResolver());
   }

   /**
    * Create a SchemaBinding from and xsd reader.
    *
    * @param xsdReader - xsd reader
    * @param encoding  - optional reader encoding
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(Reader xsdReader, String encoding, String baseURI)
   {
      MultiClassSchemaResolver resolver = new MultiClassSchemaResolver();
      resolver.setBaseURI(baseURI);
      return bind(xsdReader, encoding, resolver);
   }

   /**
    * Create a SchemaBinding from and xsd reader.
    *
    * @param xsdReader - xsd reader
    * @param encoding  - optional reader encoding
    * @param resolver  the resolver will be used to resolve imported schemas in the schema being loaded
    *                  and also will be set on the returned instance of SchemaBinding
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(Reader xsdReader, String encoding, SchemaBindingResolver resolver)
   {
      XSModel model = Util.loadSchema(xsdReader, encoding, resolver);
      return bind(model, resolver);
   }

   /**
    * Create a SchemaBinding from and xsd string.
    *
    * @param xsd      - xsd string
    * @param encoding - optional string encoding
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(String xsd, String encoding)
   {
      return bind(xsd, encoding, new MultiClassSchemaResolver());
   }

   /**
    * Create a SchemaBinding from and xsd string.
    *
    * @param xsd      - xsd string
    * @param encoding - optional string encoding
    * @param resolver the resolver will be used to resolve imported schemas in the schema being loaded
    *                 and also will be set on the returned instance of SchemaBinding
    * @return SchemaBinding mapping
    */
   public static SchemaBinding bind(String xsd, String encoding, SchemaBindingResolver resolver)
   {
      XSModel model = Util.loadSchema(xsd, encoding);
      return bind(model, resolver);
   }

   public static SchemaBinding bind(XSModel model, SchemaBindingResolver resolver)
   {
      return bind(model,resolver, true);
   }

   public static SchemaBinding bind(XSModel model, SchemaBindingResolver resolver, boolean processAnnotations)
   {
      XsdBinder binder = new XsdBinder();
      binder.setProcessAnnotations(processAnnotations);
      binder.setSchemaResolver(resolver);
      return binder.parse(model);
   }

   /**
    * @param schema schema binding the type should be added to
    * @param type   type definition to be bound
    * @deprecated <i>This method is added temporary to get anonymous type binding working in JBossWS.
    *             It will be removed when anonymous type binding in JBossWS is implemented properly.
    *             No one else should use this method.</i>
    *
    *             <p>This method binds a type definition and adds it as a global type to the passed in schema binding.
    */
   public static void bindType(SchemaBinding schema, XSTypeDefinition type)
   {
      XsdBinder binder = new XsdBinder(schema);
      TypeBinding typeBinding = binder.bindType(type);
      schema.addType(typeBinding);
   }

   /**
    * @param schema             schema binding the type should be added to
    * @param element            element declaration to be bound
    * @param minOccurs
    * @param maxOccurs
    * @param maxOccursUnbounded
    * @deprecated <i>This method is added temporary to get anonymous type binding working in JBossWS.
    *             It will be removed when anonymous type binding in JBossWS is implemented properly.
    *             No one else should use this method.</i>
    *
    *             <p>This method binds an element declaration and adds it as a global element to the passed in schema binding.
    */
   public static void bindElement(SchemaBinding schema,
                                  XSElementDeclaration element,
                                  int minOccurs,
                                  int maxOccurs,
                                  boolean maxOccursUnbounded)
   {
      XsdBinder binder = new XsdBinder(schema);
      ParticleBinding particle = binder.bindElement(element,
         minOccurs,
         maxOccurs,
         maxOccursUnbounded
      );
      schema.addElementParticle(particle);
   }

   // Exposed attributes
   
   private boolean processAnnotations = true;
   private SchemaBindingResolver resolver;
   private boolean simpleContentWithIdAsSimpleType = true;
   private boolean unresolvedContentBoundToDOM = true;
   
   // Internal attributes
   
   private boolean trace = log.isTraceEnabled();
   private final SchemaBinding schema;
   private SharedElements sharedElements = new SharedElements();
   private final List<Object> typeGroupStack = new ArrayList<Object>();

   // Ctors
   
   private XsdBinder()
   {
      this(new SchemaBinding());
   }

   private XsdBinder(SchemaBinding schema)
   {
      this.schema = schema;
   }

   // Public
   
   public void setProcessAnnotations(boolean processAnnotations)
   {
      this.processAnnotations = processAnnotations;
   }

   public boolean isProcessAnnotations()
   {
      return processAnnotations;
   }

   public void setSchemaResolver(SchemaBindingResolver resolver)
   {
      this.resolver = resolver;
   }

   public SchemaBindingResolver getSchemaResolver()
   {
      return resolver;
   }

   public void setSimpleContentWithIdAsSimpleType(boolean simpleContentWithIdAsSimpleType)
   {
      this.simpleContentWithIdAsSimpleType = simpleContentWithIdAsSimpleType;
   }

   public boolean isSimpleContentWithIdAsSimpleType()
   {
      return simpleContentWithIdAsSimpleType;
   }

   public void setUnresolvedContentBoundToDOM(boolean toDOM)
   {
      this.unresolvedContentBoundToDOM = toDOM;      
   }
   
   public boolean isUnresolvedContentBoundToDOM()
   {
      return this.unresolvedContentBoundToDOM;
   }
   
   public SchemaBinding parse(String xsdUrl)
   {
      if(resolver == null)
      {
         resolver = new MultiClassSchemaResolver();
      }

      XSModel model = Util.loadSchema(xsdUrl, resolver);
      return parse(model);
   }
   
   public SchemaBinding parse(InputStream xsdStream, String encoding)
   {
      if(resolver == null)
      {
         resolver = new MultiClassSchemaResolver();
      }

      XSModel model = Util.loadSchema(xsdStream, encoding, resolver);
      return parse(model);
   }
   
   public SchemaBinding parse(Reader xsdReader, String encoding)
   {
      if(resolver == null)
      {
         resolver = new MultiClassSchemaResolver();
      }
  
      XSModel model = Util.loadSchema(xsdReader, encoding, resolver);
      return parse(model);
   }

   
   // Private

   private SchemaBinding parse(XSModel model)
   {      
      schema.setSchemaResolver(resolver);

      // read annotations. for now just log the ones that are going to be used
      if (processAnnotations)
      {
         XSObjectList annotations = model.getAnnotations();
         if (trace)
         {
            log.trace("started binding schema " + schema);
            log.trace("Schema annotations: " + annotations.getLength());
         }

         for(int i = 0; i < annotations.getLength(); ++i)
         {
            XSAnnotation annotation = (XSAnnotation)annotations.item(i);
            XsdAnnotation an = XsdAnnotation.unmarshal(annotation.getAnnotationString());
            XsdAppInfo appinfo = an.getAppInfo();
            if(appinfo != null)
            {
               SchemaMetaData schemaBindings = appinfo.getSchemaMetaData();
               if(schemaBindings != null)
               {
                  // Get the ignoreUnresolvedFieldOrClass
                  schema.setIgnoreUnresolvedFieldOrClass(schemaBindings.isIgnoreUnresolvedFieldOrClass());
                  // Get the ignoreUnresolvedFieldOrClass
                  schema.setReplacePropertyRefs(schemaBindings.isReplacePropertyRefs());
                  // Get the default package
                  PackageMetaData packageMetaData = schemaBindings.getPackage();
                  if(packageMetaData != null)
                  {
                     if (trace)
                        log.trace("schema default package: " + packageMetaData.getName());
                     schema.setPackageMetaData(packageMetaData);
                  }
               }
            }
         }
      }

      StringList namespaceList = model.getNamespaces();
      Set<String> namespaces = new LinkedHashSet<String>(namespaceList.getLength());
      for (int i = 0; i < namespaceList.getLength(); ++i)
         namespaces.add(namespaceList.item(i));
      schema.setNamespaces(namespaces);
      
      XSNamedMap groups = model.getComponents(XSConstants.MODEL_GROUP_DEFINITION);
      if (trace)
         log.trace("Model groups: " + groups.getLength());
      // First make sure we bind all the groups
      for(int i = 0; i < groups.getLength(); ++i)
      {
         XSModelGroupDefinition groupDef = (XSModelGroupDefinition)groups.item(i);
         bindGlobalGroup(groupDef);
      }
      // Now bind the particles which may have references to the other groups
      for(int i = 0; i < groups.getLength(); ++i)
      {
         XSModelGroupDefinition groupDef = (XSModelGroupDefinition)groups.item(i);
         bindGlobalGroupParticles(groupDef.getModelGroup());
      }

      XSNamedMap types = model.getComponents(XSConstants.TYPE_DEFINITION);
      if (trace)
         log.trace("Model types: " + types.getLength());
      for(int i = 0; i < types.getLength(); ++i)
      {
         XSTypeDefinition type = (XSTypeDefinition)types.item(i);
         if(!Constants.NS_XML_SCHEMA.equals(type.getNamespace()))
         {
            bindType(type);
         }
      }

      XSNamedMap elements = model.getComponents(XSConstants.ELEMENT_DECLARATION);
      if (trace)
         log.trace("Model elements: " + types.getLength());
      for(int i = 0; i < elements.getLength(); ++i)
      {
         XSElementDeclaration element = (XSElementDeclaration)elements.item(i);
         bindElement(element, 1, 0, false);
      }

      if(unresolvedContentBoundToDOM)
      {
         schema.setUnresolvedContentBoundToDOM(unresolvedContentBoundToDOM);
      }
      
      if (trace)
      {
         log.trace("finished binding schema " + schema);
      }

      return schema;
   }

   // Private
   
   private TypeBinding bindType(XSTypeDefinition type)
   {
      TypeBinding binding;
      switch(type.getTypeCategory())
      {
         case XSTypeDefinition.SIMPLE_TYPE:
            binding = bindSimpleType((XSSimpleTypeDefinition)type);
            break;
         case XSTypeDefinition.COMPLEX_TYPE:
            binding = bindComplexType((XSComplexTypeDefinition)type);
            break;
         default:
            throw new JBossXBRuntimeException("Unexpected type category: " + type.getTypeCategory());
      }
      return binding;
   }

   private TypeBinding bindSimpleType(XSSimpleTypeDefinition type)
   {
      QName typeName = type.getName() == null ? null : new QName(type.getNamespace(), type.getName());
      TypeBinding binding = typeName == null ? null : schema.getType(typeName);
      if(binding != null)
      {
         return binding;
      }

      if(trace)
      {
         log.trace("binding simple type " + typeName);
      }

      XSTypeDefinition baseTypeDef = type.getBaseType();
      TypeBinding baseType = baseTypeDef == null ? null : bindType(baseTypeDef);
      if(baseType == null)
      {
         binding = new TypeBinding(typeName);
      }
      else
      {
         binding = new TypeBinding(typeName, baseType);
         if(Constants.NS_XML_SCHEMA.equals(baseTypeDef.getNamespace()))
            binding.setCharactersHandler(DefaultHandlers.CHARACTERS_HANDLER_FACTORY.newCharactersHandler());
      }

      StringList strList = type.getLexicalPattern();
      if(strList != null && strList.getLength() > 0)
      {
         for(int i = 0; i < strList.getLength(); ++i)
         {
            binding.addLexicalPattern(strList.item(i));
         }
      }

      strList = type.getLexicalEnumeration();
      if(strList != null && strList.getLength() > 0)
      {
         for(int i = 0; i < strList.getLength(); ++i)
         {
            binding.addEnumValue(strList.item(i));
         }
      }

      if(type.getItemType() != null)
      {
         TypeBinding itemType = bindSimpleType(type.getItemType());
         binding.setItemType(itemType);
      }

      if(typeName != null)
      {
         schema.addType(binding);
      }

      if(trace)
      {
         String msg = typeName == null ? "bound simple anonymous type" : "bound simple type " + typeName;
         if(baseType != null)
         {
            msg += " inherited binding metadata from " + baseType.getQName();
         }
         log.trace(msg);
      }

      // customize binding with annotations
      if(processAnnotations)
      {
         XSObjectList annotations = type.getAnnotations();
         if(annotations != null)
         {
            if(trace)
            {
               log.trace(typeName + " annotations " + annotations.getLength());
            }
            for(int i = 0; i < annotations.getLength(); ++i)
            {
               XSAnnotation an = (XSAnnotation)annotations.item(i);
               XsdAnnotation xsdAn = XsdAnnotation.unmarshal(an.getAnnotationString());
               XsdAppInfo appInfo = xsdAn.getAppInfo();
               if(appInfo != null)
               {
                  ClassMetaData classMetaData = appInfo.getClassMetaData();
                  if(classMetaData != null)
                  {
                     if(trace)
                     {
                        log.trace("simple type " +
                           type.getName() +
                           ": impl=" +
                           classMetaData.getImpl());
                     }
                     binding.setClassMetaData(classMetaData);
                  }

                  ValueMetaData valueMetaData = appInfo.getValueMetaData();
                  if(valueMetaData != null)
                  {
                     if(trace)
                     {
                        log.trace("simple type " +
                           type.getName() +
                           ": unmarshalMethod=" +
                           valueMetaData.getUnmarshalMethod() +
                           ", marshalMethod=" +
                           valueMetaData.getMarshalMethod());
                     }
                     binding.setValueMetaData(valueMetaData);
                  }
               }
            }
         }
      }

      binding.setSchemaBinding(schema);

      return binding;
   }

   private TypeBinding bindComplexType(XSComplexTypeDefinition type)
   {
      QName typeName = type.getName() == null ? null : new QName(type.getNamespace(), type.getName());
      TypeBinding binding = typeName == null ? null : schema.getType(typeName);
      if(binding != null)
      {
         return binding;
      }

      XSTypeDefinition baseTypeDef = type.getBaseType();
      // anyType is the parent of all the types, even the parent of itself according to xerces :)
      TypeBinding baseType = null;
      if(baseTypeDef != null && !Constants.QNAME_ANYTYPE.equals(typeName))
      {
         baseType = bindType(baseTypeDef);
         // sometimes binding the base type can lead to another request
         // to bind the type being bound here
         if(typeName != null)
         {
            binding = schema.getType(typeName);
            if(binding != null)
            {
               return binding;
            }
         }
      }

      if (trace)
         log.trace("binding complex " + (typeName == null ? "anonymous type" : "type " + typeName));

      binding = new TypeBinding(typeName);
      binding.setBaseType(baseType);
      binding.setSimple(false);

      if(type.getSimpleType() != null)
      {
         TypeBinding simpleType = bindSimpleType(type.getSimpleType());
         binding.setSimpleType(simpleType);
      }
      else if(type.getContentType() == XSComplexTypeDefinition.CONTENTTYPE_MIXED)
      {
         TypeBinding stringType = schema.getType(Constants.QNAME_STRING);
         if(stringType == null)
         {
            throw new JBossXBRuntimeException("xsd:string has not been bound yet!");
         }
         binding.setSimpleType(stringType);
      }

      if(typeName != null)
      {
         schema.addType(binding);
      }

      binding.setSchemaBinding(schema);

      XSObjectList attrs = type.getAttributeUses();
      if (trace)
         log.trace(typeName + " attributes " + attrs.getLength());

      AttributeBinding attrBinding = null;
      boolean hasOnlyIdAttrs = true;
      for(int i = 0; i < attrs.getLength(); ++i)
      {
         XSAttributeUse attr = (XSAttributeUse)attrs.item(i);
         attrBinding = bindAttribute(attr);
         binding.addAttribute(attrBinding);
         if(hasOnlyIdAttrs && !Constants.QNAME_ID.equals(attrBinding.getType().getQName()))
         {
            hasOnlyIdAttrs = false;
         }
      }
      
      // customize binding with xsd annotations
      if (processAnnotations)
      {
         XSObjectList annotations = type.getAnnotations();
         if(annotations != null)
         {
            if (trace)
               log.trace(typeName + " annotations " + annotations.getLength());
            for(int i = 0; i < annotations.getLength(); ++i)
            {
               XSAnnotation an = (XSAnnotation)annotations.item(i);
               XsdAnnotation xsdAn = XsdAnnotation.unmarshal(an.getAnnotationString());
               XsdAppInfo appInfo = xsdAn.getAppInfo();
               if(appInfo != null)
               {
                  ClassMetaData classMetaData = appInfo.getClassMetaData();
                  if(classMetaData != null)
                  {
                     if (trace)
                     {
                        log.trace("complex type " +
                           type.getName() +
                           ": impl=" +
                           classMetaData.getImpl()
                        );
                     }
                     binding.setClassMetaData(classMetaData);
                  }

                  CharactersMetaData charactersMetaData = appInfo.getCharactersMetaData();
                  if(charactersMetaData != null)
                  {
                     if (trace)
                     {
                        PropertyMetaData propertyMetaData = charactersMetaData.getProperty();
                        if(propertyMetaData != null)
                        {
                           log.trace("complex type " +
                              type.getName() +
                              ": characters bound to " + propertyMetaData.getName()
                           );
                        }

                        ValueMetaData valueMetaData = charactersMetaData.getValue();
                        if(valueMetaData != null)
                        {
                           log.trace("complex type " +
                              type.getName() +
                              ": characters unmarshalMethod=" +
                              valueMetaData.getUnmarshalMethod() +
                              ", marshalMethod=" + valueMetaData.getMarshalMethod()
                           );
                        }

                        boolean mapEntryKey = appInfo.isMapEntryKey();
                        if(mapEntryKey)
                        {
                           log.trace("complex type " +
                              type.getName() +
                              ": characters are bound as a key in a map entry"
                           );
                        }

                        boolean mapEntryValue = appInfo.isMapEntryValue();
                        if(mapEntryValue)
                        {
                           log.trace("complex type " +
                              type.getName() +
                              ": characters are bound as a value in a map entry"
                           );
                        }
                     }
                     binding.setCharactersMetaData(charactersMetaData);
                  }

                  MapEntryMetaData mapEntryMetaData = appInfo.getMapEntryMetaData();
                  if(mapEntryMetaData != null)
                  {
                     if (trace)
                     {
                        log.trace("complex type " +
                           type.getName() +
                           " is bound to a map entry: impl=" +
                           mapEntryMetaData.getImpl() +
                           ", getKeyMethod=" +
                           mapEntryMetaData.getGetKeyMethod() +
                           ", setKeyMethod=" +
                           mapEntryMetaData.getSetKeyMethod() +
                           ", getValueMethod=" +
                           mapEntryMetaData.getGetValueMethod() +
                           ", setValueMethod=" +
                           mapEntryMetaData.getSetValueMethod() +
                           ", valueType=" +
                           mapEntryMetaData.getValueType() +
                           ", nonNullValue=" + mapEntryMetaData.isNonNullValue()
                        );
                     }

                     if(classMetaData != null)
                     {
                        throw new JBossXBRuntimeException("Illegal binding: both jbxb:class and jbxb:mapEntry are specified for complex type " +
                           type.getName()
                        );
                     }
                     binding.setMapEntryMetaData(mapEntryMetaData);
                  }

                  boolean skip = appInfo.isSkip();
                  if(skip)
                  {
                     if (trace)
                     {
                        log.trace("complex type " +
                           type.getName() +
                           ": elements of this type will be skipped; their attrs, character content " +
                           "and elements will be set the parent."
                        );
                     }
                     binding.setSkip(skip);
                  }

                  PropertyMetaData propertyMetaData = appInfo.getPropertyMetaData();
                  if(propertyMetaData != null)
                  {
                     if (trace)
                     {
                        log.trace("complex type " +
                           type.getName() +
                           ": the content of elements of this type is bound to property " + propertyMetaData.getName()
                        );
                     }
                     binding.setPropertyMetaData(propertyMetaData);
                  }

                  AddMethodMetaData addMethodMetaData = appInfo.getAddMethodMetaData();
                  if(addMethodMetaData != null)
                  {
                     if (trace)
                     {
                        log.trace("complex type " +
                           type.getName() +
                           ": elements of this type will be added to parent objects with addMethod=" +
                           addMethodMetaData.getMethodName() + ", valueType=" + addMethodMetaData.getValueType()
                        );
                     }
                     binding.setAddMethodMetaData(addMethodMetaData);
                  }
               }
            }
         }
      }

      XSParticle particle = type.getParticle();
      if(particle != null)
      {
         pushType(binding);
         bindParticle(particle);
         popType();
      }

      if(binding.getClassMetaData() == null &&
            simpleContentWithIdAsSimpleType &&
            particle == null && hasOnlyIdAttrs)
      {
         binding.setStartElementCreatesObject(false);
      }
      else
      {
         binding.setStartElementCreatesObject(true);
      }
      
      if(binding.hasOnlyXmlMimeAttributes())
      {
         addXOPInclude(binding, schema);
      }

      if(trace)
      {
         log.trace(typeName == null ? "bound complex anonymous type" : "bound complex type " + typeName);
      }

      return binding;
   }

   private AttributeBinding bindAttribute(XSAttributeUse attrUse)
   {
      XSAttributeDeclaration attr = attrUse.getAttrDeclaration();
      QName attrName = new QName(attr.getNamespace(), attr.getName());

      XSSimpleTypeDefinition attrType = attr.getTypeDefinition();
      TypeBinding typeBinding = bindSimpleType(attrType);

      if (trace)
      {
         log.trace("binding attribute " + attrName + ", required=" + attrUse.getRequired());
      }

      AttributeBinding binding = new AttributeBinding(schema, attrName, typeBinding, DefaultHandlers.ATTRIBUTE_HANDLER);
      binding.setRequired(attrUse.getRequired());
      if(attrUse.getConstraintType() == XSConstants.VC_DEFAULT)
      {
         // Associate the default value with the binding
         binding.setDefaultConstraint(attrUse.getConstraintValue());
      }

      if (processAnnotations)
      {
         XSAnnotation an = attr.getAnnotation();
         if(an != null)
         {
            if (trace)
            {
               log.trace(attrName + " attribute annotation");
            }

            XsdAnnotation xsdAn = XsdAnnotation.unmarshal(an.getAnnotationString());
            XsdAppInfo appInfo = xsdAn.getAppInfo();
            if(appInfo != null)
            {
               PropertyMetaData propertyMetaData = appInfo.getPropertyMetaData();
               if(propertyMetaData != null)
               {
                  binding.setPropertyMetaData(propertyMetaData);
               }

               boolean mapEntryKey = appInfo.isMapEntryKey();
               if(mapEntryKey)
               {
                  binding.setMapEntryKey(mapEntryKey);
               }

               boolean mapEntryValue = appInfo.isMapEntryValue();
               if(mapEntryValue)
               {
                  binding.setMapEntryValue(mapEntryValue);
               }
            }
         }
      }


      if (trace)
      {
         String msg = "bound attribute " + attrName;

         if(binding.getPropertyMetaData() != null)
         {
            msg += " property=" +
               binding.getPropertyMetaData().getName() +
               ", collectionType=" + binding.getPropertyMetaData().getCollectionType();
         }
         else if(binding.isMapEntryKey())
         {
            msg += "bound as a key in a map entry";
         }
         else if(binding.isMapEntryValue())
         {
            msg += "bound as a value in a map entry";
         }
         else
         {
            msg += " type=" + attrType.getName();
         }

         if(binding.getDefaultConstraint() != null)
         {
            msg += ", default=" + binding.getDefaultConstraint();
         }

         log.trace(msg);
      }
      
      return binding;
   }

   private void bindParticle(XSParticle particle)
   {
      ParticleBinding particleBinding = null;
      XSTerm term = particle.getTerm();
      switch(term.getType())
      {
         case XSConstants.MODEL_GROUP:
            XSModelGroup modelGroup = (XSModelGroup)term;
            // todo: investigate this
            if(modelGroup.getParticles().getLength() > 0)
            {
               ModelGroupBinding groupBinding = bindModelGroup(modelGroup);

               particleBinding = new ParticleBinding(groupBinding);
               particleBinding.setMaxOccursUnbounded(particle.getMaxOccursUnbounded());
               particleBinding.setMinOccurs(particle.getMinOccurs());
               particleBinding.setMaxOccurs(particle.getMaxOccurs());

               Object o = peekTypeOrGroup();
               if(o instanceof ModelGroupBinding)
               {
                  ModelGroupBinding parentGroup = (ModelGroupBinding)o;
                  parentGroup.addParticle(particleBinding);
                  if (trace)
                  {
                     log.trace("added " + groupBinding + " to " + parentGroup);
                  }
               }
               else if(o instanceof TypeBinding)
               {
                  TypeBinding typeBinding = (TypeBinding)o;
                  typeBinding.setParticle(particleBinding);
                  if (trace)
                  {
                     log.trace("added " + groupBinding + " to type " + typeBinding.getQName());
                  }
               }

               if (groupBinding.getParticles().isEmpty())
               {
                  pushModelGroup(groupBinding);
                  bindModelGroupParticles(modelGroup);
                  popModelGroup();
               }
            }
            break;
         case XSConstants.WILDCARD:
            particleBinding = bindWildcard(particle);
            break;
         case XSConstants.ELEMENT_DECLARATION:
            particleBinding = bindElement(
               (XSElementDeclaration)term,
               particle.getMinOccurs(),
               particle.getMaxOccurs(),
               particle.getMaxOccursUnbounded()
            );
            break;
         default:
            throw new IllegalStateException("Unexpected term type: " + term.getType());
      }
      
      if(particleBinding != null)
         particleBinding.getTerm().setRepeatableHandler(ValueListRepeatableParticleHandler.INSTANCE);
   }

   private ModelGroupBinding bindModelGroup(XSModelGroup modelGroup)
   {
      // Is this a global group?
      ModelGroupBinding groupBinding = sharedElements.getGlobalGroup(modelGroup);
      if (groupBinding != null)
         return groupBinding;
      
      switch(modelGroup.getCompositor())
      {
         case XSModelGroup.COMPOSITOR_ALL:
            groupBinding = new AllBinding(schema);
            break;
         case XSModelGroup.COMPOSITOR_CHOICE:
            groupBinding = new ChoiceBinding(schema);
            break;
         case XSModelGroup.COMPOSITOR_SEQUENCE:
            groupBinding = new SequenceBinding(schema);
            break;
         default:
            throw new JBossXBRuntimeException("Unexpected model group: " + modelGroup.getCompositor());
      }

      if (trace)
         log.trace("created model group " + groupBinding);

      if (processAnnotations)
      {
         XSAnnotation annotation = modelGroup.getAnnotation();
         if (annotation != null)
            customizeTerm(annotation, groupBinding, trace);
      }
      return groupBinding;
   }

   private ParticleBinding bindWildcard(XSParticle particle)
   {
      WildcardBinding binding = new WildcardBinding(schema);

      ModelGroupBinding group = (ModelGroupBinding)peekTypeOrGroup();
      ParticleBinding particleBinding = new ParticleBinding(binding);
      particleBinding.setMaxOccurs(particle.getMaxOccurs());
      particleBinding.setMaxOccursUnbounded(particle.getMaxOccursUnbounded());
      particleBinding.setMinOccurs(particle.getMinOccurs());
      group.addParticle(particleBinding);

      XSWildcard wildcard = (XSWildcard)particle.getTerm();
      if(wildcard.getName() != null)
      {
         binding.setQName(new QName(wildcard.getNamespace(), wildcard.getName()));
      }

      binding.setProcessContents(wildcard.getProcessContents());

      if (processAnnotations)
      {
         XSAnnotation annotation = wildcard.getAnnotation();
         if(annotation != null)
         {
            customizeTerm(annotation, binding, trace);
         }
      }
      return particleBinding;
   }

   private ParticleBinding bindElement(XSElementDeclaration elementDec,
                                       int minOccurs,
                                       int maxOccurs,
                                       boolean maxOccursUnbounded)
   {
      QName qName = new QName(elementDec.getNamespace(), elementDec.getName());

      ModelGroupBinding parentGroup = (ModelGroupBinding)peekTypeOrGroup();

      boolean global = elementDec.getScope() == XSConstants.SCOPE_GLOBAL;
      ElementBinding element = schema.getElement(qName);
      ParticleBinding particle;
      if(global && element != null)
      {
         particle = new ParticleBinding(element);
         if(parentGroup != null)
         {
            parentGroup.addParticle(particle);
         }

         particle.setMinOccurs(minOccurs);
         if(maxOccursUnbounded)
         {
            particle.setMaxOccursUnbounded(maxOccursUnbounded);
         }
         else
         {
            particle.setMaxOccurs(maxOccurs);
         }

         return particle;
      }

      TypeBinding type = null;

      boolean shared = sharedElements.isShared(elementDec);
      if(shared)
      {
         type = sharedElements.getTypeBinding(elementDec);
      }

      if(type == null)
      {
         type = bindType(elementDec.getTypeDefinition());
         if(shared)
         {
            sharedElements.setTypeBinding(elementDec, type);
         }
      }

      element = new ElementBinding(schema, qName, type);
      element.setNillable(elementDec.getNillable());
      particle = new ParticleBinding(element);
      particle.setMinOccurs(minOccurs);
      particle.setMaxOccurs(maxOccurs);
      particle.setMaxOccursUnbounded(maxOccursUnbounded);
      if(global)
      {
         schema.addElementParticle(particle);
      }

      if(parentGroup != null)
      {
         parentGroup.addParticle(particle);
         if (trace)
         {
            log.trace("Element " + element.getQName() + " added to " + parentGroup);
         }
      }

      if (trace)
      {
         TypeBinding parentType = peekType();
         QName parentQName = null;
         if (parentType != null)
            parentQName = parentType.getQName();
         log.trace("element: name=" +
            qName +
            ", type=" +
            type.getQName() +
            ", repeatable=" +
            particle.isRepeatable() +
            ", nillable=" +
            element.isNillable() +
            ", minOccurs=" + minOccurs +
            ", maxOccurs=" + (maxOccursUnbounded ? "unbounded" : "" + maxOccurs) +
            ", " + (global ? "global scope" : " owner type=" + parentQName)
         );
      }

      // customize element with annotations
      if (processAnnotations)
      {
         XSAnnotation an = elementDec.getAnnotation();
         if(an != null)
         {
            customizeTerm(an, element, trace);
         }
      }
      return particle;
   }

   private void bindModelGroupParticles(XSModelGroup modelGroup)
   {
      XSObjectList particles = modelGroup.getParticles();
      for(int i = 0; i < particles.getLength(); ++i)
      {
         XSParticle particle = (XSParticle)particles.item(i);
         bindParticle(particle);
      }
   }

   // Private

   private static void addXOPInclude(TypeBinding binding, SchemaBinding schema)
   {
      binding.setHandler(DefaultHandlers.XOP_HANDLER);
      if(binding.getParticle() != null)
      {
         throw new JBossXBRuntimeException(
            "XOP optimizable type has a particle which is unexpected, please, open a JIRA issue!"
         );
      }

      TypeBinding anyUriType = schema.getType(Constants.QNAME_ANYURI);
      if(anyUriType == null)
      {
         log.warn("Type " + Constants.QNAME_ANYURI + " not bound.");
      }

      TypeBinding xopIncludeType = new TypeBinding(new QName(Constants.NS_XOP_INCLUDE, "Include"));
      xopIncludeType.setSchemaBinding(schema);
      xopIncludeType.addAttribute(new QName("href"), anyUriType, DefaultHandlers.ATTRIBUTE_HANDLER);
      xopIncludeType.setHandler(new XOPIncludeHandler(binding));

      ElementBinding xopInclude = new ElementBinding(schema, new QName(Constants.NS_XOP_INCLUDE, "Include"), xopIncludeType);

      ParticleBinding particleBinding = new ParticleBinding(xopInclude);
      particleBinding.setMinOccurs(0);

      binding.addParticle(particleBinding);
   }

   private static void customizeTerm(XSAnnotation an, TermBinding term, boolean trace)
   {
      XsdAnnotation xsdAn = XsdAnnotation.unmarshal(an.getAnnotationString());
      XsdAppInfo appInfo = xsdAn.getAppInfo();
      if(appInfo != null)
      {
         Boolean skip = null;

         ClassMetaData classMetaData = appInfo.getClassMetaData();
         if(classMetaData != null)
         {
            if (trace)
            {
               String msg;
               if(term.isModelGroup())
               {
                  msg = term + " bound to ";
               }
               else if(term.isWildcard())
               {
                  msg = " wildcard bound to ";
               }
               else
               {
                  msg = "element: name=" + ((ElementBinding)term).getQName() + ", class=";
               }

               msg += classMetaData.getImpl();
               log.trace(msg);
            }
            term.setClassMetaData(classMetaData);
            skip = Boolean.FALSE;
         }

         PropertyMetaData propertyMetaData = appInfo.getPropertyMetaData();
         if(propertyMetaData != null)
         {
            if (trace)
            {
               String msg = term.isWildcard() || term.isModelGroup() ? term + " " : "element: name=" +
                  ((ElementBinding)term).getQName() + ", ";
               msg += " property=" +
                  propertyMetaData.getName() +
                  ", collectionType=" + propertyMetaData.getCollectionType();
               log.trace(msg);
            }
            term.setPropertyMetaData(propertyMetaData);
         }

         MapEntryMetaData mapEntryMetaData = appInfo.getMapEntryMetaData();
         if(mapEntryMetaData != null)
         {
            if(propertyMetaData != null)
            {
               String msg = "A term can be bound either as a property or as a map" +
                  " entry but not both: " +
                  (term.isModelGroup() ? term.toString() : ((ElementBinding)term).getQName().toString());
               throw new JBossXBRuntimeException(msg);
            }

            if (trace)
            {
               String msg = term.isWildcard() || term.isModelGroup() ? term.toString() : "element name=" +
                  ((ElementBinding)term).getQName();

               msg += " is bound to a map entry: impl=" +
                  mapEntryMetaData.getImpl() +
                  ", getKeyMethod=" +
                  mapEntryMetaData.getGetKeyMethod() +
                  ", setKeyMethod=" +
                  mapEntryMetaData.getSetKeyMethod() +
                  ", getValueMethod=" +
                  mapEntryMetaData.getGetValueMethod() +
                  ", setValueMethod=" +
                  mapEntryMetaData.getSetValueMethod() +
                  ", valueType=" +
                  mapEntryMetaData.getValueType() +
                  ", nonNullValue=" + mapEntryMetaData.isNonNullValue();
               log.trace(msg);
            }

            if(classMetaData != null)
            {
               String msg = "Invalid customization: both jbxb:class and jbxb:mapEntry are specified for term " +
                  (term.isWildcard() || term.isModelGroup() ? term.toString() : ((ElementBinding)term).getQName().toString());
               throw new JBossXBRuntimeException(msg);
            }
            term.setMapEntryMetaData(mapEntryMetaData);
            skip = Boolean.FALSE;
         }

         PutMethodMetaData putMethodMetaData = appInfo.getPutMethodMetaData();
         if(putMethodMetaData != null)
         {
            if (trace)
            {
               String msg = term.isWildcard() || term.isModelGroup() ? term.toString() : "element: name=" +
                  ((ElementBinding)term).getQName() + ",";

               msg += " putMethod=" +
                  putMethodMetaData.getName() +
                  ", keyType=" +
                  putMethodMetaData.getKeyType() +
                  ", valueType=" + putMethodMetaData.getValueType();
               log.trace(msg);
            }
            term.setPutMethodMetaData(putMethodMetaData);
         }

         AddMethodMetaData addMethodMetaData = appInfo.getAddMethodMetaData();
         if(addMethodMetaData != null)
         {
            if (trace)
            {
               String msg = term.isWildcard() || term.isModelGroup() ? term.toString() : "element: name=" +
                  ((ElementBinding)term).getQName() + ",";
               msg += " addMethod=" +
                  addMethodMetaData.getMethodName() +
                  ", valueType=" +
                  addMethodMetaData.getValueType() +
                  ", isChildType=" + addMethodMetaData.isChildType();
               log.trace(msg);
            }
            term.setAddMethodMetaData(addMethodMetaData);
         }

         ValueMetaData valueMetaData = appInfo.getValueMetaData();
         if(valueMetaData != null)
         {
            if (trace)
            {
               String msg = term.isWildcard() || term.isModelGroup() ? term.toString() : "element " +
                  ((ElementBinding)term).getQName();
               msg += ": unmarshalMethod=" + valueMetaData.getUnmarshalMethod();
               log.trace(msg);
            }
            term.setValueMetaData(valueMetaData);
            if(term.isElement())
            {
               ElementBinding e = (ElementBinding)term;
               TypeBinding currentType = e.getType();
               // that's not nice, i.e. creating another type with the same name but not adding it to SchemaBinding
               // but it would be wrong to do that in case of built-in types
               TypeBinding newType = new TypeBinding(currentType.getQName(), currentType);
               CharactersHandler ch = DefaultHandlers.CHARACTERS_HANDLER_FACTORY.newCharactersHandler(RtCharactersHandler.VALUE_METADATA_UNMARSHAL_HANDLER);
               newType.setCharactersHandler(ch);
               e.setType(newType);
            }
         }

         boolean mapEntryKey = appInfo.isMapEntryKey();
         if(mapEntryKey)
         {
            if (trace)
            {
               String msg = term.isWildcard() || term.isModelGroup() ? term.toString() : "element name=" +
                  ((ElementBinding)term).getQName();
               msg += ": is bound to a key in a map entry";
               log.trace(msg);
            }
            term.setMapEntryKey(mapEntryKey);
            skip = Boolean.FALSE;
         }

         boolean mapEntryValue = appInfo.isMapEntryValue();
         if(mapEntryValue)
         {
            if (trace)
            {
               String msg = term.isWildcard() || term.isModelGroup() ? term.toString() : "element name=" +
                  ((ElementBinding)term).getQName();
               msg += ": is bound to a value in a map entry";
               log.trace(msg);
            }
            term.setMapEntryValue(mapEntryValue);
            skip = Boolean.FALSE;
         }

         boolean skipAnnotation = appInfo.isSkip();
         if(skip != null)
         {
            term.setSkip(skip);
         }
         else if(skipAnnotation)
         {
            if (trace)
            {
               String msg = term.isWildcard() || term.isModelGroup() ? term.toString() : "element name=" +
                  ((ElementBinding)term).getQName();
               msg += ": will be skipped, it's attributes, character content and children will be set on the parent";
               log.trace(msg);
            }
            term.setSkip(skipAnnotation);
         }
      }
   }

   private void bindGlobalGroup(XSModelGroupDefinition groupDef)
   {
      QName groupName = new QName(groupDef.getNamespace(), groupDef.getName());
      XSModelGroup group = groupDef.getModelGroup();
      ModelGroupBinding groupBinding = bindModelGroup(group);
      groupBinding.setQName(groupName);
      sharedElements.addGlobalGroup(group, groupBinding);
      schema.addGroup(groupName, groupBinding);
   }

   private void bindGlobalGroupParticles(XSModelGroup group)
   {
      ModelGroupBinding groupBinding = sharedElements.getGlobalGroup(group);
      if (groupBinding.getParticles().isEmpty())
      {
         pushModelGroup(groupBinding);
         bindModelGroupParticles(group);
         popModelGroup();
      }
   }

   private void popType()
   {
      Object o = typeGroupStack.remove(typeGroupStack.size() - 1);
      if(!(o instanceof TypeBinding))
      {
         throw new JBossXBRuntimeException("Should have poped type binding but got " + o);
      }
   }

   private void pushType(TypeBinding binding)
   {
      typeGroupStack.add(binding);
   }

   private void popModelGroup()
   {
      Object o = typeGroupStack.remove(typeGroupStack.size() - 1);
      if(!(o instanceof ModelGroupBinding))
      {
         throw new JBossXBRuntimeException("Should have poped model group binding but got " + o);
      }
   }

   private void pushModelGroup(ModelGroupBinding binding)
   {
      typeGroupStack.add(binding);
   }

   private Object peekTypeOrGroup()
   {
      return typeGroupStack.isEmpty() ? null : typeGroupStack.get(typeGroupStack.size() - 1);
   }

   private TypeBinding peekType()
   {
      TypeBinding binding = null;
      for(ListIterator<Object> i = typeGroupStack.listIterator(typeGroupStack.size()); i.hasPrevious();)
      {
         Object o = i.previous();
         if(o instanceof TypeBinding)
         {
            binding = (TypeBinding)o;
            break;
         }
      }
      return binding;
   }

   // Inner

   private static final class SharedElements
   {
      private Map<XSElementDeclaration, TypeBinding> elements = Collections.emptyMap();
      private Map<XSModelGroup, ModelGroupBinding> globalGroups = Collections.emptyMap();
      
      public void add(XSElementDeclaration element)
      {
         switch(elements.size())
         {
            case 0:
               elements = Collections.singletonMap(element, null);
               break;
            case 1:
               elements = new HashMap<XSElementDeclaration, TypeBinding>(elements);
            default:
               elements.put(element, null);
         }
      }

      public boolean isShared(XSElementDeclaration element)
      {
         return elements.containsKey(element);
      }

      public TypeBinding getTypeBinding(XSElementDeclaration element)
      {
         return (TypeBinding)elements.get(element);
      }

      public void setTypeBinding(XSElementDeclaration element, TypeBinding type)
      {
         switch(elements.size())
         {
            case 0:
               elements = Collections.singletonMap(element, type);
               break;
            case 1:
               elements = new HashMap<XSElementDeclaration, TypeBinding>(elements);
            default:
               elements.put(element, type);
         }
      }
      
      public void addGlobalGroup(XSModelGroup group, ModelGroupBinding groupBinding)
      {
         switch(globalGroups.size())
         {
            case 0:
               globalGroups = Collections.singletonMap(group, groupBinding);
               break;
            case 1:
               globalGroups = new HashMap<XSModelGroup, ModelGroupBinding>(globalGroups);
            default:
               globalGroups.put(group, groupBinding);
         }
      }

      public ModelGroupBinding getGlobalGroup(XSModelGroup group)
      {
         return (ModelGroupBinding) globalGroups.get(group);
      }
   }
}
