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

package org.biomoby.client;

import java.util.Hashtable;
import java.util.Vector;

import org.biomoby.shared.MobyData;
import org.biomoby.shared.MobyDataType;
import org.biomoby.shared.MobyNamespace;
import org.biomoby.shared.MobyPrimaryDataSet;
import org.biomoby.shared.MobyPrimaryDataSimple;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.Utils;

/**
 * This class contains knowledge (an algorithm) how individual Moby
 * services could be connected together based on their input and
 * output data types.  <p>
 *
 * The algorithms is separated here because it can be used (I hope)
 * independently for various types of clients. It is used, for
 * example, by the clients creating graphs of all Moby services, but
 * can be used also by those real client exchanging data between
 * services.  <p>
 *
 * This class does not contact Moby registry - its methods get all
 * data from the caller (who usually uses {@link CentralImpl} class to
 * obtain the data from a Moby registry).  <p>
 *
 * @author <A HREF="mailto:senger@ebi.ac.uk">Martin Senger</A>
 * @version $Id: ServiceConnections.java,v 1.7 2005/09/22 16:07:09 senger Exp $
 */

public abstract class ServiceConnections {



    /*************************************************************************
     * Make the data types better searchable. This is a public method
     * because for optimalization purposes it is often better to make
     * the hashtable only once.
     *
     * @param dataTypes contains definitions of the data types
     *
     * @return a table where keys are names of the data types (type
     * String) and values are of type {@link
     * org.biomoby.shared.MobyDataType}
     *************************************************************************/
    public static Hashtable optimizedDataTypes (MobyDataType[] dataTypes) {
	Hashtable dataTypesTable = new Hashtable();
	for (int i = 0; i < dataTypes.length; i++) {
	    MobyDataType dataType = dataTypes[i];
	    dataTypesTable.put (Utils.pureName (dataType.getName()).toLowerCase(), dataType);
	}
	return dataTypesTable;
    }

    //
    public static DataServiceEdge[] findStartingEdges (MobyPrimaryDataSimple sourceData,
						       MobyDataType[] dataTypes,
						       MobyService[] services) {

	// make the data types better searchable
	Hashtable dataTypesTable = optimizedDataTypes (dataTypes);

	// here we are going to build the resulting edges
	Vector v = new Vector();

	String sourceDataTypeName = sourceData.getDataType().getName();
	MobyDataType sourceDataType =
	    (MobyDataType)dataTypesTable.get (Utils.pureName (sourceDataTypeName).toLowerCase());
	if (sourceDataType == null)
	    return new DataServiceEdge[] {};

	// find services having this type as an input
	for (int t = 0; t < services.length; t++) {
	    MobyService targetService = services[t];
	    MobyData[] serviceInputs = targetService.getPrimaryInputs();
	    // service without any inputs
	    if (serviceInputs == null || serviceInputs.length == 0)
		continue;

	    boolean tailCollection = false;
	    MobyPrimaryDataSimple serviceInput;
	    for (int j = 0; j < serviceInputs.length; j++) {
		if (serviceInputs[j] instanceof MobyPrimaryDataSet) {
		    MobyPrimaryDataSet collection = (MobyPrimaryDataSet)serviceInputs[j];
		    MobyPrimaryDataSimple[] elements = collection.getElements();
		    if (elements.length == 0)
			continue;   // ignoring empty collections
		    // I assume that all elements are of the same type
		    // - (which is not generally true :-(   TBD )
		    serviceInput = elements[0];
		    tailCollection = true;
		} else if (serviceInputs[j] instanceof MobyPrimaryDataSimple) {
		    serviceInput = (MobyPrimaryDataSimple)serviceInputs[j];
		} else {
		    // it is a programming bug if this happens
		    System.err.println ("Service " + targetService.getName() +
					": Input is represented by an unknown Java type " +
					serviceInputs[j].getClass().getName());
		    continue;
		}

		DataTypeConnector connector = compareTypes (sourceData, serviceInput, dataTypesTable);
		if (connector != null) {
		    DataServiceEdge edge = new DataServiceEdge (sourceDataType, targetService, connector.toString());
		    edge.setConnectionType (tailCollection ?
					    ServicesEdge.TAIL_COLLECTION_CONNECTION :
					    ServicesEdge.SIMPLE_CONNECTION);
		    v.addElement (edge);
		}
	    }
	}

	// pack and return all resulting edges
	DataServiceEdge[] results = new DataServiceEdge [v.size()];
	v.copyInto (results);
	return results;
    }


    //
    public static DataServiceEdge[] findEndingEdges (MobyPrimaryDataSimple targetData,
						     MobyDataType[] dataTypes,
						     MobyService[] services) {

	// make the data types better searchable
	Hashtable dataTypesTable = optimizedDataTypes (dataTypes);

	// here we are going to build the resulting edges
	Vector v = new Vector();

	String targetDataTypeName = targetData.getDataType().getName();
	MobyDataType targetDataType =
	    (MobyDataType)dataTypesTable.get (Utils.pureName (targetDataTypeName).toLowerCase());
	if (targetDataType == null)
	    return new DataServiceEdge[] {};

	// find services having this type as an output
	for (int s = 0; s < services.length; s++) {
	    MobyService sourceService = services[s];
	    MobyData[] serviceOutputs = sourceService.getPrimaryOutputs();
	    
	    // service without any output (any sense?)
	    if (serviceOutputs == null || serviceOutputs.length == 0)
		continue;

	    boolean headCollection = false;
	    MobyPrimaryDataSimple serviceOutput;
	    for (int j = 0; j < serviceOutputs.length; j++) {
		if (serviceOutputs[j] instanceof MobyPrimaryDataSet) {
		    MobyPrimaryDataSet collection = (MobyPrimaryDataSet)serviceOutputs[j];
		    MobyPrimaryDataSimple[] elements = collection.getElements();
		    if (elements.length == 0)
			continue;   // ignoring empty collections
		    // I assume that all elements are of the same type
		    // - (which is not generally true :-(   TBD )
		    serviceOutput = elements[0];
		    headCollection = true;
		} else if (serviceOutputs[j] instanceof MobyPrimaryDataSimple) {
		    serviceOutput = (MobyPrimaryDataSimple)serviceOutputs[j];
		} else {
		    // it is a programming bug if this happens
		    System.err.println ("Service " + sourceService.getName() +
					": Output is represented by an unknown Java type " +
					serviceOutputs[j].getClass().getName());
		    continue;
		}

		DataTypeConnector connector = compareTypes (serviceOutput, targetData, dataTypesTable);
		if (connector != null) {
		    DataServiceEdge edge = new DataServiceEdge (sourceService, targetDataType, connector.toString());
		    edge.setConnectionType (headCollection ?
					    ServicesEdge.HEAD_COLLECTION_CONNECTION :
					    ServicesEdge.SIMPLE_CONNECTION);
		    v.addElement (edge);
		}
	    }
	}

	// pack and return all resulting edges
	DataServiceEdge[] results = new DataServiceEdge [v.size()];
	v.copyInto (results);
	return results;
    }


    /*************************************************************************
     * Creates all (allowed) connections between given 'services'
     * based on the given 'dataTypes'. The returned array of found
     * connections contains elements of service pairs and each pair
     * also expresses some property of that connection (e.g. if a
     * connection is purely based on a simple data type, or if the
     * source or destinantion service produces or expects a collection
     * of simple data types).
     * <p>
     *
     * It reports warning on the STDERR if it finds an unknown data
     * type used by any service. This would indicate an error in the
     * Moby registry (or in this code :-)).
     * <p>
     *
     * @param dataTypes contains definitions of the data types
     *
     * @param services contains definitions of the Moby services
     *
     * @return a list of all allowed pairs of services. Some 'pairs'
     * can be crippled in a way that they contain only one service -
     * the other one is missing because this service either does not
     * have any input, or does not have any output, or has an output
     * of a type that is not consumed by any other service in the
     * given list of 'services'
     *
     *************************************************************************/
    public static ServicesEdge[] build (MobyDataType[] dataTypes,
					MobyService[] services) {

	// make the data types better searchable
	Hashtable dataTypesTable = optimizedDataTypes (dataTypes);

	// here we are going to build a list of resulting edges
	Vector v = new Vector();

	for (int s = 0; s < services.length; s++) {
	    MobyService sourceService = services[s];;
	    String name = sourceService.getName();
	    MobyData[] outputs = sourceService.getPrimaryOutputs();
	    
	    // service without any output (any sense?)
	    if (outputs == null || outputs.length == 0) {
		v.addElement (new ServicesEdge (sourceService, ServicesEdge.NO_OUTPUT));
		continue;
	    }
	    
	    // go throught all possible outputs...
	    for (int i = 0; i < outputs.length; i++) {

		// we need to remember all connections (edges) created for this output
		// because only after we finish with it we will find if the connections
		// are 'weak' or not (as described in ServicesEdge.isWeakConnection)

		boolean headCollection = false;
		MobyPrimaryDataSimple output;
		if (outputs[i] instanceof MobyPrimaryDataSet) {
		    MobyPrimaryDataSet collection = (MobyPrimaryDataSet)outputs[i];
		    MobyPrimaryDataSimple[] elements = collection.getElements();
		    if (elements.length == 0)
			continue;   // ignoring empty collections
		    // I assume that all elements are of the same type - is this correct? (TBD)
		    output = elements[0];
		    headCollection = true;
		} else if (outputs[i] instanceof MobyPrimaryDataSimple) {
		    output = (MobyPrimaryDataSimple)outputs[i];
		} else {
		    // it is a programming bug if this happens
		    System.err.println ("Service " + name +
					": Output is represented by an unknown Java type " +
					outputs[i].getClass().getName());
		    continue;
		}

		// ...find the data type of this output		
		String dataTypeName = output.getDataType().getName();
		MobyDataType dataType =
		    (MobyDataType)dataTypesTable.get (Utils.pureName (dataTypeName).toLowerCase());
		if (dataType == null) {
		    // this means that there is something wrong with the Moby central registration
		    System.err.println ("Service " + name +
					" has an unknown output data type '" +
					Utils.pureName (dataTypeName) + "'");
		    System.err.println ("\tLSID: " + dataTypeName);
		    continue;
		}

		// ...and look for services having this type as an input
		boolean found = false;
		for (int t = 0; t < services.length; t++) {
		    MobyService targetService = services[t];
		    MobyData[] targetInputs = targetService.getPrimaryInputs();

		    boolean tailCollection = false;
		    MobyPrimaryDataSimple input;
		    for (int j = 0; j < targetInputs.length; j++) {
			if (targetInputs[j] instanceof MobyPrimaryDataSet) {
			    MobyPrimaryDataSet collection = (MobyPrimaryDataSet)targetInputs[j];
			    MobyPrimaryDataSimple[] elements = collection.getElements();
			    if (elements.length == 0)
				continue;   // ignoring empty collections
			    // I assume that all elements are of the same type - is this correct? (TBD)
			    input = elements[0];
			    tailCollection = true;
			} else if (targetInputs[j] instanceof MobyPrimaryDataSimple) {
			    input = (MobyPrimaryDataSimple)targetInputs[j];
			} else {
			    // it is a programming bug if this happens
			    System.err.println ("Service " + name +
						": Input is represented by an unknown Java type " +
						targetInputs[j].getClass().getName());
			    continue;
			}

			DataTypeConnector connector = compareTypes (output, input, dataTypesTable);
			if (connector != null) {
			    ServicesEdge edge = new ServicesEdge (sourceService, targetService, connector.toString());
			    if (headCollection && tailCollection)
				edge.setConnectionType (ServicesEdge.BOTH_COLLECTIONS_CONNECTION);
			    else if (headCollection)
				edge.setConnectionType (ServicesEdge.HEAD_COLLECTION_CONNECTION);
			    else if (tailCollection)
				edge.setConnectionType (ServicesEdge.TAIL_COLLECTION_CONNECTION);
			    else
				edge.setConnectionType (ServicesEdge.SIMPLE_CONNECTION);
			    v.addElement (edge);
			    found = true;
			}
		    }
		}
		if (found) { 
		    // TBD: here investigate 'weakness'
		} else {
		    // no corresponding service was found
		    v.addElement (new ServicesEdge (sourceService, createInputConnector (sourceService)));
		    v.addElement (new ServicesEdge (sourceService, createOutputConnector (sourceService)));
		}
	    }
	}

	// pack and return all resulting edges
	ServicesEdge[] results = new ServicesEdge [v.size()];
	v.copyInto (results);
	return results;
    }

    /*************************************************************************
     * Try to find a data-type match between the 'output' and
     * 'input'. All data types are in 'dataTypes' (because we need
     * them to find parents of the 'output').
     *
     * It returns a string illustrating found match, or null if no
     * match was found. The returned string (if not null) contains
     * either data type name, or namespace and data type name
     * separated by ServicesEdge.NS_DIVIDER, such as:
     *    global_keywords/string
     *    ncbi_gi/genericsequence
     *    virtualsequence
     *    object
     *************************************************************************/
    static DataTypeConnector compareTypes (MobyPrimaryDataSimple output,
					   MobyPrimaryDataSimple input,
					   Hashtable dataTypes) {

	DataTypeConnector connector = new DataTypeConnector();
	connector.inputDataType = input.getDataType();

	// first try to find if there is a matching namespace between
	// 'output' and 'input' (a matching is found also when there
	// are no namespaces on either side)
	String matchingNamespace = null;
	MobyNamespace[] outNamespaces = output.getNamespaces();
	MobyNamespace[] inpNamespaces = input.getNamespaces();

	if (outNamespaces.length == 0 && inpNamespaces.length > 0)
	    return null;
	if (outNamespaces.length > 0 && inpNamespaces.length > 0) {
	    boolean found = false;
	    NAMESPACES:
	    for (int i = 0; i < outNamespaces.length; i++) {
		for (int j = 0; j < inpNamespaces.length; j++) {
		    String nsName = outNamespaces[i].getName();
		    if (nsName.equals (inpNamespaces[j].getName())) {
			found = true;
			matchingNamespace = nsName;
			break NAMESPACES;
		    }
		}
	    }
	    if (! found) {
		return null;
	    }
	}

	// now try to find if input and output data types match: first
	// try a direct match between output and input types, then use
	// the inheritance lader and match when the input type is a
	// parent of the output type (in other words a service should
	// be able to accept data that are more specialized than the
	// service claim or can use)

	MobyDataType outputType =
	    (MobyDataType)dataTypes.get (Utils.pureName (output.getDataType().getName()).toLowerCase());
	if (outputType == null) // strange...
	    return null;
	String outputName = Utils.pureName (outputType.getName());
	String inputName = Utils.pureName (input.getDataType().getName());

	if (outputName.equals (inputName))
	    connector.outputDataType = outputType;
	else if (findMatchInParents (outputType.getParentNames(), inputName, dataTypes))
	    connector.outputDataType = outputType;
	else
	    return null;

	// now we have a connector, let's add there there namespace (if any)
	if (matchingNamespace != null)
	    connector.matchingNamespaceName = matchingNamespace;

	return connector;
    }

    /*************************************************************************
     * It's part of compareTypes() method but it is separated here
     * because it calls itself recursively to investigate all parents.
     *************************************************************************/
    static boolean findMatchInParents (String[] parents, String inputName,
				       Hashtable dataTypes) {

	if (parents == null || parents.length == 0)
	    return false;

	for (int i = 0; i < parents.length; i++) {
	    if (inputName.equals (parents[i]))
		return true;
	    MobyDataType outputType =
		(MobyDataType)dataTypes.get (Utils.pureName (parents[i]).toLowerCase());
	    if (outputType == null)   // strange?
		return false;
	    if (findMatchInParents (outputType.getParentNames(), inputName, dataTypes))
		return true;
	}
	return false;
    }

    /*************************************************************************
     *
     *************************************************************************/
    static String createOutputConnector (MobyService service) {
	return "TBD-O";
    }

    /*************************************************************************
     *
     *************************************************************************/
    static String createInputConnector (MobyService service) {
	return "TBD-I";
    }
}
