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

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.Map;

import javax.xml.namespace.QName;

import org.apache.cxf.aegis.DatabindingException;
import org.apache.cxf.aegis.type.basic.ArrayType;
import org.apache.cxf.aegis.type.basic.ObjectType;
import org.apache.cxf.aegis.type.collection.CollectionType;
import org.apache.cxf.aegis.type.collection.MapType;
import org.apache.cxf.aegis.util.NamespaceHelper;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.helpers.ServiceUtils;
import org.apache.cxf.wsdl.WSDLConstants;
import org.apache.ws.commons.schema.constants.Constants;

public abstract class AbstractTypeCreator implements TypeCreator {
    public static final String HTTP_CXF_APACHE_ORG_ARRAYS = "http://cxf.apache.org/arrays";

    protected TypeMapping tm;

    protected AbstractTypeCreator nextCreator;

    private TypeCreationOptions typeConfiguration;

    private TypeCreator parent;

    public TypeMapping getTypeMapping() {
        return tm;
    }

    public TypeCreator getTopCreator() {
        TypeCreator top = this;
        TypeCreator next = top;
        while (next != null) {
            top = next;
            next = top.getParent();
        }
        return top;
    }

    public TypeCreator getParent() {
        return parent;
    }

    public void setParent(TypeCreator parent) {
        this.parent = parent;
    }

    public void setTypeMapping(TypeMapping typeMapping) {
        this.tm = typeMapping;

        if (nextCreator != null) {
            nextCreator.setTypeMapping(tm);
        }
    }

    public void setNextCreator(AbstractTypeCreator creator) {
        this.nextCreator = creator;
        nextCreator.parent = this;
    }

    public TypeClassInfo createClassInfo(Field f) {
        TypeClassInfo info = createBasicClassInfo(f.getType());
        info.setDescription("field " + f.getName() + " in  " + f.getDeclaringClass());
        return info;
    }

    public TypeClassInfo createBasicClassInfo(Type type) {
        TypeClassInfo info = new TypeClassInfo();
        Class<?> typeClass = TypeUtil.getTypeClass(type, false);
        if (typeClass != null) {
            info.setDescription("class '" + typeClass.getName() + "'");
        } else {
            info.setDescription("type '" + type + "'");
        }
        info.setType(type);

        return info;
    }

    public AegisType createTypeForClass(TypeClassInfo info) {

        Class<?> javaClass = TypeUtil.getTypeRelatedClass(info.getType());
        final AegisType result;
        boolean newType = true;
        if (info.getType() instanceof TypeVariable) {
            //it's the generic type
            result = getOrCreateGenericType(info);
        } else if (info.getAegisTypeClass() != null) {
            result = createUserType(info);
        } else if (isArray(javaClass)) {
            result = createArrayType(info);
        } else if (isMap(javaClass)) {
            result = createMapType(info);
        }  else if (isHolder(javaClass)) {
            result = createHolderType(info);
        } else if (isCollection(javaClass)) {
            result = createCollectionType(info);
        } else if (isEnum(javaClass)) {
            result = createEnumType(info);
        } else if (javaClass.equals(byte[].class)) {
            result = getTypeMapping().getType(javaClass);
        } else {
            AegisType type = getTypeMapping().getType(info.getType());
            if (type == null
                || (info.getTypeName() != null && !type.getSchemaType().equals(info.getTypeName()))) {
                if (info.getTypeName() != null) {
                    type = getTypeMapping().getType(info.getTypeName());
                }
                if (type == null) {
                    type = getTypeMapping().getType(javaClass);
                }
                if (type == null) {
                    type = createDefaultType(info);
                } else {
                    newType = false;
                }
            } else {
                newType = false;
            }

            result = type;
        }

        if (newType
            && !getConfiguration().isDefaultNillable()) {
            result.setNillable(false);
        }

        return result;
    }


    protected boolean isHolder(Class<?> javaType) {
        return "javax.xml.ws.Holder".equals(javaType.getName());
    }

    protected AegisType createHolderType(TypeClassInfo info) {

        Type heldType = TypeUtil.getSingleTypeParameter(info.getType(), 0);
        if (heldType == null) {
            throw new UnsupportedOperationException("Invalid holder type " + info.getType());
        }

        info.setType(heldType);
        return createType(heldType);
    }


    protected boolean isArray(Class<?> javaType) {
        return javaType.isArray() && !javaType.equals(byte[].class);
    }

    protected AegisType createUserType(TypeClassInfo info) {
        try {
            AegisType type = info.getAegisTypeClass().getDeclaredConstructor().newInstance();

            QName name = info.getTypeName();
            if (name == null) {
                // We do not want to use the java.lang.whatever schema type.
                // If the @ annotation or XML file didn't specify a schema type,
                // but the natural type has a schema type mapping, we use that rather
                // than create nonsense.
                Class<?> typeClass = TypeUtil.getTypeRelatedClass(info.getType());
                if (typeClass.getPackage().getName().startsWith("java")) {
                    name = tm.getTypeQName(typeClass);
                }
                // if it's still null, we'll take our lumps, but probably end up with
                // an invalid schema.
                if (name == null) {
                    name = createQName(typeClass);
                }
            }

            type.setSchemaType(name);
            type.setTypeClass(info.getType());
            type.setTypeMapping(getTypeMapping());

            return type;
        } catch (InstantiationException | IllegalArgumentException | InvocationTargetException 
                | NoSuchMethodException e) {
            throw new DatabindingException("Couldn't instantiate type classs "
                                           + info.getAegisTypeClass().getName(), e);
        } catch (IllegalAccessException | SecurityException e) {
            throw new DatabindingException("Couldn't access type classs "
                                           + info.getAegisTypeClass().getName(), e);
        }
    }

    protected AegisType createArrayType(TypeClassInfo info) {
        ArrayType type = new ArrayType();
        type.setTypeMapping(getTypeMapping());
        type.setTypeClass(info.getType());
        type.setSchemaType(createCollectionQName(info, type.getComponentType()));

        if (info.getMinOccurs() != -1) {
            type.setMinOccurs(info.getMinOccurs());
        } else {
            type.setMinOccurs(typeConfiguration.getDefaultMinOccurs());
        }

        if (info.getMaxOccurs() != -1) {
            type.setMaxOccurs(info.getMaxOccurs());
        }

        type.setFlat(info.isFlat());

        return type;
    }

    protected QName createQName(Class<?> javaType) {
        String clsName = javaType.getName();

        String ns = NamespaceHelper.makeNamespaceFromClassName(clsName, "http");
        String localName = ServiceUtils.makeServiceNameFromClassName(javaType);

        return new QName(ns, localName);
    }

    protected boolean isCollection(Class<?> javaType) {
        return Collection.class.isAssignableFrom(javaType);
    }

    protected AegisType createCollectionTypeFromGeneric(TypeClassInfo info) {
        AegisType component = getOrCreateGenericType(info);

        CollectionType type = new CollectionType(component);
        type.setTypeMapping(getTypeMapping());

        QName name = info.getTypeName();
        if (name == null) {
            name = createCollectionQName(info, component);
        }

        type.setSchemaType(name);

        type.setTypeClass(info.getType());

        if (info.getMinOccurs() != -1) {
            type.setMinOccurs(info.getMinOccurs());
        }
        if (info.getMaxOccurs() != -1) {
            type.setMaxOccurs(info.getMaxOccurs());
        }

        type.setFlat(info.isFlat());

        return type;
    }

    protected AegisType getOrCreateGenericType(TypeClassInfo info) {
        return createObjectType();
    }

    protected AegisType getOrCreateMapKeyType(TypeClassInfo info) {
        return nextCreator.getOrCreateMapKeyType(info);
    }

    protected AegisType createObjectType() {
        ObjectType type = new ObjectType();
        type.setSchemaType(Constants.XSD_ANYTYPE);
        type.setTypeClass(Object.class);
        type.setTypeMapping(getTypeMapping());
        return type;
    }

    protected AegisType getOrCreateMapValueType(TypeClassInfo info) {
        return nextCreator.getOrCreateMapValueType(info);
    }

    protected AegisType createMapType(TypeClassInfo info, AegisType keyType, AegisType valueType) {
        QName schemaType = createMapQName(info, keyType, valueType);
        MapType type = new MapType(schemaType, keyType, valueType);
        type.setTypeMapping(getTypeMapping());
        type.setTypeClass(info.getType());

        return type;
    }

    protected AegisType createMapType(TypeClassInfo info) {
        AegisType keyType = getOrCreateMapKeyType(info);
        AegisType valueType = getOrCreateMapValueType(info);

        return createMapType(info, keyType, valueType);
    }

    protected QName createMapQName(TypeClassInfo info, AegisType keyType, AegisType valueType) {
        String name = keyType.getSchemaType().getLocalPart() + '2' + valueType.getSchemaType().getLocalPart();


        Class<?> cls = TypeUtil.getTypeRelatedClass(info.getType());
        name += cls.getSimpleName();

        // TODO: Get namespace from XML?
        return new QName(tm.getMappingIdentifierURI(), name);
    }

    protected boolean isMap(Class<?> javaType) {
        return Map.class.isAssignableFrom(javaType);
    }

    public abstract TypeClassInfo createClassInfo(PropertyDescriptor pd);

    protected boolean isEnum(Class<?> javaType) {
        return false;
    }

    public AegisType createEnumType(TypeClassInfo info) {
        return null;
    }

    public abstract AegisType createCollectionType(TypeClassInfo info);

    public abstract AegisType createDefaultType(TypeClassInfo info);

    protected QName createCollectionQName(TypeClassInfo info, AegisType type) {
        String ns;

        if (type.isComplex()) {
            ns = type.getSchemaType().getNamespaceURI();
        } else {
            ns = tm.getMappingIdentifierURI();
        }
        if (WSDLConstants.NS_SCHEMA_XSD.equals(ns)) {
            ns = HTTP_CXF_APACHE_ORG_ARRAYS;
        }

        String localName = "ArrayOf" + StringUtils.capitalize(type.getSchemaType().getLocalPart());
        if (info.nonDefaultAttributes()) {
            localName += "-";
            if (info.getMinOccurs() >= 0) {
                localName += info.getMinOccurs();
            }
            localName += "-";
            if (info.getMaxOccurs() >= 0) {
                localName += info.getMaxOccurs();
            }
            if (info.isFlat()) {
                localName += "Flat";
            }
        }

        return new QName(ns, localName);
    }

    public abstract TypeClassInfo createClassInfo(Method m, int index);

    /**
     * Create a AegisType for a Method parameter.
     *
     * @param m the method to create a type for
     * @param index The parameter index. If the index is less than zero, the
     *            return type is used.
     */
    public AegisType createType(Method m, int index) {
        TypeClassInfo info = createClassInfo(m, index);
        info.setDescription((index == -1 ? "return type" : "parameter " + index) + " of method "
                            + m.getName() + " in " + m.getDeclaringClass());
        return createTypeForClass(info);
    }

    public QName getElementName(Method m, int index) {
        TypeClassInfo info = createClassInfo(m, index);

        return info.getMappedName();
    }

    /**
     * Create type information for a PropertyDescriptor.
     *
     * @param pd the propertydescriptor
     */
    public AegisType createType(PropertyDescriptor pd) {
        TypeClassInfo info = createClassInfo(pd);
        info.setDescription("property " + pd.getName());
        return createTypeForClass(info);
    }

    /**
     * Create type information for a <code>Field</code>.
     *
     * @param f the field to create a type from
     */
    public AegisType createType(Field f) {
        TypeClassInfo info = createClassInfo(f);
        info.setDescription("field " + f.getName() + " in " + f.getDeclaringClass());
        return createTypeForClass(info);
    }

    /**
     * Create an Aegis type from a reflected type description.
     * This will only work for the restricted set of collection
     * types supported by Aegis.
     * @param t the reflected type.
     * @return the type
     */
    public AegisType createType(Type t) {
        TypeClassInfo info = new TypeClassInfo();
        info.setType(t);
        info.setDescription("reflected type " + t.toString());
        return createTypeForClass(info);

    }

    public AegisType createType(Class<?> clazz) {
        TypeClassInfo info = createBasicClassInfo(clazz);
        info.setDescription(clazz.toString());
        return createTypeForClass(info);
    }

    public TypeCreationOptions getConfiguration() {
        return typeConfiguration;
    }

    public void setConfiguration(TypeCreationOptions tpConfiguration) {
        this.typeConfiguration = tpConfiguration;
    }
}
