/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.cxf.jaxb;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.logging.Logger;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlList;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.namespace.QName;

import org.apache.cxf.common.WSDLConstants;
import org.apache.cxf.common.i18n.Message;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.ReflectionInvokationHandler;
import org.apache.cxf.common.xmlschema.SchemaCollection;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.service.ServiceModelVisitor;
import org.apache.cxf.service.model.FaultInfo;
import org.apache.cxf.service.model.MessagePartInfo;
import org.apache.cxf.service.model.SchemaInfo;
import org.apache.cxf.service.model.ServiceInfo;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaComplexType;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.XmlSchemaForm;
import org.apache.ws.commons.schema.XmlSchemaSequence;
import org.apache.ws.commons.schema.XmlSchemaSimpleType;
import org.apache.ws.commons.schema.XmlSchemaSimpleTypeList;
import org.apache.ws.commons.schema.XmlSchemaType;
import org.apache.ws.commons.schema.utils.NamespaceMap;

/**
 * Walks the service model and sets up the element/type names.
 */
class JAXBSchemaInitializer extends ServiceModelVisitor {
    private static final Logger LOG = LogUtils.getLogger(JAXBSchemaInitializer.class);

    private SchemaCollection schemas;
    private JAXBContextProxy context;
    private final boolean qualifiedSchemas;

    public JAXBSchemaInitializer(ServiceInfo serviceInfo,
                                 SchemaCollection col,
                                 JAXBContext context,
                                 boolean q) {
        super(serviceInfo);
        schemas = col;
        this.context = ReflectionInvokationHandler.createProxyWrapper(context, JAXBContextProxy.class);
        this.qualifiedSchemas = q;
    }

    static Class<?> getArrayComponentType(Type cls) {
        if (cls instanceof Class) {
            if (((Class)cls).isArray()) {
                return ((Class)cls).getComponentType();
            } else {
                return (Class)cls;
            }
        } else if (cls instanceof ParameterizedType) {
            for (Type t2 : ((ParameterizedType)cls).getActualTypeArguments()) {
                return getArrayComponentType(t2);
            }
        } else if (cls instanceof GenericArrayType) {
            GenericArrayType gt = (GenericArrayType)cls;
            Class ct = (Class) gt.getGenericComponentType();
            return Array.newInstance(ct, 0).getClass();
        }
        return null;
    }

    public JAXBBeanInfo getBeanInfo(Type cls) {
        if (cls instanceof Class) {
            if (((Class)cls).isArray()) {
                return getBeanInfo(((Class)cls).getComponentType());
            } else {
                return getBeanInfo((Class)cls);
            }
        } else if (cls instanceof ParameterizedType) {
            for (Type t2 : ((ParameterizedType)cls).getActualTypeArguments()) {
                return getBeanInfo(t2);
            }
        } else if (cls instanceof GenericArrayType) {
            GenericArrayType gt = (GenericArrayType)cls;
            Class ct = (Class) gt.getGenericComponentType();
            ct = Array.newInstance(ct, 0).getClass();

            return getBeanInfo(ct);
        }

        return null;
    }

    public JAXBBeanInfo getBeanInfo(Class<?> cls) {
        return getBeanInfo(context, cls);
    }

    public static JAXBBeanInfo getBeanInfo(JAXBContextProxy context, Class<?> cls) {
        return JAXBUtils.getBeanInfo(context, cls);
    }

    @Override
    public void begin(MessagePartInfo part) {
        // Check to see if the WSDL information has been filled in for us.
        if (part.getTypeQName() != null || part.getElementQName() != null) {
            checkForExistence(part);
            return;
        }

        Class<?> clazz = part.getTypeClass();
        if (clazz == null) {
            return;
        }

        boolean isFromWrapper = part.getMessageInfo().getOperation().isUnwrapped();
        boolean isList = false;
        if (clazz.isArray()) {
            if (isFromWrapper && !Byte.TYPE.equals(clazz.getComponentType())) {
                clazz = clazz.getComponentType();
            } else if (!isFromWrapper) {
                Annotation[] anns = (Annotation[])part.getProperty("parameter.annotations");
                for (Annotation a : anns) {
                    if (a instanceof XmlList) {
                        part.setProperty("honor.jaxb.annotations", Boolean.TRUE);
                        clazz = clazz.getComponentType();
                        isList = true;
                    }
                }
            }
        }

        JAXBBeanInfo beanInfo = getBeanInfo(clazz);
        if (beanInfo == null) {
            Annotation[] anns = (Annotation[])part.getProperty("parameter.annotations");
            XmlJavaTypeAdapter jta = findFromTypeAdapter(clazz, anns);
            if (jta != null) {
                beanInfo = findFromTypeAdapter(jta.value());
                if (anns == null) {
                    anns = new Annotation[] {jta};
                } else {
                    boolean found = false;
                    for (Annotation t : anns) {
                        if (t == jta) {
                            found = true;
                        }
                    }
                    if (!found) {
                        Annotation tmp[] = new Annotation[anns.length + 1];
                        System.arraycopy(anns, 0, tmp, 0, anns.length);
                        tmp[anns.length] = jta;
                        anns = tmp;
                    }
                }
                part.setProperty("parameter.annotations", anns);
                part.setProperty("honor.jaxb.annotations", Boolean.TRUE);
            }
        }
        if (beanInfo == null) {
            if (Exception.class.isAssignableFrom(clazz)) {
                QName name = (QName)part.getMessageInfo().getProperty("elementName");
                part.setElementQName(name);
                buildExceptionType(part, clazz);
            }
            return;
        }
        boolean isElement = beanInfo.isElement()
            && !Boolean.TRUE.equals(part.getMessageInfo().getOperation()
                                        .getProperty("operation.force.types"));
        boolean hasType = !beanInfo.getTypeNames().isEmpty();
        if (isElement && isFromWrapper && hasType) {
            //if there is both a Global element and a global type, AND we are in a wrapper,
            //make sure we use the type instead of a ref to the element to
            //match the rules for wrapped/unwrapped
            isElement = false;
        }

        part.setElement(isElement);

        if (isElement) {
            QName name = new QName(beanInfo.getElementNamespaceURI(null),
                                   beanInfo.getElementLocalName(null));
            XmlSchemaElement el = schemas.getElementByQName(name);
            if (el != null && el.getRef().getTarget() != null) {
                part.setTypeQName(el.getRef().getTargetQName());
            } else {
                part.setElementQName(name);
            }
            part.setXmlSchema(el);
        } else  {
            QName typeName = getTypeName(beanInfo);
            if (typeName != null) {
                XmlSchemaType type = schemas.getTypeByQName(typeName);
                if  (isList && type instanceof XmlSchemaSimpleType) {
                    XmlSchemaSimpleType simpleType = new XmlSchemaSimpleType(type.getParent(), false);
                    XmlSchemaSimpleTypeList list = new XmlSchemaSimpleTypeList();
                    XmlSchemaSimpleType stype = (XmlSchemaSimpleType)type;
                    list.setItemTypeName(stype.getQName());
                    simpleType.setContent(list);
                    part.setXmlSchema(simpleType);
                    if (part.getConcreteName() == null) {
                        part.setConcreteName(new QName(null, part.getName().getLocalPart()));
                    }
                } else {
                    part.setTypeQName(typeName);
                    part.setXmlSchema(type);
                }
            }
        }
    }

    private XmlJavaTypeAdapter findFromTypeAdapter(Class<?> clazz, Annotation[] anns) {
        JAXBBeanInfo ret = null;
        if (anns != null) {
            for (Annotation a : anns) {
                if (XmlJavaTypeAdapter.class.isAssignableFrom(a.annotationType())) {
                    ret = findFromTypeAdapter(((XmlJavaTypeAdapter)a).value());
                    if (ret != null) {
                        return (XmlJavaTypeAdapter)a;
                    }
                }
            }
        }
        XmlJavaTypeAdapter xjta = clazz.getAnnotation(XmlJavaTypeAdapter.class);
        if (xjta != null) {
            ret = findFromTypeAdapter(xjta.value());
            if (ret != null) {
                return xjta;
            }
        }
        return null;
    }

    private JAXBBeanInfo findFromTypeAdapter(Class<? extends XmlAdapter> aclass) {
        Class<?> c2 = aclass;
        Type sp = c2.getGenericSuperclass();
        while (!XmlAdapter.class.equals(c2) && c2 != null) {
            sp = c2.getGenericSuperclass();
            c2 = c2.getSuperclass();
        }
        if (sp instanceof ParameterizedType) {
            Type tp = ((ParameterizedType)sp).getActualTypeArguments()[0];
            if (tp instanceof Class) {
                return getBeanInfo((Class<?>)tp);
            }
        }
        return null;
    }

    private QName getTypeName(JAXBBeanInfo beanInfo) {
        Iterator<QName> itr = beanInfo.getTypeNames().iterator();
        if (!itr.hasNext()) {
            return null;
        }

        return itr.next();
    }
    public void checkForExistence(MessagePartInfo part) {
        QName qn = part.getElementQName();
        if (qn != null) {
            XmlSchemaElement el = schemas.getElementByQName(qn);
            if (el == null) {
                Class<?> clazz = part.getTypeClass();
                if (clazz == null) {
                    return;
                }

                boolean isFromWrapper = part.getMessageInfo().getOperation().isUnwrapped();
                if (isFromWrapper && clazz.isArray() && !Byte.TYPE.equals(clazz.getComponentType())) {
                    clazz = clazz.getComponentType();
                }
                JAXBBeanInfo beanInfo = getBeanInfo(clazz);
                if (beanInfo == null) {
                    if (Exception.class.isAssignableFrom(clazz)) {
                        QName name = (QName)part.getMessageInfo().getProperty("elementName");
                        part.setElementQName(name);
                        buildExceptionType(part, clazz);
                    }
                    return;
                }

                QName typeName = getTypeName(beanInfo);

                createBridgeXsElement(part, qn, typeName);
            } else if (part.getXmlSchema() == null) {
                part.setXmlSchema(el);
            }
        }
    }

    private void createBridgeXsElement(MessagePartInfo part, QName qn, QName typeName) {
        XmlSchemaElement el = null;
        SchemaInfo schemaInfo = serviceInfo.getSchema(qn.getNamespaceURI());
        if (schemaInfo != null) {
            el = schemaInfo.getElementByQName(qn);
            if (el == null) {
                el = createXsElement(schemaInfo.getSchema(), part, typeName, schemaInfo);

            } else if (!typeName.equals(el.getSchemaTypeName())) {
                throw new Fault(new Message("CANNOT_CREATE_ELEMENT", LOG,
                                            qn, typeName, el.getSchemaTypeName()));
            }
            return;
        }

        XmlSchema schema = schemas.newXmlSchemaInCollection(qn.getNamespaceURI());
        if (qualifiedSchemas) {
            schema.setElementFormDefault(XmlSchemaForm.QUALIFIED);
        }
        schemaInfo = new SchemaInfo(qn.getNamespaceURI(), qualifiedSchemas, false);
        schemaInfo.setSchema(schema);

        el = createXsElement(schema, part, typeName, schemaInfo);

        NamespaceMap nsMap = new NamespaceMap();
        nsMap.add(WSDLConstants.CONVENTIONAL_TNS_PREFIX, schema.getTargetNamespace());
        nsMap.add(WSDLConstants.NP_SCHEMA_XSD, WSDLConstants.NS_SCHEMA_XSD);
        schema.setNamespaceContext(nsMap);

        serviceInfo.addSchema(schemaInfo);
    }

    private XmlSchemaElement createXsElement(XmlSchema schema,
                                             MessagePartInfo part,
                                             QName typeName, SchemaInfo schemaInfo) {
        XmlSchemaElement el = new XmlSchemaElement(schema, true);
        el.setName(part.getElementQName().getLocalPart());
        el.setNillable(true);
        el.setSchemaTypeName(typeName);
        part.setXmlSchema(el);
        schemaInfo.setElement(null);
        return el;
    }

    public void end(FaultInfo fault) {
        MessagePartInfo part = fault.getMessageParts().get(0);
        Class<?> cls = part.getTypeClass();
        Class<?> cl2 = (Class)fault.getProperty(Class.class.getName());
        if (cls != cl2) {
            QName name = (QName)fault.getProperty("elementName");
            part.setElementQName(name);
            JAXBBeanInfo beanInfo = getBeanInfo(cls);
            if (beanInfo == null) {
                throw new Fault(new Message("NO_BEAN_INFO", LOG, cls.getName()));
            }
            SchemaInfo schemaInfo = serviceInfo.getSchema(part.getElementQName().getNamespaceURI());
            if (schemaInfo != null
                && !isExistSchemaElement(schemaInfo.getSchema(), part.getElementQName())) {

                XmlSchemaElement el = new XmlSchemaElement(schemaInfo.getSchema(), true);
                el.setName(part.getElementQName().getLocalPart());
                el.setNillable(true);

                schemaInfo.setElement(null);

                Iterator<QName> itr = beanInfo.getTypeNames().iterator();
                if (!itr.hasNext()) {
                    return;
                }
                QName typeName = itr.next();
                el.setSchemaTypeName(typeName);
            }
        } else if (part.getXmlSchema() == null) {
            try {
                cls.getConstructor(new Class[] {String.class});
            } catch (Exception e) {
                try {
                    cls.getConstructor(new Class[0]);
                } catch (Exception e2) {
                    //no String or default constructor, we cannot use it
                    return;
                }
            }

            //not mappable in JAXBContext directly, we'll have to do it manually :-(
            SchemaInfo schemaInfo = serviceInfo.getSchema(part.getElementQName().getNamespaceURI());
            if (schemaInfo == null
                || isExistSchemaElement(schemaInfo.getSchema(), part.getElementQName())) {
                return;
            }

            XmlSchemaElement el = new XmlSchemaElement(schemaInfo.getSchema(), true);
            el.setName(part.getElementQName().getLocalPart());

            schemaInfo.setElement(null);

            part.setXmlSchema(el);

            XmlSchemaComplexType ct = new XmlSchemaComplexType(schemaInfo.getSchema(), false);
            el.setSchemaType(ct);
            XmlSchemaSequence seq = new XmlSchemaSequence();
            ct.setParticle(seq);

            Method methods[] = cls.getMethods();
            for (Method m : methods) {
                if (m.getName().startsWith("get")
                    || m.getName().startsWith("is")) {
                    int beginIdx = m.getName().startsWith("get") ? 3 : 2;
                    try {
                        m.getDeclaringClass().getMethod("set" + m.getName().substring(beginIdx),
                                                        m.getReturnType());

                        JAXBBeanInfo beanInfo = getBeanInfo(m.getReturnType());
                        if (beanInfo != null) {
                            el = new XmlSchemaElement(schemaInfo.getSchema(), false);
                            el.setName(m.getName().substring(beginIdx));
                            Iterator<QName> itr = beanInfo.getTypeNames().iterator();
                            if (!itr.hasNext()) {
                                return;
                            }
                            QName typeName = itr.next();
                            el.setSchemaTypeName(typeName);
                        }

                        seq.getItems().add(el);
                    } catch (Exception e) {
                        //not mappable
                    }
                }
            }
        }
    }


    private void buildExceptionType(MessagePartInfo part, Class<?> cls) {
        SchemaInfo schemaInfo = null;
        for (SchemaInfo s : serviceInfo.getSchemas()) {
            if (s.getNamespaceURI().equals(part.getElementQName().getNamespaceURI())) {
                schemaInfo = s;
                break;
            }
        }
        XmlSchema schema;
        if (schemaInfo == null) {
            schema = schemas.newXmlSchemaInCollection(part.getElementQName().getNamespaceURI());

            if (qualifiedSchemas) {
                schema.setElementFormDefault(XmlSchemaForm.QUALIFIED);
            }

            NamespaceMap nsMap = new NamespaceMap();
            nsMap.add(WSDLConstants.CONVENTIONAL_TNS_PREFIX, schema.getTargetNamespace());
            nsMap.add(WSDLConstants.NP_SCHEMA_XSD, WSDLConstants.NS_SCHEMA_XSD);
            schema.setNamespaceContext(nsMap);


            schemaInfo = new SchemaInfo(part.getElementQName().getNamespaceURI());
            schemaInfo.setSchema(schema);
            serviceInfo.addSchema(schemaInfo);
        } else {
            schema = schemaInfo.getSchema();
        }

        // Before updating everything, make sure we haven't added this
        // type yet.  Multiple methods that throw the same exception
        // types will cause duplicates.
        String partLocalName = part.getElementQName().getLocalPart();
        XmlSchemaType existingType = schema.getTypeByName(partLocalName);
        if (existingType != null) {
            return;
        }

        XmlSchemaComplexType ct = new XmlSchemaComplexType(schema, true);
        ct.setName(partLocalName);

        XmlSchemaElement el = new XmlSchemaElement(schema, true);
        el.setName(partLocalName);
        part.setXmlSchema(el);

        schemaInfo.setElement(null);
        el.setSchemaTypeName(ct.getQName());

        XmlSchemaSequence seq = new XmlSchemaSequence();
        ct.setParticle(seq);
        String namespace = part.getElementQName().getNamespaceURI();

        XmlAccessorType accessorType = cls.getAnnotation(XmlAccessorType.class);
        if (accessorType == null && cls.getPackage() != null) {
            accessorType = cls.getPackage().getAnnotation(XmlAccessorType.class);
        }
        XmlAccessType accessType = accessorType != null ? accessorType.value() : XmlAccessType.PUBLIC_MEMBER;


        for (Field f : cls.getDeclaredFields()) {
            if (JAXBContextInitializer.isFieldAccepted(f, accessType)) {
                //map field
                Type type = f.getGenericType();
                JAXBBeanInfo beanInfo = getBeanInfo(type);
                if (beanInfo != null) {
                    addElement(schema, seq, beanInfo, new QName(namespace, f.getName()), isArray(type));
                }
            }
        }
        for (Method m : cls.getMethods()) {
            if (JAXBContextInitializer.isMethodAccepted(m, accessType)) {
                //map field
                Type type = m.getGenericReturnType();
                JAXBBeanInfo beanInfo = getBeanInfo(type);
                if (beanInfo != null) {
                    int idx = m.getName().startsWith("get") ? 3 : 2;
                    String name = m.getName().substring(idx);
                    name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
                    addElement(schema, seq, beanInfo, new QName(namespace, name), isArray(type));
                }
            }
        }
        // Create element in xsd:sequence for Exception.class
        if (cls.equals(Exception.class)) {
            JAXBBeanInfo beanInfo = getBeanInfo(java.lang.String.class);
            XmlSchemaElement exEle = new XmlSchemaElement(schema, false);
            exEle.setName("message");
            exEle.setSchemaTypeName(getTypeName(beanInfo));
            exEle.setMinOccurs(0);
            seq.getItems().add(exEle);

        }
        part.setProperty(JAXBDataBinding.class.getName() + ".CUSTOM_EXCEPTION", Boolean.TRUE);
    }

    static boolean isArray(Type cls) {
        if (cls instanceof Class) {
            return ((Class)cls).isArray();
        } else if (cls instanceof ParameterizedType) {
            return true;
        } else if (cls instanceof GenericArrayType) {
            return true;
        }
        return false;
    }

    protected void addElement(XmlSchema schema,
                              XmlSchemaSequence seq, JAXBBeanInfo beanInfo,
                              QName name, boolean isArray) {
        XmlSchemaElement el = new XmlSchemaElement(schema, false);
        el.setName(name.getLocalPart());

        if (isArray) {
            el.setMinOccurs(0);
            el.setMaxOccurs(Long.MAX_VALUE);
        } else {
            el.setMinOccurs(1);
            el.setMaxOccurs(1);
            el.setNillable(true);
        }

        if (beanInfo.isElement()) {
            QName ename = new QName(beanInfo.getElementNamespaceURI(null),
                                   beanInfo.getElementLocalName(null));
            XmlSchemaElement el2 = schemas.getElementByQName(ename);
            el.setName(null);
            el.getRef().setTargetQName(el2.getRef().getTargetQName());
        } else {
            Iterator<QName> itr = beanInfo.getTypeNames().iterator();
            if (!itr.hasNext()) {
                return;
            }
            QName typeName = itr.next();
            el.setSchemaTypeName(typeName);
        }

        seq.getItems().add(el);
    }


    private boolean isExistSchemaElement(XmlSchema schema, QName qn) {
        return schema.getElementByName(qn) != null;
    }
}
