/*
 * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.xml.internal.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.internal.FinalArrayList;
import com.sun.xml.internal.bind.Util;
import com.sun.xml.internal.bind.api.AccessorException;
import com.sun.xml.internal.bind.v2.ClassFactory;
import com.sun.xml.internal.bind.v2.WellKnownNamespace;
import com.sun.xml.internal.bind.v2.model.core.ID;
import com.sun.xml.internal.bind.v2.model.runtime.RuntimeClassInfo;
import com.sun.xml.internal.bind.v2.model.runtime.RuntimePropertyInfo;
import com.sun.xml.internal.bind.v2.runtime.property.AttributeProperty;
import com.sun.xml.internal.bind.v2.runtime.property.Property;
import com.sun.xml.internal.bind.v2.runtime.property.PropertyFactory;
import com.sun.xml.internal.bind.v2.runtime.reflect.Accessor;
import com.sun.xml.internal.bind.v2.runtime.unmarshaller.Loader;
import com.sun.xml.internal.bind.v2.runtime.unmarshaller.StructureLoader;
import com.sun.xml.internal.bind.v2.runtime.unmarshaller.UnmarshallingContext;
import com.sun.xml.internal.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();

}
