// Taverna.java
//
//    senger@ebi.ac.uk
//    October 2004
//

package org.biomoby.client;

import java.util.Hashtable;
import java.util.Properties;

import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.Utils;
import org.embl.ebi.escience.scufl.DataConstraint;
import org.embl.ebi.escience.scufl.InputPort;
import org.embl.ebi.escience.scufl.OutputPort;
import org.embl.ebi.escience.scufl.Processor;
import org.embl.ebi.escience.scufl.ScuflModel;
import org.embl.ebi.escience.scufl.view.XScuflView;
import org.embl.ebi.escience.scuflworkers.biomoby.BiomobyProcessor;
import org.embl.ebi.escience.scuflworkers.java.LocalServiceProcessor;
import org.embl.ebi.escience.scuflworkers.stringconstant.StringConstantProcessor;

/**
 * A utility class that understands how to create workflow definitions
 * for <a href="http://taverna.sf.net/">Taverna</a>. <p>
 *
 * @author <A HREF="mailto:senger@ebi.ac.uk">Martin Senger</A>
 * @version $Id: Taverna.java,v 1.4 2005/07/19 12:39:59 senger Exp $
 */

public abstract class Taverna {

    /** Property name. A boolean value. If set to true the generated
     * workflow definition does not include input data processor
     * (converting basic data types into Moby XML structure). Default
     * is false.
     */
    public static final String PROP_RAWINPUT = "rawinput";

    /** Property name. A boolean value. If set to true the generated
     * workflow definition does not include output data
     * processor (converting basic data types from Moby XML
     * structure). Default is false.
     */
    public static final String PROP_RAWOUTPUT = "rawoutput";

    /** Property name. A boolean value. If set to true workflow
     * creation produces many Taverna messages. Default is false.
     */
    public static final String PROP_VERBOSE = "tverbose";

    /*************************************************************************
     * Creates a workflow definition (an XML string known as 'scufl'
     * or 'xscufl') from the set of graph edges. The definition
     * includes also pointer to a BioMoby registry where the services
     * (used by graph edges) are registered. <p>
     *
     * @param edges represent services and their connectors in the
     * created graph; some edges may be of type {@link
     * DataServiceEdge} (which is a subclass of ServicesEdge) - those
     * represent a special type of connection between a service and an
     * input or ouput data type; the graph represented by these edges
     * should not have cycles and should not have forking and merging
     * branches
     *
     * @param props are some properties that can influence how the
     * workflow definition will look like; see the property names
     * elsewhere in this API what properties are understood
     *
     * @return a string with a scufl workflow definition
     *************************************************************************/
    public static String buildWorkflow (ServicesEdge[] edges, String mobyEndpoint,
					Properties props)
	throws MobyException {
	try {
	    // properties influencing workflow definition
	    boolean verbose = ( props.containsKey (PROP_VERBOSE) &&
				props.getProperty (PROP_VERBOSE).equals ("true") );
	    boolean rawinput = ( props.containsKey (PROP_RAWINPUT) &&
				 props.getProperty (PROP_RAWINPUT).equals ("true") );
	    boolean rawoutput = ( props.containsKey (PROP_RAWOUTPUT) &&
				  props.getProperty (PROP_RAWOUTPUT).equals ("true") );

	    ScuflModel model = new ScuflModel();
	    model.setOffline (true);
	    model.isFiringEvents = verbose;

	    Hashtable processors = new Hashtable();
	    for (int i = 0; i < edges.length; i++) {

		if (edges[i] instanceof DataServiceEdge) {
		    DataServiceEdge edge = (DataServiceEdge)edges[i];
		    if (edge.isEndingEdge()) {

			// add a (potentially new) BioMoby processor (representing a source service)
			String sourceName = addMobyProcessor (model, edge.getSourceService(),
							      mobyEndpoint, processors);

			// add a sink for result
			String sinkName;
			if (rawoutput) {
			    sinkName = model.getValidProcessorName (edge.getConnector());
			    Processor sinkProcessor = model.getWorkflowSinkProcessor();
			    InputPort sinkPort = new InputPort (sinkProcessor, sinkName);
			    sinkPort.getMetadata().setDescription (edge.getDataType().getDescription());
			    sinkProcessor.addPort (sinkPort);

			} else {

			    // create a local processor that extract
			    // from XML/Moby formatted output data into
			    // several output pieces (namespace, id [, value])
			    String mobyOutputName = model.getValidProcessorName ("MobyOutput");
			    String mobyPort = "mobydata";
			    sinkName = mobyOutputName + ":" + mobyPort;
			    Processor extractMobyDataProcessor =
				new LocalServiceProcessor (model, mobyOutputName,
							   "org.embl.ebi.escience.scuflworkers.biomoby.ExtractMobyData");	   
			    extractMobyDataProcessor.setDescription (edge.getDataType().getDescription());
			    model.addProcessor (extractMobyDataProcessor);

			    // create output boxes (for namespace, id,
			    // type, value)
			    Processor sinkProcessor = model.getWorkflowSinkProcessor();
			    String namespaceName = model.getValidProcessorName ("namespace");
			    sinkProcessor.addPort (new InputPort (sinkProcessor, namespaceName));
			    model.addDataConstraint (new DataConstraint (model, mobyOutputName + ":namespace", namespaceName));

			    String idName = model.getValidProcessorName ("id");
			    sinkProcessor.addPort (new InputPort (sinkProcessor, idName));
 			    model.addDataConstraint (new DataConstraint (model, mobyOutputName + ":id", idName));

			    String typeName = model.getValidProcessorName ("type");
			    sinkProcessor.addPort (new InputPort (sinkProcessor, typeName));
 			    model.addDataConstraint (new DataConstraint (model, mobyOutputName + ":type", typeName));

			    String valueName = model.getValidProcessorName ("value");
			    sinkProcessor.addPort (new InputPort (sinkProcessor, valueName));
 			    model.addDataConstraint (new DataConstraint (model, mobyOutputName + ":value", valueName));
			}

			// connect them together
			DataConstraint dc = new DataConstraint (model,
								sourceName + ":output",
								sinkName);
 			model.addDataConstraint (dc);

		    } else {

			// add a (potentially new) BioMoby processor (representing a target service)
			String targetName = addMobyProcessor (model, edge.getTargetService(),
							      mobyEndpoint, processors);

			// add a source for workflow main input
			String inputName;
			if (rawinput) {
			    inputName = model.getValidProcessorName (edge.getConnector());
			    Processor sourceProcessor = model.getWorkflowSourceProcessor();
			    OutputPort sourcePort = new OutputPort (sourceProcessor, inputName);
			    sourcePort.getMetadata().setDescription (edge.getDataType().getDescription());
			    sourceProcessor.addPort (sourcePort);

			} else {
			    // create a local processor that creates
			    // Moby input data in XML/Moby format from
			    // several input pieces (namespace, id [, value])
			    String mobyInputName = model.getValidProcessorName ("MobyInput");
			    String mobyPort = "mobydata";
			    inputName = mobyInputName + ":" + mobyPort;
			    Processor createMobyDataProcessor =
				new LocalServiceProcessor (model, mobyInputName,
							   "org.embl.ebi.escience.scuflworkers.biomoby.CreateMobyData");	   
			    createMobyDataProcessor.setDescription (edge.getDataType().getDescription());
			    model.addProcessor (createMobyDataProcessor);

			    // create input boxes:
			    Processor sourceProcessor = model.getWorkflowSourceProcessor();
			    //   a) always for namespace, id
			    String ns = edge.extractNamespace();
			    String namespaceName = model.getValidProcessorName ("namespace");
			    if (ns.equals ("")) {
				sourceProcessor.addPort (new OutputPort (sourceProcessor, namespaceName));
				model.addDataConstraint (new DataConstraint (model, namespaceName, mobyInputName + ":namespace"));
			    } else {
				// when we know a Moby namespace of this input, make it a string constant
 				String procName = model.getValidProcessorName (ns);
				Processor stringProcessor = new StringConstantProcessor (model, procName, ns);
				stringProcessor.addPort (new OutputPort (sourceProcessor, ns));
				model.addProcessor (stringProcessor);
				model.addDataConstraint (new DataConstraint (model,
									     procName + ":value",
									     mobyInputName + ":namespace"));
			    }
			    String idName = model.getValidProcessorName ("id");
			    sourceProcessor.addPort (new OutputPort (sourceProcessor, idName));
 			    model.addDataConstraint (new DataConstraint (model, idName, mobyInputName + ":id"));

			    //   b) for basic types (if the input is a basic type)
			    String valueName = model.getValidProcessorName ("value");
			    String dt = Utils.pureName (edge.getDataType().getName());
			    if (dt.equalsIgnoreCase ("String")) {
				sourceProcessor.addPort (new OutputPort (sourceProcessor, valueName));
				model.addDataConstraint (new DataConstraint (model, valueName, mobyInputName + ":stringvalue"));
			    } else if (dt.equalsIgnoreCase ("Integer")) {
				sourceProcessor.addPort (new OutputPort (sourceProcessor, valueName));
				model.addDataConstraint (new DataConstraint (model, valueName, mobyInputName + ":intvalue"));
			    } else if (dt.equalsIgnoreCase ("Float")) {
				sourceProcessor.addPort (new OutputPort (sourceProcessor, valueName));
				model.addDataConstraint (new DataConstraint (model, valueName, mobyInputName + ":floatvalue"));

// 			    } else if (! dt.equalsIgnoreCase ("Object")) {
// 				sourceProcessor.addPort (new OutputPort (sourceProcessor, valueName));
// 				model.addDataConstraint (new DataConstraint (model, valueName, mobyInputName + ":payload"));
			    }
			}

			// connect them together
			DataConstraint dc = new DataConstraint (model,
								inputName,
								targetName + ":input");
 			model.addDataConstraint (dc);
		    }

		} else {
		    ServicesEdge edge = edges[i];

		    // add a new BioMoby processor (representing a source service)
		    String sourceName = addMobyProcessor (model, edge.getSourceService(), mobyEndpoint, processors);

		    // add a new BioMoby processor (representing a target service)
		    String targetName = addMobyProcessor (model, edge.getTargetService(), mobyEndpoint, processors);

		    // connect these two nodes/services/processors
		    DataConstraint dc = new DataConstraint (model,
							    sourceName + ":output",
							    targetName + ":input");
		    model.addDataConstraint (dc);
		}
	    }

	    // XScuflParser.java for more details.
	    XScuflView xsv = new XScuflView (model);
	    return xsv.getXMLText();

	} catch (MobyException e) {
	    throw (e);
	} catch (Exception e) {
	    e.printStackTrace();
	    throw new MobyException (e.toString());
	}
    }

    // returns a name of the new (or existing) processor
    static String addMobyProcessor (ScuflModel model,
				    MobyService service,
				    String mobyEndpoint,
				    Hashtable processorsSoFar)
	throws MobyException {
	try {
	    String serviceId = service.getUniqueName();
	    if (processorsSoFar.containsKey (serviceId))
		return (String)processorsSoFar.get (serviceId);

	    String processorName = model.getValidProcessorName (service.getName());
	    BiomobyProcessor mobyProcessor =
		new BiomobyProcessor (model, processorName, service, mobyEndpoint);
	    String description = service.getDescription();
	    if (description != null)
		mobyProcessor.setDescription (description);
	    model.addProcessor (mobyProcessor);
	    processorsSoFar.put (serviceId, processorName);
	    return processorName;
	} catch (Exception e) {
	    throw new MobyException (e.toString());
	}
    }

    // StringConstantProcessor
}
