// ServicesTree.java
//
// Created: November 2005
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.service.dashboard;

import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.MobyServiceType;
import org.biomoby.shared.MobyDataType;
import org.biomoby.shared.MobyData;
import org.biomoby.shared.MobyPrimaryData;

import org.apache.commons.lang.StringUtils;

import javax.swing.AbstractAction;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;

import java.awt.event.ActionEvent;

import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Enumeration;
import java.util.Vector;

/**
 * A component showing and manipulating a tree of services
 * registered by a Biomoby registry. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: ServicesTree.java,v 1.15 2008/03/02 12:45:26 senger Exp $
 */

public class ServicesTree
    extends CommonTree {

    private static org.apache.commons.logging.Log log =
       org.apache.commons.logging.LogFactory.getLog (ServicesTree.class);

    // action commands for popup menu items
    protected final static String AC_TSORT = "ac-tsort";
    protected final static String AC_ISORT = "ac-isort";
    protected final static String AC_OSORT = "ac-osort";

    // remembered from constructor
    RegistryModel registryModel;
    CommonConsole console;

    protected final static String SERVICES_ACCESS_ERROR =
    "An error happened when accessing a list of available services.\n\n"
    + ACCESS_ERROR_INTRO;

    /*********************************************************************
     * Constructor.
     ********************************************************************/
    public ServicesTree (RegistryModel registryModel,
			 CommonConsole console) {
	super ("Services");
	this.registryModel = registryModel;
	this.console = console;
	createPopups ("Services Menu");
	setLeafIcon (sLeafIcon);
    }

    /*********************************************************************
     *
     ********************************************************************/
    protected void createPopups (String title) {
	super.createPopups (title);
	addSortingItems();
	popup.add
	    (createMenuItem (new AbstractAction ("Sort by service types") {
		    public void actionPerformed (ActionEvent e) {
			update (lastSorted = SORTED_BY_SERVICE_TYPE, null);
		    }
		}, AC_TSORT, smallTIcon, smallTIconDis));
	popup.add
	    (createMenuItem (new AbstractAction ("Sort by input data types") {
		    public void actionPerformed (ActionEvent e) {
			update (lastSorted = SORTED_BY_INPUT_DATA, null);
		    }
		}, AC_ISORT, smallIIcon, smallIIconDis));
	popup.add
	    (createMenuItem (new AbstractAction ("Sort by output data types") {
		    public void actionPerformed (ActionEvent e) {
			update (lastSorted = SORTED_BY_OUTPUT_DATA, null);
		    }
		}, AC_OSORT, smallOIcon, smallOIconDis));
    }

    /*********************************************************************
     * Get data (usually from a registry model, but if not null, take
     * them from 'newData') and update the tree.
     ********************************************************************/
    public void update (int howSorted, Object newData) {
	if (howSorted < 0) howSorted = lastSorted;
	lastSorted = howSorted;
    
	setEnabledPopup (false);
	final Object source = this;
	final int sorted = howSorted;
	final MobyService[] newServices =
	    (newData == null ? null : (MobyService[])newData);

	final SwingWorker worker = new SwingWorker() {
	    MobyException updateException = null;
	    MobyService[] services = null;
	    MobyServiceType[] serviceTypes = null;
	    MobyDataType[] dataTypes = null;
		public Object construct() {
		    try {
		    	if (log.isDebugEnabled())
		    		log.debug ("Tree " + treeId + " update request. Sorted: " +
					   sorted + ", Data: " + newServices);
			// get services (unless you already have them)
			if (newServices == null) {
			    services = registryModel.getServices (source);
			} else {
			    services = newServices;
			}

			// ...and perhaps add some other types
			if (sorted == SORTED_BY_SERVICE_TYPE)
			    serviceTypes = registryModel.getServiceTypes (source);
			else if ( sorted == SORTED_BY_INPUT_DATA ||
				  sorted == SORTED_BY_OUTPUT_DATA )
			    dataTypes = registryModel.getDataTypes (source);

		    } catch (MobyException e) {
			updateException = e;
		    }
		    return services;  // not used here
		}

		// runs on the event-dispatching thread.
		public void finished() {
		    if (updateException != null)
			error (SERVICES_ACCESS_ERROR, updateException);
		    if (services != null) {
			if (sorted == SORTED_BY_AUTHORITY)
			    onUpdateServicesTreeByAuth (services);
			else if (sorted == SORTED_BY_SERVICE_TYPE && serviceTypes != null)
			    onUpdateServicesTreeByType (services, serviceTypes);
			else if (sorted == SORTED_BY_INPUT_DATA && dataTypes != null)
			    onUpdateServicesTreeByData (services, dataTypes,
							SORTED_BY_INPUT_DATA);
 			else if (sorted == SORTED_BY_OUTPUT_DATA && dataTypes != null)
 			    onUpdateServicesTreeByData (services,dataTypes,
							SORTED_BY_OUTPUT_DATA);
			else
			    onUpdateServicesTree (services);
			if (services.length > 0)
			    setEnabledPopup (true);
		    }
		}
	    };
	worker.start(); 
    }

    void onUpdateServicesTree (MobyService[] theServices) {
    	MobyService[] services = copy (theServices);
	    java.util.Arrays.sort (services);
    	
	DefaultTreeModel tModel = (DefaultTreeModel)getModel();
	DefaultMutableTreeNode root = (DefaultMutableTreeNode)tModel.getRoot();
	root.removeAllChildren();   // does not harm if no children exist
	Map<String,DefaultMutableTreeNode> nodes =
	    new HashMap<String,DefaultMutableTreeNode> (services.length);
	for (int i = 0; i < services.length; i++) {
	    String thisName = services[i].getUniqueName();
	    DefaultMutableTreeNode thisNode = nodes.get (thisName);
	    if (thisNode == null) {
		thisNode = new DefaultMutableTreeNode (new CommonNode (services[i].getName(), thisName, CommonNode.NODE_SERVICE));
		nodes.put (thisName, thisNode);
	    }
	    String startingWith = thisName.substring (0, 1).toUpperCase();
	    DefaultMutableTreeNode parentNode = nodes.get (startingWith);
	    if (parentNode == null) {
		parentNode = new DefaultMutableTreeNode (new CommonNode (startingWith));
		nodes.put (startingWith, parentNode);
		root.add (parentNode);
	    }
	    parentNode.add (thisNode);
	}
	tModel.reload();
    }


    void onUpdateServicesTreeByAuth (MobyService[] theServices) {
    	MobyService[] services = copy (theServices);
	java.util.Arrays.sort (services, MobyService.getAuthorityComparator());
    	
    	DefaultTreeModel tModel = (DefaultTreeModel)getModel();
	DefaultMutableTreeNode root = (DefaultMutableTreeNode)tModel.getRoot();
	root.removeAllChildren();   // does not harm if no children exist
	Map<String,DefaultMutableTreeNode> nodes =
	    new HashMap<String,DefaultMutableTreeNode> (services.length);
	for (int i = 0; i < services.length; i++) {
	    String thisName = services[i].getUniqueName();
	    DefaultMutableTreeNode thisNode = nodes.get (thisName);
	    if (thisNode == null) {
		thisNode = new DefaultMutableTreeNode (new CommonNode (services[i].getName(), thisName, CommonNode.NODE_SERVICE));
		nodes.put (thisName, thisNode);
	    }
	    String authority = services[i].getAuthority();
	    if (StringUtils.isBlank (authority))
		authority = "<unknown>";
	    DefaultMutableTreeNode authNode = nodes.get (authority);
	    if (authNode == null) {
		authNode = new DefaultMutableTreeNode (new CommonNode (authority, CommonNode.NODE_AUTHORITY));
		nodes.put (authority, authNode);
		root.add (authNode);
	    }
	    authNode.add (thisNode);
	}
	tModel.reload();
    }

    void onUpdateServicesTreeByType (MobyService[] theServices,
				     MobyServiceType[] serviceTypes) {
    	MobyService[] services = copy (theServices);
	    java.util.Arrays.sort (services);
    	
	DefaultTreeModel tModel = (DefaultTreeModel)getModel();
	DefaultMutableTreeNode root = (DefaultMutableTreeNode)tModel.getRoot();
	root.removeAllChildren();   // does not harm if no children exist
	Map<String,DefaultMutableTreeNode> nodes =
	    new HashMap<String,DefaultMutableTreeNode> (serviceTypes.length);

	// add service types
	for (int i = 0; i < serviceTypes.length; i++) {
	    String thisName = serviceTypes[i].getName();
	    DefaultMutableTreeNode thisNode = nodes.get (thisName);
	    if (thisNode == null) {
		thisNode = new DefaultMutableTreeNode (new CommonNode (thisName, CommonNode.NODE_SERVICE_TYPE));
		nodes.put (thisName, thisNode);
	    }
	    String[] parents = serviceTypes[i].getParentNames();
	    if (parents.length == 0) {   // we have a top-level object
		root.add (thisNode);
	    } else {
		String parentName = parents[0];
		DefaultMutableTreeNode parentNode = nodes.get (parentName);
		if (parentNode == null) {
		    parentNode = new DefaultMutableTreeNode (new CommonNode (parentName, CommonNode.NODE_SERVICE_TYPE));
		    nodes.put (parentName, parentNode);
		}
		parentNode.add (thisNode);
	    }
	}

	// add services below their service types
	for (int i = 0; i < services.length; i++) {
	    String type = services[i].getType();
	    if (StringUtils.isBlank (type))
		continue;
	    String thisName = services[i].getUniqueName();
	    DefaultMutableTreeNode thisNode = 
		new DefaultMutableTreeNode (new CommonNode (services[i].getName(), thisName, CommonNode.NODE_SERVICE));
	    DefaultMutableTreeNode typeNode = nodes.get (type);
	    if (typeNode == null) {
		typeNode = new DefaultMutableTreeNode (new CommonNode (type, CommonNode.NODE_SERVICE_TYPE));
		nodes.put (type, typeNode);
		root.add (typeNode);
	    }
	    typeNode.add (thisNode);
	}

	// remove service types (leaves) that do not have any services
	Vector<DefaultMutableTreeNode> leaves = new Vector<DefaultMutableTreeNode>();
	for (Enumeration en = root.depthFirstEnumeration(); en.hasMoreElements(); ) {
	    DefaultMutableTreeNode node = (DefaultMutableTreeNode)en.nextElement();
	    if ( node.isLeaf() && 
		 ((CommonNode)node.getUserObject()).getType() == CommonNode.NODE_SERVICE_TYPE )
		leaves.addElement (node);
	}
	for (Enumeration<DefaultMutableTreeNode> en = leaves.elements(); en.hasMoreElements(); ) {
	    DefaultMutableTreeNode node = en.nextElement();
	    node.removeFromParent();
	}

	tModel.reload();
    }

    void onUpdateServicesTreeByData (MobyService[] theServices,
				     MobyDataType[] dataTypes,
				     int whatData) {
    	MobyService[] services = copy (theServices);
	    java.util.Arrays.sort (services);
    	
	DefaultTreeModel tModel = (DefaultTreeModel)getModel();
	DefaultMutableTreeNode root = (DefaultMutableTreeNode)tModel.getRoot();
	root.removeAllChildren();   // does not harm if no children exist
	Map<String,DefaultMutableTreeNode> nodes =
	    new HashMap<String,DefaultMutableTreeNode> (dataTypes.length);

	// add data types
	for (int i = 0; i < dataTypes.length; i++) {
	    String thisName = dataTypes[i].getName();
	    DefaultMutableTreeNode thisNode = nodes.get (thisName);
	    if (thisNode == null) {
		thisNode = new DefaultMutableTreeNode (new CommonNode (thisName, CommonNode.NODE_DATA_TYPE));
		nodes.put (thisName, thisNode);
	    }
	    String[] parents = dataTypes[i].getParentNames();
	    if (parents.length == 0) {   // we have a top-level object
		root.add (thisNode);
	    } else {
		String parentName = parents[0];
		DefaultMutableTreeNode parentNode = nodes.get (parentName);
		if (parentNode == null) {
		    parentNode = new DefaultMutableTreeNode (new CommonNode (parentName, CommonNode.NODE_DATA_TYPE));
		    nodes.put (parentName, parentNode);
		}
		parentNode.add (thisNode);
	    }
	}

	// add services below their data types
	for (int i = 0; i < services.length; i++) {
	    MobyData[] data;
	    if (whatData == SORTED_BY_INPUT_DATA)
		data = services[i].getPrimaryInputs();
	    else
		data = services[i].getPrimaryOutputs();
	    if (data == null || data.length == 0)
		continue;
	    String thisName = services[i].getUniqueName();
	    DefaultMutableTreeNode thisNode = 
		new DefaultMutableTreeNode (new CommonNode (services[i].getName(), thisName, CommonNode.NODE_SERVICE));
	    for (int j = 0; j < data.length; j++) {
		MobyPrimaryData datum = (MobyPrimaryData)data[j];
		MobyDataType dataType = datum.getDataType();
// 		if (data[j] instanceof MobyPrimaryDataSimple)
// 		    dataType = ((MobyPrimaryDataSimple)data[j]).getDataType();
// 		else if (data[j] instanceof MobyPrimaryDataSet)
// 		    dataType = ((MobyPrimaryDataSet)data[j]).getDataType();
		if (dataType == null) continue;
		String dataName = dataType.getName();
		DefaultMutableTreeNode dataNode = nodes.get (dataName);
		if (dataNode == null) {
		    dataNode = new DefaultMutableTreeNode (new CommonNode (dataName, CommonNode.NODE_DATA_TYPE));
		    nodes.put (dataName, dataNode);
		    root.add (dataNode);
		}
		dataNode.add (thisNode);
	    }
	}

	// remove data types (leaves) that do not have any services
	Vector<DefaultMutableTreeNode> leaves = new Vector<DefaultMutableTreeNode>();
	for (Enumeration en = root.depthFirstEnumeration(); en.hasMoreElements(); ) {
	    DefaultMutableTreeNode node = (DefaultMutableTreeNode)en.nextElement();
	    if ( node.isLeaf() &&
		 ((CommonNode)node.getUserObject()).getType() == CommonNode.NODE_DATA_TYPE )
		leaves.addElement (node);
	}
	for (Enumeration<DefaultMutableTreeNode> en = leaves.elements(); en.hasMoreElements(); ) {
	    DefaultMutableTreeNode node = en.nextElement();
	    node.removeFromParent();
	}

	tModel.reload();
    }

    /*********************************************************************
     * Make a private copy (of pointers) that will be used for sorting...
     ********************************************************************/
    private MobyService[] copy (MobyService[] s) {
        synchronized (s) {
        	MobyService[] result = new MobyService [s.length];
            System.arraycopy (s, 0, result, 0, s.length);
            return result;
        }
    }

    /*********************************************************************
     * Reload the tree from the Biomoby registry, ignoring (and
     * updating) cache.
     ********************************************************************/
    protected void reload() {
	if (lastSorted > SORTED_UNUSUAL)
	    lastSorted = SORTED_BY_NAME;
	update (lastSorted, null);
    }

    /*********************************************************************
     * Search underlying objects and highlight nodes corresponding to
     * the found objects.
     ********************************************************************/
    protected void search (String searchText) {
	final String regex = searchText;
	final SwingWorker worker = new SwingWorker() {
		Set<String> found = new HashSet<String>();
		public Object construct() {
		    try {
			if (StringUtils.isNotBlank (regex))
			    found = registryModel.findInServices (regex);
		    } catch (MobyException e) {
			error (SERVICES_ACCESS_ERROR, e);
		    }
		    return found;  // not used here
		}

		// runs on the event-dispatching thread.
		public void finished() {
		    if (found != null)
			highlightAndJumpTo (found);
		}
	    };
	worker.start(); 
    }

    /*********************************************************************
     * Get selected item from a registry model and print it to a
     * console window. Note that the 'node' can be null (if nothing is
     * selected).
     ********************************************************************/
    protected void selected (DefaultMutableTreeNode node) {
	if (node == null) return;
	final CommonNode nodeObject = (CommonNode)node.getUserObject();
	final SwingWorker worker = new SwingWorker() {
		MobyService service;
		public Object construct() {
		    try {
			service = registryModel.getService (nodeObject.getValue());
		    } catch (MobyException e) {
			error (SERVICES_ACCESS_ERROR, e);
		    }
		    return service;  // not used here
		}

		// runs on the event-dispatching thread.
		public void finished() {
		    if (service != null) {
			propertyChannel.fire (DP_S_SELECTED, service);
			if (console != null)
			    console.setText (service.toString());
		    }
		}
	    };
	worker.start(); 
    }

}
