/*
 * 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.runtime;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.bind.ValidationEvent;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.helpers.ValidationEventImpl;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;

import com.sun.istack.FinalArrayList;
import com.sun.xml.bind.Util;
import com.sun.xml.bind.api.AccessorException;
import com.sun.xml.bind.v2.ClassFactory;
import com.sun.xml.bind.v2.WellKnownNamespace;
import com.sun.xml.bind.v2.model.core.ID;
import com.sun.xml.bind.v2.model.runtime.RuntimeClassInfo;
import com.sun.xml.bind.v2.model.runtime.RuntimePropertyInfo;
import com.sun.xml.bind.v2.runtime.property.AttributeProperty;
import com.sun.xml.bind.v2.runtime.property.Property;
import com.sun.xml.bind.v2.runtime.property.PropertyFactory;
import com.sun.xml.bind.v2.runtime.reflect.Accessor;
import com.sun.xml.bind.v2.runtime.unmarshaller.Loader;
import com.sun.xml.bind.v2.runtime.unmarshaller.StructureLoader;
import com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext;
import com.sun.xml.bind.v2.runtime.unmarshaller.XsiTypeLoader;

import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.LocatorImpl;

/**
 * {@link JaxBeanInfo} implementation for j2s bean.
 *
 * @author Kohsuke Kawaguchi (kohsuke.kawaguchi@sun.com)
 */
public final class ClassBeanInfoImpl<BeanT> extends JaxBeanInfo<BeanT> implements AttributeAccessor<BeanT> {

    /**
     * Properties of this bean class but not its ancestor classes.
     */
    public final Property<BeanT>[] properties;

    /**
     * Non-null if this bean has an ID property.
     */
    private Property<? super BeanT> idProperty;

    /**
     * Immutable configured loader for this class.
     *
     * <p>
     * Set from the link method, but considered final.
     */
    private Loader loader;
    private Loader loaderWithTypeSubst;

    /**
     * Set only until the link phase to avoid leaking memory.
     */
    private RuntimeClassInfo ci;

    private final Accessor<? super BeanT,Map<QName,String>> inheritedAttWildcard;
    private final Transducer<BeanT> xducer;

    /**
     * {@link ClassBeanInfoImpl} that represents the super class of {@link #jaxbType}.
     */
    public final ClassBeanInfoImpl<? super BeanT> superClazz;

    private final Accessor<? super BeanT,Locator> xmlLocatorField;

    private final Name tagName;

    private boolean retainPropertyInfo = false;
            
    /**
     * The {@link AttributeProperty}s for this type and all its ancestors.
     * If {@link JAXBContextImpl#c14nSupport} is true, this is sorted alphabetically.
     */
    private /*final*/ AttributeProperty<BeanT>[] attributeProperties;

    /**
     * {@link Property}s that need to receive {@link Property#serializeURIs(Object, XMLSerializer)} callback.
     */
    private /*final*/ Property<BeanT>[] uriProperties;

    private final Method factoryMethod;
    
    /*package*/ ClassBeanInfoImpl(JAXBContextImpl owner, RuntimeClassInfo ci) {
        super(owner,ci,ci.getClazz(),ci.getTypeName(),ci.isElement(),false,true);

        this.ci = ci;
        this.inheritedAttWildcard = ci.getAttributeWildcard();
        this.xducer = ci.getTransducer();
        this.factoryMethod = ci.getFactoryMethod();
        this.retainPropertyInfo = owner.retainPropertyInfo;
        
        // make the factory accessible
        if(factoryMethod!=null) {
            int classMod = factoryMethod.getDeclaringClass().getModifiers();

            if(!Modifier.isPublic(classMod) || !Modifier.isPublic(factoryMethod.getModifiers())) {
                // attempt to make it work even if the constructor is not accessible
                try {
                    factoryMethod.setAccessible(true);
                } catch(SecurityException e) {
                    // but if we don't have a permission to do so, work gracefully.
                    logger.log(Level.FINE,"Unable to make the method of "+factoryMethod+" accessible",e);
                    throw e;
                }
            }
        }

        
        if(ci.getBaseClass()==null)
            this.superClazz = null;
        else
            this.superClazz = owner.getOrCreate(ci.getBaseClass());

        if(superClazz!=null && superClazz.xmlLocatorField!=null)
            xmlLocatorField = superClazz.xmlLocatorField;
        else
            xmlLocatorField = ci.getLocatorField();

        // create property objects
        Collection<? extends RuntimePropertyInfo> ps = ci.getProperties();
        this.properties = new Property[ps.size()];
        int idx=0;
        boolean elementOnly = true;
        for( RuntimePropertyInfo info : ps ) {
            Property p = PropertyFactory.create(owner,info);
            if(info.id()==ID.ID)
                idProperty = p;
            properties[idx++] = p;
            elementOnly &= info.elementOnlyContent();
            checkOverrideProperties(p);
        }
        // super class' idProperty might not be computed at this point,
        // so check that later

        hasElementOnlyContentModel( elementOnly );
        // again update this value later when we know that of the super class

        if(ci.isElement())
            tagName = owner.nameBuilder.createElementName(ci.getElementName());
        else
            tagName = null;

        setLifecycleFlags();
    }

    private void checkOverrideProperties(Property p) {
        ClassBeanInfoImpl bi = this;
        while ((bi = bi.superClazz) != null) {
            Property[] props = bi.properties;
            if (props == null) break;
            for (Property superProperty : props) {
                if (superProperty != null) {
                    String spName = superProperty.getFieldName();
                    if ((spName != null) && (spName.equals(p.getFieldName()))) {
                        superProperty.setHiddenByOverride(true);
                    }
                }
            }
        }
    }
    
    @Override
    protected void link(JAXBContextImpl grammar) {
        if(uriProperties!=null)
            return; // avoid linking twice

        super.link(grammar);

        if(superClazz!=null)
            superClazz.link(grammar);

        getLoader(grammar,true);    // make sure to build the loader if we haven't done so.

        // propagate values from super class
        if(superClazz!=null) {
            if(idProperty==null)
                idProperty = superClazz.idProperty;

            if(!superClazz.hasElementOnlyContentModel())
                hasElementOnlyContentModel(false);
        }

        // create a list of attribute/URI handlers
        List<AttributeProperty> attProps = new FinalArrayList<AttributeProperty>();
        List<Property> uriProps = new FinalArrayList<Property>();
        for (ClassBeanInfoImpl bi = this; bi != null; bi = bi.superClazz) {
            for (int i = 0; i < bi.properties.length; i++) {
                Property p = bi.properties[i];
                if(p instanceof AttributeProperty)
                    attProps.add((AttributeProperty) p);
                if(p.hasSerializeURIAction())
                    uriProps.add(p);
            }
        }
        if(grammar.c14nSupport)
            Collections.sort(attProps);

        if(attProps.isEmpty())
            attributeProperties = EMPTY_PROPERTIES;
        else
            attributeProperties = attProps.toArray(new AttributeProperty[attProps.size()]);

        if(uriProps.isEmpty())
            uriProperties = EMPTY_PROPERTIES;
        else
            uriProperties = uriProps.toArray(new Property[uriProps.size()]);
    }

    @Override
    public void wrapUp() {
        for (Property p : properties)
            p.wrapUp();
        ci = null;
        super.wrapUp();
    }

    public String getElementNamespaceURI(BeanT bean) {
        return tagName.nsUri;
    }

    public String getElementLocalName(BeanT bean) {
        return tagName.localName;
    }

    public BeanT createInstance(UnmarshallingContext context) throws IllegalAccessException, InvocationTargetException, InstantiationException, SAXException {
        
        BeanT bean = null;        
        if (factoryMethod == null){
           bean = ClassFactory.create0(jaxbType);
        }else {
            Object o = ClassFactory.create(factoryMethod);
            if( jaxbType.isInstance(o) ){
                bean = (BeanT)o;
            } else {
                throw new InstantiationException("The factory method didn't return a correct object");
            }
        }
        
        if(xmlLocatorField!=null)
            // need to copy because Locator is mutable
            try {
                xmlLocatorField.set(bean,new LocatorImpl(context.getLocator()));
            } catch (AccessorException e) {
                context.handleError(e);
            }
        return bean;
    }

    public boolean reset(BeanT bean, UnmarshallingContext context) throws SAXException {
        try {
            if(superClazz!=null)
                superClazz.reset(bean,context);
            for( Property<BeanT> p : properties )
                p.reset(bean);
            return true;
        } catch (AccessorException e) {
            context.handleError(e);
            return false;
        }
    }

    public String getId(BeanT bean, XMLSerializer target) throws SAXException {
        if(idProperty!=null) {
            try {
                return idProperty.getIdValue(bean);
            } catch (AccessorException e) {
                target.reportError(null,e);
            }
        }
        return null;
    }

    public void serializeRoot(BeanT bean, XMLSerializer target) throws SAXException, IOException, XMLStreamException {
        if(tagName==null) {
            Class beanClass = bean.getClass();
            String message;
            if (beanClass.isAnnotationPresent(XmlRootElement.class)) {
                message = Messages.UNABLE_TO_MARSHAL_UNBOUND_CLASS.format(beanClass.getName());
            } else {
                message = Messages.UNABLE_TO_MARSHAL_NON_ELEMENT.format(beanClass.getName());
            }
            target.reportError(new ValidationEventImpl(ValidationEvent.ERROR,message,null, null));
        } else {
            target.startElement(tagName,bean);
            target.childAsSoleContent(bean,null);
            target.endElement();
            if (retainPropertyInfo) {
                target.currentProperty.remove();
            }
        }
    }

    public void serializeBody(BeanT bean, XMLSerializer target) throws SAXException, IOException, XMLStreamException {
        if (superClazz != null) {
            superClazz.serializeBody(bean, target);
        }
        try {
            for (Property<BeanT> p : properties) {
                if (retainPropertyInfo) {
                    target.currentProperty.set(p);
                }
                boolean isThereAnOverridingProperty = p.isHiddenByOverride();
                if (!isThereAnOverridingProperty || bean.getClass().equals(jaxbType)) {
                    p.serializeBody(bean, target, null);
                } else if (isThereAnOverridingProperty) { 
                    // need to double check the override - it should be safe to do after the model has been created because it's targeted to override properties only
                    Class beanClass = bean.getClass();
                    if (Utils.REFLECTION_NAVIGATOR.getDeclaredField(beanClass, p.getFieldName()) == null) {
                        p.serializeBody(bean, target, null);
                    }
                }
            }
        } catch (AccessorException e) {
            target.reportError(null, e);
        }
    }

    public void serializeAttributes(BeanT bean, XMLSerializer target) throws SAXException, IOException, XMLStreamException {
        for( AttributeProperty<BeanT> p : attributeProperties )
            try {
                if (retainPropertyInfo) {
                final Property parentProperty = target.getCurrentProperty();
                target.currentProperty.set(p);
                p.serializeAttributes(bean,target);
                target.currentProperty.set(parentProperty);
                } else {
                    p.serializeAttributes(bean,target);
                }
                if (p.attName.equals(WellKnownNamespace.XML_SCHEMA_INSTANCE, "nil")) {
                    isNilIncluded = true;
                }
            } catch (AccessorException e) {
                target.reportError(null,e);
            }

        try {
            if(inheritedAttWildcard!=null) {
                Map<QName,String> map = inheritedAttWildcard.get(bean);
                target.attWildcardAsAttributes(map,null);
            }
        } catch (AccessorException e) {
            target.reportError(null,e);
        }
    }

    public void serializeURIs(BeanT bean, XMLSerializer target) throws SAXException {
        try {
            if (retainPropertyInfo) {
            final Property parentProperty = target.getCurrentProperty();
            for( Property<BeanT> p : uriProperties ) {
                target.currentProperty.set(p);
                p.serializeURIs(bean,target);
            }
            target.currentProperty.set(parentProperty);
            } else {
                for( Property<BeanT> p : uriProperties ) {
                    p.serializeURIs(bean,target);
                }
            }
            if(inheritedAttWildcard!=null) {
                Map<QName,String> map = inheritedAttWildcard.get(bean);
                target.attWildcardAsURIs(map,null);
            }
        } catch (AccessorException e) {
            target.reportError(null,e);
        }
    }

    public Loader getLoader(JAXBContextImpl context, boolean typeSubstitutionCapable) {
        if(loader==null) {
            // these variables have to be set before they are initialized,
            // because the initialization may build other loaders and they may refer to this.
            StructureLoader sl = new StructureLoader(this);
            loader = sl;
            if(ci.hasSubClasses())
                loaderWithTypeSubst = new XsiTypeLoader(this);
            else
                // optimization. we know there can be no @xsi:type
                loaderWithTypeSubst = loader;


            sl.init(context,this,ci.getAttributeWildcard());
        }
        if(typeSubstitutionCapable)
            return loaderWithTypeSubst;
        else
            return loader;
    }

    public Transducer<BeanT> getTransducer() {
        return xducer;
    }

    private static final AttributeProperty[] EMPTY_PROPERTIES = new AttributeProperty[0];

    private static final Logger logger = Util.getClassLogger();

}

