/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.xml.bind.v2.model.impl;

import java.util.Collection;
import java.lang.annotation.Annotation;

import javax.activation.MimeType;
import javax.xml.bind.annotation.XmlAttachmentRef;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlID;
import javax.xml.bind.annotation.XmlIDREF;
import javax.xml.bind.annotation.XmlInlineBinaryData;
import javax.xml.bind.annotation.XmlMimeType;
import javax.xml.bind.annotation.XmlSchema;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.namespace.QName;

import com.sun.xml.bind.v2.TODO;
import com.sun.xml.bind.v2.model.annotation.AnnotationReader;
import com.sun.xml.bind.v2.model.annotation.Locatable;
import com.sun.xml.bind.v2.model.core.Adapter;
import com.sun.xml.bind.v2.model.core.ID;
import com.sun.xml.bind.v2.model.core.PropertyInfo;
import com.sun.xml.bind.v2.model.core.TypeInfo;
import com.sun.xml.bind.v2.model.core.TypeInfoSet;
import com.sun.xml.bind.v2.model.nav.Navigator;
import com.sun.xml.bind.v2.runtime.IllegalAnnotationException;
import com.sun.xml.bind.v2.runtime.Location;
import com.sun.xml.bind.v2.runtime.SwaRefAdapter;

/**
 * Default partial implementation for {@link PropertyInfo}.
 *
 * @author Kohsuke Kawaguchi
 */
abstract class PropertyInfoImpl<T,C,F,M>
    implements PropertyInfo<T,C>, Locatable, Comparable<PropertyInfoImpl> /*by their names*/ {

    /**
     * Object that reads annotations.
     */
    protected final PropertySeed<T,C,F,M> seed;

    private final boolean isCollection;

    private final ID id;

    private final MimeType expectedMimeType;
    private final boolean inlineBinary;
    private final QName schemaType;

    protected final ClassInfoImpl<T,C,F,M> parent;

    private final Adapter<T,C> adapter;

    protected PropertyInfoImpl(ClassInfoImpl<T,C,F,M> parent, PropertySeed<T,C,F,M> spi) {
        this.seed = spi;
        this.parent = parent;

        if(parent==null)
            /*
                Various people reported a bug where this parameter is somehow null.
                In an attempt to catch the error better, let's do an explicit check here.

                http://forums.java.net/jive/thread.jspa?threadID=18479
                http://forums.java.net/jive/thread.jspa?messageID=165946
             */
            throw new AssertionError();

        MimeType mt = Util.calcExpectedMediaType(seed,parent.builder);
        if(mt!=null && !kind().canHaveXmlMimeType) {
            parent.builder.reportError(new IllegalAnnotationException(
                Messages.ILLEGAL_ANNOTATION.format(XmlMimeType.class.getName()),
                seed.readAnnotation(XmlMimeType.class)
            ));
            mt = null;
        }
        this.expectedMimeType = mt;
        this.inlineBinary = seed.hasAnnotation(XmlInlineBinaryData.class);

        T t = seed.getRawType();

        // check if there's an adapter applicable to the whole property
        XmlJavaTypeAdapter xjta = getApplicableAdapter(t);
        if(xjta!=null) {
            isCollection = false;
            adapter = new Adapter<T,C>(xjta,reader(),nav());
        } else {
            // check if the adapter is applicable to the individual item in the property

            this.isCollection = nav().isSubClassOf(t, nav().ref(Collection.class))
                             || nav().isArrayButNotByteArray(t);

            xjta = getApplicableAdapter(getIndividualType());
            if(xjta==null) {
                // ugly ugly hack, but we implement swaRef as adapter
                XmlAttachmentRef xsa = seed.readAnnotation(XmlAttachmentRef.class);
                if(xsa!=null) {
                    parent.builder.hasSwaRef = true;
                    adapter = new Adapter<T,C>(nav().asDecl(SwaRefAdapter.class),nav());
                } else {
                    adapter = null;

                    // if this field has adapter annotation but not applicable,
                    // that must be an error of the user
                    xjta = seed.readAnnotation(XmlJavaTypeAdapter.class);
                    if(xjta!=null) {
                        T ad = reader().getClassValue(xjta,"value");
                        parent.builder.reportError(new IllegalAnnotationException(
                            Messages.UNMATCHABLE_ADAPTER.format(
                                    nav().getTypeName(ad), nav().getTypeName(t)),
                            xjta
                        ));
                    }
                }
            } else {
                adapter = new Adapter<T,C>(xjta,reader(),nav());
            }
        }

        this.id = calcId();
        this.schemaType = Util.calcSchemaType(reader(),seed,parent.clazz,
                getIndividualType(),this);
    }


    public ClassInfoImpl<T,C,F,M> parent() {
        return parent;
    }

    protected final Navigator<T,C,F,M> nav() {
        return parent.nav();
    }
    protected final AnnotationReader<T,C,F,M> reader() {
        return parent.reader();
    }

    public T getRawType() {
        return seed.getRawType();
    }

    public T getIndividualType() {
        if(adapter!=null)
            return adapter.defaultType;
        T raw = getRawType();
        if(!isCollection()) {
            return raw;
        } else {
            if(nav().isArrayButNotByteArray(raw))
                return nav().getComponentType(raw);

            T bt = nav().getBaseClass(raw, nav().asDecl(Collection.class) );
            if(nav().isParameterizedType(bt))
                return nav().getTypeArgument(bt,0);
            else
                return nav().ref(Object.class);
        }
    }

    public final String getName() {
        return seed.getName();
    }

    /**
     * Checks if the given adapter is applicable to the declared property type.
     */
    private boolean isApplicable(XmlJavaTypeAdapter jta, T declaredType ) {
        if(jta==null)   return false;

        T type = reader().getClassValue(jta,"type");
        if(nav().isSameType(declaredType, type))
            return true;    // for types explicitly marked in XmlJavaTypeAdapter.type()
        
        T ad = reader().getClassValue(jta,"value");
        T ba = nav().getBaseClass(ad, nav().asDecl(XmlAdapter.class));
        if(!nav().isParameterizedType(ba))
            return true;   // can't check type applicability. assume Object, which means applicable to any.
        T inMemType = nav().getTypeArgument(ba, 1);

        return nav().isSubClassOf(declaredType,inMemType);
    }

    private XmlJavaTypeAdapter getApplicableAdapter(T type) {
        XmlJavaTypeAdapter jta = seed.readAnnotation(XmlJavaTypeAdapter.class);
        if(jta!=null && isApplicable(jta,type))
            return jta;

        // check the applicable adapters on the package
        XmlJavaTypeAdapters jtas = reader().getPackageAnnotation(XmlJavaTypeAdapters.class, parent.clazz, seed );
        if(jtas!=null) {
            for (XmlJavaTypeAdapter xjta : jtas.value()) {
                if(isApplicable(xjta,type))
                    return xjta;
            }
        }
        jta = reader().getPackageAnnotation(XmlJavaTypeAdapter.class, parent.clazz, seed );
        if(isApplicable(jta,type))
            return jta;

        // then on the target class
        C refType = nav().asDecl(type);
        if(refType!=null) {
            jta = reader().getClassAnnotation(XmlJavaTypeAdapter.class, refType, seed );
            if(jta!=null && isApplicable(jta,type)) // the one on the type always apply.
                return jta;
        }

        return null;
    }

    /**
     * This is the default implementation of the getAdapter method
     * defined on many of the {@link PropertyInfo}-derived classes.
     */
    public Adapter<T,C> getAdapter() {
        return adapter;
    }


    public final String displayName() {
        return nav().getClassName(parent.getClazz())+'#'+getName();
    }

    public final ID id() {
        return id;
    }

    private ID calcId() {
        if(seed.hasAnnotation(XmlID.class)) {
            // check the type
            if(!nav().isSameType(getIndividualType(), nav().ref(String.class)))
                parent.builder.reportError(new IllegalAnnotationException(
                    Messages.ID_MUST_BE_STRING.format(getName()), seed )
                );
            return ID.ID;
        } else
        if(seed.hasAnnotation(XmlIDREF.class)) {
            return ID.IDREF;
        } else {
            return ID.NONE;
        }
    }

    public final MimeType getExpectedMimeType() {
        return expectedMimeType;
    }

    public final boolean inlineBinaryData() {
        return inlineBinary;
    }

    public final QName getSchemaType() {
        return schemaType;
    }

    public final boolean isCollection() {
        return isCollection;
    }

    /**
     * Called after all the {@link TypeInfo}s are collected into the governing {@link TypeInfoSet}.
     *
     * Derived class can do additional actions to complete the model.
     */
    protected void link() {
        if(id==ID.IDREF) {
            // make sure that the refereced type has ID
            for (TypeInfo<T,C> ti : ref()) {
                if(!ti.canBeReferencedByIDREF())
                    parent.builder.reportError(new IllegalAnnotationException(
                    Messages.INVALID_IDREF.format(
                        parent.builder.nav.getTypeName(ti.getType())), this ));
            }
        }
    }

    /**
     * A {@link PropertyInfoImpl} is always referenced by its enclosing class,
     * so return that as the upstream.
     */
    public Locatable getUpstream() {
        return parent;
    }

    public Location getLocation() {
        return seed.getLocation();
    }


//
//
// convenience methods for derived classes
//
//


    /**
     * Computes the tag name from a {@link XmlElement} by taking the defaulting into account.
     */
    protected final QName calcXmlName(XmlElement e) {
        if(e!=null)
            return calcXmlName(e.namespace(),e.name());
        else
            return calcXmlName("##default","##default");
    }

    /**
     * Computes the tag name from a {@link XmlElementWrapper} by taking the defaulting into account.
     */
    protected final QName calcXmlName(XmlElementWrapper e) {
        if(e!=null)
            return calcXmlName(e.namespace(),e.name());
        else
            return calcXmlName("##default","##default");
    }

    private QName calcXmlName(String uri,String local) {
        // compute the default
        TODO.checkSpec();
        if(local.length()==0 || local.equals("##default"))
            local = seed.getName();
        if(uri.equals("##default")) {
            XmlSchema xs = reader().getPackageAnnotation( XmlSchema.class, parent.getClazz(), this );
            // JAX-RPC doesn't want the default namespace URI swapping to take effect to
            // local "unqualified" elements. UGLY.
            if(xs!=null) {
                switch(xs.elementFormDefault()) {
                case QUALIFIED:
                    QName typeName = parent.getTypeName();
                    if(typeName!=null)
                        uri = typeName.getNamespaceURI();
                    else
                        uri = xs.namespace();
                    if(uri.length()==0)
                        uri = parent.builder.defaultNsUri;
                    break;
                case UNQUALIFIED:
                case UNSET:
                    uri = "";
                }
            } else {
                uri = "";
            }
        }
        return new QName(uri.intern(),local.intern());
    }

    public int compareTo(PropertyInfoImpl that) {
        return this.getName().compareTo(that.getName());
    }

    public final <A extends Annotation> A readAnnotation(Class<A> annotationType) {
        return seed.readAnnotation(annotationType);
    }

    public final boolean hasAnnotation(Class<? extends Annotation> annotationType) {
        return seed.hasAnnotation(annotationType);
    }
}
