/*
 * Copyright (c) 2007-2008, Dennis M. Sosnoski. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
 * disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 * following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of
 * JiBX nor the names of its contributors may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.jibx.ws.wsdl.tools;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jibx.binding.generator.BindGen;
import org.jibx.binding.generator.BindingMappingDetail;
import org.jibx.binding.model.BindingHolder;
import org.jibx.binding.model.CollectionElement;
import org.jibx.binding.model.MappingElement;
import org.jibx.binding.model.MappingElementBase;
import org.jibx.custom.classes.ClassCustom;
import org.jibx.custom.classes.CustomBase;
import org.jibx.custom.classes.GlobalCustom;
import org.jibx.runtime.JiBXException;
import org.jibx.runtime.QName;
import org.jibx.runtime.Utility;
import org.jibx.schema.IComponent;
import org.jibx.schema.SchemaHolder;
import org.jibx.schema.elements.AnnotationElement;
import org.jibx.schema.elements.ComplexTypeElement;
import org.jibx.schema.elements.DocumentationElement;
import org.jibx.schema.elements.ElementElement;
import org.jibx.schema.elements.SchemaElement;
import org.jibx.schema.elements.SequenceElement;
import org.jibx.schema.generator.MappingDetail;
import org.jibx.schema.generator.SchemaGen;
import org.jibx.schema.types.Count;
import org.jibx.util.IClass;
import org.jibx.util.IClassLocator;
import org.jibx.util.InsertionOrderedSet;
import org.jibx.util.Types;
import org.jibx.ws.wsdl.model.Definitions;
import org.jibx.ws.wsdl.model.Message;
import org.jibx.ws.wsdl.model.MessagePart;
import org.jibx.ws.wsdl.model.Operation;
import org.jibx.ws.wsdl.tools.custom.FaultCustom;
import org.jibx.ws.wsdl.tools.custom.OperationCustom;
import org.jibx.ws.wsdl.tools.custom.ServiceCustom;
import org.jibx.ws.wsdl.tools.custom.ThrowsCustom;
import org.jibx.ws.wsdl.tools.custom.ValueCustom;
import org.jibx.ws.wsdl.tools.custom.WsdlCustom;
import org.w3c.dom.Node;

/**
 * Start-from-code WSDL generator using JiBX data binding. This starts from one or more service classes, each with one
 * or more methods to be exposed as service operations, and generates complete bindings and WSDL for the services.
 * 
 * @author Dennis M. Sosnoski
 */
public class Jibx2Wsdl
{
    /** Parameter information for generation. */
    private final WsdlGeneratorCommandLine m_generationParameters;
    
    /** Binding generator. */
    private final BindGen m_bindingGenerator;
    
    /** Schema generator. */
    private final SchemaGen m_schemaGenerator;
    
    /** Map from schema namespace URIs to schema holders. */
    private final Map m_uriSchemaMap;
    
    /** Map from fully qualified class name to schema type name. */
    private Map m_classTypeMap;
    
    /** Map from class name to instance. This is used to keep track of JavaDoc formatter instances. */
    private final Map m_classInstances;
    
    /**
     * Constructor.
     * 
     * @param parms generation parameters
     */
    private Jibx2Wsdl(WsdlGeneratorCommandLine parms) {
        m_generationParameters = parms;
        GlobalCustom global = parms.getGlobal();
        m_bindingGenerator = new BindGen(global);
        m_schemaGenerator = new SchemaGen(parms.getLocator(), global, parms.getUriNames());
        m_uriSchemaMap = new HashMap();
        m_classInstances = new HashMap();
    }
    
    /**
     * Get the qualified name used for an abstract mapping. This throws an exception if the qualified name is not found.
     * 
     * @param type
     * @param mapping
     * @return qualified name
     */
    private QName getMappingQName(String type, MappingElement mapping) {
        MappingDetail detail = m_schemaGenerator.getMappingDetail(mapping);
        if (detail == null) {
            throw new IllegalStateException("No mapping found for type " + type);
        } else if (detail.isType()) {
            return detail.getTypeName();
        } else {
            throw new IllegalStateException("Need abstract mapping for type " + type);
        }
    }
    
    /**
     * Build an element representing a parameter or return value.
     * 
     * @param parm
     * @param typemap map from parameterized type to abstract mapping name
     * @param hold containing schema holder
     * @return constructed element
     */
    private ElementElement buildValueElement(ValueCustom parm, Map typemap, SchemaHolder hold) {
        
        // create the basic element definition
        ElementElement elem = new ElementElement();
        if (!parm.isRequired()) {
            elem.setMinOccurs(Count.COUNT_ZERO);
        }
        String type = parm.getType();
        if (type.endsWith("[]")) {
            elem.setMaxOccurs(Count.COUNT_UNBOUNDED);
        }
        
        // check type or reference for element
        boolean isref = false;
        String ptype = parm.getBoundType();
        QName tname = (QName)typemap.get(ptype);
        if (tname == null) {
            tname = Types.schemaType(ptype);
            if (tname == null) {
                String usetype = ptype.endsWith(">") ? type : ptype;
                BindingMappingDetail detail = m_bindingGenerator.getMappingDetail(usetype);
                if (detail == null) {
                    throw new IllegalStateException("No mapping found for type " + usetype);
                } else if (detail.isExtended()) {
                    elem.setRef(detail.getElementQName());
                    isref = true;
                } else {
                    MappingElement mapping = detail.getAbstractMapping();
                    tname = mapping.getTypeQName();
                    if (tname == null) {
                        tname = getMappingQName(usetype, mapping);
                    }
                }
            }
        }
        if (!isref) {
            
            // set element type and name
            m_schemaGenerator.setElementType(tname, elem, hold);
            String ename = parm.getElementName();
            if (ename == null) {
                ename = tname.getName();
            }
            elem.setName(ename);
        }
        
        // add documentation if available
        List nodes = parm.getDocumentation();
        if (nodes != null) {
            AnnotationElement anno = new AnnotationElement();
            DocumentationElement doc = new DocumentationElement();
            for (Iterator iter = nodes.iterator(); iter.hasNext();) {
                Node node = (Node)iter.next();
                doc.addContent(node);
            }
            anno.getItemsList().add(doc);
            elem.setAnnotation(anno);
        }
        return elem;
    }
    
    /**
     * Add reference defined by element to schema. This finds the namespace of the type or element reference used by the
     * provided element, and adds that namespace to the schema references.
     * 
     * @param elem
     * @param holder
     */
    private void addSchemaReference(ElementElement elem, SchemaHolder holder) {
        QName qname = elem.getType();
        if (qname == null) {
            qname = elem.getRef();
        }
        if (qname != null) {
            String rns = qname.getUri();
            if (!Utility.safeEquals(holder.getNamespace(), rns) && !IComponent.SCHEMA_NAMESPACE.equals(rns)) {
                holder.addReference((SchemaHolder)m_uriSchemaMap.get(rns));
            }
        }
    }
    
    /**
     * Build WSDL for service.
     * 
     * @param service
     * @param typemap map from parameterized type to abstract mapping name
     * @return constructed WSDL definitions
     */
    private Definitions buildWSDL(ServiceCustom service, Map typemap) {
        
        // initialize root object of definition
        String wns = service.getWsdlNamespace();
        String sns = service.getNamespace();
        String spfx = wns.equals(sns) ? "tns" : "sns";
        Definitions def = new Definitions(service.getPortTypeName(), service.getBindingName(), service.getServiceName(),
            service.getPortName(), "tns", wns, spfx, sns);
        def.setServiceLocation(service.getServiceAddress());
        
        // add service documentation if available
        IClassLocator locator = m_generationParameters.getLocator();
        IClass info = locator.getClassInfo(service.getClassName());
        if (info != null) {
            List nodes = m_generationParameters.getWsdlCustom().getFormatter(service).docToNodes(info.getJavaDoc());
            def.setPortTypeDocumentation(nodes);
        }
        
        // find or create the schema element and namespace
        SchemaHolder holder = (SchemaHolder)m_uriSchemaMap.get(sns);
        if (holder == null) {
            holder = new SchemaHolder(sns);
        }
        SchemaElement schema = holder.getSchema();
        def.getSchemas().add(schema);
        
        // process messages and operations used by service
        ArrayList ops = service.getOperations();
        Map fltmap = new HashMap();
        for (int i = 0; i < ops.size(); i++) {
            
            // get information for operation
            OperationCustom odef = (OperationCustom)ops.get(i);
            String oname = odef.getOperationName();
            Operation op = new Operation(oname);
            op.setDocumentation(odef.getDocumentation());
            op.setSoapAction(odef.getSoapAction());
            
            // generate input message information
            QName qname = new QName(sns, odef.getRequestWrapperName());
            MessagePart part = new MessagePart("part", qname);
            Message msg = new Message(odef.getRequestMessageName(), wns, part);
            op.addInputMessage(msg);
            def.addMessage(msg);
            
            // add corresponding schema definition to schema
            SequenceElement seq = new SequenceElement();
            ArrayList parms = odef.getParameters();
            for (int j = 0; j < parms.size(); j++) {
                ValueCustom parm = (ValueCustom)parms.get(j);
                ElementElement pelem = buildValueElement(parm, typemap, holder);
                seq.getParticleList().add(pelem);
                addSchemaReference(pelem, holder);
            }
            ComplexTypeElement tdef = new ComplexTypeElement();
            tdef.setContentDefinition(seq);
            ElementElement elem = new ElementElement();
            elem.setName(odef.getRequestWrapperName());
            elem.setTypeDefinition(tdef);
            schema.getTopLevelChildren().add(elem);
            
            // generate output message information
            qname = new QName(sns, odef.getResponseWrapperName());
            part = new MessagePart("part", qname);
            msg = new Message(odef.getResponseMessageName(), wns, part);
            op.addOutputMessage(msg);
            def.addMessage(msg);
            
            // add corresponding schema definition to schema
            seq = new SequenceElement();
            ValueCustom ret = odef.getReturn();
            if (!"void".equals(ret.getType())) {
                ElementElement relem = buildValueElement(ret, typemap, holder);
                seq.getParticleList().add(relem);
                addSchemaReference(relem, holder);
            }
            tdef = new ComplexTypeElement();
            tdef.setContentDefinition(seq);
            elem = new ElementElement();
            elem.setName(odef.getResponseWrapperName());
            elem.setTypeDefinition(tdef);
            schema.getTopLevelChildren().add(elem);
            
            // process fault message(s) for operation
            ArrayList thrws = odef.getThrows();
            WsdlCustom wsdlcustom = m_generationParameters.getWsdlCustom();
            for (int j = 0; j < thrws.size(); j++) {
                ThrowsCustom thrw = (ThrowsCustom)thrws.get(j);
                String type = thrw.getType();
                msg = (Message)fltmap.get(type);
                if (msg == null) {
                    
                    // first time for this throwable, create the message
                    FaultCustom fault = wsdlcustom.forceFaultCustomization(type);
                    qname = new QName(sns, fault.getElementName());
                    part = new MessagePart("fault", qname);
                    msg = new Message(fault.getFaultName(), wns, part);
                    def.addMessage(msg);
                    
                    // make sure the corresponding mapping exists
                    BindingMappingDetail detail = m_bindingGenerator.getMappingDetail(fault.getDataType());
                    if (detail == null) {
                        throw new IllegalStateException("No mapping found for type " + type);
                    }
                    
                    // record that the fault has been defined
                    fltmap.put(type, msg);
                }
                
                // add fault to operation definition
                op.addFaultMessage(msg);
            }
            
            // add operation to list of definitions
            def.addOperation(op);
            
        }
        holder.finish();
        return def;
    }
    
    /**
     * Accumulate data type(s) from value to be included in binding.
     * 
     * @param value
     * @param dataset set of types for binding
     */
    private void accumulateData(ValueCustom value, Set dataset) {
        String type = value.getBoundType();
        if (!dataset.contains(type) && !Types.isSimpleValue(type)) {
            String itype = value.getItemType();
            if (itype == null) {
                dataset.add(type);
            } else {
                dataset.add(itype);
            }
        }
    }
    
    /**
     * Add the &lt;mapping> definition for a typed collection to a binding. This always creates an abstract mapping with
     * the type name based on both the item type and the collection type.
     * 
     * @param value collection value
     * @param typemap map from parameterized type to abstract mapping name
     * @param bind target binding
     * @return qualified name for collection
     */
    public QName addCollectionBinding(ValueCustom value, Map typemap, BindingHolder bind) {
        
        // check for existing mapping
        String ptype = value.getBoundType();
        QName qname = (QName)typemap.get(ptype);
        if (qname == null) {
            
            // create abstract mapping for collection class type
            MappingElementBase mapping = new MappingElement();
            mapping.setClassName(value.getType());
            mapping.setAbstract(true);
            mapping.setCreateType(value.getCreateType());
            mapping.setFactoryName(value.getFactoryMethod());
            
            // generate the mapping type name from item class name and suffix
            String suffix;
            String type = value.getType();
            GlobalCustom global = m_generationParameters.getGlobal();
            IClass clas = global.getClassInfo(type);
            if (clas.isImplements("Ljava/util/List;")) {
                suffix = "List";
            } else if (clas.isImplements("Ljava/util/Set;")) {
                suffix = "Set";
            } else {
                suffix = "Collection";
            }
            String itype = value.getItemType();
            ClassCustom cust = global.addClassCustomization(itype);
            
            // register the type name for mapping
            String name = cust.getSimpleName() + suffix;
            qname = new QName(bind.getNamespace(), CustomBase.convertName(name, CustomBase.CAMEL_CASE_NAMES));
            mapping.setTypeQName(qname);
            typemap.put(ptype, qname);
            
            // add collection definition details
            CollectionElement coll = new CollectionElement();
            m_bindingGenerator.defineCollection(itype, value.getItemElementName(), coll, bind);
            mapping.addChild(coll);
            
            // add mapping to binding
            bind.addMapping(mapping);
        }
        return qname;
    }
    
    /**
     * Generate based on list of service classes.
     * 
     * @param classes service class list
     * @param extras list of extra classes for binding
     * @return list of WSDLs
     * @throws JiBXException
     * @throws IOException
     */
    private List generate(List classes, List extras) throws JiBXException, IOException {
        
        // add any service classes not already present in customizations
        WsdlCustom wsdlcustom = m_generationParameters.getWsdlCustom();
        for (int i = 0; i < classes.size(); i++) {
            String sclas = (String)classes.get(i);
            if (wsdlcustom.getServiceCustomization(sclas) == null) {
                wsdlcustom.addServiceCustomization(sclas);
            }
        }
        
        // accumulate the data classes used by all service operations
        // TODO: throws class handling, with multiple services per WSDL
        InsertionOrderedSet abstrs = new InsertionOrderedSet();
        InsertionOrderedSet concrs = new InsertionOrderedSet();
        ArrayList qnames = new ArrayList();
        List services = wsdlcustom.getServices();
        String[] adduris = new String[services.size()];
        int index = 0;
        for (Iterator iter = services.iterator(); iter.hasNext();) {
            ServiceCustom service = (ServiceCustom)iter.next();
            adduris[index++] = service.getNamespace();
            List ops = service.getOperations();
            for (Iterator iter1 = ops.iterator(); iter1.hasNext();) {
                OperationCustom op = (OperationCustom)iter1.next();
                List parms = op.getParameters();
                for (Iterator iter2 = parms.iterator(); iter2.hasNext();) {
                    accumulateData((ValueCustom)iter2.next(), abstrs);
                }
                accumulateData(op.getReturn(), abstrs);
                ArrayList thrws = op.getThrows();
                for (int i = 0; i < thrws.size(); i++) {
                    
                    // add concrete mapping for data type, if used
                    ThrowsCustom thrw = (ThrowsCustom)thrws.get(i);
                    FaultCustom fault = wsdlcustom.forceFaultCustomization(thrw.getType());
                    if (!concrs.contains(fault.getDataType())) {
                        concrs.add(fault.getDataType());
                        qnames.add(new QName(service.getNamespace(), fault.getElementName()));
                    }
                }
            }
        }
        
        // include extra classes as needing concrete mappings
        GlobalCustom global = m_generationParameters.getGlobal();
        for (int i = 0; i < extras.size(); i++) {
            String type = (String)extras.get(i);
            if (!concrs.contains(type)) {
                concrs.add(type);
                global.addClassCustomization(type);
                qnames.add(null);
            }
        }
        
        // generate bindings for all data classes used
        m_bindingGenerator.generateSpecified(qnames, concrs.asList(), abstrs.asList());
        
        // add binding definitions for collections passed or returned
        Map typemap = new HashMap();
        for (Iterator iter = services.iterator(); iter.hasNext();) {
            ServiceCustom service = (ServiceCustom)iter.next();
            List ops = service.getOperations();
            BindingHolder hold = null;
            String uri = service.getNamespace();
            for (Iterator iter1 = ops.iterator(); iter1.hasNext();) {
                OperationCustom op = (OperationCustom)iter1.next();
                List parms = op.getParameters();
                for (Iterator iter2 = parms.iterator(); iter2.hasNext();) {
                    ValueCustom parm = (ValueCustom)iter2.next();
                    if (parm.getItemType() != null) {
                        if (hold == null) {
                            hold = m_bindingGenerator.findBinding(uri);
                        }
                        addCollectionBinding(parm, typemap, hold);
                    }
                }
                ValueCustom ret = op.getReturn();
                if (ret.getItemType() != null) {
                    if (hold == null) {
                        hold = m_bindingGenerator.findBinding(uri);
                    }
                    addCollectionBinding(ret, typemap, hold);
                }
            }
        }
        
        // write the binding(s), then validate from file (to get line numbers)
        String name = m_generationParameters.getBindingName();
        File path = m_generationParameters.getGeneratePath();
        BindingHolder rhold = m_bindingGenerator.finish(name, adduris);
        List bindings = m_bindingGenerator.validateFiles(path, m_generationParameters.getLocator(), rhold);
        if (bindings == null) {
            return null;
        } else {
            
            // build and record the schemas
            List schemas = m_schemaGenerator.generate(bindings);
            for (Iterator iter = schemas.iterator(); iter.hasNext();) {
                SchemaHolder holder = (SchemaHolder)iter.next();
                m_uriSchemaMap.put(holder.getNamespace(), holder);
            }
            
            // build the WSDL for each service
            ArrayList wsdls = new ArrayList();
            for (Iterator iter = services.iterator(); iter.hasNext();) {
                wsdls.add(buildWSDL((ServiceCustom)iter.next(), typemap));
            }
            return wsdls;
            
        }
    }
    
    /**
     * Run the WSDL generation using command line parameters.
     * 
     * @param args
     * @throws JiBXException
     * @throws IOException
     */
    public static void main(String[] args) throws JiBXException, IOException {
        WsdlGeneratorCommandLine parms = new WsdlGeneratorCommandLine();
        if (args.length > 0 && parms.processArgs(args)) {
            
            // generate services, bindings, and WSDLs
            Jibx2Wsdl inst = new Jibx2Wsdl(parms);
            ArrayList extras = new ArrayList(parms.getExtraTypes());
            ArrayList classes = parms.getGlobal().getUnmarshalledClasses();
            for (int i = 0; i < classes.size(); i++) {
                ClassCustom clas = (ClassCustom)classes.get(i);
                if (clas.isForceMapping()) {
                    extras.add(clas.getName());
                }
            }
            List wsdls = inst.generate(parms.getExtraArgs(), extras);
            if (wsdls != null) {
                
                // write the schemas and WSDLS
                SchemaGen.writeSchemas(parms.getGeneratePath(), inst.m_uriSchemaMap.values());
                WsdlWriter writer = new WsdlWriter();
                for (Iterator iter = wsdls.iterator(); iter.hasNext();) {
                    Definitions def = (Definitions)iter.next();
                    File file = new File(parms.getGeneratePath(), def.getServiceName() + ".wsdl");
                    writer.writeWSDL(def, new FileOutputStream(file));
                }
            }
            
        } else {
            if (args.length > 0) {
                System.err.println("Terminating due to command line errors");
            } else {
                parms.printUsage();
            }
            System.exit(1);
        }
    }
}