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

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;

import org.apache.cxf.aegis.type.AbstractTypeCreator;
import org.apache.cxf.aegis.type.DefaultTypeCreator;
import org.apache.cxf.aegis.type.DefaultTypeMapping;
import org.apache.cxf.aegis.type.Type;
import org.apache.cxf.aegis.type.TypeCreationOptions;
import org.apache.cxf.aegis.type.TypeCreator;
import org.apache.cxf.aegis.type.TypeMapping;
import org.apache.cxf.aegis.type.TypeUtil;
import org.apache.cxf.aegis.type.XMLTypeCreator;
import org.apache.cxf.aegis.type.basic.BeanType;
import org.apache.cxf.aegis.type.java5.Java5TypeCreator;
import org.apache.cxf.common.classloader.ClassLoaderUtils;
import org.apache.cxf.common.util.SOAPConstants;

/**
 * The Aegis Databinding context object. This object coordinates the data binding process: reading
 * and writing XML.
 * 
 * By default, this object sets up a default set of type mappings. 
 * This consists of two DefaultTypeMapping objects. The first is empty
 * and has the Default, Java5, and XML TypeCreator classes configured. The second contains the
 * standard mappings of the stock types. If a type can't be mapped in either, then the creators 
 * create a mapping and store it in the first one.
 * 
 * The application can control some parameters of the type creators by creating a TypeCreationOptions
 * object and setting properties. The application can add custom mappings to the type mapping, or 
 * even use its own classes for the TypeMapping or TypeCreator objects.
 *
 * Aegis, unlike JAXB, has no concept of a 'root element'. So, an application that 
 * uses Aegis without a web service has to either depend on xsi:type (at least for 
 * root elements) or have its own mapping from elements to classes, and pass the 
 * resulting Class objects to the readers.
 * At this level, the application must specify the initial set of classes to make 
 * make use of untyped collections or .aegis.xml files.
 * 
 * If the application leaves this list empty, and reads XML messages, then no .aegis.xml files 
 * are used unless the application has specified a Class&lt;T&gt; for the root of a 
 * particular item read. Specifically, if the application just leaves it to Aegis to
 * map an element tagged with an xsi:type to a class, Aegis can't know that some arbitrary class in
 * some arbitrary package is mapped to a particular schema type by QName in a
 * mapping XML file.
 * 
 * At the level of the CXF data binding, the 'root elements' are defined by the WSDL message parts.
 * Additional classes that participate are termed 'override' classes.
 * 
 */
public class AegisContext {
    private boolean writeXsiTypes;
    private boolean readXsiTypes = true;

    private Set<String> rootClassNames;
    private Set<Class<?>> rootClasses;
    private Set<QName> rootTypeQNames;
    // this type mapping is the front of the chain of delegating type mappings.
    private TypeMapping typeMapping;
    private Set<Type> rootTypes;
    private Map<Class<?>, String> beanImplementationMap;
    private TypeCreationOptions configuration;
    private boolean mtomEnabled;
    private boolean mtomUseXmime;
    // this URI goes into the type map.
    private String mappingNamespaceURI;
    /**
     * Construct a context.
     */
    public AegisContext() {
        beanImplementationMap = new HashMap<Class<?>, String>();
        rootClasses = new HashSet<Class<?>>();
        rootTypeQNames = new HashSet<QName>();
    }
    
    public TypeCreator createTypeCreator() {
        AbstractTypeCreator xmlCreator = createRootTypeCreator();

        Java5TypeCreator j5Creator = new Java5TypeCreator();
        j5Creator.setNextCreator(createDefaultTypeCreator());
        j5Creator.setConfiguration(getTypeCreationOptions());
        xmlCreator.setNextCreator(j5Creator);

        return xmlCreator;
    }

    protected AbstractTypeCreator createRootTypeCreator() {
        AbstractTypeCreator creator = new XMLTypeCreator();
        creator.setConfiguration(getTypeCreationOptions());
        return creator;
    }

    protected AbstractTypeCreator createDefaultTypeCreator() {
        AbstractTypeCreator creator = new DefaultTypeCreator();
        creator.setConfiguration(getTypeCreationOptions());
        return creator;
    }


    /**
     * Initialize the context. The encodingStyleURI allows .aegis.xml files to have multiple mappings 
     * for, say, SOAP 1.1 versus SOAP 1.2. Passing null uses a default URI. 
     * @param mappingNamespaceURI URI to select mappings based on the encoding.
     */
    public void initialize() {
        // allow spring config of an alternative mapping.
        if (configuration == null) {
            configuration = new TypeCreationOptions();
        }
        if (typeMapping == null) {
            boolean defaultNillable = configuration.isDefaultNillable();
            TypeMapping baseTM = DefaultTypeMapping.createDefaultTypeMapping(defaultNillable, mtomUseXmime);
            // The use of the XSD URI in the mapping is, MAGIC. 
            if (mappingNamespaceURI == null) {
                mappingNamespaceURI = SOAPConstants.XSD;
            }
            DefaultTypeMapping defaultTypeMapping = new DefaultTypeMapping(mappingNamespaceURI, baseTM);
            defaultTypeMapping.setTypeCreator(createTypeCreator());
            typeMapping = defaultTypeMapping;
        }
        
        processRootTypes();
    }
    
    
    public AegisReader<org.w3c.dom.Element>
    createDomElementReader() {
        return new AegisElementDataReader(this);
    }
    
    public AegisReader<XMLStreamReader>
    createXMLStreamReader() {
        return new AegisXMLStreamDataReader(this);
    }
    
    public AegisWriter<org.w3c.dom.Element>
    createDomElementWriter() {
        return new AegisElementDataWriter(this);
    }
    
    public AegisWriter<XMLStreamWriter>
    createXMLStreamWriter() {
        return new AegisXMLStreamDataWriter(this);
    }
    
    /**
     * If a class was provided as part of the 'root' list, retrieve it's Type by
     * Class.
     * @param clazz
     * @return
     */
    public Type getRootType(Class clazz) {
        if (rootClasses.contains(clazz)) {
            return typeMapping.getType(clazz);
        } else {
            return null;
        }
    }
    
    /**
     * If a class was provided as part of the root list, retrieve it's Type by schema
     * type QName.
     * @param schemaTypeName
     * @return
     */
    public Type getRootType(QName schemaTypeName) {
        if (rootTypeQNames.contains(schemaTypeName)) {
            return typeMapping.getType(schemaTypeName);
        } else {
            return null;
        }
    }
    
    /**
     * Examine a list of override classes, and register all of them.
     * @param tm      type manager for this binding
     * @param classes list of class names
     */
    private void processRootTypes() {
        rootTypes = new HashSet<Type>();
        // app may have already supplied classes.
        if (rootClasses == null) {
            rootClasses = new HashSet<Class<?>>();
        }
        rootTypeQNames = new HashSet<QName>();
        if (this.rootClassNames != null) {
            for (String typeName : rootClassNames) {
                Class c = null;
                try {
                    c = ClassLoaderUtils.loadClass(typeName, TypeUtil.class);
                } catch (ClassNotFoundException e) {
                    throw new DatabindingException("Could not find override type class: " + typeName, e);
                }
                
                rootClasses.add(c);
            }
        }
         
        for (Class<?> c : rootClasses) {
            Type t = typeMapping.getType(c);
            if (t == null) {
                t = typeMapping.getTypeCreator().createType(c);
                typeMapping.register(t);
            }
            rootTypeQNames.add(t.getSchemaType());
            if (t instanceof BeanType) {
                BeanType bt = (BeanType)t;
                bt.getTypeInfo().setExtension(true);
                rootTypes.add(bt);
            }
        }
    }

    /**
     * Retrieve the set of root class names. Note that if the application
     * specifies the root classes by Class instead of by name, this will
     * return null.
     * @return
     */
    public Set<String> getRootClassNames() {
        return rootClassNames;
    }
    
    /**
     * Set the root class names. This function is a convenience for Spring
     * configuration. It sets the same underlying 
     * collection as {@link #setRootClasses(Set)}.
     * 
     * @param classNames
     */
    public void setRootClassNames(Set<String> classNames) {
        rootClassNames = classNames;
    }

    /** 
     * Return the type mapping configuration associated with this context.
     * @return Returns the configuration.
     * @deprecated 2.1
     */
    public TypeCreationOptions getConfiguration() {
        return configuration;
    }

    /** 
     * Return the type mapping configuration associated with this context.
     * @return Returns the configuration.
     */
    public TypeCreationOptions getTypeCreationOptions() {
        return configuration;
    }

    /**
     * Set the configuration object. The configuration specifies default
     * type mapping behaviors. 
     * @param configuration The configuration to set.
     * @deprecated 2.1
     */
    public void setConfiguration(TypeCreationOptions newConfiguration) {
        this.configuration = newConfiguration;
    }

    /**
     * Set the configuration object. The configuration specifies default
     * type mapping behaviors.
     * @param configuration The configuration to set.
     */
    public void setTypeCreationOptions(TypeCreationOptions newConfiguration) {
        this.configuration = newConfiguration;
    }

    public boolean isWriteXsiTypes() {
        return writeXsiTypes;
    }

    public boolean isReadXsiTypes() {
        return readXsiTypes;
    }

    /**
     * Controls whether Aegis writes xsi:type attributes on all elements.
     * False by default.
     * @param flag
     */
    public void setWriteXsiTypes(boolean flag) {
        this.writeXsiTypes = flag;
    }

    /**
     * Controls the use of xsi:type attributes when reading objects. By default,
     * xsi:type reading is enabled. When disabled, Aegis will only map for objects
     * that the application manually maps in the type mapping. 
     * @param flag
     */
    public void setReadXsiTypes(boolean flag) {
        this.readXsiTypes = flag;
    }

    /**
     * Return the type mapping object used by this context.
     * @return
     */
    public TypeMapping getTypeMapping() {
        return typeMapping;
    }

    /**
     * Set the type mapping object used by this context.
     * @param typeMapping
     */
    public void setTypeMapping(TypeMapping typeMapping) {
        this.typeMapping = typeMapping;
    }

    /**
     * Retrieve the Aegis type objects for the root classes.
     * @return the set of type objects.
     */
    public Set<Type> getRootTypes() {
        return rootTypes;
    }
    
    
    /** 
     * This property provides support for interfaces. If there is a mapping from an interface's Class<T>
     * to a string containing a class name, Aegis will create proxy objects of that class name.
     * @see org.apache.cxf.aegis.type.basic.BeanType
     * @return
     */
    public Map<Class<?>, String> getBeanImplementationMap() {
        return beanImplementationMap;
    }

    public void setBeanImplementationMap(Map<Class<?>, String> beanImplementationMap) {
        this.beanImplementationMap = beanImplementationMap;
    }

    public Set<Class<?>> getRootClasses() {
        return rootClasses;
    }

    /**
     * The list of initial classes.
     * @param rootClasses
     */
    public void setRootClasses(Set<Class<?>> rootClasses) {
        this.rootClasses = rootClasses;
    }

    /**
     * Is MTOM enabled in this context?
     * @return
     */
    public boolean isMtomEnabled() {
        return mtomEnabled;
    }

    public void setMtomEnabled(boolean mtomEnabled) {
        this.mtomEnabled = mtomEnabled;
    }

    /**
     * Should this service use schema for MTOM types xmime:base64Binary instead of xsd:base64Binary?
     * @return
     */
    public boolean isMtomUseXmime() {
        return mtomUseXmime;
    }

    public void setMtomUseXmime(boolean mtomUseXmime) {
        this.mtomUseXmime = mtomUseXmime;
    }

    /**
     * What URI identifies the type mapping for this context? 
     * When the XMLTypeCreator reads .aegis.xml file, it will only read mappings for
     * this URI (or no URI). When the abstract type creator is otherwise at a loss
     * for a namespace URI, it will use this URI.
     * @return
     */
    public String getMappingNamespaceURI() {
        return mappingNamespaceURI;
    }

    public void setMappingNamespaceURI(String mappingNamespaceURI) {
        this.mappingNamespaceURI = mappingNamespaceURI;
    }
}
