/*
 * 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 java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.UserTransaction;
import javax.wsdl.Definition;
import javax.wsdl.Fault;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPFault;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.bpel.iapi.Message;
import org.apache.ode.bpel.iapi.MessageExchange;
import org.apache.ode.bpel.iapi.PartnerRoleMessageExchange;
import org.apache.ode.bpel.iapi.Scheduler;
import org.apache.ode.utils.DOMUtils;
import org.jboss.soa.bpel.runtime.engine.EndpointReference;
import org.jboss.soa.bpel.runtime.engine.PartnerChannel;
import org.jboss.soa.bpel.runtime.engine.ode.ExecutionEnvironment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Represents a WS invocation.
 * Delegates to a JAX-WS {@link javax.xml.ws.Dispatch} implementation.
 *
 * @author Heiko.Braun <heiko.braun@jboss.com>
 */
public class WebServiceClient implements PartnerChannel
{
  protected final Log log = LogFactory.getLog(getClass());

  private URL wsdlUrl;

  private String id;

  private Dispatch<SOAPMessage> dispatcher = null;
  private EndpointMetaData metaData = null;
  private Definition wsdlDefintion;

  private final QName serviceName;
  private final QName port;

  private SOAPMessageAdapter messageAdapter;

  private ExecutionEnvironment executionEnvironment;

  public WebServiceClient(EndpointMetaData metaData, Definition wsdlDefintion, URL wsdlUrl , ExecutionEnvironment env)
  {
    this.executionEnvironment = env;
    this.metaData = metaData;
    this.wsdlDefintion = wsdlDefintion;
    this.wsdlUrl = wsdlUrl;

    this.id = metaData.getEndpointId();
    this.serviceName = metaData.getServiceName();
    this.port = new QName(serviceName.getNamespaceURI(), metaData.getPortName());

    this.messageAdapter = new SOAPMessageAdapter(this.wsdlDefintion, serviceName, port.getLocalPart());
  }

  public EndpointReference getEndpointReference()
  {
    return new ClientEndpointReference(this.id);
  }


  public void invoke(final PartnerRoleMessageExchange mex)
  {
    log.debug("Invoking dispatcher "+this.id);

    boolean isTwoWay = mex.getMessageExchangePattern()
        == org.apache.ode.bpel.iapi.MessageExchange.MessageExchangePattern.REQUEST_RESPONSE;

    if(isTwoWay)
    {
      // Defer the invoke until the transaction commits.
      Scheduler scheduler = executionEnvironment.getScheduler();
      scheduler.registerSynchronizer(
          new Scheduler.Synchronizer()
          {

            public void afterCompletion(boolean success)
            {
              // If the TX is rolled back, then we don't send the request.
              if (!success) return;

              // The invocation must happen in a separate thread, holding on the afterCompletion
              // blocks other operations that could have been listed there as well.
              ExecutorService executorService = executionEnvironment.getExecutorService();
              executorService.submit(new Callable<Object>() {
                public Object call() throws Exception {

                  // TODO: If we use Scheduler.execTransaction
                  // we could probably bypass the UserTX
                  UserTransaction tx = getUserTransaction();
                  tx.begin();

                  try
                  {
                    // Create SOAPMessage
                    SOAPMessage soapRequestMessage = MessageFactory.newInstance().createMessage();
                    if(log.isDebugEnabled())
                      log.debug( "ODE outbound message: \n" +DOMWriter.printNode(mex.getRequest().getMessage(), true) );
                    
                    messageAdapter.createSoapRequest(soapRequestMessage, mex.getRequest(), mex.getOperation());
                    
                    if (log.isDebugEnabled()) {
                    	log.debug("Riftsaw soap request message: \n" + JavaUtils.getSoapMessageASString(soapRequestMessage));
                    }
                    
                    // The actual WS invocation
                    Dispatch proxy = getDispatcher(port);
                    
                    try {
	                    SOAPMessage soapResponseMessage = (SOAPMessage)proxy.invoke(soapRequestMessage);
	
	                    // Create ODE response
	                    Message odeResponse = mex.createMessage(mex.getOperation().getOutput().getMessage().getQName());
	                    if(soapResponseMessage.getSOAPBody().hasFault())
	                    {
	                      // fault handling
	                      Document odeMsg = DOMUtils.newDocument();
	                      Element odeMsgEl = odeMsg.createElementNS(null, "message");
	                      odeMsg.appendChild(odeMsgEl);
	                      
	                      Fault fault = messageAdapter.parseSoapFault(
	                          odeMsgEl, soapResponseMessage, mex.getOperation()
	                      );
	
	                      handleFault(mex, fault, soapResponseMessage.getSOAPBody().getFault(), odeMsgEl);
	                      
	                    }
	                    else
	                    {
	                      messageAdapter.parseSoapResponse(odeResponse,soapResponseMessage,mex.getOperation());
	                      mex.reply(odeResponse);
	                    }
	
	                    if(log.isDebugEnabled())
	                      log.debug( "ODE inbound message: \n" +DOMWriter.printNode(odeResponse.getMessage(), true) );
	                    
                    } catch(javax.xml.ws.soap.SOAPFaultException fe) {
                    	
	                      // fault handling
	                      Document odeMsg = DOMUtils.newDocument();
	                      Element odeMsgEl = odeMsg.createElementNS(null, "message");
	                      odeMsg.appendChild(odeMsgEl);
	                      
	                      Fault fault=messageAdapter.parseSoapFault(odeMsgEl, fe.getFault(),
	                    		  				mex.getOperation());

	                      handleFault(mex, fault, fe.getFault(), odeMsgEl);
                    }

                    tx.commit();
                  }
                  catch (Throwable e)
                  {
                    tx.rollback();
                    log.error("WS invocation failed", e);
                    mex.replyWithFailure(MessageExchange.FailureType.COMMUNICATION_ERROR, e.getMessage(), null);
                  }
                  return null;
                }
              });
            }

            public void beforeCompletion(){}
          }
      );

      mex.replyAsync();
    }
    else
    {
      // one-way invocation
      ExecutorService executorService = executionEnvironment.getExecutorService();
      executorService.submit(
          new Callable<Object>()
          {
            public Object call() throws Exception
            {

              UserTransaction tx = getUserTransaction();
              tx.begin();
              try
              {
                // Create SOAPMessage
                SOAPMessage soapRequestMessage = MessageFactory.newInstance().createMessage();
                if(log.isDebugEnabled())
                  log.debug( "ODE outbound message: \n" +DOMWriter.printNode(mex.getRequest().getMessage(), true) );
                
                messageAdapter.createSoapRequest(soapRequestMessage, mex.getRequest(), mex.getOperation());

                if (log.isDebugEnabled()) {
                	log.debug("Riftsaw soap request message: \n" + JavaUtils.getSoapMessageASString(soapRequestMessage));
                }
                
                // The actual WS invocation
                Dispatch proxy = getDispatcher(port);                
                proxy.invokeOneWay(soapRequestMessage);

                tx.commit();
              }
              catch (Throwable e)
              {
                tx.rollback();
                log.error("WS invocation failed", e);
                mex.replyWithFailure(MessageExchange.FailureType.COMMUNICATION_ERROR, e.getMessage(), null);
              }

              return null;
            }
          }
      );

      mex.replyOneWayOk();
    }
  }
  
  private void handleFault(PartnerRoleMessageExchange mex, Fault fault, SOAPFault soapFault,
		  							Element odeMsgEl) {
      if (fault != null)
      {
        if (log.isWarnEnabled())
          log.warn("Fault response: faultName="
              + fault.getName() + " faultType=" + fault.getMessage().getQName()
              + "\n" + DOMWriter.printNode(odeMsgEl, true));

        QName faultType = fault.getMessage().getQName();
        QName faultName = new QName(wsdlDefintion.getTargetNamespace(), fault.getName());
        Message response = mex.createMessage(faultType);
        response.setMessage(odeMsgEl);

        mex.replyWithFault(faultName, response);
      }
      else
      {
        if (log.isWarnEnabled())
          log.warn("Fault response: faultType=(unknown)");

        mex.replyWithFailure(MessageExchange.FailureType.OTHER, "Unspecified", soapFault.getDetail());
      }
  }

  private UserTransaction getUserTransaction()
      throws NamingException
  {
    InitialContext ctx = new InitialContext();
    UserTransaction tx = (UserTransaction)ctx.lookup("UserTransaction");
    return tx;
  }

  public Element invoke(String operation, Element mesg) throws Exception
  {
    throw new RuntimeException("Not implemented. Should be removed form interface");
  }

  private Dispatch getDispatcher(QName portName)
  {
    if(null==dispatcher)
    {
      log.debug("Creating Dispatcher ("+this.id+") on " + wsdlUrl + ": "+serviceName);

      Service service = Service.create(this.wsdlUrl, serviceName);
      dispatcher = service.createDispatch(
          portName,
          SOAPMessage.class,
          Service.Mode.MESSAGE
      );
    }

    return dispatcher;
  }

  public String toString()
  {
    return "WebServiceClient {service="+serviceName+",port="+port+"}";
  }

}
