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

package org.apache.axis.description;

import org.apache.axis.components.logger.LogFactory;
import org.apache.axis.utils.BeanPropertyDescriptor;
import org.apache.axis.utils.BeanUtils;
import org.apache.axis.utils.Messages;
import org.apache.axis.utils.cache.MethodCache;

import org.apache.commons.logging.Log;

import javax.xml.namespace.QName;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * 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 implements Serializable {
    public static final Class [] noClasses = new Class [] {};
    public static final Object[] noObjects = new Object[] {};
    
    /** A map of {class -> TypeDesc}} */
    private static Map classMap = Collections.synchronizedMap(new WeakHashMap());

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

    /** Can this instance search for metadata in parents of the type it describes? */
    private boolean canSearchParents = true;
    private boolean hasSearchedParents = false;
    
    /** My superclass TypeDesc */
    private TypeDesc parentDesc = null;

    protected static Log log =
        LogFactory.getLog(TypeDesc.class.getName());

    /**
     * Creates a new <code>TypeDesc</code> instance.  The type desc can search
     * the metadata of its type'sparent classes.
     *
     * @param javaClass a <code>Class</code> value
     */
    public TypeDesc(Class javaClass) {
        this(javaClass, true);
    }
    
    /**
     * Creates a new <code>TypeDesc</code> instance.
     *
     * @param javaClass a <code>Class</code> value
     * @param canSearchParents whether the type desc can search the metadata of
     * its type's parent classes.
     */
    public TypeDesc(Class javaClass, boolean canSearchParents) {
        this.javaClassRef = new WeakReference(javaClass);
        this.canSearchParents = canSearchParents;
        Class cls = javaClass.getSuperclass();
        if (cls != null && !cls.getName().startsWith("java.")) {
            parentDesc = getTypeDescForClass(cls);
        }        
    }

    /**
     * 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.  
     *
     * 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).
     *
     */
    public static TypeDesc getTypeDescForClass(Class cls)
    {
        // First see if we have one explicitly registered
        // or cached from previous lookup
        TypeDesc result = (TypeDesc)classMap.get(cls);

        if (result == null) {
            try {
                Method getTypeDesc = 
                    MethodCache.getInstance().getMethod(cls, 
                                                        "getTypeDesc", 
                                                        noClasses);
                if (getTypeDesc != null) {
                    result = (TypeDesc)getTypeDesc.invoke(null, noObjects);
                    if (result != null) {
                        classMap.put(cls, result);
                    }
                }
            } catch (Exception e) {
            }
        }
        
        return result;
    }

    /** WeakReference to the Java class for this type */
    private WeakReference javaClassRef = null;

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

    /** 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 = null;
    
    /** Are there any fields which are serialized as attributes? */
    private boolean _hasAttributes = false;

    /** Introspected property descriptors */
    private BeanPropertyDescriptor[] propertyDescriptors = null;
    /** Map with key = property descriptor name, value = descriptor */
    private Map propertyMap = null;

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

    public BeanPropertyDescriptor getAnyDesc() {
        return anyDesc;
    }

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

    public FieldDesc[] getFields(boolean searchParents) {
        // note that if canSearchParents is false, this is identical
        // to getFields(), because the parent type's metadata is off
        // limits for restricted types which are required to provide a
        // complete description of their content model in their own
        // metadata, per the XML schema rules for
        // derivation-by-restriction
        if (canSearchParents && searchParents && !hasSearchedParents) {
            // check superclasses if they exist
            if (parentDesc != null) {
                FieldDesc [] parentFields = parentDesc.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
                }
            }
            
            hasSearchedParents = true;
        }

        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()) {
                _hasAttributes = true;
            }
            fieldNameMap.put(field.getFieldName(), field);
        }
    }

    /**
     * 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
            // and we are allowed to look
            if (canSearchParents) {
                if (parentDesc != null) {
                    return parentDesc.getElementNameForField(fieldName);
                }
            }
        } else if (desc.isElement()) {
            return desc.getXmlName();
        } 
        return null;
    }
    
    /**
     * 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
            // and we are allowed to look
            if (canSearchParents) {
                if (parentDesc != null) {
                    return parentDesc.getAttributeNameForField(fieldName);
                }
            }
        } else if (!desc.isElement()) {
            QName ret = desc.getXmlName();
            if (ret == null) {
                ret = new QName("", fieldName);
            }
            return ret;
        }
        return null;
    }

    /**
     * Get the field name associated with this QName, but only if it's
     * marked as an element.
     * 
     * If the "ignoreNS" argument is true, just compare localNames.
     */
    public String getFieldNameForElement(QName qname, boolean ignoreNS)
    {
        // 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 (ignoreNS || qname.getNamespaceURI().
                                        equals(xmlName.getNamespaceURI())) {
                        result = field.getFieldName();
                        break;
                    }
                }
            }
        }
        
        // check superclasses if they exist
        // and we are allowed to look
        if (result == null && canSearchParents) {
            if (parentDesc != null) {
                result = parentDesc.getFieldNameForElement(qname, ignoreNS);
            }
        }

        // 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 && canSearchParents) {
            // check superclasses if they exist
            // and we are allowed to look
            if (parentDesc != null) {
                possibleMatch = parentDesc.getFieldNameForAttribute(qname);
            }
        }
        
        return possibleMatch;
    }

    /**
     * Get a FieldDesc by field name.
     */
    public FieldDesc getFieldByName(String name)
    {
        FieldDesc ret = (FieldDesc)fieldNameMap.get(name);
        if (ret == null && canSearchParents) {
            if (parentDesc != null) {
                ret = parentDesc.getFieldByName(name);
            }
        }
        return ret;
    }

    /**
     * Do we have any FieldDescs marked as attributes?
     */
    public boolean hasAttributes() {
        if (_hasAttributes)
            return true;
        
        if (canSearchParents) {
            if (parentDesc != null) {
                return parentDesc.hasAttributes();
            }
        }

        return false;
    }

    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) {
            makePropertyDescriptors();
        }
        return propertyDescriptors;
    }
    
    private synchronized void makePropertyDescriptors() {
        if (propertyDescriptors != null)
            return;

        // javaClassRef is a WeakReference. So, our javaClass may have been GC'ed.
        // Protect against this case...
        Class javaClass = (Class)javaClassRef.get();
        if (javaClass == null) {
            // could throw a RuntimeException, but instead log error and dummy up descriptors
            log.error(Messages.getMessage("classGCed"));
            propertyDescriptors = new BeanPropertyDescriptor[0];
            anyDesc = null;
            lookedForAny = true;
            return;
        }

        propertyDescriptors = BeanUtils.getPd(javaClass, this);
        if (!lookedForAny) {
            anyDesc = BeanUtils.getAnyContentPD(javaClass);
            lookedForAny = true;
        }
    }

    public BeanPropertyDescriptor getAnyContentDescriptor() {
        if (!lookedForAny) {
            // javaClassRef is a WeakReference. So, our javaClass may have been GC'ed.
            // Protect against this case...
            Class javaClass = (Class)javaClassRef.get();
            if (javaClass != null) {
                anyDesc = BeanUtils.getAnyContentPD(javaClass);
            }
            else {
                log.error(Messages.getMessage("classGCed"));
                anyDesc = null;
            }

            lookedForAny = true;
        }
        return anyDesc;
    }

    /**
     * Get/Cache the property descriptor map
     * @return Map with key=propertyName, value=descriptor
     */
    public Map getPropertyDescriptorMap() {
        synchronized (this) {
            // 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 descriptor = propertyDescriptors[i];
                propertyMap.put(descriptor.getName(), descriptor);
            }
        }
        return propertyMap;
    }
}
