package org.biomoby.client;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

import javax.xml.soap.*;
import java.util.Collection;

import org.biomoby.shared.MobyPrefixResolver;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.data.MobyContentInstance;
import org.biomoby.shared.data.MobyDataUtils;
import org.biomoby.shared.parser.MobyPackage;
import org.biomoby.w3c.addressing.EndpointReference;
import org.omg.lsae.notifications.AnalysisEvent;

/**
 * Implements the client side of the asynchonous SOAP-based MOBY service invocation specification.
 * Normally, given some input data, one would call sendRequest(), then poll() until the jobs are
 * finished, then getResults(), then destroy().
 *
 * @author Eddie Kawas
 * @author Paul Gordon
 */

// TODO think about how we are going to deal with SOAPFaults -note: SAX parser created for Fault parsing
public class AsyncClient {
    public static final String USER_AGENT = "jBioMOBY_async_client/0.5";
    public static final String CONTENT_TYPE = "text/xml; charset=UTF-8";

    public static final String WS_ADDRESSING_TO_TAG_NAME = "To";

    public static final String WSRP_MULTI_PROPERTY_TAG_NAME = "GetMultipleResourceProperties";
    public static final String WSRP_PROPERTY_TAG_NAME = "ResourceProperty";

    public static final String MOBY_SERVICE_INVOC_ID_TAG_NAME = "ServiceInvocationId";
    public static final String MOBY_RESULT_PROPERTY_PREFIX = "result_";

    public static EndpointReference sendRequest(MobyService service, MobyContentInstance contents) throws Exception{
	StringWriter xmlWriter = new StringWriter();
	final boolean DONT_INCL_XML_DECLARATION = false;

	MobyDataUtils.toXMLDocument(xmlWriter, contents, DONT_INCL_XML_DECLARATION);

	return sendRequest(service.getURL().toString(), xmlWriter.toString(), service.getName());
    }

    public static EndpointReference sendRequest(MobyService service, String xml) throws Exception{
	return sendRequest(service.getURL().toString(), xml, service.getName());
    }

    /**
     * Initiates a request for service.  Followup with poll() to check the job's status.
     *
     * @param serviceURL the MOBY servioce endpoint URL for posting data
     * @param xml the MOBY request payload (omit the starting XML declaration)
     * @param servicename the unqualified name of the service
     *
     * @return the service invocation reference to be used in the followup methods
     */
    public static EndpointReference sendRequest(String serviceURL, String xml,
						String servicename) throws Exception {

	MessageFactory messageFactory = MessageFactory.newInstance();
	SOAPMessage message = messageFactory.createMessage();
	// Create objects for the message parts
	SOAPPart soapPart = message.getSOAPPart();
	SOAPEnvelope envelope = soapPart.getEnvelope();
	SOAPBody body = envelope.getBody();
	// Populate the body
	// Create the main element and namespace
	SOAPElement bodyElement = body.addChildElement(envelope.createName(
									   servicename + MobyService.SUBMIT_ACTION_SUFFIX, 
									   MobyPrefixResolver.MOBY_TRANSPORT_PREFIX, 
									   MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE));
	// Add content
	bodyElement.addChildElement("data").addTextNode(xml);

	// Save the message
	message.saveChanges();

	// Check the input
	ByteArrayOutputStream bytes = new ByteArrayOutputStream();
	message.writeTo(bytes);
	bytes.close();

	// Create the connection where we're going to send the request.
	HttpURLConnection httpConn = (HttpURLConnection) new URL(serviceURL)
	    .openConnection();
	byte[] buf = bytes.toByteArray();

	// Set the appropriate HTTP parameters.
	httpConn.setRequestProperty("Content-Length", String
				    .valueOf(buf.length));
	httpConn.setRequestProperty("Content-Type", CONTENT_TYPE);
	httpConn.setRequestProperty("User-agent", USER_AGENT);
	httpConn.setRequestProperty("SOAPAction", MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE+"#"
				    + servicename + MobyService.SUBMIT_ACTION_SUFFIX);
	httpConn.setRequestMethod("POST");
	httpConn.setDoOutput(true);
	httpConn.setDoInput(true);

	// Everything's set up; send the XML that was read in to buf.
	OutputStream out = httpConn.getOutputStream();
	out.write(buf);
	out.close();

	// Return the response stream as a buffered reader
	InputStream inStream;

	if (httpConn.getResponseCode() < 400) {
	    inStream = httpConn.getInputStream();
	} else {
	    inStream = httpConn.getErrorStream();
	}

	BufferedReader reader = 
	    new BufferedReader(new InputStreamReader(inStream));

	String line = null;
	StringBuffer buffer = new StringBuffer();
	while ((line = reader.readLine()) != null)
	    buffer.append(line + "\n");
	reader.close();
	return EndpointReference.createFromXML(buffer.toString());
    }

    public static AnalysisEvent poll(EndpointReference epr, String queryId) throws Exception{
	AnalysisEvent[] events = poll(epr, new String[]{queryId});
	if(events != null && events.length > 0){
	    return events[0];
	}
	else{
	    return null;
	}
    }

    public static AnalysisEvent[] poll(EndpointReference epr, Collection<String> queryIds) throws Exception {
	return poll(epr, queryIds.toArray(new String[queryIds.size()]));
    }

    /**
     * Polls the status (e.g. running, completed) of queries associated with a service invocation.
     * Completed queries can be retrieved using the getResultXXX() methods.
     *
     * @param epr a service invocation reference returned from sendRequest()
     * @param queryIds the list of jobs about which a status update is being requested
     */
    public static AnalysisEvent[] poll(EndpointReference epr, String[] queryIds) throws Exception {
	if(epr == null){
	    throw new NullPointerException("AsyncClient was given a null EndpointReference to poll");
	}

	MessageFactory messageFactory = MessageFactory.newInstance();
	SOAPMessage message = messageFactory.createMessage();
	// Create objects for the message parts
	SOAPPart soapPart = message.getSOAPPart();

	SOAPHeader header = message.getSOAPHeader();
	SOAPEnvelope envelope = soapPart.getEnvelope();
	SOAPBody body = envelope.getBody();

	header.addChildElement(envelope.createName(WS_ADDRESSING_TO_TAG_NAME, 
						   MobyPrefixResolver.WS_ADDRESSING_PREFIX,
						   MobyPrefixResolver.WS_ADDRESSING_NAMESPACE
						   )).addTextNode(epr.getAddress());
	header.addChildElement(envelope.createName(MOBY_SERVICE_INVOC_ID_TAG_NAME,  
						   MobyPrefixResolver.MOBY_TRANSPORT_PREFIX,
						   MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE
						   )).addTextNode(epr.getServiceInvocationId());

	// Populate the body
	// Create the main element and namespace
	SOAPElement bodyElement = 
	    body.addChildElement(envelope.createName(WSRP_MULTI_PROPERTY_TAG_NAME, 
						     MobyPrefixResolver.WSRP_PREFIX,
						     MobyPrefixResolver.WSRP_NAMESPACE));
	// Add content
	for(String queryId: queryIds){
	    bodyElement.addChildElement(envelope.createName(WSRP_PROPERTY_TAG_NAME,
							    MobyPrefixResolver.WSRP_PREFIX,
							    MobyPrefixResolver.WSRP_NAMESPACE))
		.addTextNode("status_" + queryId);
	}

	// Save the message
	message.saveChanges();

	// Check the input
	ByteArrayOutputStream bytes = new ByteArrayOutputStream();
	message.writeTo(bytes);
	bytes.close();

	// Create the connection where we're going to send the request.
	HttpURLConnection httpConn = 
	    (HttpURLConnection) new URL(epr.getAddress()).openConnection();
	byte[] buf = bytes.toByteArray();

	// Set the appropriate HTTP parameters.
	httpConn.setRequestProperty("Content-Length", String
				    .valueOf(buf.length));
	httpConn.setRequestProperty("Content-Type", CONTENT_TYPE);
	httpConn.setRequestProperty("User-agent", USER_AGENT);
	httpConn
	    .setRequestProperty("SOAPAction",
				MobyPrefixResolver.WSRP_NAMESPACE+"#"+WSRP_MULTI_PROPERTY_TAG_NAME);
	httpConn.setRequestMethod("POST");
	httpConn.setDoOutput(true);
	httpConn.setDoInput(true);

	// Everything's set up; send the XML that was read in to buf.
	OutputStream out = httpConn.getOutputStream();
	out.write(buf);
	out.close();

	// Return the response stream as a buffered reader
	InputStream inStream;

	if (httpConn.getResponseCode() < 400) {
	    inStream = httpConn.getInputStream();
	} else {
	    inStream = httpConn.getErrorStream();
	}

	BufferedReader reader = 
	    new BufferedReader(new InputStreamReader(inStream));
	String line = null;
	StringBuffer buffer = new StringBuffer();
	while ((line = reader.readLine()) != null){
	    buffer.append(line + "\n");
	}
	reader.close();

	// return the events
	return AnalysisEvent.createFromXML(buffer.toString());
    }

    /**
     * Method to be called when all results have been retrieved from a 
     * service invocation, or if the client wishes the invocation to be
     * prematurely terminated.  Resources on the server that are associated
     * with the endpoint are destroyed.  No data is retrievable from the
     * endpoint reference after this call.
     *
     * @param epr the service invocation whose server-side resources should be freed
     */
    public static void destroy(EndpointReference epr) throws Exception {
	if(epr == null){
	    return;
	}

	MessageFactory messageFactory = MessageFactory.newInstance();
	SOAPMessage message = messageFactory.createMessage();
	// Create objects for the message parts
	SOAPPart soapPart = message.getSOAPPart();

	SOAPHeader header = message.getSOAPHeader();
	SOAPEnvelope envelope = soapPart.getEnvelope();
	SOAPBody body = envelope.getBody();

	header.addChildElement(envelope.createName(WS_ADDRESSING_TO_TAG_NAME, 
						   MobyPrefixResolver.WS_ADDRESSING_PREFIX,
						   MobyPrefixResolver.WS_ADDRESSING_NAMESPACE
                                                   )).addTextNode(epr.getAddress());
	header.addChildElement(envelope.createName(MOBY_SERVICE_INVOC_ID_TAG_NAME, 
						   MobyPrefixResolver.MOBY_TRANSPORT_PREFIX,
						   MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE
						   )).addTextNode(epr.getServiceInvocationId());

	// Populate the body
	// Create the main element and namespace
	body.addChildElement(envelope.createName("Destroy", 
						 MobyPrefixResolver.WSRP_PREFIX,
						 MobyPrefixResolver.WSRP_NAMESPACE));
	// Save the message
	message.saveChanges();

	// Check the input
	ByteArrayOutputStream bytes = new ByteArrayOutputStream();
	message.writeTo(bytes);
	bytes.close();

	// Create the connection where we're going to send the request.
	HttpURLConnection httpConn = (HttpURLConnection) new URL(epr.getAddress())
	    .openConnection();
	byte[] buf = bytes.toByteArray();

	// Set the appropriate HTTP parameters.
	httpConn.setRequestProperty("Content-Length", String
				    .valueOf(buf.length));
	httpConn.setRequestProperty("Content-Type", CONTENT_TYPE);
	httpConn.setRequestProperty("User-agent", USER_AGENT);
	httpConn.setRequestProperty("SOAPAction",
				    MobyPrefixResolver.WSRP_NAMESPACE+"#Destroy");
	httpConn.setRequestMethod("POST");
	httpConn.setDoOutput(true);
	httpConn.setDoInput(true);

	// Everything's set up; send the XML that was read in to buf.
	OutputStream out = httpConn.getOutputStream();
	out.write(buf);
	out.close();

	// Return the response stream as a buffered reader
	InputStream inStream;

	if (httpConn.getResponseCode() < 400) {
	    inStream = httpConn.getInputStream();
	} else {
	    inStream = httpConn.getErrorStream();
	}

	BufferedReader reader = 
	    new BufferedReader(new InputStreamReader(inStream));
	String line = null;
	StringBuffer buffer = new StringBuffer();
	while ((line = reader.readLine()) != null)
	    buffer.append(line + "\n");
	reader.close();
    }

    // Paul: does MobyPackage yet handle the asynchronous output format?  From multiple queries?
    public static MobyPackage getResultPackage(EndpointReference epr, String queryId) throws Exception {
	// TODO - error handling - what if xml doesn't have a response?
	// Catch Exception thrown from createFromXML() and do something
	//System.out.println(buffer);
	return MobyPackage.createFromXML(getResultText(epr, queryId));
    }

    // remember to close the given stream!
    public static InputStream getResultStream(EndpointReference epr, String queryId) throws Exception {
	return getResultStream(epr, new String[]{queryId});
    }

    public static InputStream getResultStream(EndpointReference epr, Collection<String> queryIds) throws Exception {
	return getResultStream(epr, queryIds.toArray(new String[queryIds.size()]));
    }

    /**
     * Retrieves the result message for the given set of jobs in a service invocation.
     * This stream is parsed, for example, in MobyRequest for asynchronous service calls,
     * and turned into a MobyContentInstance.
     * 
     * @return an InputStream giving the raw HTTP (SOAP) response from the server
     */
    public static InputStream getResultStream(EndpointReference epr, String[] queryIds) throws Exception {

	MessageFactory messageFactory = MessageFactory.newInstance();
	SOAPMessage message = messageFactory.createMessage();
	// Create objects for the message parts
	SOAPPart soapPart = message.getSOAPPart();

	SOAPHeader header = message.getSOAPHeader();
	/*
	 * <Header xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing">
	 * <wsa:To
	 * mustUnderstand="1">http://localhost:8080/wsrf/services/filesystem</wsa:To>
	 * <ServiceInvocationId>312489713948713984</ServiceInvocationId>
	 * </Header>
	 */
	SOAPEnvelope envelope = soapPart.getEnvelope();
	SOAPBody body = envelope.getBody();

	header.addChildElement(
			       envelope.createName(WS_ADDRESSING_TO_TAG_NAME, 
						   MobyPrefixResolver.WS_ADDRESSING_PREFIX,
						   MobyPrefixResolver.WS_ADDRESSING_NAMESPACE
						   )).addTextNode(epr.getAddress());
	header.addChildElement(
			       envelope.createName(MOBY_SERVICE_INVOC_ID_TAG_NAME,
						   MobyPrefixResolver.MOBY_TRANSPORT_PREFIX,
						   MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE
						   )).addTextNode(epr.getServiceInvocationId());

	// Populate the body
	// Create the main element and namespace
	SOAPElement bodyElement = body.addChildElement(envelope.createName(WSRP_MULTI_PROPERTY_TAG_NAME,
									   MobyPrefixResolver.WSRP_PREFIX,
									   MobyPrefixResolver.WSRP_NAMESPACE));
	// Add content
	for(String queryId: queryIds){
	    bodyElement.addChildElement(envelope.createName(WSRP_PROPERTY_TAG_NAME,
							    MobyPrefixResolver.WSRP_PREFIX,
							    MobyPrefixResolver.WSRP_NAMESPACE))
		.addTextNode(MOBY_RESULT_PROPERTY_PREFIX + queryId);
	}

	// Save the message
	message.saveChanges();

	// Check the input
	ByteArrayOutputStream bytes = new ByteArrayOutputStream();
	message.writeTo(bytes);
	bytes.close();

	// Create the connection where we're going to send the request.
	HttpURLConnection httpConn = 
	    (HttpURLConnection) new URL(epr.getAddress()).openConnection();
	byte[] buf = bytes.toByteArray();

	// Set the appropriate HTTP parameters.
	httpConn.setRequestProperty("Content-Length", String
				    .valueOf(buf.length));
	httpConn.setRequestProperty("Content-Type", CONTENT_TYPE);
	httpConn.setRequestProperty("User-agent", USER_AGENT);
	httpConn
	    .setRequestProperty("SOAPAction",
				MobyPrefixResolver.WSRP_NAMESPACE+"#"+WSRP_MULTI_PROPERTY_TAG_NAME);
	httpConn.setRequestMethod("POST");
	httpConn.setDoOutput(true);
	httpConn.setDoInput(true);

	// Everything's set up; send the XML that was read in to buf.
	OutputStream out = httpConn.getOutputStream();
	out.write(buf);
	out.close();

	// Return the response stream
	if (httpConn.getResponseCode() < 400) {
	    return httpConn.getInputStream();
	} else {
	    return httpConn.getErrorStream();
	}
    }

    /**
     * Retrieves the result text for the given set of jobs in a service invocation.
     * 
     * @return an string containing the raw HTTP (SOAP) response from the server
     */
    public static String getResultText(EndpointReference epr, String queryId) throws Exception {
	return getResultText(epr, new String[]{queryId});
    }

    public static String getResultText(EndpointReference epr, Collection<String> queryIds) throws Exception {
	return getResultText(epr, queryIds.toArray(new String[queryIds.size()]));
    }

    public static String getResultText(EndpointReference epr, String[] queryIds) throws Exception {
	InputStream inStream = getResultStream(epr, queryIds);

	BufferedReader reader = 
	    new BufferedReader(new InputStreamReader(inStream));
	String line = null;
	StringBuffer buffer = new StringBuffer();
	while ((line = reader.readLine()) != null){
	    buffer.append(line + "\n");
	}
	reader.close();
	return buffer.toString();
    }

}
