/*
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 2002-2003 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Axis" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.jboss.axis.description;

import org.jboss.axis.utils.BeanPropertyDescriptor;
import org.jboss.axis.utils.BeanUtils;
import org.jboss.axis.utils.ClassUtils;
import org.jboss.axis.utils.Messages;
import org.jboss.logging.Logger;

import javax.xml.namespace.QName;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;

/**
 * A TypeDesc represents a Java<->XML data binding.  It is essentially
 * a collection of FieldDescs describing how to map each field in a Java
 * class to XML.
 *
 * @author Glen Daniels (gdaniels@apache.org)
 */
public class TypeDesc
{
   // provide logging
   private final Logger log = Logger.getLogger(TypeDesc.class);

   public static final Class[] noClasses = new Class[]{};
   public static final Object[] noObjects = new Object[]{};

   /**
    * A map of class -> TypeDesc
    */
   private static Map classMap = new Hashtable();

   /**
    * Have we already introspected for the special "any" property desc?
    */
   private boolean lookedForAny;

   /**
    * The Java class for this type
    */
   private Class javaClass;

   /**
    * The XML type QName for this type
    */
   private QName xmlType;

   /**
    * The various fields in here
    */
   private FieldDesc[] fields;

   /**
    * A cache of FieldDescs by name
    */
   private HashMap fieldNameMap = new HashMap();

   /**
    * A cache of FieldDescs by Element QName
    */
   private HashMap fieldElementMap;

   /**
    * Are there any fields which are serialized as attributes?
    */
   private boolean hasAttributes;

   /**
    * Introspected property descriptors
    */
   private BeanPropertyDescriptor[] propertyDescriptors;

   /**
    * Map with key = property descriptor name, value = descriptor
    */
   private Map propertyMap;

   /**
    * Indication if this type has support for xsd:any.
    */
   private BeanPropertyDescriptor anyDesc;

   public TypeDesc(Class javaClass)
   {
      this.javaClass = javaClass;
   }

   /**
    * Static function to explicitly register a type description for
    * a given class.
    *
    * @param cls the Class we're registering metadata about
    * @param td  the TypeDesc containing the metadata
    */
   public static void registerTypeDescForClass(Class cls, TypeDesc td)
   {
      classMap.put(cls, td);
   }

   /**
    * Static function for centralizing access to type metadata for a
    * given class.
    * <p/>
    * This checks for a static getTypeDesc() method on the
    * class or _Helper class.
    * Eventually we may extend this to provide for external
    * metadata config (via files sitting in the classpath, etc).
    * <p/>
    * (Could introduce a cache here for speed as an optimization)
    */
   public static TypeDesc getTypeDescForClass(Class cls)
   {
      // First see if we have one explicitly registered
      TypeDesc result = (TypeDesc)classMap.get(cls);
      if (result != null)
      {
         return result;
      }

      try
      {
         Method getTypeDesc = null;
         try
         {
            getTypeDesc =
                    cls.getMethod("getTypeDesc", noClasses);
         }
         catch (NoSuchMethodException e)
         {
         }
         if (getTypeDesc == null)
         {
            // Look for a Helper Class
            Class helper = ClassUtils.forName(cls.getName() + "_Helper");
            try
            {
               getTypeDesc =
                       helper.getMethod("getTypeDesc", noClasses);
            }
            catch (NoSuchMethodException e)
            {
            }
         }
         if (getTypeDesc != null)
         {
            return (TypeDesc)getTypeDesc.invoke(null,
                    noObjects);
         }
      }
      catch (Exception e)
      {
      }
      return null;
   }

   public BeanPropertyDescriptor getAnyDesc()
   {
      return anyDesc;
   }

   /**
    * Obtain the current array of FieldDescs
    */
   public FieldDesc[] getFields()
   {
      return fields;
   }

   public FieldDesc[] getFields(boolean searchParents)
   {
      if (searchParents)
      {
         // check superclasses if they exist
         Class cls = javaClass.getSuperclass();
         if (cls != null && !cls.getName().startsWith("java."))
         {
            TypeDesc superDesc = getTypeDescForClass(cls);
            if (superDesc != null)
            {
               FieldDesc[] parentFields = superDesc.getFields(true);
// START FIX http://nagoya.apache.org/bugzilla/show_bug.cgi?id=17188
               if (parentFields != null)
               {
                  if (fields != null)
                  {
                     FieldDesc[] ret = new FieldDesc[parentFields.length + fields.length];
                     System.arraycopy(parentFields, 0, ret, 0, parentFields.length);
                     System.arraycopy(fields, 0, ret, parentFields.length, fields.length);
                     fields = ret;
                  }
                  else
                  {
                     FieldDesc[] ret = new FieldDesc[parentFields.length];
                     System.arraycopy(parentFields, 0, ret, 0, parentFields.length);
                     fields = ret;
                  }
               }
// END FIX http://nagoya.apache.org/bugzilla/show_bug.cgi?id=17188
            }
         }
      }

      return fields;
   }

   /**
    * Replace the array of FieldDescs, making sure we keep our convenience
    * caches in sync.
    */
   public void setFields(FieldDesc[] newFields)
   {
      fieldNameMap = new HashMap();
      fields = newFields;
      hasAttributes = false;
      fieldElementMap = null;

      for (int i = 0; i < newFields.length; i++)
      {
         FieldDesc field = newFields[i];
         if (field.isElement())
         {
            fieldNameMap.put(field.getFieldName(), field);
         }
         else
         {
            hasAttributes = true;
         }
      }
   }

   /**
    * Add a new FieldDesc, keeping the convenience fields in sync.
    */
   public void addFieldDesc(FieldDesc field)
   {
      if (field == null)
      {
         throw new IllegalArgumentException(Messages.getMessage("nullFieldDesc"));
      }

      int numFields = 0;
      if (fields != null)
      {
         numFields = fields.length;
      }
      FieldDesc[] newFields = new FieldDesc[numFields + 1];
      if (fields != null)
      {
         System.arraycopy(fields, 0, newFields, 0, numFields);
      }
      newFields[numFields] = field;
      fields = newFields;

      // Keep track of the field by name for fast lookup
      fieldNameMap.put(field.getFieldName(), field);

      if (!hasAttributes && !field.isElement())
         hasAttributes = true;
   }

   /**
    * Get the QName associated with this field, but only if it's
    * marked as an element.
    */
   public QName getElementNameForField(String fieldName)
   {
      FieldDesc desc = (FieldDesc)fieldNameMap.get(fieldName);
      if (desc == null)
      {
         // check superclasses if they exist
         Class cls = javaClass.getSuperclass();
         if (cls != null && !cls.getName().startsWith("java."))
         {
            TypeDesc superDesc = getTypeDescForClass(cls);
            if (superDesc != null)
            {
               return superDesc.getElementNameForField(fieldName);
            }
         }
      }
      else if (!desc.isElement())
      {
         return null;
      }
      return desc.getXmlName();
   }

   /**
    * Get the QName associated with this field, but only if it's
    * marked as an attribute.
    */
   public QName getAttributeNameForField(String fieldName)
   {
      FieldDesc desc = (FieldDesc)fieldNameMap.get(fieldName);
      if (desc == null)
      {
         // check superclasses if they exist
         Class cls = javaClass.getSuperclass();
         if (cls != null && !cls.getName().startsWith("java."))
         {
            TypeDesc superDesc = getTypeDescForClass(cls);
            if (superDesc != null)
            {
               return superDesc.getAttributeNameForField(fieldName);
            }
         }
      }
      else if (desc.isElement())
      {
         return null;
      }
      QName ret = desc.getXmlName();
      if (ret == null)
      {
         ret = new QName("", fieldName);
      }
      return ret;
   }

   /**
    * Get the field name associated with this QName, but only if it's
    * marked as an element.
    */
   public String getFieldNameForElement(QName qname)
   {
      // have we already computed the answer to this question?
      if (fieldElementMap != null)
      {
         String cached = (String)fieldElementMap.get(qname);
         if (cached != null) return cached;
      }

      String result = null;

      String localPart = qname.getLocalPart();

      // check fields in this class
      for (int i = 0; fields != null && i < fields.length; i++)
      {
         FieldDesc field = fields[i];
         if (field.isElement())
         {
            QName xmlName = field.getXmlName();
            if (localPart.equals(xmlName.getLocalPart()))
            {
               if (qname.getNamespaceURI().equals(xmlName.getNamespaceURI()))
               {
                  result = field.getFieldName();
                  break;
               }
            }
         }
      }

      // check superclasses if they exist
      if (result == null)
      {
         Class cls = javaClass.getSuperclass();
         if (cls != null && !cls.getName().startsWith("java."))
         {
            TypeDesc superDesc = getTypeDescForClass(cls);
            if (superDesc != null)
            {
               result = superDesc.getFieldNameForElement(qname);
            }
         }
      }

      // cache the answer away for quicker retrieval next time.
      if (result != null)
      {
         if (fieldElementMap == null) fieldElementMap = new HashMap();
         fieldElementMap.put(qname, result);
      }

      return result;
   }

   /**
    * Get the field name associated with this QName, but only if it's
    * marked as an attribute.
    */
   public String getFieldNameForAttribute(QName qname)
   {
      String possibleMatch = null;

      for (int i = 0; fields != null && i < fields.length; i++)
      {
         FieldDesc field = fields[i];
         if (!field.isElement())
         {
            // It's an attribute, so if we have a solid match, return
            // its name.
            if (qname.equals(field.getXmlName()))
            {
               return field.getFieldName();
            }
            // Not a solid match, but it's still possible we might match
            // the default (i.e. QName("", fieldName))
            if (qname.getNamespaceURI().equals("") &&
                    qname.getLocalPart().equals(field.getFieldName()))
            {
               possibleMatch = field.getFieldName();
            }
         }
      }

      if (possibleMatch == null)
      {
         // check superclasses if they exist
         Class cls = javaClass.getSuperclass();
         if (cls != null && !cls.getName().startsWith("java."))
         {
            TypeDesc superDesc = getTypeDescForClass(cls);
            if (superDesc != null)
            {
               possibleMatch = superDesc.getFieldNameForAttribute(qname);
            }
         }
      }

      return possibleMatch;
   }

   /**
    * Get a FieldDesc by field name.
    */
   public FieldDesc getFieldByName(String name)
   {
      FieldDesc ret = (FieldDesc)fieldNameMap.get(name);
      if (ret == null)
      {
         Class cls = javaClass.getSuperclass();
         if (cls != null && !cls.getName().startsWith("java."))
         {
            TypeDesc superDesc = getTypeDescForClass(cls);
            if (superDesc != null)
            {
               ret = superDesc.getFieldByName(name);
            }
         }
      }
      return ret;
   }

   /**
    * Do we have any FieldDescs marked as attributes?
    */
   public boolean hasAttributes()
   {
      return hasAttributes;
   }

   public QName getXmlType()
   {
      return xmlType;
   }

   public void setXmlType(QName xmlType)
   {
      this.xmlType = xmlType;
   }

   /**
    * Get/Cache the property descriptors
    *
    * @return PropertyDescriptor
    */
   public BeanPropertyDescriptor[] getPropertyDescriptors()
   {
      // Return the propertyDescriptors if already set.
      // If not set, use BeanUtils.getPd to get the property descriptions.
      //
      // Since javaClass is a generated class, there
      // may be a faster way to set the property descriptions than
      // using BeanUtils.getPd.  But for now calling getPd is sufficient.
      if (propertyDescriptors == null)
      {
         propertyDescriptors = BeanUtils.getPd(javaClass, this);
         if (!lookedForAny)
         {
            anyDesc = BeanUtils.getAnyContentPD(javaClass);
            lookedForAny = true;
         }
      }
      return propertyDescriptors;
   }

   /**
    * Set the property descriptors for this type, for example in a different order
    */
   public void setPropertyDescriptors(BeanPropertyDescriptor[] propertyDescriptors)
   {
      this.propertyDescriptors = propertyDescriptors;
      this.propertyMap = null;
   }

   public BeanPropertyDescriptor getAnyContentDescriptor()
   {
      if (!lookedForAny)
      {
         anyDesc = BeanUtils.getAnyContentPD(javaClass);
         lookedForAny = true;
      }
      return anyDesc;
   }

   /**
    * Get/Cache the property descriptor map
    *
    * @return Map with key=propertyName, value=descriptor
    */
   public Map getPropertyDescriptorMap()
   {
      // Return map if already set.
      if (propertyMap != null)
      {
         return propertyMap;
      }

      // Make sure properties exist
      if (propertyDescriptors == null)
      {
         getPropertyDescriptors();
      }

      // Build the map
      propertyMap = new HashMap();
      for (int i = 0; i < propertyDescriptors.length; i++)
      {
         BeanPropertyDescriptor bpd = propertyDescriptors[i];
         String bpName = bpd.getName();

         if ("class".equals(bpName) == false)
         {
            // TDI 13-Feb-2005 (hack allert)
            // Seen when running samples2docclient
            // fieldName == Person_2
            // bpName == person_2
            if (fieldNameMap.keySet().contains(bpName) == false)
            {
               log.debug("Cannot find field name '" + bpName + "' in: " + fieldNameMap.keySet());

               Iterator it = fieldNameMap.keySet().iterator();
               while (it.hasNext())
               {
                  String fieldName = (String)it.next();
                  if (fieldName.equalsIgnoreCase(bpName))
                  {
                     log.debug("Additionaly map BeanPropertyDescriptor to: '" + fieldName + "'");
                     propertyMap.put(fieldName, bpd);
                     break;
                  }
               }
            }
         }

         propertyMap.put(bpName, bpd);
      }

      return propertyMap;
   }
}