/*
  * 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;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import javax.xml.namespace.QName;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.xerces.xs.StringList;
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.XSNamedMap;
import org.apache.xerces.xs.XSObject;
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.xb.binding.sunday.unmarshalling.SchemaBindingResolver;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

/**
 * @deprecated In favor of org.jboss.xb.binding.sunday.marshalling.MarshallerImpl.
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version <tt>$Revision: 2913 $</tt>
 */
public class XercesXsMarshaller
   extends AbstractMarshaller
{
   private Stack stack = new StackImpl();

   /**
    * ObjectModelProvider for this marshaller
    */
   private GenericObjectModelProvider provider;

   private Object root;

   /**
    * Whether NULL values should be ignored or marshalled as xsi:nil='1'
    */
   private boolean supportNil = true;

   private QName rootTypeQName;

   private SchemaBindingResolver schemaResolver;

   private XSModel model;

   private boolean ignoreUnresolvedWildcard;

   private XSAttributeUse currentAttribute;
   private XSTypeDefinition currentElementType;

   private String simpleContentProperty = "value";
   
   private MarshallingContext ctx = new MarshallingContext()
   {
      private ContentHandler ch;

      public boolean isAttributeRequired()
      {
         if(currentAttribute == null)
         {
            throw new JBossXBRuntimeException("There is no current attribute!");
         }
         return currentAttribute.getRequired();
      }

      public boolean isTypeComplex()
      {
         if(currentElementType == null)
         {
            throw new JBossXBRuntimeException("There is no current element!");
         }
         return currentElementType.getTypeCategory() == XSTypeDefinition.COMPLEX_TYPE;
      }

      public String getSimpleContentProperty()
      {
         return simpleContentProperty;
      }

      public ContentHandler getContentHandler()
      {
         if(ch == null)
         {
            ch = new ContentHandlerAdaptor();
         }
         return ch;
      }

      public NamespaceRegistry getNamespaceContext()
      {
         return nsRegistry;
      }
   };

   public String getSimpleContentProperty()
   {
      return simpleContentProperty;
   }

   public void setSimpleContentProperty(String simpleContentProperty)
   {
      this.simpleContentProperty = simpleContentProperty;
   }

   public boolean isIgnoreUnresolvedWildcard()
   {
      return ignoreUnresolvedWildcard;
   }

   public void setIgnoreUnresolvedWildcard(boolean ignoreUnresolvedWildcard)
   {
      this.ignoreUnresolvedWildcard = ignoreUnresolvedWildcard;
   }

   public SchemaBindingResolver getSchemaResolver()
   {
      return schemaResolver;
   }

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

   public QName getRootTypeQName()
   {
      return rootTypeQName;
   }

   public void setRootTypeQName(QName rootTypeQName)
   {
      this.rootTypeQName = rootTypeQName;
   }

   public boolean isSupportNil()
   {
      return supportNil;
   }

   public void setSupportNil(boolean supportNil)
   {
      this.supportNil = supportNil;
   }

   /**
    * Adds an attribute to the top most elements.
    * First, we check whether there is a namespace associated with the passed in prefix.
    * If the prefix was not declared, an exception is thrown.
    *
    * @param prefix    the prefix of the attribute to be declared
    * @param localName local name of the attribute
    * @param type      the type of the attribute
    * @param value     the value of the attribute
    */
   public void addAttribute(String prefix, String localName, String type, String value)
   {
      // todo addAttribute(String prefix, String localName, String type, String value)
   }

   // AbstractMarshaller implementation

   public void marshal(Reader xsdReader, ObjectModelProvider provider, Object root, Writer writer)
      throws IOException, SAXException, ParserConfigurationException
   {
      XSModel model = Util.loadSchema(xsdReader, null, schemaResolver);
      marshallInternal(provider, root, model, writer);
   }

   public void marshal(String xsdURL, ObjectModelProvider provider, Object root, Writer writer) throws IOException,
      SAXException
   {
      XSModel model = Util.loadSchema(xsdURL, schemaResolver);
      marshallInternal(provider, root, model, writer);
   }

   public void marshal(XSModel model, ObjectModelProvider provider, Object root, Writer writer) throws IOException,
      SAXException
   {
      marshallInternal(provider, root, model, writer);
   }

   private void marshallInternal(ObjectModelProvider provider, Object root, XSModel model, Writer writer)
      throws IOException, SAXException
   {
      if(model == null)
      {
         throw new JBossXBRuntimeException("XSModel is not available!");
      }

      this.model = model;
      this.provider = provider instanceof GenericObjectModelProvider ?
         (GenericObjectModelProvider)provider : new DelegatingObjectModelProvider(provider);

      this.root = root;

      content.startDocument();

      if(rootTypeQName != null)
      {
         if(rootQNames.isEmpty())
         {
            throw new JBossXBRuntimeException("If type name (" +
               rootTypeQName +
               ") for the root element is specified then the name for the root element is required!"
            );
         }
         QName rootQName = rootQNames.get(0);

         XSTypeDefinition type = model.getTypeDefinition(rootTypeQName.getLocalPart(),
            rootTypeQName.getNamespaceURI()
         );
         if(type == null)
         {
            throw new JBossXBRuntimeException("Global type definition is not found: " + rootTypeQName);
         }

         if(isArrayWrapper(type))
         {
            Object o = provider.getRoot(root, null, rootQName.getNamespaceURI(), rootQName.getLocalPart());
            stack.push(o);
            marshalComplexType(rootQName.getNamespaceURI(),
               rootQName.getLocalPart(),
               (XSComplexTypeDefinition)type,
               true,
               false
            );
            stack.pop();
         }
         else
         {
            Object o = provider.getRoot(root, null, rootQName.getNamespaceURI(), rootQName.getLocalPart());
            marshalElementOccurence(rootQName.getNamespaceURI(),
               rootQName.getLocalPart(),
               type,
               o,
               false,
               false,
               true
            );
         }
      }
      else if(rootQNames.isEmpty())
      {
         XSNamedMap components = model.getComponents(XSConstants.ELEMENT_DECLARATION);
         if(components.getLength() == 0)
         {
            throw new JBossXBRuntimeException("The schema doesn't contain global element declarations.");
         }

         for(int i = 0; i < components.getLength(); ++i)
         {
            XSElementDeclaration element = (XSElementDeclaration)components.item(i);
            Object o = provider.getRoot(root, null, element.getNamespace(), element.getName());
            marshalElementOccurence(element.getNamespace(),
               element.getName(),
               element.getTypeDefinition(),
               o,
               element.getNillable(),
               false,
               true
            );
         }
      }
      else
      {
         for(int i = 0; i < rootQNames.size(); ++i)
         {
            QName qName = rootQNames.get(i);
            XSElementDeclaration element = model.getElementDeclaration(qName.getLocalPart(), qName.getNamespaceURI());
            if(element == null)
            {
               XSNamedMap components = model.getComponents(XSConstants.ELEMENT_DECLARATION);
               String roots = "";
               for(int j = 0; j < components.getLength(); ++j)
               {
                  XSObject xsObject = components.item(j);
                  if(j > 0)
                  {
                     roots += ", ";
                  }
                  roots += "{" + xsObject.getNamespace() + "}" + xsObject.getName();
               }
               throw new IllegalStateException("Root element not found: " + qName + " among " + roots);
            }

            Object o = provider.getRoot(root, null, element.getNamespace(), element.getName());
            marshalElementOccurence(element.getNamespace(),
               element.getName(),
               element.getTypeDefinition(),
               o,
               element.getNillable(),
               false,
               true
            );
         }
      }

      content.endDocument();

      // version & encoding
      writeXmlVersion(writer);

      ContentWriter contentWriter = new ContentWriter(writer,
         propertyIsTrueOrNotSet(Marshaller.PROP_OUTPUT_INDENTATION)
      );
      content.handleContent(contentWriter);

      if(log.isTraceEnabled())
      {
         java.io.StringWriter traceWriter = new java.io.StringWriter();
         contentWriter = new ContentWriter(traceWriter,
            propertyIsTrueOrNotSet(Marshaller.PROP_OUTPUT_INDENTATION)
         );
         content.handleContent(contentWriter);
         log.trace("marshalled:\n" + traceWriter.getBuffer().toString());
      }
   }

   private boolean marshalElement(String elementNs, String elementLocal,
                                  XSTypeDefinition type,
                                  boolean optional,
                                  boolean nillable,
                                  boolean declareNs,
                                  boolean declareXsiType)
   {
      Object value = stack.peek();
      boolean result = value != null || value == null && (optional || nillable);
      boolean trace = log.isTraceEnabled() && result;
      if(trace)
      {
         String prefix = getPrefix(elementNs);
         log.trace("started element ns=" + elementNs + ", local=" + elementLocal + ", prefix=" + prefix);
      }

      if(value != null)
      {
         marshalElementType(elementNs, elementLocal, type, declareNs, nillable, declareXsiType);
      }
      else if(nillable)
      {
         writeNillable(elementNs, elementLocal, nillable);
      }

      if(trace)
      {
         log.trace("finished element ns=" + elementNs + ", local=" + elementLocal);
      }

      return result;
   }

   private void marshalElementType(String elementNs,
                                   String elementLocal,
                                   XSTypeDefinition type,
                                   boolean declareNs,
                                   boolean nillable,
                                   boolean declareXsiType)
   {
      switch(type.getTypeCategory())
      {
         case XSTypeDefinition.SIMPLE_TYPE:
            marshalSimpleType(elementNs,
               elementLocal,
               (XSSimpleTypeDefinition)type,
               declareNs,
               nillable,
               declareXsiType
            );
            break;
         case XSTypeDefinition.COMPLEX_TYPE:
            marshalComplexType(elementNs, elementLocal, (XSComplexTypeDefinition)type, declareNs, declareXsiType);
            break;
         default:
            throw new IllegalStateException("Unexpected type category: " + type.getTypeCategory());
      }
   }

   private void marshalSimpleType(String elementUri,
                                  String elementLocal,
                                  XSSimpleTypeDefinition type,
                                  boolean declareNs,
                                  boolean nillable,
                                  boolean declareXsiType)
   {
      Object value = stack.peek();
      if(value != null)
      {
         String prefix = getPrefix(elementUri);
         boolean genPrefix = prefix == null && elementUri != null && elementUri.length() > 0;
         if(genPrefix)
         {
            prefix = "ns_" + elementLocal;
         }

         AttributesImpl attrs = null;
         String typeName = type.getName();
         if(SimpleTypeBindings.XS_QNAME_NAME.equals(typeName) ||
            SimpleTypeBindings.XS_NOTATION_NAME.equals(typeName) ||
            type.getItemType() != null &&
            (SimpleTypeBindings.XS_QNAME_NAME.equals(type.getItemType().getName()) ||
            SimpleTypeBindings.XS_NOTATION_NAME.equals(type.getItemType().getName())
            )
         )
         {
            attrs = new AttributesImpl(5);
         }

         String marshalled = marshalCharacters(elementUri, prefix, type, value, attrs);

         if((declareNs || declareXsiType) && nsRegistry.size() > 0)
         {
            if(attrs == null)
            {
               attrs = new AttributesImpl(nsRegistry.size() + 1);
            }
            declareNs(attrs);
         }

         if(declareXsiType)
         {
            declareXsiType(type, attrs);
         }

         if(genPrefix)
         {
            if(attrs == null)
            {
               attrs = new AttributesImpl(1);
            }
            attrs.add(null, prefix, "xmlns:" + prefix, null, (String)elementUri);
         }

         String qName = prefixLocalName(prefix, elementLocal);

         content.startElement(elementUri, elementLocal, qName, attrs);
         content.characters(marshalled.toCharArray(), 0, marshalled.length());
         content.endElement(elementUri, elementLocal, qName);
      }
      else
      {
         writeNillable(elementUri, elementLocal, nillable);
      }
   }

   private void marshalComplexType(String elementNsUri,
                                   String elementLocalName,
                                   XSComplexTypeDefinition type,
                                   boolean declareNs,
                                   boolean declareXsiType)
   {
      Object o = stack.peek();
      XSParticle particle = type.getParticle();

      XSObjectList attributeUses = type.getAttributeUses();
      int attrsTotal = declareNs || declareXsiType ?
         nsRegistry.size() + attributeUses.getLength() + 1 :
         attributeUses.getLength();
      AttributesImpl attrs = attrsTotal > 0 ? new AttributesImpl(attrsTotal) : null;

      if(declareNs && nsRegistry.size() > 0)
      {
         declareNs(attrs);
      }

      String generatedPrefix = null;
      if(declareXsiType)
      {
         generatedPrefix = declareXsiType(type, attrs);
         if(generatedPrefix != null)
         {
            String typeNsWithGeneratedPrefix = type.getNamespace();
            declareNs(attrs, generatedPrefix, typeNsWithGeneratedPrefix);
            declareNamespace(generatedPrefix, typeNsWithGeneratedPrefix);
         }
      }

      String prefix = getPrefix(elementNsUri);
      boolean genPrefix = prefix == null && elementNsUri != null && elementNsUri.length() > 0;
      if(genPrefix)
      {
         // todo: it's possible that the generated prefix already mapped. this should be fixed
         prefix = "ns_" + elementLocalName;
         declareNamespace(prefix, elementNsUri);
         if(attrs == null)
         {
            attrs = new AttributesImpl(1);
         }
         attrs.add(null, prefix, "xmlns:" + prefix, null, elementNsUri);
      }

      for(int i = 0; i < attributeUses.getLength(); ++i)
      {
         currentAttribute = (XSAttributeUse)attributeUses.item(i);
         XSAttributeDeclaration attrDec = currentAttribute.getAttrDeclaration();
         String attrNs = attrDec.getNamespace();
         String attrLocal = attrDec.getName();
         Object attrValue = provider.getAttributeValue(o, ctx, attrNs, attrLocal);

         if(attrValue != null)
         {
            if(attrs == null)
            {
               attrs = new AttributesImpl(5);
            }

            String attrPrefix = null;
            if(attrNs != null)
            {
               attrPrefix = getPrefix(attrNs);
               if(attrPrefix == null && attrNs != null && attrNs.length() > 0)
               {
                  attrPrefix = "ns_" + attrLocal;
                  attrs.add(null, attrPrefix, "xmlns:" + attrPrefix, null, attrNs);
               }
            }

            String qName = attrPrefix == null || attrPrefix.length() == 0 ? attrLocal : attrPrefix + ":" + attrLocal;

            // todo: this is a quick fix for boolean pattern (0|1 or true|false) should be refactored
            XSSimpleTypeDefinition attrType = attrDec.getTypeDefinition();
            if(attrType.getItemType() != null)
            {
               XSSimpleTypeDefinition itemType = attrType.getItemType();
               if(Constants.NS_XML_SCHEMA.equals(itemType.getNamespace()))
               {
                  List<QName> list;
                  if(attrValue instanceof List)
                  {
                     list = (List<QName>)attrValue;
                  }
                  else if(attrValue.getClass().isArray())
                  {
                     list = Arrays.asList((QName[])attrValue);
                  }
                  else
                  {
                     throw new JBossXBRuntimeException("Expected value for list type is an array or " +
                        List.class.getName() +
                        " but got: " +
                        attrValue
                     );
                  }

                  if(Constants.QNAME_QNAME.getLocalPart().equals(itemType.getName()))
                  {
                     for(int listInd = 0; listInd < list.size(); ++listInd)
                     {
                        QName item = (QName)list.get(listInd);
                        String itemNs = item.getNamespaceURI();
                        if(itemNs != null && itemNs.length() > 0)
                        {
                           String itemPrefix;
                           if(itemNs.equals(elementNsUri))
                           {
                              itemPrefix = prefix;
                           }
                           else
                           {
                              itemPrefix = getPrefix(itemNs);
                              if(itemPrefix == null)
                              {
                                 itemPrefix = attrLocal + listInd;
                                 declareNs(attrs, itemPrefix, itemNs);
                              }
                           }
                           item = new QName(item.getNamespaceURI(), item.getLocalPart(), itemPrefix);
                           list.set(listInd, item);
                        }
                     }
                  }

                  attrValue = SimpleTypeBindings.marshalList(itemType.getName(), list, null);
               }
               else
               {
                  throw new JBossXBRuntimeException("Marshalling of list types with item types not from " +
                     Constants.NS_XML_SCHEMA + " is not supported."
                  );
               }
            }
            else if(attrType.getLexicalPattern().item(0) != null
               &&
               attrType.derivedFrom(Constants.NS_XML_SCHEMA,
                  Constants.QNAME_BOOLEAN.getLocalPart(),
                  XSConstants.DERIVATION_RESTRICTION
               ))
            {
               String item = attrType.getLexicalPattern().item(0);
               if(item.indexOf('0') != -1 && item.indexOf('1') != -1)
               {
                  attrValue = ((Boolean)attrValue).booleanValue() ? "1" : "0";
               }
               else
               {
                  attrValue = ((Boolean)attrValue).booleanValue() ? "true" : "false";
               }
            }
            else if(Constants.QNAME_QNAME.getNamespaceURI().equals(attrType.getNamespace()) &&
               Constants.QNAME_QNAME.getLocalPart().equals(attrType.getName()))
            {
               QName qNameValue = (QName)attrValue;

               String qNamePrefix = null;
               boolean declarePrefix = false;
               String ns = qNameValue.getNamespaceURI();
               if(ns != null && ns.length() > 0)
               {
                  qNamePrefix = getPrefix(ns);
                  if(qNamePrefix == null)
                  {
                     qNamePrefix = qNameValue.getPrefix();
                     if(qNamePrefix == null || qNamePrefix.length() == 0)
                     {
                        qNamePrefix = "ns_" + qNameValue.getLocalPart();
                     }
                     declareNs(attrs, qNamePrefix, ns);
                     nsRegistry.addPrefixMapping(qNamePrefix, ns);
                     declarePrefix = true;
                  }
               }

               attrValue = SimpleTypeBindings.marshalQName(qNameValue, nsRegistry);

               if(declarePrefix)
               {
                  nsRegistry.removePrefixMapping(qNamePrefix);
               }
            }
            else
            {
               attrValue = attrValue.toString();
            }

            attrs.add(attrNs,
               attrLocal,
               qName,
               attrDec.getTypeDefinition().getName(),
               attrValue.toString()
            );
         }
      }
      currentAttribute = null;

      String characters = null;
      if(type.getSimpleType() != null)
      {
         Object value = getSimpleContentValue(elementNsUri, elementLocalName, type);
         if(value != null)
         {
            XSSimpleTypeDefinition simpleType = type.getSimpleType();
            String typeName = simpleType.getName();
            if(attrs == null && (SimpleTypeBindings.XS_QNAME_NAME.equals(typeName) ||
               SimpleTypeBindings.XS_NOTATION_NAME.equals(typeName) ||
               simpleType.getItemType() != null &&
               (SimpleTypeBindings.XS_QNAME_NAME.equals(simpleType.getItemType().getName()) ||
               SimpleTypeBindings.XS_NOTATION_NAME.equals(simpleType.getItemType().getName())
               )
               )
            )
            {
               attrs = new AttributesImpl(5);
            }

            characters = marshalCharacters(elementNsUri, prefix, simpleType, value, attrs);
         }
      }

      String qName = prefixLocalName(prefix, elementLocalName);
      content.startElement(elementNsUri, elementLocalName, qName, attrs);

      if(particle != null)
      {
         marshalParticle(particle, false);
      }

      if(characters != null)
      {
         content.characters(characters.toCharArray(), 0, characters.length());
      }
      content.endElement(elementNsUri, elementLocalName, qName);

      if(genPrefix)
      {
         removePrefixMapping(prefix);
      }

      if(generatedPrefix != null)
      {
         removePrefixMapping(generatedPrefix);
      }
   }

   private boolean marshalParticle(XSParticle particle, boolean declareNs)
   {
      boolean marshalled;
      XSTerm term = particle.getTerm();
      Object o;
      Iterator<?> i;
      switch(term.getType())
      {
         case XSConstants.MODEL_GROUP:
            o = stack.peek();
            i = o != null && isRepeatable(particle) ? getIterator(o) : null;
            if(i != null)
            {
               marshalled = true;
               while(i.hasNext() && marshalled)
               {
                  Object value = i.next();
                  stack.push(value);
                  marshalled = marshalModelGroup(particle, declareNs);
                  stack.pop();
               }
            }
            else
            {
               marshalled = marshalModelGroup(particle, declareNs);
            }
            break;
         case XSConstants.WILDCARD:
            o = stack.peek();

            boolean popWildcardValue = false;
            ObjectLocalMarshaller marshaller = null;
            FieldToWildcardMapping mapping = (FieldToWildcardMapping)field2WildcardMap.get(o.getClass());
            if(mapping != null)
            {
               marshaller = mapping.marshaller;
               o = mapping.fieldInfo.getValue(o);
               stack.push(o);
               popWildcardValue = true;
            }

            i = o != null && isRepeatable(particle) ? getIterator(o) : null;
            if(i != null)
            {
               marshalled = true;
               while(i.hasNext() && marshalled)
               {
                  Object value = i.next();
                  marshalled = marshalWildcardOccurence(particle, marshaller, value, declareNs);
               }
            }
            else
            {
               marshalled = marshalWildcardOccurence(particle, marshaller, o, declareNs);
            }

            if(popWildcardValue)
            {
               stack.pop();
            }

            break;
         case XSConstants.ELEMENT_DECLARATION:
            XSElementDeclaration element = (XSElementDeclaration)term;
            XSTypeDefinition type = element.getTypeDefinition();
            o = getElementValue(element.getNamespace(), element.getName(), type);

            i = o != null && isRepeatable(particle) ? getIterator(o) : null;
            if(i != null)
            {
               marshalled = true;
               while(i.hasNext() && marshalled)
               {
                  Object value = i.next();
                  marshalled =
                     marshalElementOccurence(element.getNamespace(),
                        element.getName(),
                        type,
                        value,
                        element.getNillable(),
                        particle.getMinOccurs() == 0,
                        declareNs
                     );
               }
            }
            else
            {
               marshalled =
                  marshalElementOccurence(element.getNamespace(),
                     element.getName(),
                     type,
                     o,
                     element.getNillable(),
                     particle.getMinOccurs() == 0,
                     declareNs
                  );
            }
            break;
         default:
            throw new IllegalStateException("Unexpected term type: " + term.getType());
      }
      return marshalled;
   }

   private boolean marshalElementOccurence(String elementNs,
                                           String elementLocal,
                                           XSTypeDefinition type,
                                           Object value,
                                           boolean nillable,
                                           boolean optional,
                                           boolean declareNs)
   {
      boolean declareXsiType = false;
      QName xsiTypeQName = null;
      if(value != null)
      {
         xsiTypeQName = (QName)cls2TypeMap.get(value.getClass());
         if(xsiTypeQName != null &&
            !(type.getName().equals(xsiTypeQName.getLocalPart()) &&
            type.getNamespace().equals(xsiTypeQName.getNamespaceURI())
            ))
         {
            declareXsiType = true;
            if(log.isTraceEnabled())
            {
               log.trace(value.getClass() + " is mapped to xsi:type " + xsiTypeQName);
            }

            XSTypeDefinition xsiType = model.getTypeDefinition(xsiTypeQName.getLocalPart(),
               xsiTypeQName.getNamespaceURI()
            );

            if(xsiType == null)
            {
               log.warn("Class " +
                  value.getClass() +
                  " is mapped to type " +
                  xsiTypeQName +
                  " but the type is not found in schema."
               );
            }
            // todo should check derivation also, i.e. if(xsiType.derivedFrom())
            else
            {
               type = xsiType;
            }
         }
      }

      stack.push(value);
      boolean marshalled = marshalElement(elementNs,
         elementLocal,
         type,
         optional,
         nillable,
         declareNs,
         declareXsiType
      );
      stack.pop();

      return marshalled;
   }

   private boolean marshalWildcardOccurence(XSParticle particle,
                                            ObjectLocalMarshaller marshaller,
                                            Object value,
                                            boolean declareNs)
   {
      boolean marshalled = true;
      if(marshaller != null)
      {
         marshaller.marshal(ctx, value);
      }
      else
      {
         stack.push(value);
         marshalled = marshalWildcard(particle, declareNs);
         stack.pop();
      }
      return marshalled;
   }

   private boolean marshalWildcard(XSParticle particle, boolean declareNs)
   {
      XSWildcard wildcard = (XSWildcard)particle.getTerm();
      Object o = stack.peek();
      ClassMapping mapping = getClassMapping(o.getClass());
      if(mapping == null)
      {
         // todo: YAH (yet another hack)
         QName autoType = SimpleTypeBindings.typeQName(o.getClass());
         if(autoType != null)
         {
            String marshalled = SimpleTypeBindings.marshal(autoType.getLocalPart(), o, null);
            content.characters(marshalled.toCharArray(), 0, marshalled.length());
            return true;
         }
         else
         {
            if(ignoreUnresolvedWildcard)
            {
               log.warn("Failed to marshal wildcard. Class mapping not found for " +
                  o.getClass() +
                  "@" +
                  o.hashCode() +
                  ": " + o
               );
               return true;
            }
            else
            {
               throw new IllegalStateException("Failed to marshal wildcard. Class mapping not found for " +
                  o.getClass() +
                  "@" +
                  o.hashCode() +
                  ": " + o
               );
            }
         }
      }

      GenericObjectModelProvider parentProvider = this.provider;
      Object parentRoot = this.root;
      Stack parentStack = this.stack;
      XSModel parentModel = this.model;

      this.root = o;
      this.stack = new StackImpl();
      this.model = mapping.schemaUrl == null ? this.model : Util.loadSchema(mapping.schemaUrl, schemaResolver);
      if(mapping.provider != null)
      {
         this.provider = mapping.provider;
      }

      boolean marshalled;
      if(mapping.elementName != null)
      {
         XSElementDeclaration elDec = model.getElementDeclaration(mapping.elementName.getLocalPart(),
            mapping.elementName.getNamespaceURI()
         );

         if(elDec == null)
         {
            throw new JBossXBRuntimeException("Element " + mapping.elementName + " is not declared in the schema.");
         }

         Object elementValue = provider.getRoot(root, null, elDec.getNamespace(), elDec.getName());
         marshalled = marshalElementOccurence(elDec.getNamespace(),
            elDec.getName(),
            elDec.getTypeDefinition(),
            elementValue,
            elDec.getNillable(),
            particle.getMinOccurs() == 0,
            declareNs
         );
      }
      else if(mapping.typeName != null)
      {
         XSTypeDefinition typeDef = model.getTypeDefinition(mapping.typeName.getLocalPart(),
            mapping.typeName.getNamespaceURI()
         );

         if(typeDef == null)
         {
            List<QName> typeNames = new ArrayList<QName>();
            XSNamedMap types = model.getComponents(XSConstants.TYPE_DEFINITION);
            for(int i = 0; i < types.getLength(); ++i)
            {
               XSObject type = types.item(i);
               if(!Constants.NS_XML_SCHEMA.equals(type.getNamespace()))
               {
                  typeNames.add(new QName(type.getNamespace(), type.getName()));
               }
            }
            throw new JBossXBRuntimeException("Type " +
               mapping.typeName +
               " is not defined in the schema." +
               " Defined types are: " + typeNames
            );
         }

         Object elementValue = provider.getRoot(root, null, wildcard.getNamespace(), wildcard.getName());
         marshalled =
            marshalElementOccurence(wildcard.getNamespace(),
               wildcard.getName(),
               typeDef,
               elementValue,
               true,
               particle.getMinOccurs() == 0,
               declareNs
            );
      }
      else
      {
         throw new JBossXBRuntimeException("Class mapping for " +
            mapping.cls +
            " is associated with neither global element name nor global type name."
         );
      }

      this.root = parentRoot;
      this.provider = parentProvider;
      this.stack = parentStack;
      this.model = parentModel;

      return marshalled;
   }

   private boolean marshalModelGroup(XSParticle particle, boolean declareNs)
   {
      XSModelGroup modelGroup = (XSModelGroup)particle.getTerm();
      boolean marshalled;
      switch(modelGroup.getCompositor())
      {
         case XSModelGroup.COMPOSITOR_ALL:
            marshalled = marshalModelGroupAll(modelGroup.getParticles(), declareNs);
            break;
         case XSModelGroup.COMPOSITOR_CHOICE:
            marshalled = marshalModelGroupChoice(modelGroup.getParticles(), declareNs);
            break;
         case XSModelGroup.COMPOSITOR_SEQUENCE:
            marshalled = marshalModelGroupSequence(modelGroup.getParticles(), declareNs);
            break;
         default:
            throw new IllegalStateException("Unexpected compsitor: " + modelGroup.getCompositor());
      }
      return marshalled;
   }

   private boolean marshalModelGroupAll(XSObjectList particles, boolean declareNs)
   {
      boolean marshalled = false;
      for(int i = 0; i < particles.getLength(); ++i)
      {
         XSParticle particle = (XSParticle)particles.item(i);
         marshalled |= marshalParticle(particle, declareNs);
      }
      return marshalled;
   }

   private boolean marshalModelGroupChoice(XSObjectList particles, boolean declareNs)
   {
      boolean marshalled = false;
      Content mainContent = this.content;
      for(int i = 0; i < particles.getLength() && !marshalled; ++i)
      {
         XSParticle particle = (XSParticle)particles.item(i);
         this.content = new Content();
         marshalled = marshalParticle(particle, declareNs);
      }

      if(marshalled)
      {
         mainContent.append(this.content);
      }
      this.content = mainContent;

      return marshalled;
   }

   private boolean marshalModelGroupSequence(XSObjectList particles, boolean declareNs)
   {
      boolean marshalled = true;
      for(int i = 0; i < particles.getLength(); ++i)
      {
         XSParticle particle = (XSParticle)particles.item(i);
         marshalled &= marshalParticle(particle, declareNs);
      }
      return marshalled;
   }

   private String marshalCharacters(String elementUri,
                                    String elementPrefix,
                                    XSSimpleTypeDefinition type,
                                    Object value,
                                    AttributesImpl attrs)
   {
      String marshalled;
      if(type.getItemType() != null)
      {
         XSSimpleTypeDefinition itemType = type.getItemType();
         if(Constants.NS_XML_SCHEMA.equals(itemType.getNamespace()))
         {
            List<?> list;
            if(value instanceof List)
            {
               list = (List<?>)value;
            }
            else if(value.getClass().isArray())
            {
               list = asList(value);
            }
            else
            {
               // todo: qname are also not yet supported
               throw new JBossXBRuntimeException(
                  "Expected value for list type is an array or " + List.class.getName() + " but got: " + value
               );
            }

            marshalled = SimpleTypeBindings.marshalList(itemType.getName(), list, null);
         }
         else
         {
            throw new JBossXBRuntimeException("Marshalling of list types with item types not from " +
               Constants.NS_XML_SCHEMA + " is not supported."
            );
         }
      }
      else if(Constants.NS_XML_SCHEMA.equals(type.getNamespace()))
      {
         String typeName = type.getName();

         String prefix = null;
         boolean removePrefix = false;
         if(SimpleTypeBindings.XS_QNAME_NAME.equals(typeName) ||
            SimpleTypeBindings.XS_NOTATION_NAME.equals(typeName))
         {
            QName qName = (QName)value;
            if(qName.getNamespaceURI() != null && qName.getNamespaceURI().length() > 0)
            {
               prefix = nsRegistry.getPrefix(qName.getNamespaceURI());
               if(prefix == null)
               {
                  prefix = qName.getPrefix();
                  if(prefix == null || prefix.length() == 0)
                  {
                     prefix = qName.getLocalPart() + "_ns";
                  }
                  nsRegistry.addPrefixMapping(prefix, qName.getNamespaceURI());
                  declareNs(attrs, prefix, qName.getNamespaceURI());

                  removePrefix = true;
               }
            }
         }
         marshalled = SimpleTypeBindings.marshal(typeName, value, nsRegistry);

         if(removePrefix)
         {
            nsRegistry.removePrefixMapping(prefix);
         }
      }
      // todo: this is a quick fix for boolean pattern (0|1 or true|false) should be refactored
      else if(type.getLexicalPattern().item(0) != null
         &&
         type.derivedFrom(Constants.NS_XML_SCHEMA,
            Constants.QNAME_BOOLEAN.getLocalPart(),
            XSConstants.DERIVATION_RESTRICTION
         ))
      {
         String item = type.getLexicalPattern().item(0);
         if(item.indexOf('0') != -1 && item.indexOf('1') != -1)
         {
            marshalled = ((Boolean)value).booleanValue() ? "1" : "0";
         }
         else
         {
            marshalled = ((Boolean)value).booleanValue() ? "true" : "false";
         }
      }
      else
      {
         StringList lexicalEnumeration = type.getLexicalEnumeration();
         if(lexicalEnumeration != null && lexicalEnumeration.getLength() > 0)
         {
            Method getValue;
            try
            {
               getValue = value.getClass().getMethod("value", null);
            }
            catch(NoSuchMethodException e)
            {
               try
               {
                  getValue = value.getClass().getMethod("getValue", null);
               }
               catch(NoSuchMethodException e1)
               {
                  List<String> values = new ArrayList<String>(lexicalEnumeration.getLength());
                  for(int i = 0; i < lexicalEnumeration.getLength(); ++i)
                  {
                     values.add(lexicalEnumeration.item(i));
                  }

                  throw new JBossXBRuntimeException("Failed to find neither value() nor getValue() in " +
                     value.getClass() +
                     " which is bound to enumeration type (" +
                     type.getNamespace() +
                     ", " +
                     type.getName() + "): " + values
                  );
               }
            }

            try
            {
               value = getValue.invoke(value, null);
            }
            catch(Exception e)
            {
               throw new JBossXBRuntimeException(
                  "Failed to invoke getValue() on " + value + " to get the enumeration value", e
               );
            }
         }

         marshalled = marshalCharacters(elementUri,
            elementPrefix,
            (XSSimpleTypeDefinition)type.getBaseType(),
            value, attrs
         );
      }
      return marshalled;
   }

   /**
    * Adds xsi:type attribute and optionally declares namespaces for xsi and type's namespace.
    * @param type  the type to declare xsi:type attribute for
    * @param attrs  the attributes to add xsi:type attribute to
    * @return  prefix for the type's ns if it was generated
    */
   private String declareXsiType(XSTypeDefinition type, AttributesImpl attrs)
   {
      String result = null;
      String xsiPrefix = nsRegistry.getPrefix(Constants.NS_XML_SCHEMA_INSTANCE);
      if(xsiPrefix == null)
      {
         attrs.add(Constants.NS_XML_SCHEMA, "xmlns", "xmlns:xsi", null, Constants.NS_XML_SCHEMA_INSTANCE);
         xsiPrefix = "xsi";
      }

      String pref = getPrefix(type.getNamespace());
      if(pref == null)
      {
         // the ns is not declared
         result = pref = type.getName() + "_ns";
      }

      String typeQName = pref == null ? type.getName() : pref + ':' + type.getName();
      attrs.add(Constants.NS_XML_SCHEMA_INSTANCE, "type", xsiPrefix + ":type", null, typeQName);
      return result;
   }

   private Object getElementValue(String elementNs, String elementLocal, XSTypeDefinition type)
   {
      Object value;
      Object peeked = stack.isEmpty() ? root : stack.peek();
      if(peeked == null)
      {
         value = null;
      }
      else if(peeked instanceof Collection || peeked.getClass().isArray())
      {
         // collection is the provider
         value = peeked;
      }
      else
      {
         XSTypeDefinition parentType = currentElementType;
         currentElementType = type;

         value = provider.getChildren(peeked, ctx, elementNs, elementLocal);
         if(value == null)
         {
            value = provider.getElementValue(peeked, ctx, elementNs, elementLocal);
         }

         currentElementType = parentType;
      }
      return value;
   }

   private Object getSimpleContentValue(String elementNs, String elementLocal, XSTypeDefinition type)
   {
      Object value;
      Object peeked = stack.isEmpty() ? root : stack.peek();
      if(peeked == null)
      {
         value = null;
      }
      else
      {
         XSTypeDefinition parentType = currentElementType;
         currentElementType = type;
         value = provider.getElementValue(peeked, ctx, elementNs, elementLocal);
         currentElementType = parentType;
      }
      return value;
   }

   private void writeNillable(String elementNs, String elementLocal, boolean nillable)
   {
      if(!supportNil)
      {
         return;
      }

      if(!nillable)
      {
         throw new JBossXBRuntimeException("Failed to marshal " +
            new QName(elementNs, elementLocal) +
            ": Java value is null but the element is not nillable."
         );
      }

      AttributesImpl attrs;
      String prefix = getPrefix(elementNs);
      if(prefix == null && elementNs != null && elementNs.length() > 0)
      {
         prefix = "ns_" + elementLocal;
         attrs = new AttributesImpl(2);
         attrs.add(null, prefix, "xmlns:" + prefix, null, elementNs);
      }
      else
      {
         attrs = new AttributesImpl(1);
      }

      String xsiPrefix = getPrefix(Constants.NS_XML_SCHEMA_INSTANCE);
      if(xsiPrefix == null)
      {
         xsiPrefix = "xsi";
         attrs.add(null,
            xsiPrefix,
            "xmlns:xsi",
            null,
            Constants.NS_XML_SCHEMA_INSTANCE
         );
      }

      String nilQName = xsiPrefix + ":nil";
      attrs.add(Constants.NS_XML_SCHEMA_INSTANCE, "nil", nilQName, null, "1");

      String qName = prefixLocalName(prefix, elementLocal);
      content.startElement(elementNs, elementLocal, qName, attrs);
      content.endElement(elementNs, elementLocal, qName);
   }

   private static boolean isArrayWrapper(XSTypeDefinition type)
   {
      boolean is = false;
      if(XSTypeDefinition.COMPLEX_TYPE == type.getTypeCategory())
      {
         XSComplexTypeDefinition cType = (XSComplexTypeDefinition)type;
         XSParticle particle = cType.getParticle();
         if(particle != null)
         {
            is = particle.getMaxOccursUnbounded() || particle.getMaxOccurs() > 1;
         }
      }
      return is;
   }

   private Iterator<?> getIterator(Object value)
   {
      Iterator<?> i = null;
      if(value instanceof Collection)
      {
         i = ((Collection<?>)value).iterator();
      }
      else if(value.getClass().isArray())
      {
         final Object arr = value;
         i = new Iterator()
         {
            private int curInd = 0;
            private int length = Array.getLength(arr);

            public boolean hasNext()
            {
               return curInd < length;
            }

            public Object next()
            {
               return Array.get(arr, curInd++);
            }

            public void remove()
            {
               throw new UnsupportedOperationException("remove is not implemented.");
            }
         };
      }
      else if(value instanceof Iterator)
      {
         i = (Iterator<?>)value;
      }
      else
      {
         //throw new JBossXBRuntimeException("Unexpected type for children: " + value.getClass());
      }
      return i;
   }

   private static boolean isRepeatable(XSParticle particle)
   {
      return particle.getMaxOccursUnbounded() || particle.getMaxOccurs() > 1 || particle.getMinOccurs() > 1;
   }

   private static final List asList(final Object arr)
   {
      return new AbstractList()
      {
         private final Object array = arr;

         public Object get(int index)
         {
            return Array.get(array, index);
         }

         public int size()
         {
            return Array.getLength(array);
         }
      };
   }
}
