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

import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.HashSet;

import javax.wsdl.Definition;
import javax.wsdl.WSDLException;
import javax.wsdl.factory.WSDLFactory;
import javax.xml.namespace.QName;

import org.jboss.ws.Constants;
import org.jboss.ws.WSException;
import org.jboss.ws.metadata.wsdl.Extendable;
import org.jboss.ws.metadata.wsdl.WSDLBinding;
import org.jboss.ws.metadata.wsdl.WSDLBindingMessageReference;
import org.jboss.ws.metadata.wsdl.WSDLBindingOperation;
import org.jboss.ws.metadata.wsdl.WSDLBindingOperationInput;
import org.jboss.ws.metadata.wsdl.WSDLBindingOperationOutput;
import org.jboss.ws.metadata.wsdl.WSDLDefinitions;
import org.jboss.ws.metadata.wsdl.WSDLEndpoint;
import org.jboss.ws.metadata.wsdl.WSDLExtensibilityElement;
import org.jboss.ws.metadata.wsdl.WSDLImport;
import org.jboss.ws.metadata.wsdl.WSDLInterface;
import org.jboss.ws.metadata.wsdl.WSDLInterfaceFault;
import org.jboss.ws.metadata.wsdl.WSDLInterfaceMessageReference;
import org.jboss.ws.metadata.wsdl.WSDLInterfaceOperation;
import org.jboss.ws.metadata.wsdl.WSDLInterfaceOperationInput;
import org.jboss.ws.metadata.wsdl.WSDLInterfaceOperationOutfault;
import org.jboss.ws.metadata.wsdl.WSDLInterfaceOperationOutput;
import org.jboss.ws.metadata.wsdl.WSDLProperty;
import org.jboss.ws.metadata.wsdl.WSDLRPCPart;
import org.jboss.ws.metadata.wsdl.WSDLRPCSignatureItem;
import org.jboss.ws.metadata.wsdl.WSDLSOAPHeader;
import org.jboss.ws.metadata.wsdl.WSDLService;
import org.jboss.ws.metadata.wsdl.WSDLRPCSignatureItem.Direction;
import org.jboss.wsf.common.DOMUtils;
import org.jboss.wsf.common.DOMWriter;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * A WSDL Writer that writes a WSDL 1.1 file. It works off
 * of the WSDL20 Object Graph.
 * @author <mailto:Anil.Saldhana@jboss.org>Anil Saldhana
 */
public class WSDL11Writer extends WSDLWriter
{
   //Used Internally
   private String wsdlStyle = Constants.RPC_LITERAL;

   // Used to prevent duplicates
   private HashSet<String> writtenFaultMessages = new HashSet<String>();

   /** Use WSDLDefinitions.writeWSDL instead. */
   public WSDL11Writer(WSDLDefinitions wsdl)
   {
      super(wsdl);
   }

   public void write(Writer writer) throws IOException
   {
      write(writer, null);
   }


   public void write(Writer writer, String charset) throws IOException
   {
      write(writer, charset, null);
   }

   public void write(Writer writer, String charset, WSDLWriterResolver resolver) throws IOException
   {
      // Write out the wsdl-1.1 represention (only path to obtain is from WSDL11Reader)
      if (wsdl.getWsdlOneOneDefinition() != null)
      {
        Definition wsdlDefinition = wsdl.getWsdlOneOneDefinition();
        try
        {
           WSDLFactory wsdlFactory = WSDLFactory.newInstance();
           javax.wsdl.xml.WSDLWriter wsdlWriter = wsdlFactory.newWSDLWriter();
           wsdlWriter.writeWSDL(wsdlDefinition, writer);
        }
        catch (WSDLException e)
        {
           this.logException(e);
           throw new IOException(e.toString());
        }
      }
      else
      {
         StringBuilder buffer = new StringBuilder();

         //Detect the WSDL Style early
         wsdlStyle = utils.getWSDLStyle(wsdl);

         StringBuilder importBuffer = new StringBuilder();
         for (WSDLImport wsdlImport : wsdl.getImports())
         {
            if (resolver == null)
               continue;

            WSDLWriterResolver resolved = resolver.resolve(wsdlImport.getLocation());
            if (resolved == null)
               continue;

            String namespace = wsdlImport.getNamespace();
            importBuffer.append("<import namespace='" + namespace + "' location='" + resolved.actualFile + "'/>");
            if (resolved != null)
            {
               StringBuilder builder = new StringBuilder();

               appendDefinitions(builder, namespace);
               appendBody(builder, namespace);
               try
               {
                  writeBuilder(builder, resolved.writer, resolved.charset);
               }
               finally
               {
                  resolved.writer.close();
               }
            }
         }

         appendDefinitions(buffer, wsdl.getTargetNamespace());
         if (importBuffer.length() > 0)
            buffer.append(importBuffer);

         appendBody(buffer, wsdl.getTargetNamespace());
         writeBuilder(buffer, writer, charset);
      }
   }

   private void writeBuilder(StringBuilder builder, Writer writer, String charset) throws IOException
   {
      Element element = DOMUtils.parse(builder.toString());

      if (charset != null)
         writer.write("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\n");
      new DOMWriter(writer).setPrettyprint(true).print(element);
   }

   protected void appendBody(StringBuilder builder, String namespace)
   {
      writtenFaultMessages.clear();

      appendTypes(builder, namespace);
      appendUnknownExtensibilityElements(builder, wsdl);
      appendMessages(builder, namespace);
      appendInterfaces(builder, namespace);
      appendBindings(builder, namespace);
      appendServices(builder, namespace);
      builder.append("</definitions>");
   }
   
   protected void appendUnknownExtensibilityElements(StringBuilder builder, Extendable extendable)
   {
      for (WSDLExtensibilityElement ext : extendable.getAllExtensibilityElements())
      {
         appendPolicyElements(builder, ext);
         //add processing of further extensibility element types below
      }
   }
   
   private void appendPolicyElements(StringBuilder builder, WSDLExtensibilityElement extElem)
   {
      if (Constants.WSDL_ELEMENT_POLICY.equalsIgnoreCase(extElem.getUri()) ||
            Constants.WSDL_ELEMENT_POLICYREFERENCE.equalsIgnoreCase(extElem.getUri()))
      {
         appendElementSkippingKnownNs(builder, extElem.getElement());
      }
   }
   
   private void appendElementSkippingKnownNs(StringBuilder builder, Element el)
   {
      builder.append("<"+el.getNodeName());
      NamedNodeMap attributes = el.getAttributes();
      for (int i = 0; i < attributes.getLength(); i++)
      {
         Attr attr = (Attr)attributes.item(i);
         if (attr.getName().startsWith("xmlns:") && attr.getValue()!=null)
         {
            String prefix = attr.getName().substring(6);
            if (attr.getValue().equalsIgnoreCase(wsdl.getNamespaceURI(prefix)))
               continue;
         }
         builder.append(" "+attr.getName()+"='"+attr.getValue()+"'");
      }
      builder.append(">");
      NodeList childrenList = el.getChildNodes();
      for (int i=0; i<childrenList.getLength(); i++)
      {
         Node node = childrenList.item(i);
         if (node instanceof Element)
         {
            appendElementSkippingKnownNs(builder, (Element)node);
         }
         else
         {
            builder.append(DOMWriter.printNode(node, false));
         }
      }
      builder.append("</"+el.getNodeName()+">");
   }
   
   protected void appendMessages(StringBuilder buffer, String namespace)
   {
      WSDLInterface[] interfaces = wsdl.getInterfaces();
      int len = interfaces != null ? interfaces.length : 0;
      for (int i = 0; i < len; i++)
      {
         WSDLInterface intf = interfaces[i];
         if (! namespace.equals(intf.getName().getNamespaceURI()))
            continue;

         WSDLInterfaceOperation[] operations = intf.getSortedOperations();
         int lenOps = operations.length;
         for (int j = 0; j < lenOps; j++)
         {
            appendMessage(buffer, operations[j]);
            appendMessagesForExceptions(buffer, operations[j]);
         }
      }
   }

   private void appendMessage(StringBuilder buffer, WSDLInterfaceOperation operation)
   {
      String opname = operation.getName().getLocalPart();
      //Determine the style of the wsdl
      if (Constants.URI_STYLE_RPC.equals(operation.getStyle()) == false)
         wsdlStyle = Constants.DOCUMENT_LITERAL; //Not RPC/Literal

      String interfaceName = operation.getWsdlInterface().getName().getLocalPart();
      buffer.append("<message name='" + interfaceName + "_" + opname + "' >");
      for (WSDLInterfaceOperationInput input : operation.getInputs())
      {
         appendUnknownExtensibilityElements(buffer, input); //only one may contain extensibility elements
         appendMessageParts(buffer, input);
      }
      buffer.append("</message>");

      if (! Constants.WSDL20_PATTERN_IN_ONLY.equals(operation.getPattern()))
      {
         buffer.append("<message name='" + interfaceName + "_" + opname + "Response' >");
         WSDLInterfaceOperationOutput[] outputs = operation.getOutputs();
         for (WSDLInterfaceOperationOutput output : outputs)
            appendMessageParts(buffer, output);
         buffer.append("</message>");
      }
   }

   private void appendMessagesForExceptions(StringBuilder buffer, WSDLInterfaceOperation operation)
   {
      //Get the outfaults
      WSDLInterfaceOperationOutfault[] faults = operation.getOutfaults();
      int len = faults != null ? faults.length : 0;

      for (int i = 0; i < len; i++)
      {
         WSDLInterfaceOperationOutfault fault = faults[i];
         String exceptionName = fault.getRef().getLocalPart();
         if (writtenFaultMessages.contains(exceptionName))
            continue;

         QName xmlName = fault.getRef();

         buffer.append("<message name='" + exceptionName + "' >");
         String prefix = wsdl.getPrefix(xmlName.getNamespaceURI());
         String xmlNameStr = prefix + ":" + xmlName.getLocalPart();
         buffer.append("<part name='" + exceptionName + "' element='" + xmlNameStr + "' />");
         buffer.append("</message>");

         writtenFaultMessages.add(exceptionName);
      }
   }

   private String getReferenceString(QName name)
   {
      String namespaceURI = name.getNamespaceURI();
      String prefix = wsdl.getPrefix(namespaceURI);
      if (prefix == null)
         throw new WSException("Prefix not bound for namespace: " + namespaceURI);

      return prefix + ":" + name.getLocalPart();
   }

   private void appendMessageParts(StringBuilder buffer, WSDLInterfaceMessageReference reference)
   {
      if (wsdlStyle.equals(Constants.RPC_LITERAL))
      {
         for (WSDLRPCPart part : reference.getChildParts())
         {
            buffer.append("<part name='" + part.getName()).append('\'');
            buffer.append(" type='" + getReferenceString(part.getType()) + "'/>");
         }
      }
      else
      {
         QName element = reference.getElement();
         // Null represents empty message
         if (element != null)
         {
            buffer.append("<part name='" + reference.getPartName() + '\'');
            buffer.append(" element='" + getReferenceString(element) + "'/>");
         }
      }

      WSDLBindingMessageReference bindingReference = getBindingReference(reference);
      if (bindingReference == null)
         return;

      for (WSDLSOAPHeader header : bindingReference.getSoapHeaders())
      {
         if (header.isIncludeInSignature());
         {
            QName element = header.getElement();
            buffer.append("<part name='" + header.getPartName() + '\'');
            buffer.append(" element='" + getReferenceString(element) + "'/>");
         }
      }
   }

   private WSDLBindingMessageReference getBindingReference(WSDLInterfaceMessageReference reference)
   {
      WSDLInterfaceOperation wsdlOperation = reference.getWsdlOperation();
      WSDLInterface wsdlInterface = wsdlOperation.getWsdlInterface();
      WSDLBinding binding = wsdlInterface.getWsdlDefinitions().getBindingByInterfaceName(wsdlInterface.getName());
      WSDLBindingOperation bindingOperation = binding.getOperationByRef(wsdlOperation.getName());
      WSDLBindingMessageReference[] bindingReferences;

      if (reference instanceof WSDLInterfaceOperationInput)
         bindingReferences = bindingOperation.getInputs();
      else
         bindingReferences = bindingOperation.getOutputs();

      if (bindingReferences.length > 1)
         throw new IllegalArgumentException("WSDl 1.1 only supports In-Only, and In-Out MEPS, more than reference input found");

      if (bindingReferences.length == 1)
         return bindingReferences[0];

      return null;
   }

   protected void appendInterfaces(StringBuilder buffer, String namespace)
   {
      WSDLInterface[] intfs = wsdl.getInterfaces();
      for (int i = 0; i < intfs.length; i++)
      {
         WSDLInterface intf = intfs[i];
         if (!namespace.equals(intf.getName().getNamespaceURI()))
            continue;

         buffer.append("<portType name='" + intf.getName().getLocalPart() + "'");
         WSDLProperty policyProp = intf.getProperty(Constants.WSDL_PROPERTY_POLICYURIS);
         if (policyProp != null)
         {
            String prefix = wsdl.getPrefix(Constants.URI_WS_POLICY);
            buffer.append(" ");
            buffer.append(prefix);
            buffer.append(":");
            buffer.append(Constants.WSDL_ATTRIBUTE_WSP_POLICYURIS.getLocalPart());
            buffer.append("='");
            buffer.append(policyProp.getValue());
            buffer.append("'");
         }
         buffer.append(">");
         appendPortOperations(buffer, intf);
         buffer.append("</portType>");
      }
   }

   private String getParameterOrder(WSDLInterfaceOperation operation)
   {
      StringBuilder builder = new StringBuilder();
      for (WSDLRPCSignatureItem item : operation.getRpcSignatureItems())
      {
         if (item.getDirection() != Direction.RETURN)
         {
            if (builder.length() > 0)
               builder.append(' ');
            builder.append(item.getName());
         }
      }

      return builder.toString();
   }

   protected void appendPortOperations(StringBuilder buffer, WSDLInterface intf)
   {
      String prefix = wsdl.getPrefix(intf.getName().getNamespaceURI());
      WSDLInterfaceOperation[] operations = intf.getSortedOperations();
      for (int i = 0; i < operations.length; i++)
      {
         WSDLInterfaceOperation operation = operations[i];
         buffer.append("<operation name='" + operation.getName().getLocalPart() + "'");

         // JBWS-1501 wsimport RI fails when processing parameterOrder on one-way operations
         if (! Constants.WSDL20_PATTERN_IN_ONLY.equals(operation.getPattern()))
         {
            String parameterOrder = getParameterOrder(operation);
            if (parameterOrder.length() > 0)
               buffer.append(" parameterOrder='").append(parameterOrder).append("'");
          
         }
         buffer.append(">");
         appendUnknownExtensibilityElements(buffer, operation);

         String opname = operation.getName().getLocalPart();
         String interfaceName = operation.getWsdlInterface().getName().getLocalPart();
         String msgEl = prefix + ":" + interfaceName + "_" + opname;

         buffer.append("<input message='" + msgEl + "'>").append("</input>");

         if (! Constants.WSDL20_PATTERN_IN_ONLY.equals(operation.getPattern()))
         {
            buffer.append("<output message='" + msgEl + "Response'>");
            buffer.append("</output>");
         }

         //Append the Faults
         for (WSDLInterfaceOperationOutfault fault : operation.getOutfaults())
         {
            QName element = fault.getRef();
            buffer.append("<fault  message='" + prefix + ":" + element.getLocalPart());
            buffer.append("' name='" + element.getLocalPart() + "'/>");
         }

         buffer.append("</operation>");
      }
   }

   protected void appendBindings(StringBuilder buffer, String namespace)
   {
      WSDLBinding[] bindings = wsdl.getBindings();
      for (int i = 0; i < bindings.length; i++)
      {
         WSDLBinding binding = bindings[i];
         if (!namespace.equals(binding.getName().getNamespaceURI()))
            continue;
         buffer.append("<binding name='" + binding.getName().getLocalPart() + "' type='" + getQNameRef(binding.getInterfaceName()) + "'>");
         //TODO:Need to derive the WSDLStyle from the Style attribute of InterfaceOperation
         if (wsdlStyle == null)
            throw new IllegalArgumentException("WSDL Style is null (should be rpc or document");
         String style = "rpc";
         if (wsdlStyle.equals(Constants.DOCUMENT_LITERAL))
            style = "document";
         appendUnknownExtensibilityElements(buffer, binding);
         
         // The value of the REQUIRED transport attribute (of type xs:anyURI) indicates which transport of SOAP this binding corresponds to. 
         // The URI value "http://schemas.xmlsoap.org/soap/http" corresponds to the HTTP binding. 
         // Other URIs may be used here to indicate other transports (such as SMTP, FTP, etc.).
         
         buffer.append("<" + soapPrefix + ":binding transport='" + Constants.URI_SOAP_HTTP + "' style='" + style + "'/>");
         appendBindingOperations(buffer, binding);
         buffer.append("</binding>");
      }
   }

   protected void appendBindingOperations(StringBuilder buffer, WSDLBinding binding)
   {
      WSDLBindingOperation[] operations = binding.getOperations();
      Arrays.sort(operations);

      for (int i = 0; i < operations.length; i++)
      {
         WSDLBindingOperation operation = operations[i];
         QName interfaceName = operation.getWsdlBinding().getInterfaceName();

         WSDLInterface wsdlInterface = wsdl.getInterface(interfaceName);
         if (wsdlInterface == null)
            throw new WSException("WSDL Interface should not be null");
         WSDLInterfaceOperation interfaceOperation = wsdlInterface.getOperation(operation.getRef());

         buffer.append("<operation name='" + interfaceOperation.getName().getLocalPart() + "'>");
         String soapAction = (operation.getSOAPAction() != null ? operation.getSOAPAction() : "");
         appendUnknownExtensibilityElements(buffer, operation);
         buffer.append("<" + soapPrefix + ":operation soapAction=\"" + soapAction + "\"/>");

         WSDLBindingOperationInput[] inputs = operation.getInputs();
         if (inputs.length != 1)
            throw new WSException("WSDl 1.1 only supports In-Only, and In-Out MEPS.");

         buffer.append("<input>");
         appendUnknownExtensibilityElements(buffer, inputs[0]);
         appendSOAPBinding(buffer, wsdlInterface, operation, inputs);
         buffer.append("</input>");

         if (! Constants.WSDL20_PATTERN_IN_ONLY.equals(getBindingOperationPattern(operation)))
         {
            buffer.append("<output>");
            WSDLBindingOperationOutput[] outputs = operation.getOutputs();
            appendSOAPBinding(buffer, wsdlInterface, operation, outputs);
            buffer.append("</output>");
         }

         //Append faults
         WSDLInterfaceOperationOutfault[] faults = interfaceOperation.getOutfaults();
         if (faults != null)
         {
            for (WSDLInterfaceOperationOutfault fault : faults)
            {
               String n = "name='" + fault.getRef().getLocalPart() + "'";
               buffer.append("<fault  " + n + ">");
               buffer.append("<" + soapPrefix + ":fault  " + n + " use='literal' />");
               buffer.append("</fault>");
            }
            buffer.append("</operation>");
         }
      }
   }

   private void appendSOAPBinding(StringBuilder buffer, WSDLInterface wsdlInterface, WSDLBindingOperation operation, WSDLBindingMessageReference[] inputs)
   {
      String tns = wsdl.getTargetNamespace();
      WSDLInterfaceOperation interfaceOperation = wsdlInterface.getOperation(operation.getRef());
      WSDLInterfaceMessageReference reference = (inputs instanceof WSDLBindingOperationInput[]) ? interfaceOperation.getInputs()[0]
            : interfaceOperation.getOutputs()[0];

      StringBuilder bodyParts = new StringBuilder();
      if (Constants.DOCUMENT_LITERAL == wsdlStyle)
      {
         // Empty bare body
         if (reference.getPartName() != null)
            bodyParts.append(reference.getPartName());
      }
      else
      {
         for (WSDLRPCPart part : reference.getChildParts())
         {
            if (bodyParts.length() > 0)
               bodyParts.append(" ");
            bodyParts.append(part.getName());
         }
      }

      StringBuilder soapHeader = new StringBuilder();
      for (WSDLSOAPHeader header : inputs[0].getSoapHeaders())
      {
         if (header.isIncludeInSignature())
         {
            String messageName = wsdlInterface.getName().getLocalPart() + "_" + operation.getRef().getLocalPart();
            if (reference instanceof WSDLInterfaceOperationOutput)
               messageName += "Response";
            soapHeader.append("<").append(soapPrefix).append(":header use='literal' message='tns:").append(messageName);
            soapHeader.append("' part='").append(header.getPartName()).append("'/>");
         }
      }

      buffer.append("<" + soapPrefix + ":body use='literal'");
      if (wsdlStyle != Constants.DOCUMENT_LITERAL)
         buffer.append(" namespace='" + tns + "'");
      if (soapHeader.length() > 0)
      {
         buffer.append(" parts='").append(bodyParts).append("'/>");
         buffer.append(soapHeader);
      }
      else
      {
         buffer.append("/>");
      }
   }

   private String getBindingOperationPattern(WSDLBindingOperation operation)
   {
      WSDLBinding binding = operation.getWsdlBinding();
      String pattern = binding.getInterface().getOperation(operation.getRef()).getPattern();

      return pattern;
   }

   protected void appendServices(StringBuilder buffer, String namespace)
   {
      WSDLService[] services = wsdl.getServices();
      int len = services.length;
      for (int i = 0; i < len; i++)
      {

         WSDLService service = services[i];
         if (!namespace.equals(service.getName().getNamespaceURI()))
            continue;
         buffer.append("<service name='" + service.getName().getLocalPart() + "'>");
         appendUnknownExtensibilityElements(buffer, service);
         WSDLEndpoint[] endpoints = service.getEndpoints();
         int lenend = endpoints.length;
         for (int j = 0; j < lenend; j++)
         {
            WSDLEndpoint endpoint = endpoints[j];
            appendServicePort(buffer, endpoint);
         }

         buffer.append("</service>");
      }
   }

   protected void appendServicePort(StringBuilder buffer, WSDLEndpoint endpoint)
   {
      String name = endpoint.getName().getLocalPart();
      QName endpointBinding = endpoint.getBinding();
      String prefix = endpointBinding.getPrefix();
      prefix = wsdl.getPrefix(endpointBinding.getNamespaceURI());
      String ebname = prefix + ":" + endpointBinding.getLocalPart();
      buffer.append("<port name='" + name + "' binding='" + ebname + "'>");
      buffer.append("<" + soapPrefix + ":address location='" + endpoint.getAddress() + "'/>");
      appendUnknownExtensibilityElements(buffer, endpoint);
      buffer.append("</port>");
   }
}