// Graphviz.java
//
//    senger@ebi.ac.uk
//    October 2003
//

package org.biomoby.client;

import java.util.HashSet;
import java.util.Properties;

import org.biomoby.shared.MobyDataType;
import org.biomoby.shared.MobyRelationship;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.MobyServiceType;
import org.biomoby.shared.Utils;

/**
 * A utility class that understands how to create
 * <a href="http://www.research.att.com/sw/tools/graphviz/">graphviz</a> graphs from a set of
 * {@link org.biomoby.client.ServicesEdge ServiceEdges}, or from other
 * data structures.
 *
 * @author <A HREF="mailto:senger@ebi.ac.uk">Martin Senger</A>
 * @version $Id: Graphviz.java,v 1.9 2005/08/26 06:27:04 senger Exp $
 */

public abstract class Graphviz {

    /** Property name.  Sets direction of graph layout. If sets to
     * "LR", the graph is laid out from left to right, i.e., directed
     * edges tend to go from left to right. Other value is "TB" where
     * graphs are laid out from top to bottom. Default is "LR".
     */
    public static final String PROP_RANKDIR = "rankdir";

    /** Property name.  It contains a name of an element that should
	be highlighted in the resulting graph.
     */
    public static final String PROP_HIGHLIGHT = "highlight";

    /** Property name.  It contains a name of a color that should be
	used to highlight an element (see {@link
	#PROP_HIGHLIGHT}. Default is "cyan2".
     */
    public static final String PROP_HIGHLIGHT_COLOR = "highlightcolor";

    /** Property name. It indicates that the graph should create URLs
	for graph nodes (except the highlighted one - see @{link
	#PROP_HIGHLIGHT}). The default URL is taken from this
	property. If this property exists but has an empty value, the
	URL for individual nodes are still going to be created, but
	not the deafult one. The URLs for individual nodes are taken
	from the "comment" field of the data objects for whom are
	nodes created. No URL is created for a node where the comment
	field is empty.
     */
    public static final String PROP_IMAGEMAP = "imagemap";

    /*************************************************************************
     * Creates a graph connecting Moby services as defined in a set of
     * the graph 'edges'.  <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 betweern a service and
     * an input or ouput data type
     *
     * @param props are some properties that can influence how the
     * graph will look like; see the property names elswhere in this
     * API what properties are understood
     *
     * @return a string with all definitions as understood by 'dot'
     * program (from the graphviz package); this string can be saved
     * in a '.dot' file that can be passed to a dot program to produce
     * graphs in many available image formats
     *
     *************************************************************************/
    static public String createServicesGraph (ServicesEdge[] edges,
					      Properties props) {

	StringBuffer buf = new StringBuffer();
	buf.append ("digraph MobyServices {\n");
	buf.append ("\trankdir=" + props.getProperty (PROP_RANKDIR, "LR") + ";\n");
	buf.append (createBodyServicesGraph (edges, 0));
	buf.append ("}\n");
	return new String (buf);
    }

    /*************************************************************************
     * Creates a graph connecting Moby services as defined in a set of
     * the graph 'paths'. The resulting graph will include only paths
     * (as separate clusters) starting from paths[fromPath] to
     * paths[toPath].
     *
     * @param paths is an array of edges; each set of edges defines
     * one path; some edges may be of type {@link DataServiceEdge}
     * (which is a subclass of ServicesEdge) - those represent a
     * special type of connection betweern a service and an input or
     * ouput data type
     *
     * @param fromPath a starting index in array 'paths'
     *
     * @param toPath an ending index in array 'paths'
     *
     * @param pathNames gives names of 'paths' (it may be used to
     * label individual graph clusters); the array should have the
     * same dimension as 'path' - and their elements should correspond
     * to each other
     *
     * @param props are some properties that can influence how the
     * graph will look like; see the property names elswhere in this
     * API what properties are understood
     *
     * @return a string with all definitions as understood by 'dot'
     * program (from the graphviz package); this string can be saved
     * in a '.dot' file that can be passed to a dot program to produce
     * graphs in many available image formats
     *
     *************************************************************************/
    static public String createServicesGraph (ServicesEdge[][] paths, int fromPath, int toPath,
					      String[] pathNames,
					      Properties props) {

	StringBuffer buf = new StringBuffer();
	buf.append ("digraph MobyServices {\n");
	buf.append ("\trankdir=" + props.getProperty (PROP_RANKDIR, "LR") + ";\n");
	for (int i = fromPath; i <= toPath; i++) {
	    buf.append ("subgraph " + i + "{\n");
	    buf.append (createBodyServicesGraph (paths[i], (i+1)));
	    buf.append ("}\n");
	}
	buf.append ("}\n");
	return new String (buf);
    }


    // do the main job - only the graph itself, no surrounding
    // brackets etc; separated here so more graphs can be created and
    // then put together as subgraphs; that's why it identifies graph
    // nodes by given number (so other subgraph may use the same nodes
    // but without beign connected between subgraphs)
    static String createBodyServicesGraph (ServicesEdge[] edges,
					   int graphId) {

	HashSet nodes = new HashSet();   // to prevent creating nodes several times
	StringBuffer buf = new StringBuffer();
	for (int i = 0; i < edges.length; i++) {
	    ServicesEdge edge = edges[i];
	    StringBuffer edgeAttrs = edgeAttributes (edge);

	    if (edge instanceof DataServiceEdge) {
		DataServiceEdge dedge = (DataServiceEdge)edge;
		String dataTypeName = Utils.pureName (dedge.getDataType().getName());
		if (! nodes.contains (dataTypeName)) {
		    buf.append ("\t" +
				quoteIt (dataTypeName + "_" + graphId) +
				new String (dataTypeAttributes (dataTypeName)) +
				"\n");
		    nodes.add (dataTypeName);
		}
		buf.append ("\t" +
			    new String (serviceNode (dedge.getService(), nodes, graphId)) +
			    "\n");
		if (dedge.isEndingEdge()) {
		    buf.append ("\t" +
				quoteIt (dedge.getService().getName() + "_" + graphId) + " -> " +
				quoteIt (dataTypeName + "_" + graphId) +
				new String (edgeAttrs) + "\n");
		} else {
		    buf.append ("\t" +
				quoteIt (dataTypeName + "_" + graphId) + " -> " +
				quoteIt (dedge.getService().getName() + "_" + graphId) +
 				new String (edgeAttrs) + "\n");
		}

	    } else {
		buf.append ("\t" +
			    new String (serviceNode (edge.sourceService, nodes, graphId)) +
			    "\n");
		buf.append ("\t" +
			    new String (serviceNode (edge.targetService, nodes, graphId)) +
			    "\n");
		if (edge.sourceService != null && edge.targetService != null) {
		    buf.append ("\t" +
				quoteIt (edge.sourceService.getName() + "_" + graphId) + " -> " +
				quoteIt (edge.targetService.getName() + "_" + graphId) +
				new String (edgeAttrs) + "\n");
		}
	    }
	}
	return new String (buf);
    }

    // a service node
    static StringBuffer serviceNode (MobyService service, HashSet nodes, int graphId) {
	StringBuffer buf = new StringBuffer();
	if (service == null)
	    return buf;
	String srvName = service.getName();
	if (! nodes.contains (srvName)) {
	    buf.append (quoteIt (srvName + "_" + graphId));
	    buf.append (" [label=\"");
	    buf.append (srvName);
	    buf.append ("\"]");
	    nodes.add (srvName);
	}
	return buf;
    }

    //
    static StringBuffer edgeAttributes (ServicesEdge edge) {
	StringBuffer edgeAttrs = new StringBuffer();
	if (edge.isWeakConnection()) {
	    appendAfterComma (edgeAttrs, "style=dotted");
	}
	switch (edge.getConnectionType()) {
	case ServicesEdge.SIMPLE_CONNECTION:
	    appendAfterComma (edgeAttrs, "arrowtail=none");
	    appendAfterComma (edgeAttrs, "arrowhead=open");
	    break;
	case ServicesEdge.HEAD_COLLECTION_CONNECTION:
	    appendAfterComma (edgeAttrs, "arrowtail=inv");
	    appendAfterComma (edgeAttrs, "arrowhead=open");
	    break;
	case ServicesEdge.TAIL_COLLECTION_CONNECTION:
	    appendAfterComma (edgeAttrs, "arrowtail=none");
	    appendAfterComma (edgeAttrs, "arrowhead=normal");
	    break;
	case ServicesEdge.BOTH_COLLECTIONS_CONNECTION:
	    appendAfterComma (edgeAttrs, "arrowtail=inv");
	    appendAfterComma (edgeAttrs, "arrowhead=normal");
	    break;
	}
	String connector = edge.getConnector();
	if (! connector.equals (""))
	    appendAfterComma (edgeAttrs, "label=\"" + connector + "\"");
	if (edgeAttrs.length() > 0) {
	    edgeAttrs.insert (0, " [");
	    edgeAttrs.append ("]");
	}
	return edgeAttrs;
    }

    //
    static StringBuffer dataTypeAttributes (String nodeLabel) {
	StringBuffer attrs = new StringBuffer();
	appendAfterComma (attrs, "label=\"" + nodeLabel + "\"");
	appendAfterComma (attrs, "shape=\"box\"");
	appendAfterComma (attrs, "fillcolor=\"skyblue\"");
	appendAfterComma (attrs, "style=\"filled\"");
	attrs.insert (0, " [");
	attrs.append ("]");
	return attrs;
    }

    /*************************************************************************
     * Creates a graph connecting 'dataTypes' using their ISA
     * relationship and showing also their HASA children.
     * <p>
     *
     * @param dataTypes represent nodes in the created graph
     * @param props are some properties that can influence how the
     * graph will look like; see the property names elswhere in this
     * API what properties are understood
     *
     * @return a string with all definitions as understood by 'dot'
     * program (from the graphviz package); this string can be saved
     * in a '.dot' file that can be passed to a dot program to produce
     * graphs in many available formats
     *
     *************************************************************************/
    static public String createDataTypesGraph (MobyDataType[] dataTypes,
					       Properties props) {

	StringBuffer buf = new StringBuffer();
	buf.append ("digraph MobyDataTypes {\n");
	buf.append ("\trankdir=" + props.getProperty (PROP_RANKDIR, "LR") + ";\n");
	buf.append ("\tedge [dir=back,arrowtail=empty];\n");

	String highlighted = props.getProperty (PROP_HIGHLIGHT);
	if (highlighted != null) {
	    for (int i = 0; i < dataTypes.length; i++) {
		if (highlighted.equals (dataTypes[i].getName())) {
		    buf.append ("\t");
		    buf.append (quoteIt (trName (Utils.pureName (highlighted))));
		    buf.append (" [fillcolor=");
		    buf.append (quoteIt (props.getProperty (PROP_HIGHLIGHT_COLOR, "cyan2")));
		    buf.append (",style=filled];\n");
		    break;
		}
	    }
	}

	String defaultURL = props.getProperty (PROP_IMAGEMAP);
	if (defaultURL != null) {
	    if (! "".equals (defaultURL)) {
		buf.append ("\tURL=\"");
		buf.append (defaultURL);
		buf.append ("\";\n");
	    }
	    for (int i = 0; i < dataTypes.length; i++) {
		String name = dataTypes[i].getName();
		if (name.equals (highlighted))
		    continue;   // no hyperlink for the highlighted one
		String hyperlink = dataTypes[i].getComment();
		if (hyperlink != null && ! "".equals (hyperlink.trim())) {
		    buf.append ("\t");
		    buf.append (quoteIt (trName (Utils.pureName (name))));
		    buf.append (" [URL=\"");
		    buf.append (hyperlink);
		    buf.append ("\"];\n");
		}
	    }
	}

	for (int d = 0; d < dataTypes.length; d++) {
	    MobyDataType type = dataTypes[d];
	    String name = Utils.pureName (type.getName());
	    String[] parents = type.getParentNames();
	    for (int i = 0; i < parents.length; i++) {
		buf.append ("\t");
		buf.append (quoteIt (trName (Utils.pureName (parents[i]))));
		buf.append (" -> ");
		buf.append (quoteIt (trName (name)));
		buf.append (";\n");
	    }
	    MobyRelationship[] children = type.getChildren();
	    if (children.length > 0) {
		String dummyName = trName (name) + "_HASA";
		buf.append ("\t" + quoteIt (trName (name)) + " -> " + quoteIt (dummyName));
		buf.append (" [style=dotted,dir=forward,arrowhead=none,arrowtail=ediamond];\n");
		StringBuffer hasaBuf = new StringBuffer();
		for (int i = 0; i < children.length; i++) {
		    String childName = Utils.pureName (children[i].getName());
		    String childType = Utils.pureName (children[i].getDataTypeName());
		    if (childType == null)
			childType = "n/a";  // should not happen I guess
		    hasaBuf.append (" | {" + childType + "|" + childName + "}");
		}
		buf.append ("\t" + quoteIt (dummyName) + " [shape=record, label=\"HAS[A]");
		buf.append (hasaBuf);
		buf.append ("\"];\n");
	    }
	}
	buf.append ("}\n");
	return new String (buf);
    }

    /*************************************************************************
     * Creates a graph connecting 'serviceTypes' using their ISA
     * relationship.  <p>
     *
     * @param serviceTypes represent nodes in the created graph
     * @param props are some properties that can influence how the
     * graph will look like; see the property names elswhere in this
     * API what properties are understood
     *
     * @return a string with all definitions as understood by 'dot'
     * program (from the graphviz package); this string can be saved
     * in a '.dot' file that can be passed to a dot program to produce
     * graphs in many available formats
     *
     *************************************************************************/
    static public String createServiceTypesGraph (MobyServiceType[] serviceTypes,
						  Properties props) {

	StringBuffer buf = new StringBuffer();
	buf.append ("digraph MobyServicesTypes {\n");
	buf.append ("\trankdir=" + props.getProperty (PROP_RANKDIR, "LR") + ";\n");
	buf.append ("\tedge [dir=back,arrowtail=empty];\n");
	for (int t = 0; t < serviceTypes.length; t++) {
	    MobyServiceType type = serviceTypes[t];
	    String name = Utils.pureName (type.getName());
	    String[] parents = type.getParentNames();
	    for (int i = 0; i < parents.length; i++) {
		buf.append ("\t");
		buf.append (quoteIt (trName (Utils.pureName (parents[i])).toLowerCase()));
		buf.append (" -> ");
		buf.append (quoteIt (trName (name).toLowerCase()));
		buf.append (";\n");
	    }
	}
	buf.append ("}\n");
	return new String (buf);
    }

    /*************************************************************************
     * Append 'value' to 'buf'. If it is not the first value there, it
     * prefix it by a comma.
     *************************************************************************/
    static void appendAfterComma (StringBuffer buf, String value) {
	if (buf.length() > 0)
	    buf.append (",");
	buf.append (value);
    }

    /*************************************************************************
     * Replaces dashes by underscores in 'name'. Because I found that
     * sometimes the dashes in the node names in Graphviz caused me
     * some problems (or was I mistaken?).
     *
     * @param name to be changed
     * @return changed 'name'
     *************************************************************************/
    public static String trName (String name) {
	return name.replace ('-', '_');
    }

    /*************************************************************************
     * Surround given text by quotes. This will prevent errors
     * encoutered by 'dot' when names have whitespaces or when they
     * are identical to the 'dot's keywords (such as 'edge').
     *
     * @param name to be quoted
     * @return changed 'name'
     *************************************************************************/
    public static String quoteIt (String name) {
	return "\"" + name + "\"";
    }

}
