/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.ws.tools.helpers;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.rpc.encoding.TypeMapping;
import javax.xml.rpc.holders.Holder;

import org.apache.xerces.xs.XSTypeDefinition;
import org.jboss.ws.Constants;
import org.jboss.ws.WSException;
import org.jboss.ws.core.soap.Style;
import org.jboss.ws.metadata.jaxrpcmapping.ExceptionMapping;
import org.jboss.ws.metadata.jaxrpcmapping.JavaWsdlMapping;
import org.jboss.ws.metadata.jaxrpcmapping.JavaXmlTypeMapping;
import org.jboss.ws.metadata.jaxrpcmapping.MethodParamPartsMapping;
import org.jboss.ws.metadata.jaxrpcmapping.PackageMapping;
import org.jboss.ws.metadata.jaxrpcmapping.PortMapping;
import org.jboss.ws.metadata.jaxrpcmapping.ServiceEndpointInterfaceMapping;
import org.jboss.ws.metadata.jaxrpcmapping.ServiceEndpointMethodMapping;
import org.jboss.ws.metadata.jaxrpcmapping.ServiceInterfaceMapping;
import org.jboss.ws.metadata.jaxrpcmapping.VariableMapping;
import org.jboss.ws.metadata.jaxrpcmapping.WsdlMessageMapping;
import org.jboss.ws.metadata.jaxrpcmapping.WsdlReturnValueMapping;
import org.jboss.ws.metadata.umdm.EndpointMetaData;
import org.jboss.ws.metadata.umdm.FaultMetaData;
import org.jboss.ws.metadata.umdm.OperationMetaData;
import org.jboss.ws.metadata.umdm.ParameterMetaData;
import org.jboss.ws.metadata.umdm.WrappedParameter;
import org.jboss.ws.metadata.wsdl.WSDLBinding;
import org.jboss.ws.metadata.wsdl.WSDLInterface;
import org.jboss.ws.metadata.wsdl.WSDLService;
import org.jboss.ws.metadata.wsdl.WSDLTypes;
import org.jboss.ws.metadata.wsdl.WSDLUtils;
import org.jboss.ws.metadata.wsdl.XSModelTypes;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSElementDeclaration;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSModel;
import org.jboss.ws.metadata.wsdl.xmlschema.WSSchemaUtils;
import org.jboss.ws.metadata.wsdl.xsd.SchemaUtils;
import org.jboss.ws.tools.JavaToXSD;
import org.jboss.ws.tools.interfaces.JavaToXSDIntf;
import org.jboss.ws.tools.interfaces.SchemaCreatorIntf;
import org.jboss.ws.tools.wsdl.WSDLGenerator;
import org.jboss.wsf.common.JavaUtils;

/**
 *  Java To WSDL Helper which uses UnifiedMetaData
 *  @author <mailto:Anil.Saldhana@jboss.org>Anil Saldhana
 *  @since  Oct 7, 2005
 */
public class JavaToWSDLHelper extends WSDLGenerator
{
   private JavaToXSDIntf javaToXSD = new JavaToXSD();
   private JavaWsdlMapping javaWsdlMapping = new JavaWsdlMapping();
   private Map<QName, JavaXmlTypeMapping> mappedTypes = new HashMap<QName, JavaXmlTypeMapping>();
   private Set<String> mappedPackages = new HashSet<String>();
   private Set<String> mappedExceptions = new HashSet<String>();

   protected void processTypes()
   {
      // All type processing is done in processService()
      wsdl.setWsdlTypes(new XSModelTypes());
   }

   @Override
   protected void processOperation(WSDLInterface wsdlInterface, WSDLBinding wsdlBinding, OperationMetaData operation)
   {
      super.processOperation(wsdlInterface, wsdlBinding, operation);

      try
      {
         //Generate Types for the individual parameters, faults and return value
         for (ParameterMetaData paramMetaData : operation.getParameters())
         {
            generateTypesForXSD(paramMetaData);
         }

         for (FaultMetaData faultMetaData : operation.getFaults())
         {
            generateTypesForXSD(faultMetaData);
         }

         ParameterMetaData returnMetaData = operation.getReturnParameter();
         if (returnMetaData != null)
         {
            generateTypesForXSD(returnMetaData);
         }
      }
      catch (IOException io)
      {
         throw new WSException(io);
      }
   }

   public void generateTypesForXSD(ParameterMetaData pmd) throws IOException
   {
      //Types always deals with TypeNamespace
      QName xmlType = pmd.getXmlType();
      if(xmlType.getNamespaceURI().equals(Constants.NS_SCHEMA_XSD) == false)
        generateType(xmlType, pmd.getJavaType(), buildElementNameMap(pmd));

      if (pmd.getOperationMetaData().getStyle() == Style.DOCUMENT || pmd.isInHeader())
         generateElement(pmd.getXmlName(), xmlType);

      //Attachment type
      if(pmd.isSwA())
         wsdl.registerNamespaceURI(Constants.NS_SWA_MIME, "mime");
   }

   private Map<String, QName> buildElementNameMap(ParameterMetaData pmd)
   {
      List<WrappedParameter> wrappedParameters = pmd.getWrappedParameters();

      if (wrappedParameters == null)
         return null;

      Map<String, QName> map = new LinkedHashMap<String, QName>(wrappedParameters.size());

      for (WrappedParameter param : wrappedParameters)
         map.put(param.getVariable(), param.getName());

      return map;
   }

   public void generateTypesForXSD(FaultMetaData fmd) throws IOException
   {
      //Types always deals with TypeNamespace
      SchemaCreatorIntf sc = javaToXSD.getSchemaCreator();
      //Look at the features
      QName xmlType = fmd.getXmlType();
      if(xmlType.getNamespaceURI().equals(Constants.NS_SCHEMA_XSD) == false)
        generateType(xmlType ,fmd.getJavaType(), null);
   }

   public void processEndpoint(WSDLService service, EndpointMetaData endpoint)
   {
      super.processEndpoint(service, endpoint);

      // build JAX-RPC mapping info
      buildServiceMapping(endpoint);
   }

   /*
    * Currently we only handle 1 endpoint on 1 service, this is the way everything
    * else is handled anyway.
    */
   private void buildServiceMapping(EndpointMetaData endpoint)
   {
      QName origQName = endpoint.getServiceMetaData().getServiceName();
      String serviceInterfaceName = endpoint.getServiceEndpointInterface().getPackage().getName() + "." + origQName.getLocalPart();
      QName serviceQName = new QName(origQName.getNamespaceURI(), origQName.getLocalPart(), "serviceNS");

      ServiceInterfaceMapping serviceMapping = new ServiceInterfaceMapping(javaWsdlMapping);
      serviceMapping.setServiceInterface(serviceInterfaceName);
      serviceMapping.setWsdlServiceName(serviceQName);

      String endpointName = endpoint.getPortName().getLocalPart();
      PortMapping portMapping = new PortMapping(serviceMapping);
      portMapping.setJavaPortName(endpointName);
      portMapping.setPortName(endpointName);
      serviceMapping.addPortMapping(portMapping);

      javaWsdlMapping.addServiceInterfaceMappings(serviceMapping);

      String interfaceName = endpoint.getPortTypeName().getLocalPart();
      ServiceEndpointInterfaceMapping seiMapping = new ServiceEndpointInterfaceMapping(javaWsdlMapping);
      seiMapping.setServiceEndpointInterface(endpoint.getServiceEndpointInterfaceName());
      seiMapping.setWsdlPortType(new QName(wsdl.getTargetNamespace(), interfaceName, "portTypeNS"));
      seiMapping.setWsdlBinding(new QName(wsdl.getTargetNamespace(), interfaceName + "Binding", "bindingNS"));
      for (OperationMetaData operation : endpoint.getOperations())
      {
         ServiceEndpointMethodMapping methodMapping = new ServiceEndpointMethodMapping(seiMapping);
         methodMapping.setJavaMethodName(operation.getJavaName());
         methodMapping.setWsdlOperation(operation.getQName().getLocalPart());
         boolean isWrapped = operation.isDocumentWrapped();
         methodMapping.setWrappedElement(isWrapped);
         int i = 0;
         for (ParameterMetaData param : operation.getParameters())
         {
            if (isWrapped && param.isInHeader() == false)
            {
               List<WrappedParameter> wrappedParameters = param.getWrappedParameters();
               for (WrappedParameter wrapped : wrappedParameters)
               {
                  String type = JavaUtils.convertJVMNameToSourceName(wrapped.getType(), endpoint.getClassLoader());
                  String name = wrapped.getName().getLocalPart();

                  buildParamMapping(methodMapping, interfaceName, operation, name, type, "IN", false, i++);
               }
            }
            else
            {
               String name = param.getXmlName().getLocalPart();
               String type = JavaUtils.convertJVMNameToSourceName(param.getJavaTypeName(), endpoint.getClassLoader());
               buildParamMapping(methodMapping, interfaceName, operation, name, type, param.getMode().toString(), param.isInHeader(), i++);
            }
         }

         ParameterMetaData returnParam = operation.getReturnParameter();
         if (returnParam != null && ((! isWrapped) || (! returnParam.getWrappedParameters().isEmpty())))
         {
            String name, type;
            if (isWrapped)
            {
               WrappedParameter wrappedParameter = returnParam.getWrappedParameters().get(0);
               name = wrappedParameter.getName().getLocalPart();
               type = wrappedParameter.getType();
            }
            else
            {
               name = returnParam.getXmlName().getLocalPart();
               type = returnParam.getJavaTypeName();
            }

            type = JavaUtils.convertJVMNameToSourceName(type, endpoint.getClassLoader());

            buildReturnParamMapping(methodMapping, interfaceName, operation, name, type);
         }
         seiMapping.addServiceEndpointMethodMapping(methodMapping);

         for(FaultMetaData fmd : operation.getFaults())
         {
            String ns = getNamespace(fmd.getJavaType(), fmd.getXmlType().getNamespaceURI());
            QName newXmlType = new QName(ns, fmd.getXmlType().getLocalPart());
            JavaXmlTypeMapping typeMapping = mappedTypes.get(newXmlType);
            if (typeMapping == null)
               continue;

            String javaTypeName = fmd.getJavaTypeName();
            if (mappedExceptions.contains(javaTypeName))
               continue;

            mappedExceptions.add(javaTypeName);

            ExceptionMapping mapping = new ExceptionMapping(javaWsdlMapping);

            mapping.setExceptionType(javaTypeName);
            QName name = new QName(wsdl.getTargetNamespace(), fmd.getXmlName().getLocalPart());
            mapping.setWsdlMessage(name);

            // Variable mappings generated from SchemaTypesCreater have their order preserved
            for (VariableMapping variableMapping : typeMapping.getVariableMappings())
               mapping.addConstructorParameter(variableMapping.getXmlElementName());

            javaWsdlMapping.addExceptionMappings(mapping);
         }
      }

      javaWsdlMapping.addServiceEndpointInterfaceMappings(seiMapping);

      // Add package mapping for SEI
      String name = endpoint.getServiceEndpointInterface().getPackage().getName();
      String namespace = getNamespace(name);
      if (namespace == null)
         namespace = WSDLUtils.getInstance().getTypeNamespace(name);
      addPackageMapping(name, namespace);
   }

   private void buildParamMapping(ServiceEndpointMethodMapping methodMapping, String interfaceName, OperationMetaData operation,
                                  String name, String type, String mode, boolean header, int position)
   {
      MethodParamPartsMapping paramMapping = new MethodParamPartsMapping(methodMapping);
      paramMapping.setParamPosition(position);
      paramMapping.setParamType(type);

      WsdlMessageMapping messageMapping = new WsdlMessageMapping(paramMapping);
      messageMapping.setWsdlMessagePartName(name);
      String messageName = interfaceName + "_" + operation.getQName().getLocalPart();
      if ("OUT".equals(mode))
         messageName += "Response";
      QName messageQName = new QName(wsdl.getTargetNamespace(), messageName, "wsdlMsgNS");

      messageMapping.setWsdlMessage(messageQName);
      messageMapping.setParameterMode(mode);
      messageMapping.setSoapHeader(header);
      paramMapping.setWsdlMessageMapping(messageMapping);
      methodMapping.addMethodParamPartsMapping(paramMapping);
   }

   private void buildReturnParamMapping(ServiceEndpointMethodMapping methodMapping, String interfaceName, OperationMetaData operation, String name, String type)
   {
      WsdlReturnValueMapping returnMapping = new WsdlReturnValueMapping(methodMapping);
      returnMapping.setMethodReturnValue(type);
      returnMapping.setWsdlMessagePartName(name);
      String messageName = interfaceName + "_" + operation.getQName().getLocalPart() + "Response";;
      QName messageQName = new QName(wsdl.getTargetNamespace(), messageName, "wsdlMsgNS");
      returnMapping.setWsdlMessage(messageQName);
      methodMapping.setWsdlReturnValueMapping(returnMapping);
   }

   /**
    * During the WSDL generation process, a typeMapping will be
    * created that maps xml types -> java types
    *
    * @return  typeMapping
    */
   public TypeMapping getTypeMapping()
   {
      return this.javaToXSD.getSchemaCreator().getTypeMapping();
   }

   /**
    * A customized Package->Namespace map
    *
    * @param map
    */
   public void setPackageNamespaceMap(Map<String,String> map)
   {
      this.packageNamespaceMap = map;
      this.javaToXSD.setPackageNamespaceMap(map);
   }

   public void setJavaToXSD(JavaToXSDIntf jxsd)
   {
      this.javaToXSD = jxsd;
   }

   public JavaWsdlMapping getJavaWsdlMapping()
   {
      return javaWsdlMapping;
   }

   //************************************************************************
   //
   //**************************PRIVATE METHODS*****************************
   //
   //************************************************************************

   protected void generateType(QName xmlType, Class javaType, Map<String, QName> elementNames) throws IOException
   {
      if(Holder.class.isAssignableFrom(javaType))
         javaType = WSDLUtils.getInstance().getJavaTypeForHolder(javaType);
      JBossXSModel xsModel = javaToXSD.generateForSingleType(xmlType, javaType, elementNames);
      String namespace = getNamespace(javaType, xmlType.getNamespaceURI());
      //  Now that the schema object graph is built,
      //  ask JavaToXSD to provide a list of xsmodels to be plugged
      //  into WSDLTypes
      if (xsModel == null)
         throw new WSException("XSModel is null");

      WSDLTypes wsdlTypes = wsdl.getWsdlTypes();
      WSDLUtils.addSchemaModel(wsdlTypes, namespace, xsModel);
      wsdl.registerNamespaceURI(namespace, null);

      //Also get any custom namespaces
      SchemaCreatorIntf schemaCreator = javaToXSD.getSchemaCreator();
      mergeJavaWsdlMapping(schemaCreator.getJavaWsdlMapping());

      //Register the custom generated namespaces
      Map<String, String> nsmap = schemaCreator.getCustomNamespaceMap();
      Set keys = nsmap != null ? nsmap.keySet() : null;
      Iterator iter = (keys != null && !keys.isEmpty()) ? keys.iterator() : null;
      while (iter != null && iter.hasNext())
      {
         String prefix = (String)iter.next();
         String ns = nsmap.get(prefix);
         if (ns != null)
            wsdl.registerNamespaceURI(ns, null);
      }
   }

   private void mergeJavaWsdlMapping(JavaWsdlMapping source)
   {
      // For now we just merge types and packages
      for (PackageMapping packageMapping : source.getPackageMappings())
      {
         String name = packageMapping.getPackageType();
         String namespaceURI = getNamespace(name, packageMapping.getNamespaceURI());

         addPackageMapping(name, namespaceURI);
      }

      for (JavaXmlTypeMapping type : source.getJavaXmlTypeMappings())
      {
         QName name = type.getRootTypeQName();
         if (name == null)
            name = type.getAnonymousTypeQName();

         //override namespace from globalconfig
         String pkgName = getJustPackageName(type.getJavaType());
         String ns = getNamespace(pkgName, name.getNamespaceURI());
         name = new QName(ns, name.getLocalPart(), name.getPrefix());

         if (mappedTypes.containsKey(name))
            continue;

         addPackageMapping(pkgName, ns);
         mappedTypes.put(name, type);

         JavaXmlTypeMapping typeCopy = new JavaXmlTypeMapping(javaWsdlMapping);
         typeCopy.setQNameScope(type.getQnameScope());
         typeCopy.setAnonymousTypeQName(type.getAnonymousTypeQName());
         typeCopy.setJavaType(type.getJavaType());
         typeCopy.setRootTypeQName(name);

         for (VariableMapping variable : type.getVariableMappings())
         {
            VariableMapping variableCopy = new VariableMapping(typeCopy);
            variableCopy.setDataMember(variable.isDataMember());
            variableCopy.setJavaVariableName(variable.getJavaVariableName());
            variableCopy.setXmlAttributeName(variable.getXmlAttributeName());
            variableCopy.setXmlElementName(variable.getXmlElementName());
            variableCopy.setXmlWildcard(variable.getXmlWildcard());

            typeCopy.addVariableMapping(variableCopy);
         }

         javaWsdlMapping.addJavaXmlTypeMappings(typeCopy);
      }
   }

   private void addPackageMapping(String name, String namespaceURI)
   {
      if (mappedPackages.contains(name))
         return;

      mappedPackages.add(name);
      PackageMapping copy = new PackageMapping(javaWsdlMapping);
      copy.setPackageType(name);

      copy.setNamespaceURI(namespaceURI);
      javaWsdlMapping.addPackageMapping(copy);
   }

   protected void generateElement(QName xmlName, QName xmlType)
   {
      WSDLTypes types = wsdl.getWsdlTypes();
      String namespaceURI = xmlType.getNamespaceURI();
      JBossXSModel schemaModel = WSDLUtils.getSchemaModel(types);

      XSTypeDefinition type;
      if (Constants.NS_SCHEMA_XSD.equals(namespaceURI))
         type = SchemaUtils.getInstance().getSchemaBasicType(xmlType.getLocalPart());
      else
         type = schemaModel.getTypeDefinition(xmlType.getLocalPart(), namespaceURI);

      WSSchemaUtils utils = WSSchemaUtils.getInstance(schemaModel.getNamespaceRegistry(), null);
      JBossXSElementDeclaration element =
         utils.createGlobalXSElementDeclaration(xmlName.getLocalPart(), type, xmlName.getNamespaceURI());
      schemaModel.addXSElementDeclaration(element);

      wsdl.registerNamespaceURI(xmlName.getNamespaceURI(), null);
   }
}
