/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.soa.bpel.runtime.ws;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.bpel.iapi.Message;
import org.apache.ode.utils.DOMUtils;
import org.apache.ode.utils.stl.CollectionsX;
import org.apache.ode.utils.wsdl.WsdlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.wsdl.*;
import javax.wsdl.extensions.ElementExtensible;
import javax.wsdl.extensions.soap.SOAPBinding;
import javax.wsdl.extensions.soap.SOAPHeader;
import javax.xml.namespace.QName;
import javax.xml.soap.*;

import java.util.*;

/**
 * Adopts {@link javax.xml.soap.SOAPMessage}'s to ODE's internal
 * {@link org.apache.ode.bpel.iapi.Message} representation and vice versa.
 *
 * @see org.jboss.soa.bpel.runtime.ws.WebServiceClient
 *
 * @author Heiko.Braun <heiko.braun@jboss.com>
 */
public class SOAPMessageAdapter
{
  protected final Log log = LogFactory.getLog(getClass());

  private Definition wsdl;
  private QName serviceName;

  private String portName;
  private Service serviceDef;
  private Binding binding;

  private Port port;
  private boolean isRPC;

  private SOAPBinding soapBinding;

  private SOAPFactory soapFactory;

  public SOAPMessageAdapter(Definition wsdl, QName serviceName, String portName)
  {
    this.wsdl = wsdl;
    this.serviceName = serviceName;
    this.portName = portName;

    serviceDef = wsdl.getService(serviceName);
    if (serviceDef == null)
      throw new RuntimeException("Service not found "+serviceName);

    port = serviceDef.getPort(portName);
    if (port == null)
      throw new RuntimeException("Port '"+portName+"' not found on service: "+serviceName);

    binding = port.getBinding();
    if (binding == null)
      throw new RuntimeException("No binding for port "+portName);

    if (!WsdlUtils.useSOAPBinding(port)) {
      throw new RuntimeException("No SOAP binding for port"+portName);
    }
    soapBinding = (SOAPBinding) WsdlUtils.getBindingExtension(port);


    String style = soapBinding.getStyle();
    isRPC = style != null && style.equals("rpc");

    try
    {
      this.soapFactory = SOAPFactory.newInstance();
    }
    catch (SOAPException e)
    {
      throw new RuntimeException(e);
    }
  }

  public void createSoapRequest(SOAPMessage soapMessage, Message odeRequestMessage, Operation wsdlOperation) 
  {
    BindingOperation bop = binding.getBindingOperation(wsdlOperation.getName(), null, null);
    if (bop == null)
      throw new RuntimeException("Operation "+wsdlOperation.getName()+"not found on "+serviceName+"/"+portName);

    BindingInput bi = bop.getBindingInput();
    if (bi == null)
      throw new RuntimeException("Binding input not found on "+serviceName+"/"+portName);

    // Headers
    createSoapHeaders(
        soapMessage,
        getSOAPHeaders(bi),
        wsdlOperation.getInput().getMessage(),
        odeRequestMessage.getHeaderParts(),
        odeRequestMessage.getMessage()
    );


    // SOAP Body
    javax.wsdl.extensions.soap.SOAPBody wsdlSoapBody = getSOAPBody(bi);

    createSoapBody(
        soapMessage,
        wsdlSoapBody,
        wsdlOperation.getInput().getMessage(),
        odeRequestMessage.getMessage(),
        wsdlOperation.getName()
    );

  }

  public boolean isRPC()
  {
    return isRPC;
  }

  public void createSoapResponse(SOAPMessage soapMessage, Message odeResponseMessage, Operation wsdlOperation)
  {

    BindingOperation bop = binding.getBindingOperation(wsdlOperation.getName(),null,null);
    if (bop == null)
      throw new RuntimeException("Operation "+wsdlOperation.getName()+"not found on "+serviceName+"/"+portName);

    BindingOutput bo = bop.getBindingOutput();
    if (bo == null)
      throw new RuntimeException("Binding output not found on "+serviceName+"/"+portName);

    // Headers
    if (odeResponseMessage.getHeaderParts().size() > 0 || getSOAPHeaders(bo).size() > 0)
      createSoapHeaders(
          soapMessage,
          getSOAPHeaders(bo),
          wsdlOperation.getOutput().getMessage(),
          odeResponseMessage.getHeaderParts(),
          odeResponseMessage.getMessage()
      );


    // SOAP Body
    javax.wsdl.extensions.soap.SOAPBody wsdlSOAPBody = getSOAPBody(bo);
    createSoapBody(
        soapMessage,
        wsdlSOAPBody,
        wsdlOperation.getOutput().getMessage(),
        odeResponseMessage.getMessage(),
        wsdlOperation.getName() + "Response"
    );

  }

  private void createSoapBody(SOAPMessage soapMessage,
                              javax.wsdl.extensions.soap.SOAPBody wsdlSoapBody, javax.wsdl.Message wsdlMessageDef,
                              Element message, String operationName)
  {
    try
    {
      SOAPBody soapBody = soapMessage.getSOAPBody();

      SOAPElement partHolder = null;
      if(isRPC)
      {
        partHolder = soapFactory.createElement(new QName(wsdlSoapBody.getNamespaceURI(), operationName, "odens"));
      }
      else
      {
        partHolder = soapBody;
      }

      List<Part> parts = wsdlMessageDef.getOrderedParts(wsdlSoapBody.getParts());
      for(Part part : parts)
      {
        Element srcPartEl = DOMUtils.findChildByName(message, new QName(null, part.getName()));
        if (srcPartEl == null)
          throw new RuntimeException("Part is missing: " +part.getName());

        SOAPElement partElement = soapFactory.createElement(srcPartEl);
        if (isRPC)
        {
          partHolder.addChildElement(partElement);
        }
        else
        {
          for (Iterator<SOAPElement> i = partElement.getChildElements(); i.hasNext();) partHolder.addChildElement(i.next());
        }
      }

      // late bind
      if(isRPC)
        soapBody.addChildElement(partHolder);
    }
    catch (SOAPException e)
    {
      throw new RuntimeException("Failed to create soap body",e);
    }
  }

  private void createSoapHeaders(SOAPMessage soapMessage, List<SOAPHeader> headers,
                                 javax.wsdl.Message wsdlMessageDef,
                                 Map<String, Node> headerParts, Element message)
  {
	  try {
		  javax.xml.soap.SOAPHeader soapHeader = soapMessage.getSOAPHeader();
		  if (soapHeader==null) soapHeader = soapMessage.getSOAPPart().getEnvelope().addHeader();
		  for (Node headerNode : headerParts.values()) {
			  if (headerNode.getNodeType() == Node.ELEMENT_NODE) {
				  if (getFirstChildWithName(new QName(headerNode.getNamespaceURI(), headerNode.getLocalName()),soapHeader) == null) {
					  SOAPElement partElement = soapFactory.createElement((Element) headerNode);
					  soapHeader.addChildElement(partElement);
				  }
			  } else {
				  throw new RuntimeException("SOAP header must be a node_element " + headerNode);
			  }
		  }
		  
		  //Add soap header according to binding.
		  for (SOAPHeader header : headers) {
			  Element headerEl = DOMUtils.findChildByName(message, new QName(null, header.getPart()));
			  if (headerEl != null) {				  
				  SOAPElement soapHeaderEl = soapFactory.createElement(new QName(header.getMessage().getNamespaceURI(), header.getPart(),"odens"));
				  NodeList list = headerEl.getChildNodes();
				  for(int i=0; i< list.getLength(); i++) {
					  SOAPElement partElement = soapFactory.createElement((Element)list.item(i));
					  soapHeaderEl.addChildElement(partElement);
				  }
				  soapHeader.addChildElement(soapHeaderEl);
			  }
		  }
	  } catch (SOAPException e) {
		  throw new RuntimeException("Failed to create soap header",e);
	  }
  }

  public void parseSoapResponse(org.apache.ode.bpel.iapi.Message odeMessage,
                                SOAPMessage soapMessage, javax.wsdl.Operation odeOperation) {
    BindingOperation bop = binding.getBindingOperation(odeOperation.getName(), null, null);
    if (bop == null)
      throw new RuntimeException("Operation "+odeOperation.getName()+"not found on "+serviceName+"/"+portName);

    BindingOutput bo = bop.getBindingOutput();
    if (bo == null)
      throw new RuntimeException("Binding output not found on "+serviceName+"/"+portName);

    extractSoapBodyParts(odeMessage, soapMessage, getSOAPBody(bo), odeOperation.getOutput().getMessage(), odeOperation.getName() + "Response");
    extractSoapHeaderParts(odeMessage, soapMessage, getSOAPHeaders(bo), odeOperation.getOutput().getMessage());
  }

  public void parseSoapRequest(
      org.apache.ode.bpel.iapi.Message odeMessage,
      SOAPMessage soapMessage,
      Operation op)
  {

    BindingOperation bop = binding.getBindingOperation(op.getName(), null, null);

    if (bop == null)
      throw new RuntimeException("Binding operation not found ("+serviceName+"/"+portName);

    BindingInput bi = bop.getBindingInput();
    if (bi == null)
      throw new RuntimeException("Binding input not found"+serviceName+"/"+portName);

    extractSoapBodyParts(odeMessage, soapMessage, getSOAPBody(bi), op.getInput().getMessage(), op.getName());
    extractSoapHeaderParts(odeMessage, soapMessage, getSOAPHeaders(bi), op.getInput().getMessage());
  }

  public void createSoapFault(SOAPMessage soapMessage, Element message, QName faultName, Operation op)
  {
    try
    {
      Element detail = buildSoapDetail(message, faultName, op);
      SOAPFault fault = soapMessage.getSOAPBody().addFault();
      fault.setFaultCode(faultName);
      if(detail!=null)
        fault.addDetail().addChildElement(soapFactory.createElement(detail));      
    }
    catch (SOAPException e)
    {
      throw new RuntimeException("Failed to create fault", e);
    }
  }

  private Element buildSoapDetail(Element message, QName faultName, Operation op)
  {
    if (faultName.getNamespaceURI() == null)
      return toFaultDetail(faultName, message);
    if (op == null) {
      return toFaultDetail(faultName, message);
    }
    Fault f = op.getFault(faultName.getLocalPart());
    if (f == null)
      return toFaultDetail(faultName, message);

    // For faults, there will be exactly one part.
    Part p = (Part)f.getMessage().getParts().values().iterator().next();
    if (p == null)
      return toFaultDetail(faultName, message);
    Element partEl= DOMUtils.findChildByName(message,new QName(null,p.getName()));
    if (partEl == null)
      return toFaultDetail(faultName, message);
    Element detail = DOMUtils.findChildByName(partEl, p.getElementName());
    if (detail == null)
      return toFaultDetail(faultName, message);

    return detail;
  }

  private Element toFaultDetail(QName fault, Element message) {
    if (message == null) return null;
    Element firstPart = DOMUtils.getFirstChildElement(message);
    if (firstPart == null) return null;
    Element detail = DOMUtils.getFirstChildElement(firstPart);
    if (detail == null) return firstPart;
    return detail;
  }

  private void extractSoapHeaderParts(Message odeMessage, SOAPMessage soapMessage, List<SOAPHeader> headerDefs,javax.wsdl.Message wsdlMessageDef)
  {
	  try {
		  javax.xml.soap.SOAPHeader soapHeader = soapMessage.getSOAPHeader();
		  // Checking that the definitions we have are at least there
	      for (SOAPHeader headerDef : headerDefs)
	          handleSoapHeaderPartDef(odeMessage, soapHeader, headerDef, wsdlMessageDef);
	
	      // Extracting whatever header elements we find in the message, binding and abstract parts
	      // aren't reliable enough given what people do out there.
	      if (soapHeader != null) {
	      	Iterator headersIter = soapHeader.getChildElements();
	      	while (headersIter.hasNext()) {
	      		Object obj=headersIter.next();

	      		// Should be SOAPHeaderElement, but CXF also returns javax.xml.soap.Text
	      		// objects aswell
	      		if (obj instanceof javax.xml.soap.SOAPHeaderElement) {
		      		javax.xml.soap.SOAPHeaderElement headerElem = (javax.xml.soap.SOAPHeaderElement) obj;
		      		String partName = findHeaderPartName(headerDefs, headerElem.getElementQName());
		      		Document doc = DOMUtils.newDocument();
	
		      		// RIFTSAW-74 - slight modification to avoid jbossws exception when reconstructing the
		      		// SOAP message.
	
		      		//Element destPart = doc.createElementNS(null, partName);
		      		//destPart.appendChild(doc.importNode(headerElem, true));
		      		//odeMessage.setHeaderPart(partName, destPart);
		      		odeMessage.setHeaderPart(partName, (Element)doc.importNode(headerElem, true));
	      		}
	      	}
	      }
	  }
	    catch (SOAPException e)
	    {
	      throw new RuntimeException("Failed to extracts header parts",e);
	    }
  }
  
  private String findHeaderPartName(List<SOAPHeader> headerDefs, QName elmtName) {
      for (SOAPHeader headerDef : headerDefs) {
    	  javax.wsdl.Message hdrMsg = wsdl.getMessage(headerDef.getMessage());
          for (Object o : hdrMsg.getParts().values()) {
              Part p = (Part) o;
              if (p.getElementName() != null &&
            		  p.getElementName().equals(elmtName)) return p.getName();
          }
      }
      return elmtName.getLocalPart();
  }
  
  private void handleSoapHeaderPartDef(Message odeMessage, javax.xml.soap.SOAPHeader header, SOAPHeader headerdef,
		  javax.wsdl.Message msgType)  {
      // Is this header part of the "payload" messsage?
      boolean payloadMessageHeader = headerdef.getMessage() == null || headerdef.getMessage().equals(msgType.getQName());
      boolean requiredHeader = payloadMessageHeader || Boolean.TRUE.equals(headerdef.getRequired());

      if (header == null) {
      	if (requiredHeader)
      		throw new RuntimeException("Soap Header is missing a required field " + headerdef.getElementType());

      	return;
      }

      javax.wsdl.Message hdrMsg = wsdl.getMessage(headerdef.getMessage());
      if (hdrMsg == null)
          return;
      Part p = hdrMsg.getPart(headerdef.getPart());
      if (p == null || p.getElementName() == null)
          return;

      SOAPElement headerEl = getFirstChildWithName(p.getElementName(), header);
      if (requiredHeader && headerEl == null)
          throw new RuntimeException("Soap Header is missing a required field " + headerdef.getElementType());

      if (headerEl == null) return;

      /* RIFTSAW-127 - this was duplicating the header part in the consolidated (merged) message stored in the
       * BPEL process - but this code actually causes two levels of 'part' to be defined - e.g.
       * <conversionId><conversationId><details .... /></conversatioId></conversationId>
      Document doc = DOMUtils.newDocument();
      Element destPart = doc.createElementNS(null, p.getName());
      destPart.appendChild(doc.importNode(headerEl, true));
      odeMessage.setHeaderPart(p.getName(), destPart);
      */
  }

  private void extractSoapBodyParts(
      Message odeMessage,
      SOAPMessage soapMessage,
      javax.wsdl.extensions.soap.SOAPBody wsdlSOAPBody,
      javax.wsdl.Message wsdlMessageDef, String operationName)
  {
    try
    {
      SOAPBody soapBody = soapMessage.getSOAPBody();
      List<Part> parts = wsdlMessageDef.getOrderedParts(wsdlSOAPBody.getParts());

      if(isRPC)
      {
        // In RPC the body element is the operation name, wrapping parts. Order doesn't really matter as far as
        // we're concerned. All we need to do is copy the soap:body children, since doc-lit rpc looks the same
        // in ode and soap.

        QName rpcWrapQName = new QName(wsdlSOAPBody.getNamespaceURI(), operationName);
        SOAPElement partWrapper = getFirstChildWithName(rpcWrapQName, soapBody);

        if (partWrapper == null)
          throw new RuntimeException("Expected part wrapper '"+rpcWrapQName+"'missing on service:"+serviceName+"/"+portName);

        for(Part part : parts)
        {
          SOAPElement srcPart = getFirstChildWithName(new QName(null, part.getName()), partWrapper);
          if (srcPart == null)
            throw new RuntimeException("Soap body does not contain required part +"+part.getName());

          odeMessage.setPart(srcPart.getLocalName(), srcPart);
        }
      }
      else
      {
        // In doc-literal style, we expect the elements in the body to correspond (in order)
        // to the parts defined in the binding.
        // All the parts should be element-typed, otherwise it is a mess.
        List<SOAPElement> childElements = new ArrayList<SOAPElement>();
        final Iterator children = soapBody.getChildElements() ;
        while(children.hasNext())
        {
          final Node node = (Node)children.next() ;
          if (node instanceof SOAPElement)
            childElements.add((SOAPElement)node);
        }

        Iterator<SOAPElement> srcParts = childElements.iterator();
        for(Part part : parts)
        {
          SOAPElement srcPart = srcParts.next();
          Document doc = DOMUtils.newDocument();
          Element destPart = doc.createElementNS(null, part.getName());
          destPart.appendChild(doc.importNode(srcPart, true));
          odeMessage.setPart(part.getName(), destPart);
        }
      }
    }
    catch (SOAPException e)
    {
      throw new RuntimeException("Failed to extract soap body parts", e);
    }
  }

  private static SOAPElement getFirstChildWithName(QName name, SOAPElement parent)
  {
    SOAPElement match = null;
    Iterator iterator = parent.getChildElements(name);
    while(iterator.hasNext())
    {
      match= (SOAPElement)iterator.next();
    }
    return match;
  }

  /*private static Element cloneElement(Element source)
  {
    // TODO: https://jira.jboss.org/jira/browse/RIFTSAW-38
    // For now create a deep copy (performance hit)
    try
    {
      DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
      Document doc = builder.newDocument();
      return (Element)doc.importNode(source, true);
    }
    catch (ParserConfigurationException e)
    {
      throw new RuntimeException(e);
    }
  } */

  public static <T> T getFirstExtensibilityElement(ElementExtensible parent, Class<T> cls) {
    Collection<T> ee = CollectionsX.filter(parent.getExtensibilityElements(), cls);

    return ee.isEmpty() ? null : ee.iterator().next();

  }

  public static javax.wsdl.extensions.soap.SOAPBody getSOAPBody(ElementExtensible ee) {
    return getFirstExtensibilityElement(ee, javax.wsdl.extensions.soap.SOAPBody.class);
  }

  public static List<SOAPHeader> getSOAPHeaders(ElementExtensible eee) {
    return CollectionsX.filter(new ArrayList<SOAPHeader>(), (Collection<Object>) eee.getExtensibilityElements(),
        SOAPHeader.class);
  }

  public Fault parseSoapFault(
      Element odeMessage,
      SOAPMessage soapMessage,
      javax.wsdl.Operation operation)
  {
    Fault fdef = null;
    try
    {
      SOAPFault flt = soapMessage.getSOAPBody().getFault();
      Detail detail = flt.getDetail();
      fdef = inferFault(operation, flt);
      if(fdef!=null)
      {
        Part pdef = (Part)fdef.getMessage().getParts().values().iterator().next();
        Element partel = odeMessage.getOwnerDocument().createElementNS(null,pdef.getName());
        odeMessage.appendChild(partel);

        Element childByName = DOMUtils.findChildByName(detail, pdef.getElementName());
        if (childByName != null)
        {
          partel.appendChild(odeMessage.getOwnerDocument().importNode(childByName, true));
        }
        else
        {
          partel.appendChild(odeMessage.getOwnerDocument().importNode(detail,true));
        }
      }
    }
    catch (Exception e)
    {
      throw new RuntimeException("Failed to parse SOAP Fault",e);
    }

    return fdef;
  }

  public Fault parseSoapFault(
	      Element odeMessage,
	      SOAPFault flt,
	      javax.wsdl.Operation operation)
  {
      Fault fault=inferFault(operation, flt);

      if(fault!=null)
      {
          Detail detail = flt.getDetail();
        Part pdef = (Part)fault.getMessage().getParts().values().iterator().next();
        Element partel = odeMessage.getOwnerDocument().createElementNS(null,pdef.getName());
        odeMessage.appendChild(partel);

        Element childByName = DOMUtils.findChildByName(detail, pdef.getElementName());
        if (childByName != null)
        {
          partel.appendChild(odeMessage.getOwnerDocument().importNode(childByName, true));
        }
        else
        {
          partel.appendChild(odeMessage.getOwnerDocument().importNode(detail,true));
        }
      }

    return fault;
  }

  private Fault inferFault(Operation operation, SOAPFault flt) {
    if (!flt.hasDetail())
      return null;
    // The detail is a dummy <detail> node containing the interesting fault element
    Element element = DOMUtils.getFirstChildElement(flt.getDetail());
    QName elName = new QName(element.getNamespaceURI(), element.getLocalName());
    return WsdlUtils.inferFault(operation, elName);
  }
}
