/*
 * Copyright (c) 2011, 2018 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 v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Rick Barkhouse - 2.2 - Initial implementation
package org.eclipse.persistence.jaxb.javamodel.oxm;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.xml.bind.JAXBElement;

import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.jaxb.compiler.XMLProcessor;
import org.eclipse.persistence.jaxb.javamodel.*;
import org.eclipse.persistence.jaxb.xmlmodel.JavaAttribute;
import org.eclipse.persistence.jaxb.xmlmodel.JavaType;
import org.eclipse.persistence.jaxb.xmlmodel.JavaType.JavaAttributes;
import org.eclipse.persistence.jaxb.xmlmodel.XmlAnyAttribute;
import org.eclipse.persistence.jaxb.xmlmodel.XmlAnyElement;
import org.eclipse.persistence.jaxb.xmlmodel.XmlAttribute;
import org.eclipse.persistence.jaxb.xmlmodel.XmlElement;
import org.eclipse.persistence.jaxb.xmlmodel.XmlElementRef;
import org.eclipse.persistence.jaxb.xmlmodel.XmlElements;
import org.eclipse.persistence.jaxb.xmlmodel.XmlInverseReference;
import org.eclipse.persistence.jaxb.xmlmodel.XmlJoinNodes;
import org.eclipse.persistence.jaxb.xmlmodel.XmlValue;

/**
 * INTERNAL:
 * <p>
 * <b>Purpose:</b> <code>JavaClass</code> implementation wrapping MOXy's <code>xmlmodel.JavaType</code>.
 * Used when bootstrapping a <code>DynamicJAXBContext</code> from XML Bindings.
 * </p>
 *
 * <p>
 * <b>Responsibilities:</b>
 * </p>
 * <ul>
 *    <li>Provide Class information from the underlying <code>JavaType</code>.</li>
 * </ul>
 *
 * @since EclipseLink 2.2
 *
 * @see org.eclipse.persistence.jaxb.javamodel.JavaClass
 * @see org.eclipse.persistence.jaxb.xmlmodel.JavaType
 */
public class OXMJavaClassImpl implements JavaClass {

    private JavaType javaType;
    private String javaName;
    private List<String> enumValues;
    private JavaModel javaModel;

    /**
     * Construct a new instance of <code>OXMJavaClassImpl</code>.
     *
     * @param aJavaType - the XJC <code>JavaType</code> to be wrapped.
     */
    public OXMJavaClassImpl(JavaType aJavaType) {
        this.javaType = aJavaType;
    }

    /**
     * Construct a new instance of <code>OXMJavaClassImpl</code>.
     *
     * @param aJavaTypeName - the name of the JavaType to create.
     */
    public OXMJavaClassImpl(String aJavaTypeName) {
        this.javaName = aJavaTypeName;
    }

    /**
     * Construct a new instance of <code>OXMJavaClassImpl</code>
     * representing a Java <code>enum</code>.
     *
     * @param aJavaTypeName - the name of the JavaType to create.
     * @param enumValues - the list of values for this <code>enum</code>.
     */
    public OXMJavaClassImpl(String aJavaTypeName, List<String> enumValues) {
        this.javaName = aJavaTypeName;
        this.enumValues = enumValues;
    }

    // ========================================================================

    /**
     * Return the "actual" type from a parameterized type.  For example, if this
     * <code>JavaClass</code> represents <code>List&lt;Employee</code>, this method will return the
     * <code>Employee</code> <code>JavaClass</code>.
     *
     * @return a <code>Collection</code> containing the actual type's <code>JavaClass</code>.
     */
    public Collection<JavaClass> getActualTypeArguments() {
        Object jType = null;
        if (this.javaType != null) {
            jType = this.javaType;
        } else {
            try {
                Class<?> jTypeClass = PrivilegedAccessHelper.getClassForName(this.javaName);
                jType = PrivilegedAccessHelper.newInstanceFromClass(jTypeClass);
            } catch (Exception e) {
                return new ArrayList<JavaClass>();
            }
        }

        ArrayList<JavaClass> argCollection = new ArrayList<JavaClass>();
        if (jType instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType) jType;
            Type[] params = pType.getActualTypeArguments();
            for (Type type : params) {
                if (type instanceof ParameterizedType) {
                    ParameterizedType pt = (ParameterizedType) type;
                    argCollection.add(this.javaModel.getClass(pt.getRawType().getClass()));
                } else if (type instanceof WildcardType) {
                    Type[] upperTypes = ((WildcardType) type).getUpperBounds();
                    if (upperTypes.length >0) {
                        Type upperType = upperTypes[0];
                        if (upperType instanceof Class<?>) {
                            argCollection.add(this.javaModel.getClass(upperType.getClass()));
                        }
                    }
                } else if (type instanceof Class<?>) {
                    argCollection.add(this.javaModel.getClass(type.getClass()));
                } else if (type instanceof GenericArrayType) {
                    Class<?> genericTypeClass = (Class<?>) ((GenericArrayType) type).getGenericComponentType();
                    genericTypeClass = java.lang.reflect.Array.newInstance(genericTypeClass, 0).getClass();
                    argCollection.add(this.javaModel.getClass(genericTypeClass.getClass()));
                }
            }
        }
        return argCollection;
    }

    /**
     * If this <code>JavaClass</code> is an array type, return the type of the
     * array components.
     *
     * @return always returns <code>null</code>, as <code>JavaTypes</code> do not represent arrays.
     */
    public JavaClass getComponentType() {
        return null;
    }

    /**
     * Return the <code>JavaConstructor</code> for this <code>JavaClass</code> that has the
     * provided parameter types.
     *
     * @param parameterTypes the parameter list used to identify the constructor.
     *
     * @return the <code>JavaConstructor</code> with the signature matching parameterTypes.
     */
    public JavaConstructor getConstructor(JavaClass[] parameterTypes) {
        return new OXMJavaConstructorImpl(this);
    }

    /**
     * Return all of the <code>JavaConstructors</code> for this JavaClass.
     *
     * @return A <code>Collection</code> containing this <code>JavaClass'</code> <code>JavaConstructors</code>.
     */
    public Collection<JavaConstructor> getConstructors() {
        ArrayList<JavaConstructor> constructors = new ArrayList<JavaConstructor>(1);
        constructors.add(new OXMJavaConstructorImpl(this));
        return constructors;
    }

    /**
     * Return this <code>JavaClass'</code> inner classes.
     *
     * @return always returns an empty <code>ArrayList</code> as <code>JavaTypes</code> do not represent inner classes.
     */
    public Collection<JavaClass> getDeclaredClasses() {
        return new ArrayList<JavaClass>();
    }

    /**
     * Return the declared <code>JavaConstructor</code> for this <code>JavaClass</code>.
     *
     * @return the <code>JavaConstructor</code> for this <code>JavaClass</code>.
     */
    public JavaConstructor getDeclaredConstructor(JavaClass[] parameterTypes) {
        return new OXMJavaConstructorImpl(this);
    }

    /**
     * Return all of the declared <code>JavaConstructors</code> for this <code>JavaClass</code>.
     *
     * @return A <code>Collection</code> containing this <code>JavaClass'</code> <code>JavaConstructors</code>.
     */
    public Collection<JavaConstructor> getDeclaredConstructors() {
        ArrayList<JavaConstructor> constructors = new ArrayList<JavaConstructor>(1);
        constructors.add(new OXMJavaConstructorImpl(this));
        return constructors;
    }

    /**
     * Return the declared <code>JavaField</code> for this <code>JavaClass</code>, identified
     * by <code>fieldName</code>.
     *
     * @param name the name of the <code>JavaField</code> to return.
     *
     * @return the <code>JavaField</code> named <code>fieldName</code> from this <code>JavaClass</code>.
     */
    public JavaField getDeclaredField(String name) {
        Collection<JavaField> allFields = getDeclaredFields();

        for (Iterator<JavaField> iterator = allFields.iterator(); iterator.hasNext();) {
            JavaField field = iterator.next();
            if (field.getName().equals(name)) {
                return field;
            }
        }

        return null;
    }

    /**
     * Return all of the declared <code>JavaFields</code> for this <code>JavaClass</code>.
     *
     * @return A <code>Collection</code> containing this <code>JavaClass'</code> <code>JavaFields</code>.
     */
    public Collection<JavaField> getDeclaredFields() {
        List<JavaField> fieldsToReturn = new ArrayList<JavaField>();

        if (this.enumValues != null) {
            for (Iterator<String> iterator = this.enumValues.iterator(); iterator.hasNext();) {
                fieldsToReturn.add(new OXMJavaFieldImpl(iterator.next(), JAVA_LANG_OBJECT, this));
            }
        } else {
            JavaAttributes javaAttributes = this.javaType.getJavaAttributes();
            if(null != javaAttributes) {
                List<JAXBElement<? extends JavaAttribute>> fields = javaAttributes.getJavaAttribute();

                for (Iterator<JAXBElement<? extends JavaAttribute>> iterator = fields.iterator(); iterator.hasNext();) {
                    JAXBElement<? extends JavaAttribute> jaxbElement = iterator.next();

                    JavaAttribute att = (JavaAttribute) jaxbElement.getValue();

                    if (att instanceof XmlElement) {
                        XmlElement xme = (XmlElement) att;
                        String fieldName = xme.getJavaAttribute();
                        String fieldType = xme.getType();
                        fieldsToReturn.add(new OXMJavaFieldImpl(fieldName, fieldType, this));
                    } else if (att instanceof XmlElements) {
                        XmlElements xmes = (XmlElements) att;
                        String fieldName = xmes.getJavaAttribute();
                        String fieldType = JAVA_LANG_OBJECT;
                        fieldsToReturn.add(new OXMJavaFieldImpl(fieldName, fieldType, this));
                    } else if (att instanceof XmlElementRef) {
                        XmlElementRef xmer = (XmlElementRef) att;
                        String fieldName = xmer.getJavaAttribute();
                        String fieldType = xmer.getType();
                        fieldsToReturn.add(new OXMJavaFieldImpl(fieldName, fieldType, this));
                    } else if (att instanceof XmlAttribute) {
                        XmlAttribute xma = (XmlAttribute) att;
                        String fieldName = xma.getJavaAttribute();
                        String fieldType = xma.getType();
                        fieldsToReturn.add(new OXMJavaFieldImpl(fieldName, fieldType, this));
                    } else if (att instanceof XmlValue) {
                        XmlValue xmv = (XmlValue) att;
                        String fieldName = xmv.getJavaAttribute();
                        String fieldType = xmv.getType();
                        fieldsToReturn.add(new OXMJavaFieldImpl(fieldName, fieldType, this));
                    } else if (att instanceof XmlAnyElement) {
                        XmlAnyElement xmae = (XmlAnyElement) att;
                        String fieldName = xmae.getJavaAttribute();
                        String fieldType = JAVA_LANG_OBJECT;
                        fieldsToReturn.add(new OXMJavaFieldImpl(fieldName, fieldType, this));
                    } else if (att instanceof XmlAnyAttribute) {
                        XmlAnyAttribute xmaa = (XmlAnyAttribute) att;
                        String fieldName = xmaa.getJavaAttribute();
                        String fieldType = JAVA_UTIL_MAP;
                        fieldsToReturn.add(new OXMJavaFieldImpl(fieldName, fieldType, this));
                    } else if (att instanceof XmlJoinNodes) {
                        XmlJoinNodes xmjn = (XmlJoinNodes) att;
                        String fieldName = xmjn.getJavaAttribute();
                        String fieldType = xmjn.getType();
                        fieldsToReturn.add(new OXMJavaFieldImpl(fieldName, fieldType, this));
                    } else if (att instanceof XmlInverseReference) {
                        XmlInverseReference xmir = (XmlInverseReference) att;
                        String fieldName = xmir.getJavaAttribute();
                        String fieldType = xmir.getType();
                        fieldsToReturn.add(new OXMJavaFieldImpl(fieldName, fieldType, this));
                    }
                }
            }
        }

        return fieldsToReturn;
    }

    /**
     * Return the declared <code>JavaMethod</code> for this <code>JavaClass</code>,
     * identified by <code>name</code>, with the signature matching <code>args</code>.
     *
     * @param name the name of the <code>JavaMethod</code> to return.
     * @param args the parameter list used to identify the method.
     *
     * @return always returns <code>null</code>, as <code>JavaTypes</code> do not have methods.
     */
    public JavaMethod getDeclaredMethod(String name, JavaClass[] args) {
        return null;
    }

    /**
     * Return all of the declared <code>JavaMethods</code> for this <code>JavaClass</code>.
     *
     * @return always returns an empty <code>ArrayList</code>, as <code>JavaTypes</code> do not have methods.
     */
    public Collection<JavaMethod> getDeclaredMethods() {
        return new ArrayList<JavaMethod>();
    }

    /**
     * Return the <code>JavaMethod</code> for this <code>JavaClass</code>,
     * identified by <code>name</code>, with the signature matching <code>args</code>.
     *
     * @param name the name of the <code>JavaMethod</code> to return.
     * @param args the parameter list used to identify the method.
     *
     * @return always returns <code>null</code>, as <code>JavaTypes</code> do not have methods.
     */
    public JavaMethod getMethod(String name, JavaClass[] args) {
        return null;
    }

    /**
     * Return all of the <code>JavaMethods</code> for this <code>JavaClass</code>.
     *
     * @return always returns an empty <code>ArrayList</code>, as <code>JavaTypes</code> do not have methods.
     */
    public Collection<JavaMethod> getMethods() {
        return new ArrayList<JavaMethod>();
    }

    /**
     * Returns the Java language modifiers for this <code>JavaClass</code>, encoded in an integer.
     *
     * @return always returns <code>0</code> as <code>JavaTypes</code> do not have modifiers.
     *
     * @see java.lang.reflect.Modifier
     */
    public int getModifiers() {
        return 0;
    }

    /**
     * Returns the name of this <code>JavaClass</code>.
     *
     * @return the <code>String</code> name of this <code>JavaClass</code>.
     */
    public String getName() {
        if (this.javaType != null) {
            return this.javaType.getName();
        }
        return this.javaName;
    }

    /**
     * Returns the <code>JavaPackage</code> that this <code>JavaClass</code> belongs to.
     *
     * @return the <code>JavaPackage</code> of this <code>JavaClass</code>.
     */
    public JavaPackage getPackage() {
        return new OXMJavaPackageImpl(getPackageName());
    }

    /**
     * Returns the package name of this <code>JavaClass</code>.
     *
     * @return the <code>String</code> name of this <code>JavaClass'</code> <code>JavaPackage</code>.
     */
    public String getPackageName() {
        int lastDotIndex = getQualifiedName().lastIndexOf(DOT);
        if (lastDotIndex == -1) {
            return EMPTY_STRING;
        }

        return getQualifiedName().substring(0, lastDotIndex);
    }

    /**
     * Returns the fully-qualified name of this <code>JavaClass</code>.
     *
     * @return the <code>String</code> name of this <code>JavaClass</code>.
     */
    public String getQualifiedName() {
        return getName();
    }

    /**
     * Returns the raw name of this <code>JavaClass</code>.  Array types will
     * have "[]" appended to the name.
     *
     * @return the <code>String</code> raw name of this <code>JavaClass</code>.
     */
    public String getRawName() {
        return getName();
    }

    /**
     * Returns the super class of this <code>JavaClass</code>.
     *
     * @return <code>JavaClass</code> representing the super class of this <code>JavaClass</code>.
     */
    public JavaClass getSuperclass() {
        if (this.javaModel == null) {
            return null;
        }
        if (this.javaType != null) {
            if (!(this.javaType.getSuperType().equals(XMLProcessor.DEFAULT))) {
                return this.javaModel.getClass(javaType.getSuperType());
            }
        }
        return this.javaModel.getClass(JAVA_LANG_OBJECT);
    }

    @Override
    public Type[] getGenericInterfaces() {
        return new Type[0];
    }

    public Type getGenericSuperclass() {
        return null;
    }

    /**
     * Indicates if this <code>JavaClass</code> has actual type arguments, i.e. is a
     * parameterized type (for example, <code>List&lt;Employee</code>).
     *
     * @return always returns <code>false</code> as <code>JavaTypes</code> are not parameterized.
     */
    public boolean hasActualTypeArguments() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is <code>abstract</code>.
     *
     * @return always returns <code>false</code> as <code>JavaTypes</code> are never <code>abstract</code>.
     */
    public boolean isAbstract() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is an <code>Annotation</code>.
     *
     * @return always returns <code>false</code> as <code>JavaTypes</code> are never <code>Annotations</code>.
     */
    public boolean isAnnotation() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is an Array type.
     *
     * @return always returns <code>false</code>, as <code>JavaTypes</code> do not represent arrays.
     */
    public boolean isArray() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is either the same as, or is a superclass of,
     * the <code>javaClass</code> argument.
     *
     * @param arg0 the <code>Class</code> to test.
     *
     * @return <code>true</code> if this <code>JavaClass</code> is assignable from
     *         <code>javaClass</code>, otherwise <code>false</code>.
     *
     * @see java.lang.Class#isAssignableFrom(Class)
     */
    @SuppressWarnings("unchecked")
    public boolean isAssignableFrom(JavaClass arg0) {
        String thisJavaName = EMPTY_STRING;
        String argJavaName = arg0.getName();

        if (this.javaName != null) {
            thisJavaName = this.javaName;
        } else {
            thisJavaName = this.javaType.getName();
        }

        if (thisJavaName.startsWith(JAVA) && argJavaName.startsWith(JAVA)) {
            // Only try class lookup if this is a JDK class, because
            // we won't ever find classes for dynamically generated types.
            try {
                Class thisClass = PrivilegedAccessHelper.getClassForName(thisJavaName);
                Class argClass = PrivilegedAccessHelper.getClassForName(argJavaName);
                return thisClass.isAssignableFrom(argClass);
            } catch (Exception e) {
                return false;
            }
        } else {
            return thisJavaName.equals(argJavaName);
        }
    }

    /**
     * Indicates if this <code>JavaClass</code> is an <code>enum</code>.
     *
     * @return <code>true</code> if this <code>JavaClass</code> is an <code>enum</code>, otherwise <code>false</code>.
     */
    public boolean isEnum() {
        return this.enumValues != null;
    }

    /**
     * Indicates if this <code>JavaClass</code> is <code>final</code>.
     *
     * @return <code>true</code> if this <code>JavaClass</code> is <code>final</code>, otherwise <code>false</code>.
     */
    public boolean isFinal() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is an <code>interface</code>.
     *
     * @return <code>true</code> if this <code>JavaClass</code> is an <code>interface</code>, otherwise <code>false</code>.
     */
    public boolean isInterface() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is an inner <code>Class</code>.
     *
     * @return <code>true</code> if this <code>JavaClass</code> is an inner <code>Class</code>, otherwise <code>false</code>.
     */
    public boolean isMemberClass() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> represents a primitive type.
     *
     * @return <code>true</code> if this <code>JavaClass</code> represents a primitive type, otherwise <code>false</code>.
     */
    public boolean isPrimitive() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is <code>private</code>.
     *
     * @return <code>true</code> if this <code>JavaClass</code> is <code>private</code>, otherwise <code>false</code>.
     */
    public boolean isPrivate() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is <code>protected</code>.
     *
     * @return <code>true</code> if this <code>JavaClass</code> is <code>protected</code>, otherwise <code>false</code>.
     */
    public boolean isProtected() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is <code>public</code>.
     *
     * @return <code>true</code> if this <code>JavaClass</code> is <code>public</code>, otherwise <code>false</code>.
     */
    public boolean isPublic() {
        return false;
    }

    /**
     * Indicates if this <code>JavaClass</code> is <code>static</code>.
     *
     * @return <code>true</code> if this <code>JavaClass</code> is <code>static</code>, otherwise <code>false</code>.
     */
    public boolean isStatic() {
        return false;
    }

    /**
     * Not supported.
     */
    public boolean isSynthetic() {
        throw new UnsupportedOperationException("isSynthetic");
    }

    @Override
    public JavaClassInstanceOf instanceOf() {
        return JavaClassInstanceOf.OXM_JAVA_CLASS_IMPL;
    }

    /**
     * If this <code>JavaClass</code> is annotated with an <code>Annotation</code> matching <code>aClass</code>,
     * return its <code>JavaAnnotation</code> representation.
     *
     * @param aClass a <code>JavaClass</code> representing the <code>Annotation</code> to look for.
     *
     * @return always returns <code>null</code>, as <code>JavaTypes</code> do not have <code>Annotations</code>.
     */
    public JavaAnnotation getAnnotation(JavaClass aClass) {
        return null;
    }

    /**
     * Return all of the <code>Annotations</code> for this <code>JavaClass</code>.
     *
     * @return always returns an empty <code>ArrayList</code>, as <code>JavaTypes</code> do not have <code>Annotations</code>.
     */
    public Collection<JavaAnnotation> getAnnotations() {
        return new ArrayList<JavaAnnotation>();
    }

    /**
     * If this <code>JavaClass</code> declares an <code>Annotation</code> matching <code>aClass</code>,
     * return its <code>JavaAnnotation</code> representation.
     *
     * @param arg0 a <code>JavaClass</code> representing the <code>Annotation</code> to look for.
     *
     * @return always returns <code>null</code>, as <code>JavaTypes</code> do not have <code>Annotations</code>.
     */
    public JavaAnnotation getDeclaredAnnotation(JavaClass arg0) {
        return null;
    }

    /**
     * Return all of the declared <code>Annotations</code> for this <code>JavaClass</code>.
     *
     * @return always returns an empty <code>ArrayList</code>, as <code>JavaTypes</code> do not have <code>Annotations</code>.
     */
    public Collection<JavaAnnotation> getDeclaredAnnotations() {
        return new ArrayList<JavaAnnotation>();
    }

    /**
     * Set this <code>JavaClass'</code> <code>JavaModel</code>.
     *
     * @param model The <code>JavaModel</code> to set.
     */
    public void setJavaModel(JavaModel model) {
        this.javaModel = model;
    }

    /**
     * Get this <code>JavaClass'</code> <code>JavaModel</code>.
     *
     * @return The <code>JavaModel</code> associated with this <code>JavaClass</code>.
     */
    public JavaModel getJavaModel() {
        return this.javaModel;
    }

    // ========================================================================

    private static String EMPTY_STRING = "";
    private static String JAVA = "java";
    private static String DOT = ".";
    private static String JAVA_LANG_OBJECT = "java.lang.Object";
    private static String JAVA_UTIL_MAP = "java.util.Map";

}
