/*******************************************************************************
 * Copyright (c) 1998, 2013 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 *     05/16/2008-1.0M8 Guy Pelletier 
 *       - 218084: Implement metadata merging functionality between mapping files
 *     03/08/2010-2.1 Guy Pelletier 
 *       - 303632: Add attribute-type for mapping attributes to EclipseLink-ORM
 *     05/04/2010-2.1 Guy Pelletier 
 *       - 309373: Add parent class attribute to EclipseLink-ORM
 *     05/14/2010-2.1 Guy Pelletier 
 *       - 253083: Add support for dynamic persistence using ORM.xml/eclipselink-orm.xml
 *     01/25/2011-2.3 Guy Pelletier 
 *       - 333488: Serializable attribute being defaulted to a variable one to one mapping and causing exception
 *     07/16/2013-2.5.1 Guy Pelletier 
 *       - 412384: Applying Converter for parameterized basic-type for joda-time's DateTime does not work
 ******************************************************************************/  
package org.eclipse.persistence.internal.jpa.metadata.accessors.objects;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.libraries.asm.Opcodes;

/**
 * INTERNAL:
 * An object to hold onto a valid JPA decorated class.
 * 
 * @author Guy Pelletier
 * @since TopLink 10.1.3/EJB 3.0 Preview
 */
public class MetadataClass extends MetadataAnnotatedElement {
    protected boolean m_isLazy;
    protected boolean m_isAccessible;
    protected boolean m_isPrimitive;
    protected boolean m_isJDK;
    protected int m_modifiers;
    
    // Stores the implements interfaces of this class.
    protected List<String> m_interfaces;
    
    // Stores a list of enclosed classes found inside this metadata class. 
    // E.g. inner classes, enums etc.
    protected List<MetadataClass> m_enclosedClasses;
    
    // Store the classes field metadata, keyed by the field's name.
    protected Map<String, MetadataField> m_fields;
    
    // Store the classes method metadata, keyed by the method's name.
    // Method's next is used if multiple method with the same name.
    protected Map<String, MetadataMethod> m_methods;
    
    protected MetadataClass m_superclass;
    protected String m_superclassName;

    /**
     * Create the metadata class with the class name.
     */
    public MetadataClass(MetadataFactory factory, String name, boolean isLazy) {
        super(factory);
        setName(name);
        
        // By default, set the type to be the same as the name. The canonical
        // model generator relies on types which in most cases is the name, but
        // the generator resolves generic types a little differently to 
        // correctly generate model classes.
        setType(name); 
        
        m_isAccessible = true;
        m_isLazy = isLazy;
    }

    /**
     * Create the metadata class with the class name.
     */
    public MetadataClass(MetadataFactory factory, String name) {
        this(factory, name, false);
    }
    
    /**
     * Create the metadata class based on the class.
     * Mainly used for primitive defaults.
     */
    public MetadataClass(MetadataFactory factory, Class cls) {
        this(factory, cls.getName(), false);
        m_isPrimitive = cls.isPrimitive();
    }

    /**
     * INTERNAL:
     */
    public void addEnclosedClass(MetadataClass enclosedClass) {
        if (m_enclosedClasses == null) {
            m_enclosedClasses = new ArrayList<MetadataClass>();
        }
        
        m_enclosedClasses.add(enclosedClass);
    }
    
    /**
     * INTERNAL:
     */
    public void addField(MetadataField field) {
        if (m_fields == null) {
            m_fields = new HashMap<String, MetadataField>();
        }
        
        m_fields.put(field.getName(), field);
    }
    
    /**
     * INTERNAL:
     */
    public void addInterface(String interfaceName) {
        if (m_interfaces == null) {
            m_interfaces = new ArrayList<String>();
        }
        
        m_interfaces.add(interfaceName);
    }
    
    /**
     * INTERNAL:
     */
    public void addMethod(MetadataMethod method) {
        if (m_methods == null) {
            m_methods = new HashMap<String, MetadataMethod>();
        }
        
        m_methods.put(method.getName(), method);
    }
    
    /**
     * Allow comparison to Java classes and Metadata classes.
     */
    public boolean equals(Object object) {
        if (object instanceof Class) {
            if (getName() == null) {
                // Void's name is null.
                return ((Class)object).getName() == null;
            }
            
            return getName().equals(((Class)object).getName());
        }
        
        return super.equals(object);
    }
    
    /**
     * INTERNAL:
     * Return if this class is or extends, or super class extends the class.
     */
    public boolean extendsClass(Class javaClass) {
        return extendsClass(javaClass.getName());
    }
    
    /**
     * INTERNAL:
     * Return if this class is or extends, or super class extends the class.
     */
    public boolean extendsClass(String className) {
        if (getName() == null) {
            return className == null;
        }
        
        if (getName().equals(className)) {
            return true;
        }
        
        if (getSuperclassName() == null) {
            return false;
        }
        
        if (getSuperclassName().equals(className)) {
            return true;
        }
        
        return getSuperclass().extendsClass(className);
    }
    
    /**
     * INTERNAL:
     * Return if this class is or extends, or super class extends the interface.
     */
    public boolean extendsInterface(Class javaClass) {
        return extendsInterface(javaClass.getName());
    }
    
    /**
     * INTERNAL:
     * Return if this class is or extends, or super class extends the interface.
     */
    public boolean extendsInterface(String className) {
        if (getName() == null) {
            return false;
        }
        
        if (getName().equals(className)) {
            return true;
        }
        
        if (getInterfaces().contains(className)) {
            return true;
        }
        
        for (String interfaceName : getInterfaces()) {
            if (getMetadataClass(interfaceName).extendsInterface(className)) {
                return true;
            }
        }
        
        if (getSuperclassName() == null) {
            return false;
        }
        
        return getSuperclass().extendsInterface(className);
    }

    /**
     * INTERNAL:
     * Return the list of classes defined within this metadata class. E.g.
     * enums and inner classes.
     */
    public List<MetadataClass> getEnclosedClasses() {
        if (m_enclosedClasses == null) {
            m_enclosedClasses = new ArrayList<MetadataClass>();
        }
        
        return m_enclosedClasses;
    }
    
    /**
     * INTERNAL:
     * Return the field with the name.
     * Search for any declared or inherited field.
     */
    public MetadataField getField(String name) {
        return getField(name, true);
    }
    
    /**
     * INTERNAL:
     * Return the field with the name.
     * Search for any declared or inherited field.
     */
    public MetadataField getField(String name, boolean checkSuperClass) {
        MetadataField field = getFields().get(name);
        
        if (checkSuperClass && (field == null) && (getSuperclassName() != null)) {
            return getSuperclass().getField(name);
        }
        
        return field;
    }
    
    /**
     * INTERNAL:
     */
    public Map<String, MetadataField> getFields() {
        if (m_fields == null) {
            m_fields = new HashMap<String, MetadataField>();
     
            if (m_isLazy) {
                m_factory.getMetadataClass(getName(), false);
            }
        }
        
        return m_fields;
    }

    /**
     * INTERNAL:
     */
    public List<String> getInterfaces() {
        if (m_interfaces == null) {
            m_interfaces = new ArrayList<String>();
        }
        
        return m_interfaces;
    }
    
    /**
     * INTERNAL:
     * Return the method with the name and no arguments.
     */
    protected MetadataMethod getMethod(String name) {
        return getMethods().get(name);
    }

    /**
     * INTERNAL:
     * Return the method with the name and argument types.
     */
    public MetadataMethod getMethod(String name, Class[] arguments) {
        List<String> argumentNames = new ArrayList<String>(arguments.length);
        
        for (int index = 0; index < arguments.length; index++) {
            argumentNames.add(arguments[index].getName());
        }
        
        return getMethod(name, argumentNames);
    }

    /**
     * INTERNAL:
     * Return the method with the name and argument types (class names).
     */
    public MetadataMethod getMethod(String name, List<String> arguments) {
        return getMethod(name, arguments, true);
    }

    /**
     * INTERNAL:
     * Return the method with the name and argument types (class names).
     */
    public MetadataMethod getMethod(String name, List<String> arguments, boolean checkSuperClass) {
        MetadataMethod method = getMethods().get(name);
        
        while ((method != null) && !method.getParameters().equals(arguments)) {
            method = method.getNext();
        }
        
        if (checkSuperClass && (method == null) && (getSuperclassName() != null)) {
            return getSuperclass().getMethod(name, arguments);
        }
        
        return method;
    }
    
    /**
     * INTERNAL:
     * Return the method with the name and argument types (class names).
     */
    public MetadataMethod getMethod(String name, String[] arguments) {
        return getMethod(name, Arrays.asList(arguments));
    }
    
    /**
     * INTERNAL:
     * Return the method for the given property name.
     */
    public MetadataMethod getMethodForPropertyName(String propertyName) {
        MetadataMethod method;
        
        String leadingChar = String.valueOf(propertyName.charAt(0)).toUpperCase();
        String restOfName = propertyName.substring(1);
        
        // Look for a getPropertyName() method
        method = getMethod(Helper.GET_PROPERTY_METHOD_PREFIX.concat(leadingChar).concat(restOfName), new String[]{});
        
        if (method == null) {
            // Look for an isPropertyName() method
            method = getMethod(Helper.IS_PROPERTY_METHOD_PREFIX.concat(leadingChar).concat(restOfName), new String[]{});
        }
        
        if (method != null) {
            method.setSetMethod(method.getSetMethod(this));
        }
        
        return method;
    }
    
    /**
     * INTERNAL:
     */
    public Map<String, MetadataMethod> getMethods() {
        if (m_methods == null) {
            m_methods = new HashMap<String, MetadataMethod>();
            
            if (m_isLazy) {
                m_factory.getMetadataClass(getName(), false);
            }
        }
        return m_methods;
    }
    
    /**
     * INTERNAL:
     */
    public int getModifiers() {
        return m_modifiers;
    }
    
    /**
     * INTERNAL:
     */
    public MetadataClass getSuperclass() {
        if (m_superclass == null) {
            m_superclass = getMetadataClass(m_superclassName);
        }
        
        return m_superclass;
    }

    /**
     * INTERNAL:
     */
    public String getSuperclassName() {
        return m_superclassName;
    }
    
    /**
     * Return the ASM type name.
     */
    public String getTypeName() {
        if (isArray()) {
            return getName().replace('.', '/');
        } else if (isPrimitive()) {
            if (getName().equals("int")) {
                return "I";
            } else if (getName().equals("long")) {
                return "J";
            } else if (getName().equals("short")) {
                return "S";
            } else if (getName().equals("boolean")) {
                return "Z";
            } else if (getName().equals("float")) {
                return "F";
            } else if (getName().equals("double")) {
                return "D";
            } else if (getName().equals("char")) {
                return "C";
            } else if (getName().equals("byte")) {
                return "B";
            }
        }
        return "L" + getName().replace('.', '/') + ";";
    }
    
    /**
     * INTERNAL:
     * Return true is this class accessible to be found.
     */
    public boolean isAccessible() {
        return m_isAccessible;
    }
    
    /**
     * INTERNAL:
     * Return if this class is an array type.
     */
    public boolean isArray() {
        return (getName() != null) && (getName().charAt(0) == '[');
    }
    
    /**
     * INTERNAL:
     * Return if this is extends Collection.
     */
    public boolean isCollection() {
        return extendsInterface(Collection.class);
    }
    
    /**
     * INTERNAL:
     * Return if this is extends Enum.
     */
    public boolean isEnum() {
        return extendsClass(Enum.class);
    }
    
    /**
     * INTERNAL:
     * Return if this is an interface (super is null).
     */
    public boolean isInterface() {
        return (Opcodes.ACC_INTERFACE & m_modifiers) != 0;
    }
    
    /**
     * INTERNAL:
     * Return if this is a JDK (java/javax) class.
     */
    public boolean isJDK() {
        return m_isJDK;
    }
    
    /**
     * INTERNAL:
     */
    public boolean isLazy() {
        return m_isLazy;
    }
    
    /**
     * INTERNAL:
     * Return if this is extends List.
     */
    public boolean isList() {
        return extendsInterface(List.class);
    }
    
    /**
     * INTERNAL:
     * Return if this is extends Map.
     */
    public boolean isMap() {
        return extendsInterface(Map.class);
    }
    
    /**
     * INTERNAL:
     * Return if this is Object class.
     */
    public boolean isObject() {
        return getName().equals(Object.class.getName());
    }
    
    /**
     * INTERNAL:
     * Return if this is a primitive.
     */
    public boolean isPrimitive() {
        return m_isPrimitive;
    }
    
    /**
     * INTERNAL:
     * Return if this class extends Serializable or is an array type.
     */
    public boolean isSerializable() {
        if (isArray()) {
            return true;
        }
        
        return extendsInterface(Serializable.class);
    }
    
    /**
     * INTENAL:
     * Return true is this class is the Serializable.class interface.
     */
    public boolean isSerializableInterface() {
        return getName().equals(Serializable.class.getName());
    }
    
    /**
     * INTERNAL:
     * Return true if this extends Set.
     */
    public boolean isSet() {
        return extendsInterface(Set.class);
    }
    
    /**
     * INTERNAL:
     * Return if this is the void class.
     */
    public boolean isVoid() {
        return getName().equals(void.class.getName()) || getName().equals(Void.class.getName());
    }
    
    /**
     * INTERNAL:
     */
    public void setIsAccessible(boolean isAccessible) {
        m_isAccessible = isAccessible;
    }
    
    /**
     * INTERNAL:
     */
    public void setIsJDK(boolean isJDK) {
        m_isJDK = isJDK;
    }
    
    /**
     * INTERNAL:
     */
    public void setIsLazy(boolean isLazy) {
        m_isLazy = isLazy;
    }
    
    /**
     * INTERNAL:
     */
    public void setModifiers(int modifiers) {
        m_modifiers = modifiers;
    }

    /**
     * INTERNAL:
     */
    public void setName(String name) {
        super.setName(name);
        
        if ((!MetadataFactory.ALLOW_JDK) && (name.startsWith("java.") || name.startsWith("javax.")
                || name.startsWith("org.eclipse.persistence.internal."))) {
            setIsJDK(true);
        }
    }
    
    /**
     * INTERNAL:
     */
    public void setSuperclass(MetadataClass superclass) {
        m_superclass = superclass;
    } 
    
    /**
     * INTERNAL:
     */
    public void setSuperclassName(String superclass) {
        m_superclassName = superclass;
    }
}
