// MobyGraphs.java
//    A command-line client creating graphs of the Moby objects.
//
//    senger@ebi.ac.uk
//    October 2003
//

import org.biomoby.shared.*;
import org.biomoby.shared.CentralCached;

import org.biomoby.client.CmdLineHelper;
import org.biomoby.client.CentralDigestCachedImpl;
import org.biomoby.client.Graphviz;
import org.biomoby.client.RDF;
import org.biomoby.client.FilterServices;
import org.biomoby.client.ServicesEdge;
import org.biomoby.client.DataServiceEdge;
import org.biomoby.client.ServiceConnections;
import org.biomoby.client.Taverna;

import org.tulsoft.tools.BaseCmdLine;

import org.apache.commons.lang.math.NumberUtils;

import java.util.*;
import java.io.*;

/**
 * This is a command-line creating graphs of the Moby objects, and
 * generating workflow definitions for Taverna. Start with -help for
 * details. <p>
 *
 * @author <A HREF="mailto:senger@ebi.ac.uk">Martin Senger</A>
 * @version $Id: MobyGraphs.java,v 1.13 2008/02/23 13:58:32 senger Exp $
 */

public class MobyGraphs
    extends CmdLineHelper {

    /*************************************************************************
     *
     * Entry point...
     *
     *************************************************************************/
    public static void main (String [] args) {
	try {

	    BaseCmdLine cmd = getCmdLine (args, MobyGraphs.class);
	    String param;

	    // where is a Moby registry
	    CentralCached worker = getCachableRegistryWorker (cmd);

	    // collect properties customizing graphs
	    Properties props = new Properties();
	    add (props, cmd, Graphviz.PROP_RANKDIR);
	    addBool (props, cmd, Taverna.PROP_RAWINPUT);
	    addBool (props, cmd, Taverna.PROP_RAWOUTPUT);
	    if (cmd.hasOption ("-raw")) {
		props.put (Taverna.PROP_RAWINPUT, "true");
		props.put (Taverna.PROP_RAWOUTPUT, "true");
	    }

	    //
	    // read all data types and their relationships
	    //
	    decorationLn ("Retrieving data types...");
	    MobyDataType[] dataTypes = worker.getDataTypes();

	    //
	    // create a graph with data types
	    //
	    if (cmd.hasOption ("-d")) {

		decorationLn ("Creating a graph of the data types...");
		String graph = Graphviz.createDataTypesGraph (dataTypes, props);
		param = cmd.getParam ("-fd");
		if (param == null)
		    param = cmd.getParam ("-f");
		if (param == null)
		    msgln (graph);
		else
		    createFile (param, graph);
	    }

	    //
	    // create a graph with service types
	    //
	    if (cmd.hasOption ("-t")) {
		decorationLn ("Retrieving service types...");
		MobyServiceType[] serviceTypes = worker.getFullServiceTypes();
		decorationLn ("Creating a graph of the service types...");
		String graph = Graphviz.createServiceTypesGraph (serviceTypes, props);
		param = cmd.getParam ("-ft");
		if (param == null)
		    param = cmd.getParam ("-f");
		if (param == null)
		    msgln (graph);
		else
		    createFile (param, graph);
	    }

	    //
	    // create a graph with services
	    //
	    if (cmd.hasOption ("-s")) {
		decorationLn ("Retrieving services...");
		MobyService[] services = worker.getServices();
		decorationLn ("Creating a graph of the services...");
		ServicesEdge[] edges = null;
		DataServiceEdge[] debugStartingEdges = null;
		DataServiceEdge[] debugEndingEdges = null;

		// an undocumented option for debugging (see an
		// example file in 'data' directory)
		String xfile = cmd.getParam ("-X");
		boolean xfileing = (xfile != null);

		if (xfileing) {
		    Vector<ServicesEdge> ev = new Vector<ServicesEdge>();
		    Vector<DataServiceEdge> evs = new Vector<DataServiceEdge>();
		    Vector<DataServiceEdge> eve = new Vector<DataServiceEdge>();
		    String line;
		    BufferedReader data = null;
		    data = new BufferedReader
			(new InputStreamReader (new FileInputStream (xfile)));
		    while ((line = data.readLine()) != null) {
			if (line.trim().equals ("")) continue;
			if (line.trim().startsWith ("#")) continue;
			String[] fields = line.split ("\\s+");
			if (fields.length > 1) {
			    if (fields[0].equalsIgnoreCase ("start")) {
				DataServiceEdge dse = new DataServiceEdge (new MobyDataType ("start"),
									   new MobyService (fields[1]),
									   "");
				evs.addElement (dse);
			    } else if (fields[1].equalsIgnoreCase ("end")) {
				DataServiceEdge dse = new DataServiceEdge (new MobyService (fields[0]),
									   new MobyDataType ("end"),
									   "");
				eve.addElement (dse);
			    } else ev.addElement (new ServicesEdge (new MobyService (fields[0]),
								    new MobyService (fields[1]),
								    ""));
			}
		    }
		    edges = new ServicesEdge[ev.size()];
		    ev.copyInto (edges);
		    debugStartingEdges = new DataServiceEdge[evs.size()];
		    evs.copyInto (debugStartingEdges);
		    debugEndingEdges = new DataServiceEdge[eve.size()];
		    eve.copyInto (debugEndingEdges);

		} else {
		    edges = ServiceConnections.build (dataTypes, services);
		}

		msgln ("EDGES: " + edges.length);

		// filter edges
		String[] authorities = null;
		String[] serviceNames = null;
		int depth = 1;
                if ((param = cmd.getParam ("-auth")) != null )
		    authorities = param.split (",");
                if ((param = cmd.getParam ("-name")) != null )
		    serviceNames = param.split (",");
		param = cmd.getParam ("-depth");
		if (param != null) {
		    try {
			depth = Integer.valueOf (param).intValue();
		    } catch (java.lang.NumberFormatException e) {}
		}		
		edges = FilterServices.filter (edges, authorities, serviceNames, depth);

		msgln ("Filtered EDGES: " + edges.length);

		if (xfileing) {
		    msgln ("List of EDGES: ");
		    for (int i = 0; i < edges.length; i++) {
			msgln ("\t" + edges[i].toString());
		    }
		}

		if (cmd.hasParam ("-path")) {
		    String[] pathEnds = cmd.getParam ("-path", 2);
		    if (pathEnds[0] == null || pathEnds[1] == null) {
			emsgln ("Missing value for parameter '-path'. It should be followed by two service names.");
			exit (1);
		    }
// 		    edges = FilterServices.pathes (edges, pathEnds[0], pathEnds[1]);
		    edges = FilterServices.pathes2 (edges, pathEnds[0], pathEnds[1]);
		    if (edges == null) {
			emsgln ("No connection found between '" +
				pathEnds[0] + "' and '" + pathEnds[1] + "'");
			exit(1);
		    }
		}

		ServicesEdge[] allPaths = null;
		ServicesEdge[][] separatePaths = null;
		String[] pathEnds = null;
		if (cmd.hasParam ("-datapath")) {
		    pathEnds = cmd.getParam ("-datapath", 2);
		    if (pathEnds[0] == null || pathEnds[1] == null) {
			emsgln ("Missing value for parameter '-datapath'. It should be followed by two data type names.");
			exit (1);
		    }
		    MobyPrimaryDataSimple sourceData = createSimpleData (pathEnds[0]);
		    MobyPrimaryDataSimple targetData = createSimpleData (pathEnds[1]);

		    DataServiceEdge[] startingEdges = null;
		    if (debugStartingEdges == null)
			startingEdges = ServiceConnections.findStartingEdges (sourceData, dataTypes, services);
		    else
			startingEdges = debugStartingEdges;
		    DataServiceEdge[] endingEdges = null;
		    if (debugEndingEdges == null)
			endingEdges = ServiceConnections.findEndingEdges (targetData, dataTypes, services);
		    else
			endingEdges = debugEndingEdges;

		    msgln ("SE: " + startingEdges.length);
		    if (xfileing) {
			for (int i = 0; i < startingEdges.length; i++) {
			    msgln ("\t" + startingEdges[i].toString());
			}
		    }
		    msgln ("EE: " + endingEdges.length);
		    if (xfileing) {
			for (int i = 0; i < endingEdges.length; i++) {
			    msgln ("\t" + endingEdges[i].toString());
			}
		    }

		    // this creates *all* pathes, but some of them have cycles and inside branches
 		    separatePaths = FilterServices.dataPaths (startingEdges, edges, endingEdges);
		    if (separatePaths.length == 0) {
			emsgln ("No connection found between '" +
				pathEnds[0] + "' and '" + pathEnds[1] + "'");
			exit(1);
		    }

		    msgln ("After dataPaths: " + separatePaths.length);
		    if (xfileing) {
			for (int i = 0; i < separatePaths.length; i++) {
			    msgln ("Separate data path " + (i+1));
			    for (int j = 0; j < separatePaths[i].length; j++) {
				msgln ("\t" + separatePaths[i][j]);
			    }
			}
		    }

		    allPaths = FilterServices.joinPaths (separatePaths);
		    msgln ("After joinPaths: " + allPaths.length);
		    if (xfileing) {
			msgln ("Join paths: ");
			for (int i = 0; i < allPaths.length; i++) {
			    msgln ("\t" + allPaths[i].toString());
			}
		    }

		    // separate paths to straight paths (no cycles, no branches)
 		    separatePaths = FilterServices.straightDataPaths (startingEdges, allPaths, endingEdges);

		    msgln ("After straightDataPaths: " + separatePaths.length);
		}

		// create a graph (in whatever format)
		if (cmd.hasParam ("-datapath")) {
		    boolean generateScufl = ( cmd.hasOption ("-scufl") || cmd.hasOption ("-onlyscufl") );
		    boolean generateGraph = (! cmd.hasOption ("-onlyscufl") );
		    int pageSize = NumberUtils.toInt (cmd.getParam ("-join"));
 		    String[] graphs = null;
 		    String[] pathNames = null;
		    String[] scufls = null;
		    if ( (cmd.hasOption ("-separate") || cmd.hasParam ("-join") || generateScufl) ) {
			if (separatePaths.length > 0) {
			    pathNames = new String [separatePaths.length];
			    for (int i = 0; i < separatePaths.length; i++)
				pathNames[i] = String.format ("Path_%.2d", i+1);
			    if (generateGraph) {
				if (pageSize > 1) {
				    int pageBeginPos = 0;
				    int graphIndex = 0;
				    int numberOfGraphs = (separatePaths.length - 1) / pageSize + 1;
				    graphs = new String [numberOfGraphs];
				    for (int i = 0; i < separatePaths.length; i++) {
					// have we reached an end of a page?
					if ( (i+1) % pageSize == 0 || (i+1) == separatePaths.length ) {
					    // yes => create a graph containing only paths from 'pageBeginPos' to 'i'
					    graphs [graphIndex++] =
						Graphviz.createServicesGraph (separatePaths, pageBeginPos, i, pathNames, props);
					    pageBeginPos = i+1;
					}
				    }
				} else {
				    graphs = new String [separatePaths.length];
				    for (int i = 0; i < separatePaths.length; i++) {
					graphs[i] = Graphviz.createServicesGraph (separatePaths[i], props);
				    }
				}
			    }
			    if (generateScufl) {
				scufls = new String [separatePaths.length];
				for (int i = 0; i < separatePaths.length; i++) {
				    scufls[i] = Taverna.buildWorkflow (separatePaths[i], worker.getRegistryEndpoint(),
								       props);
				}
			    }
			} else {
			    emsgln ("No straight path exitsts between '" +
				    pathEnds[0] + "' and '" + pathEnds[1] + "'. Only cyclic paths found.");
			    exit(1);
			}

		    } else {
			graphs = new String [1];
 			graphs[0] = Graphviz.createServicesGraph (allPaths, props);
		    }

		    String fn = cmd.getParam ("-fs");
		    if (fn == null)
			fn = cmd.getParam ("-f");

		    // output the graph[s]
		    if (graphs != null) {
			boolean usePathNames = (pathNames != null && graphs.length == pathNames.length);
			for (int i = 0; i < graphs.length; i++) {
			    if (fn == null)
				msgln (graphs[i]);
			    else {
				if (graphs.length == 1)
				    createFile (fn, graphs[i]);
				else
				    createFile (modifyFileName (fn,
								(usePathNames ?
								 pathNames[i] :
								 String.format ("Graph_%.2d", i+1)
								 )
								),
						graphs[i]);
			    }
			}
		    }

		    // output scufl definitions
		    if (scufls != null) {
			for (int i = 0; i < scufls.length; i++) {
			    if (fn == null)
				msgln (scufls[i]);
			    else
				createFile (replaceExtension (modifyFileName (fn, pathNames [i]),
							      "xml"),
					    scufls[i]);
			}
		    }

		} else {
		    String graph;
		    if (cmd.hasOption ("-rdf"))
			graph = RDF.createServicesGraph (edges, props);
		    else
			graph = Graphviz.createServicesGraph (edges, props);

		    // output the graph
		    param = cmd.getParam ("-fs");
		    if (param == null)
			param = cmd.getParam ("-f");
		    if (param == null)
			msgln (graph);
		    else
			createFile (param, graph);
		}
	    }

	} catch (Throwable e) {
	    processErrorAndExit (e);
	}
    }

    // 'name' has one of the following formats:
    //     dataTypeName
    //     namespace/dataTypeName
    static MobyPrimaryDataSimple createSimpleData (String name) {
	MobyPrimaryDataSimple data = new MobyPrimaryDataSimple ("dummy_name_for " + name);
	int pos = name.indexOf ("/");
	if (pos > -1 && pos != name.length() - 1) {
	    data.addNamespace (new MobyNamespace (name.substring (0, pos)));
	    data.setDataType (new MobyDataType (name.substring (pos + 1)));
	} else {
	    data.setDataType (new MobyDataType (name));
	}
	return data;
    }

    /*************************************************************************
     * Create 'filename' from 'content'.
     *************************************************************************/
    static void createFile (String filename, String content)
	throws IOException {
	PrintWriter fileout = new PrintWriter
	    (new BufferedWriter (new FileWriter (filename)));
	fileout.print (content);
	fileout.close();
	decorationLn ("File created: " + filename);
    }

    /*************************************************************************
     * Add an additional part ('addon') into a file name...
     *************************************************************************/
    static String modifyFileName (String filename, String addon) {
	int pos = filename.lastIndexOf (".");
	if (pos > -1)
	    return filename.substring (0, pos+1) + addon + filename.substring (pos);
	else
	    return filename + "." + addon;
    }

    /*************************************************************************
     * Replace (or add) extension in 'filename' by 'newExtension'.
     *************************************************************************/
    static String replaceExtension (String filename, String newExtension) {
	int pos = filename.lastIndexOf (".");
	if (pos > -1)
	    return filename.substring (0, pos+1) + newExtension;
	else
	    return filename + "." + newExtension;
    }

    /*************************************************************************
     * For collecting only non-empty properties, and using property names
     * with or without starting dash.
     *************************************************************************/
    static void add (Properties props, BaseCmdLine cmd, String propertyName) {
	String value = cmd.getParam (propertyName);
	if (value == null)
	    value = cmd.getParam ("-" + propertyName);
	if (value != null)
	    props.put (propertyName, value);
    }

    /*************************************************************************
     * For collecting boolean properties, and using property names
     * with or without starting dash.
     *************************************************************************/
    static void addBool (Properties props, BaseCmdLine cmd, String propertyName) {
	if ( cmd.hasOption ("-" + propertyName) ||
	     cmd.hasOption (propertyName) ) {
	    props.put (propertyName, "true");
	}
    }

    /*************************************************************************
     * Print 'msg' but only if in verbose mode
     *************************************************************************/
    static void decoration (String msg) { qmsg (msg); }

    /*************************************************************************
     * Print 'msg' and a newline but only if in verbose mode
     *************************************************************************/
    static void decorationLn (String msg) { qmsgln (msg); }

}
