package ca.ucalgary.seahawk.gui;

import ca.ucalgary.seahawk.services.MobyClient;
import ca.ucalgary.seahawk.util.*;

import org.biomoby.shared.*;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.MobyTags;
import org.biomoby.client.CentralImpl;
import org.biomoby.client.MobyRequest;
import org.biomoby.client.MobyRequestEventHandler;

import org.w3c.dom.*;

import java.awt.event.*;
import java.awt.*;
import java.io.*;
import java.lang.ref.WeakReference;
import java.text.Collator;
import java.net.URL;
import java.util.*;
import javax.swing.*;
import javax.swing.event.PopupMenuListener;
import javax.swing.event.PopupMenuEvent;

/**
 * Implementation of the actually popup menu that lists services available, and invokes them.
 */

public class MobyServicesGUI implements ActionListener, Comparator<MobyService>, PopupMenuListener, ServiceChoiceListener{
    public final static int MAX_ID_LEN = 30;
    public final static String SERVICE_SUBMENU_NAME = "seahawkPopupSubMenuName";

    // After this many, subdivide the services for an object into sublists based of service ontology
    public final static int MAX_SERVICES_PER_SUBMENU = 12;  
    public final static int MAX_SERVICE_DESC_LEN = 50;
    public final static String CLIPBOARD_CMD = "clipboard";
    public final static String SERVICESEARCH_CMD = "service_search";
    public final static String SEARCH_LABEL = "Find service by keyword...";
    public final static String CLIPBOARD_LABEL = "Add to clipboard";

    /** Always spring MOBY response in a new window if this modifier is present for a click */
    public final static int USE_DEFAULT_HANDLER_MASK  = ActionEvent.SHIFT_MASK;
    public final static int USE_DEFAULT_SECONDARIES_MASK  = ActionEvent.CTRL_MASK;

    Collator textCollator;
    MobySecondaryInputGUI secondaryGUI = null;
    Frame rootFrame = null;
    MobyRequestEventHandler mobyRequestEventHandler = null;
    JMenuItem waitItem = null;
    MobyContentClipboard clipboard = null;
    MobyRequest mobyRequest;
    MobyClient mobyClient;
    JPopupMenu lastPopup;

    // The meat of the class, the list of moby objects with their associated services
    MobyDataServiceAssocInstance[] ms;

    MobyDataSecondaryInstance[] secondaryInputInstances = null;
    MobyPrimaryData[] primaryInput = null;
    int maxDataDesc = 22;
    // tracks nested submenus to root submenus of choices popup
    private Map<JMenu, JMenu> service2submenu; 
    private Map<JMenuItem, JMenu> search2submenu; 
    private Map<JComponent,Integer> submenu2msIndex;
    private Map<JMenu,JMenuItem> submenu2waitItem;
    // for storing callbacks to other than the default response handler
    private Map<Integer,WeakReference<MobyRequestEventHandler>> specificHandlers; 
    private static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(MobyServicesGUI.class);
    MobyContentClipboard clip = null;

    public MobyServicesGUI() throws Exception{
	mobyClient = new MobyClient();
        mobyClient.setRequiredServiceLevel(MobyService.ALIVE);

	mobyRequest = new MobyRequest(mobyClient.getMobyCentralImpl());
	waitItem = new JMenuItem("Building data list, please wait...");
	textCollator = Collator.getInstance();
	ms = new MobyDataServiceAssocInstance[0];
	submenu2msIndex = new HashMap<JComponent,Integer>();
	submenu2waitItem = new HashMap<JMenu,JMenuItem>();
	service2submenu = new HashMap<JMenu,JMenu>();
	search2submenu = new HashMap<JMenuItem,JMenu>();
	specificHandlers = new HashMap<Integer,WeakReference<MobyRequestEventHandler>>();
    }

    public CentralImpl getMobyCentralImpl(){
	return mobyClient.getMobyCentralImpl();
    }

    public MobyClient getMobyClient(){
	return mobyClient;
    }

    public void setClipboard(MobyContentClipboard clip){
	clipboard = clip;
    }

    public void actionPerformed(ActionEvent e){
	if(e.getSource() == secondaryGUI){
	    // callback that populated the secondary data after a setupService call.  Now we can run...
	    removePopupOptions(e.getSource());
	    // The id field of the event holds the hash code for the handler we should use
	    setupServiceSecondaryData(getHandlerByHashCode(e.getActionCommand()));
	}
	// MOBY command looks like "MOBY:..."	
	else if(e.getActionCommand().startsWith("MOBY")){
	    StringTokenizer st = new StringTokenizer(e.getActionCommand(), ":", false);
	    st.nextToken(); //MOBY
	    int which_data = Integer.parseInt(st.nextToken());
	    String service = st.nextToken();
	    // Special case
	    if(service.equals(CLIPBOARD_CMD)){
		synchronized(ms){
		    if(ms[which_data] == null){
			logger.warn("Ignoring clipboard action in " + getClass().getName() +
				    ", the data to add is currently null (index "+which_data+")"+
				    ", from source " + e.getSource());
			removePopupOptions(e.getSource());
			return;
		    }
		    if(clipboard == null){
			logger.warn("Ignoring clipboard action in " + getClass().getName() +
				    ", the clipboard is currently null:\n"+ms[which_data]);
			removePopupOptions(e.getSource());
			return;
		    }
		    clipboard.addCollectionData(ms[which_data]);
		    removePopupOptions(e.getSource());
		}
		return;
	    }
	    if(service.equals(SERVICESEARCH_CMD)){
		synchronized(ms){
		    if(ms[which_data] == null){
			logger.warn("Ignoring searvice search in " + getClass().getName() +
				    ", the data to add is currently null (index "+which_data+")"+
				    ", from source " + e.getSource());
			removePopupOptions(e.getSource());
			return;
		    }
		    setupSearchData(search2submenu.get((JMenuItem) e.getSource()), ms[which_data]);
		    removePopupOptions(e.getSource());
		}
		return;   
	    }
	    // Otherwise call a remote service
	    int which_service = Integer.parseInt(service);

	    MobyRequestEventHandler handler = mobyRequestEventHandler;  // use the default handler
	    // Was there a specific handler given (as an object hash code)?
	    // Also, make sure the user did hold down shift, because this means 
	    // the default handler should be used anyway...
	    if((e.getModifiers() & USE_DEFAULT_HANDLER_MASK) == 0 && 
	       st.hasMoreTokens()){
		handler = getHandlerByHashCode(Integer.parseInt(st.nextToken()));
	    }

	    synchronized(ms){
		setupService(ms[which_data].getServices()[which_service], 
			     ms[which_data], 
			     handler.hashCode(),
			     (e.getModifiers() & USE_DEFAULT_SECONDARIES_MASK) != 0);
	    }
	}
	else{
	    logger.warn("Source of ActionEvent was unrecognized: " + e.getSource());
	}
    }

    private MobyRequestEventHandler getHandlerByHashCode(String hashCode){
	try{
	    return getHandlerByHashCode(Integer.parseInt(hashCode));
	}
	catch(Exception e){
	    logger.error("Exception: Warning: Could not create hash code integer " +
			"from string " + hashCode + ": " +e); 
	    return mobyRequestEventHandler;
	}
    }

    private MobyRequestEventHandler getHandlerByHashCode(int hashCode){
	// Did you ask for the default?
	if(hashCode == mobyRequestEventHandler.hashCode()){
	    return mobyRequestEventHandler;
	}

	if(!specificHandlers.containsKey(hashCode)){
	    logger.warn("Warning: The MOBY event handler corresponding to hash code " + hashCode + 
			"cannot be found, using default handler instead.");
	    return mobyRequestEventHandler;
	}

	WeakReference<MobyRequestEventHandler> ref = specificHandlers.get(hashCode);
	MobyRequestEventHandler handler = ref.get();
	if(handler == null){
	    // The handler was destroyed (e.g. MobyContentPane closed) before we got the response
	    logger.warn("Notice: The MOBY event handler corresponding to hash code " + hashCode + 
			       "was deleted before the response was ready, using default handler instead.");
	    return mobyRequestEventHandler;
	}
	return handler;
    }

    /**
     * Called when the 'Execute Service' button in the secondary input dialog in pushed.
     * Section synchronized so simultaneous requests don't get their input data mixed up
     */
    public void setupServiceSecondaryData(MobyRequestEventHandler handler){
	MobyDataInstance[] castInputs = new MobyDataInstance[primaryInput.length];
	MobyContentInstance mci = null;
	for(int i = 0; i < primaryInput.length; i++){
	    if(!(primaryInput[i] instanceof MobyDataInstance)){
		logger.warn("Warning: setupServiceSecondaryData was called before the primary input" +
			    " for the service was instantiated, ignoring the call.  Object found was" +
			    " of class "+primaryInput[i].getClass().getName());
		return;
	    }
	    // Is one of the params a 'for each'
	    if(primaryInput[i] instanceof MobyContentCreator){
		mci = ((MobyContentCreator) primaryInput[i]).getAllContents(primaryInput[i].getName());
	    }
	    else{
		castInputs[i] = (MobyDataInstance) primaryInput[i];
	    }
	}
	// Cross product of single value primary params with param that is a content creator array
	if(mci != null){
	    for(MobyDataJob job: mci.values()){
		for(int i = 0; i < castInputs.length; i++){
		    if(castInputs[i] != null){
			job.put(castInputs[i].getName(), castInputs[i]);
		    }
		}
	    }
	}

        // code from mobyRequest.setInput() to executeService() should always be synced to avoid input mixup between requests
        synchronized(mobyRequest){
	    try{	    
	        if(mci != null){
		    mobyRequest.setInput(mci);
		}
		else{
		    mobyRequest.setInput(castInputs);
		}
	        mobyRequest.setSecondaryInput(secondaryInputInstances);
	    } catch(MobyException me){
	        me.printStackTrace();
	        logger.warn("Error while trying to set service input: " + me);
	    }
	    executeService(handler);
        }
    }

    /**
     * Call the service given, or first launch a GUI to get missing input data if not provided in the MobyDataInstance.
     */
    public void setupService(MobyService mobyService, MobyDataInstance mdi, MobyRequestEventHandler mreh){
	setupService(mobyService, mdi, mreh == null ? mobyRequestEventHandler.hashCode() : mreh.hashCode(), false);
    }

    protected void setupService(MobyService mobyService, MobyDataInstance mdi, int handlerHashCode, boolean useDefaultSecondaries){
	if(mdi != null && !(mdi instanceof MobyPrimaryData)){
	    logger.error("Failure in MOBY input, was not primary data as expected, but rather "+mdi.getClass().getName());
	    return;
	}
	
	mobyRequest.setDebugMode(System.getProperty("moby.debug") != null);
	mobyRequest.setService(mobyService);
	MobyPrimaryData[] primaryInputTemplate = mobyService.getPrimaryInputs();
	primaryInput = new MobyPrimaryData[primaryInputTemplate.length];
	System.arraycopy(primaryInputTemplate, 0, primaryInput, 0, primaryInput.length);

	if(!hasSecondaryInput(mobyService) && primaryInput.length == 1 && mdi != null){
            // code from mobyRequest.setInput() to executeService() should always be synced to avoid input mixup between requests
            synchronized(mobyRequest){
	        try{
		    if(mdi instanceof MobyContentCreator){
			mobyRequest.setInput(((MobyContentCreator) mdi).getAllContents(primaryInput[0].getName()));
		    }
		    // Implement simple input (i.e. only one input argument)
		    else{
			mobyRequest.setInput(mdi, "");
		    }
	        }
	        catch(MobyException me){
		    logger.error("Failure in MOBY input, was not acceptable:" + me);
		    return;
	        }

	        // No need for further info, just launch the request
	        removePopupOptions();
	        executeService(getHandlerByHashCode(handlerHashCode));
            }
	    return;
	}
	// else we need more info from the user to launch this service
	//logger.warn("Need to get secondary parameters: " + secondaryInputTemplate.getClass());
	MobyService metaDataMobyService = MobyService.getService(mobyService.getName(), mobyService.getAuthority());
	
	// We need to figure out where the provided data instance goes into the parameter list for the service
	Vector<String> paramMatch = new Vector<String>();
	int paramMatchDefault = 0;
	if(mdi != null){
	    MobyDataType providedDataType = ((MobyPrimaryData) mdi).getDataType();
	    for(MobyPrimaryData input: primaryInput){
		if(providedDataType.inheritsFrom(input.getDataType())){
		    paramMatch.add(input.getName());
		}
	    }
	    if(paramMatch.size() == 0){
		logger.error("Failure in MOBY input, could not match the input (" +providedDataType.getName() +
			     ") to any service param for service "+mobyService.getName());
		return;
	    }
	    
	    String targetParamName = paramMatch.elementAt(0);
	    // Need to choose among multiple parameters	    
	    if(paramMatch.size() > 1){
		targetParamName = (String) JOptionPane.showInputDialog(null,
								       "Choose the service parameter\n"+
								       "the chosen object represents", 
								       "Service Parameter Choice",
								       JOptionPane.QUESTION_MESSAGE, 
								       null,
								       paramMatch.toArray(), 
								       paramMatch.elementAt(paramMatchDefault));
	    }
	    // Finds the matching param and set it
	    for(int i = 0; i < primaryInput.length; i++){
		if(targetParamName.equals(primaryInput[i].getName())){
		    mdi.setName(targetParamName);
		    primaryInput[i] = (MobyPrimaryData) mdi;
		}
	    }
	} //end if mdi != null
	
	// We need to create the secondary parameters too, and launch the dialog to fill in the
	// uninstantiated data.
	getSecondaryInput(metaDataMobyService, handlerHashCode, useDefaultSecondaries);
    }

    // True if there are secondaries, or more than one primary input 
    // (which also needs the dialog for user data instantiation)
    private boolean hasSecondaryInput(MobyService mobyService){
	MobyData[] secondaryInputTemplate = mobyService.getSecondaryInputs();
	return secondaryInputTemplate != null && secondaryInputTemplate.length != 0 ||
	    mobyService.getPrimaryInputs().length > 1;
    }

    /**
     * If the popup is canceled, we want to remove any data we've stored, and kill any 
     * requests to MOBYCentral.
     */
    public void popupMenuCanceled(PopupMenuEvent e){
	removePopupOptions(e.getSource());
    }

    public void popupMenuWillBecomeInvisible(PopupMenuEvent e){
    }

    public void popupMenuWillBecomeVisible(PopupMenuEvent e){
    }

    public boolean isSecondaryParamDialogNew(){
	if(secondaryGUI == null){
	    return false;
	}
	return secondaryGUI.isNewShowing();
    }

    /**
     * Launches a dialog that the user can configure secondary parameters in,
     * or tries to fill in and use all defaults if valid and useDefaults is specified.
     */
    protected void getSecondaryInput(MobyService mobyService, int handlerHashCode, boolean useDefaults){
	MobySecondaryData[] secondaryInputTemplate = mobyService.getSecondaryInputs();
	if(secondaryGUI == null){
	    secondaryGUI = new MobySecondaryInputGUI(this, handlerHashCode, mobyClient);
	}

	// Create the param array that will be modified in the secondary dialog and used
	// after the callback to run the service.
	secondaryInputInstances = 
		new MobyDataSecondaryInstance[secondaryInputTemplate.length];
	for(int i = 0; i < secondaryInputInstances.length; i++){
	    secondaryInputInstances[i] = new MobyDataSecondaryInstance(secondaryInputTemplate[i]);
	}
	// Should we and can we fill in the parameters with service-provided defaults?
	if(useDefaults && secondaryGUI.defaultFillIn(secondaryInputInstances)){
	    setupServiceSecondaryData(getHandlerByHashCode(handlerHashCode));
	    return;
	}
	// The GUI will perform a callback when the user is done the input
	boolean SHOW_DIALOG = true;
	secondaryGUI.fillIn(primaryInput, secondaryInputInstances, SHOW_DIALOG);
    }

    /**
     * Executes the service using the default handler (see setResponseHandler)
     */
    protected void executeService(){
	executeService(mobyRequestEventHandler);
    }

    /**
     * Execute the service with the given handler for callbacks
     */
    protected void executeService(MobyRequestEventHandler mreh){
	// Execute service asynchronously so we don't hold up the rest of the GUI
	// Callback will be made to event handler when data is ready
	mobyRequest.invokeService(mreh);
	//logger.debug("Got to after invokeService");
	removePopupOptions();
    }

    protected void getRootFrame(JPopupMenu popup){
	Component root = popup.getInvoker();
	while(root != null && !(root instanceof Frame)){
	}
	rootFrame = (Frame) root;
    }

    /**
     * If a service is invoked by this class, the event handler registered here will 
     * get a callback when the response is ready for display (MobyServicesGUI does not
     * display service results, this is its way of delegating that responsibility).
     */
    public void setResponseHandler(MobyRequestEventHandler mreh){
	mobyRequestEventHandler = mreh;
    }

    public void addXPathMapping(String xpath, String[] mobyDataTypes){
	mobyClient.addXPathMapping(xpath, mobyDataTypes);
    }

    public void addXPathMapping(String xpath, String desc_xpath, String[] mobyDataTypes){
	addXPathMapping(xpath, mobyDataTypes);
	// The extra argument is an XPath to a textual description 
	// of the moby input data in the popup menu 
	
    }

    public void addRegexMapping(String xpath, String[] mobyDataTypes){
	mobyClient.addXPathMapping(xpath, mobyDataTypes);
    }

    public void addRegexMapping(String xpath, String desc_xpath, String[] mobyDataTypes){
	addXPathMapping(xpath, mobyDataTypes);
	// The extra argument is an XPath to a textual description 
	// of the moby input data in the popup menu 
	
    }

    public void setDataDescMax(int cutoff){
	maxDataDesc = cutoff;
    }

    /**
     * Uses default handler for response callback
     */
    public void addPopupOptions(Node targetNode, JPopupMenu popupList, boolean asynchronous){
	addPopupOptions(targetNode, popupList, asynchronous, null);
    }

    /**
     * Find the list of available services based on the XML->moby datatype mapping utility in mobyClient.
     * This method can be called asynchronously, to allow the user to interact with the program while the 
     * list is being created.
     */
    public void addPopupOptions(Node targetNode, JPopupMenu popupList, boolean asynchronous, MobyRequestEventHandler handler){

	popupList.addPopupMenuListener(this);
	if(lastPopup != popupList){
	    removePopupOptions();
	    lastPopup = popupList;
	}

	if(asynchronous){
	    // Show the user that we're doing something...
	    Point p = popupList.getLocation(null);
	    synchronized(popupList){
		popupList.setVisible(false);	    
		popupList.add(waitItem);
		//popupList.validate();
		popupList.setVisible(true);
	    }

	    // Call this method synchronously in a different thread
	    OptionLoaderThread lt = new OptionLoaderThread(targetNode, popupList, handler);
	    lt.start();
	    return;
	}

	MobyDataServiceAssocInstance[] serviceAssocObjects = null;
	JMenu[] submenus = null;
	try{
	    // Note: we could call mobyClient.getServices(targetNode), which does 
	    // both major steps here, but by doing the two steps explicitly, we can 
	    // create the top level menus faster, making the app more interactive 
	    // for the user.

	    // First, find the MOBY Objects that can be created from the node
	    MobyDataObject[] mobyObjects = mobyClient.getMobyObjects(targetNode);

	    // Create the top level menu items for these moby objects right away
	    submenus = new JMenu[mobyObjects.length];
	    for(int i = 0; i < mobyObjects.length; i++){
		JMenu submenu = createObjectSubMenu(mobyObjects[i], null);
		if(submenu == null){
		    continue;
		}
		// Add option to copy object to clipboard.  We don't need to wait until services are found.
		popupList.add(submenu);
		addClipboardItem(submenu, mobyObjects[i]);
		submenu.add(getWaitItem(submenu));
		submenus[i] = submenu;
	    }
	    if(popupList.getSubElements().length > 0){
		synchronized(popupList){
		    popupList.setVisible(false);
		    popupList.remove(waitItem);
		    //popupList.validate();
		    popupList.setVisible(true);
		}
	    }

	    // Then find the services that go with these objects
	    serviceAssocObjects = mobyClient.getServices(mobyObjects);
	}
	catch(Exception mobye){
	   logger.error("Could not retrieve list of MobyServices from initialized MobyClient using the " +
		       "context node " + targetNode.getNodeName());mobye.printStackTrace();
	    popupList.setVisible(false);
	    popupList.remove(waitItem);
	    for(int i = 0; submenus != null && i < submenus.length; i++){
		if(submenus[i] != null){
		    if(submenus[i].isPopupMenuVisible()){
			submenus[i].setPopupMenuVisible(false);
			submenus[i].remove(getWaitItem(submenus[i]));
			submenus[i].setPopupMenuVisible(true);
		    }
		    else{
			submenus[i].remove(getWaitItem(submenus[i]));
		    }
		}
	    }
	    if(popupList.getSubElements().length != 0){
		popupList.setVisible(true);
	    }
	    return;
	}
	if(ms == null || ms.length == 0){
	    popupList.remove(waitItem);
	    if(popupList.getSubElements().length == 0){
		popupList.setVisible(false);
	    }
	    return;
	}
	
	for(int i = 0; i < submenus.length; i++){
	    addServicesToSubMenu(submenus[i], serviceAssocObjects[i], handler);
	    addSearchItem(submenus[i], 1);
	    refreshMenu(submenus[i]);
	}
    }

    class OptionLoaderThread extends Thread{
	Node targetNode = null;
	MobyRequestEventHandler handler = null;
	MobyPayloadRequestListener payloadCreator = null;
	Object mobyData = null;
	String extraMenuText = null;
	JPopupMenu popupList;

	public OptionLoaderThread(Node targetNode, JPopupMenu popupList, MobyRequestEventHandler handler){
	    super();
	    this.targetNode = targetNode;
	    this.popupList = popupList;
	    this.handler = handler;
	}

	public OptionLoaderThread(Object mobyData, JPopupMenu popupList, MobyRequestEventHandler handler, 
                                  MobyPayloadRequestListener payloadCreator, String extraMenuText){
	    super();
	    this.mobyData = mobyData;
	    this.popupList = popupList;
	    this.handler = handler;
            this.payloadCreator = payloadCreator;
	    this.extraMenuText = extraMenuText;
	}

	public void run(){
	    if(targetNode != null){
		addPopupOptions(targetNode, popupList, false, handler);
	    }
	    else if(mobyData != null){
		addPopupOptions(mobyData, popupList, false, handler, payloadCreator, extraMenuText);
	    }
	    else{
		logger.warn("Warning: OptionLoaderThread has no data to work with", new Exception("Arrggh"));
	    }
	}
    }

    public void removePopupOptions(Object source){
	removePopupOptions();
    }

    public void removePopupOptions(){
	synchronized(ms){
	    if(lastPopup != null){
		//new Thread(){public void run(){
		    try{
			//logger.debug("Removing " + lastPopup);
			lastPopup.removeAll();
			lastPopup.setVisible(false);
			lastPopup = null;
		    }
		    catch(NullPointerException npe){
			// Someone beat us to the clearing of the popup,
			// now there is nothing to do.
		    }
		    //}}.start();
	    }
	    service2submenu.clear();
	    search2submenu.clear();
	    submenu2msIndex.clear();
	    submenu2waitItem.clear();
	    ms = new MobyDataServiceAssocInstance[0];
	}
    }

    public void addPopupOptions(String textData, JPopupMenu popupList, boolean asynchronous){
	addPopupOptions(textData, popupList, asynchronous, null);
    }

    public void addPopupOptions(String textData, JPopupMenu popupList, boolean asynchronous, MobyRequestEventHandler handler){
	// First, find the MOBY Objects that can be created from the node
	MobyDataObject[] mobyObjects = mobyClient.getMobyObjects(textData);
	
	// Create the top level menu items for these moby objects
	for(int i = 0; mobyObjects != null && i < mobyObjects.length; i++){
	    addPopupOptions(mobyObjects[i], popupList, asynchronous, handler);
	}
    }

    /**
     * Same as three arg addPopupOptions, but uses default response handler
     */
    public void addPopupOptions(MobyDataInstance mobyData, JPopupMenu popupList, boolean asynchronous){
	addPopupOptions(mobyData, popupList, asynchronous, null);
    }

    /**
     * Find the list of available services querying MobyCentral directly with a piece of data.
     */
    public void addPopupOptions(MobyDataInstance mobyData, JPopupMenu popupList, boolean asynchronous, MobyRequestEventHandler handler){
	addPopupOptions(mobyData, popupList, asynchronous, handler, null, null);
    }

    /**
     * Find the list of available services querying MobyCentral directly with a piece of data.
     *
     * @param mobyData a MobyDataInstance, or in the case of deferred data to be created by payloadCreator, a MobyDataType, MobyNamespace, or MobyPrimaryDataSet
     */
    public void addPopupOptions(Object mobyData, JPopupMenu popupList, boolean asynchronous, 
                                MobyRequestEventHandler handler, MobyPayloadRequestListener payloadCreator, String extraMenuText){

	if(asynchronous){
	    //Show the user that we're doing something...
	    synchronized(popupList){
		popupList.setVisible(false);
		popupList.add(waitItem);
		//popupList.revalidate();
		//popupList.repaint();
		popupList.setVisible(true);
	    }

	    // Call this method synchronously in a different thread
	    OptionLoaderThread lt = new OptionLoaderThread(mobyData, popupList, handler, payloadCreator, extraMenuText);
	    lt.start();
	    return;
	}

	// Create submenu for this object
	JMenu submenu = createObjectSubMenu(mobyData, extraMenuText);
	// Add option to copy object to clipboard.  We don't need to wait until services are found.
	if(submenu == null){
	    return;
	}
	popupList.add(submenu);
	synchronized(popupList){
	    popupList.setVisible(false);
	    popupList.remove(waitItem);
	    //popupList.validate();
	    //popupList.setSize(popupList.getPreferredSize());
	    popupList.setVisible(true);
	}

	if(mobyData instanceof MobyDataInstance){
            addClipboardItem(submenu, (MobyDataInstance) mobyData);
        }
	submenu.add(getWaitItem(submenu));

 	MobyDataServiceAssocInstance serviceAssocObject = null;  
// 	//Services for only the one piece of data
	try{
            // The data will be loaded later
            if(payloadCreator != null){
                try{
                    if(mobyData instanceof MobyPrimaryDataSet){
			serviceAssocObject = mobyClient.getServices(new MobyDataObjectSetDeferred((MobyPrimaryDataSet) mobyData, 
												  payloadCreator));
		    }
		    else if(mobyData instanceof MobyPrimaryDataSimple){  // pass in if datatype and namespace are useful
			serviceAssocObject = mobyClient.getServices(new MobyDataObjectDeferred(((MobyPrimaryDataSimple) mobyData).getDataType(),
											       ((MobyPrimaryDataSimple) mobyData).getNamespaces()[0],
											       payloadCreator));
		    }
		    else if(mobyData instanceof MobyDataType){
			serviceAssocObject = mobyClient.getServices(new MobyDataObjectDeferred((MobyDataType) mobyData, 
											       payloadCreator));
		    }
		    else if(mobyData instanceof MobyNamespace){
			serviceAssocObject = mobyClient.getServices(new MobyDataObjectDeferred((MobyNamespace) mobyData, 
											       payloadCreator));
		    }
		    else{
			logger.warn("Ignoring unrecognized template class (" + mobyData.getClass().getName()+
				    ") for deferred data creation by a MobyPayloadRequestListener"); 
			return;
		    }
		} catch(Exception e){
		    logger.error("Could not create deferred Moby data object", e);
		}
	    }
	    else if(mobyData instanceof MobyDataObjectSet){
		serviceAssocObject = mobyClient.getServices((MobyDataObjectSet) mobyData);
	    }
	    else if(mobyData instanceof MobyDataObject){
		serviceAssocObject = mobyClient.getServices((MobyDataObject) mobyData);
            }
	    else{
		logger.warn("Service options for objects other than MobyDataObject " +
			    "and MobyDataObjectSet are not yet supported unless you " +
			    "have defined a MobyPayloadRequestListener.");
		return;
	    }
	} catch(Exception mobye){
 	    logger.error("Could not retrieve list of MobyServices from initialized MobyClient using the " +
			 "MOBY data " + mobyData);
	    mobye.printStackTrace();
	    popupList.setVisible(false);
	    return;
	}

	if(serviceAssocObject == null || serviceAssocObject.getServices() == null ||
	   serviceAssocObject.getServices().length == 0){
	    popupList.setVisible(false);
 	    return;
	}

	addServicesToSubMenu(submenu, serviceAssocObject, handler);
        addSearchItem(submenu, 0);
	refreshMenu(submenu);
    }

    protected synchronized void addServicesToSubMenu(JMenu submenu, MobyDataServiceAssocInstance msadi){
	addServicesToSubMenu(submenu, msadi, null);
    }

    protected void addHandler(MobyRequestEventHandler handler){
	if(handler != null && !specificHandlers.containsKey(handler)){
	    // Java 1.5 auto-boxes the key int to Integer
	    //logger.debug("Adding handler " + handler.hashCode());
	    specificHandlers.put(handler.hashCode(), new WeakReference<MobyRequestEventHandler>(handler)); 
	}
    }

    protected synchronized void addServicesToSubMenu(JMenu submenu, MobyDataServiceAssocInstance msadi, MobyRequestEventHandler handler){
	// Fill in the data slot assigned for this submenu with the service assoc. Moby Object
	int dataIndex = 0;
	boolean isSubSubMenu = false;
	synchronized(ms){
	    if(submenu2msIndex == null || submenu == null || submenu2msIndex.size() == 0){
		// menu was cleared before we could get to it?
		return;
	    }
	    
	    // not one of the top-level submenus representing data objects? 
	    // (probably a service type sub-submenu)
	    // go up the parent menus to find the data object we need
	    else if(!submenu2msIndex.containsKey(submenu)){
		JMenu parent = service2submenu.get(submenu);
		if(parent == null){
		    logger.warn("Cannot add service options, cannot find " +
				"parent moby object index for submenu " + 
				submenu.getText());
		    return;
		}
		dataIndex = ((Integer) submenu2msIndex.get(parent)).intValue();
		isSubSubMenu = true;
	    }

	    // add to popup menu list here (is top-level submenu representing object)
	    else{
		dataIndex = ((Integer) submenu2msIndex.get(submenu)).intValue();
		
		if(ms.length > dataIndex){
		    ms[dataIndex] = msadi;
		}
		else{
		    // ms was clear before we could get to it?
		    return;
		}
	    }
	}

	// Need to subdivide services by service type ontology tree. 
	MobyService[] unsortedServices = msadi.getServices();
	boolean subDivided = false;
	if(unsortedServices.length > MAX_SERVICES_PER_SUBMENU){
	    subDivided = true;

	    // Make smaller lists based on service ontology
	    MobyServiceType[][] serviceLineages = new MobyServiceType[unsortedServices.length][];
	    int maxOntologyDepth = 0;
	    for(int i = 0; i < unsortedServices.length; i++){
		MobyServiceType serviceType = unsortedServices[i].getServiceType();
		if(serviceType == null){
		    logger.warn("No service type (ontology) was associated with service " +
				unsortedServices[i] + ", cannot add to the hierarchical service " +
				"menus (skipping)");
		    continue;
		}
		serviceLineages[i] = serviceType.getLineage();
		if(serviceLineages[i] == null){
		    logger.warn("No service type lineage (ontology position) was associated with service type " +
				serviceType + ", cannot add to the hierarchical service " +
				"menus (skipping)");
		}
		else if(serviceLineages[i].length > maxOntologyDepth){
		    maxOntologyDepth = serviceLineages[i].length;
		}
		logger.debug("Lineage for " + unsortedServices[i].getName() + 
			     "had length " +  serviceLineages[i].length);
	    }

	    String commonAncestorDesc = "";
	    int ontologyDepth = 0;
	    breadth_first_search: for(; ontologyDepth < maxOntologyDepth; ontologyDepth++){
		MobyServiceType commonServiceType = null;
		for(int i = 0; i < serviceLineages.length; i++){
		    if(serviceLineages[i] == null || serviceLineages[i].length <= ontologyDepth){
			continue;
		    }

		    // Not yet set
		    if(commonServiceType == null){
			commonServiceType = serviceLineages[i][ontologyDepth];
			commonAncestorDesc += serviceLineages[i][ontologyDepth] + " > ";
		    }
		    // Difference in lineages found, need to split the menu here...
		    else if(!commonServiceType.equals(serviceLineages[i][ontologyDepth])){
			break breadth_first_search; // labelled break
		    }
		}
	    }

	    // If true, all services share the same type, need to split in some other way, such
	    // as alphabetical subsections or by object input type in object hierarchy...
	    if(ontologyDepth >= maxOntologyDepth){
		// TO DO: for now, a long list will appear instead
		//System.err.println("In TODO big sublist ("+ontologyDepth+">="+maxOntologyDepth+")");
		// Recursively call this method for each type in the ontology at the given depth
		
		// Make smaller lists based on returned data ontology
		MobyDataType[][] outputLineages = new MobyDataType[unsortedServices.length][];
		int maxDataOntologyDepth = 0;
		for(int i = 0; i < unsortedServices.length; i++){
		    MobyData[] output = unsortedServices[i].getPrimaryOutputs();

		    MobyDataType outputType = null;
		    if(output != null && output.length != 0 && output[0] instanceof MobyPrimaryData){
			outputType = MobyDataType.getDataType(((MobyPrimaryData) output[0]).getDataType().getName(), 
                                                              SeahawkOptions.getRegistry());
		    }
		    if(outputType == null){
			logger.warn("No output data type (ontology) was associated with service " +
				    unsortedServices[i] + ", cannot add to the hierarchical service " +
				    "menus (skipping)");
			continue;
		    }
		    outputLineages[i] = outputType.getLineage();
		    if(outputLineages[i] == null){
			logger.debug("No output data type lineage (ontology position) was associated with service type " +
				    outputType + ", cannot add to the hierarchical service " +
				    "menus (skipping)");
		    }
		    else if(outputLineages[i].length > maxOntologyDepth){
			maxDataOntologyDepth = outputLineages[i].length;
		    }
		    logger.debug("Data lineage for " + unsortedServices[i].getName() + 
				 "had length " +  outputLineages[i].length);
		}
		
		String commonDataAncestorDesc = "";
		int dataOntologyDepth = 0;
	        breadth_first_data_search: for(; dataOntologyDepth < maxDataOntologyDepth; dataOntologyDepth++){
		    MobyDataType commonOutputType = null;
		    for(int i = 0; i < outputLineages.length; i++){
			if(outputLineages[i] == null || outputLineages[i].length <= dataOntologyDepth){
			    continue;
			}
			
			// Not yet set
			if(commonOutputType == null){
			    commonOutputType = outputLineages[i][dataOntologyDepth];
			    commonAncestorDesc += outputLineages[i][dataOntologyDepth] + " > ";
			}
			// Difference in lineages found, need to split the menu here...
			else if(!commonOutputType.equals(outputLineages[i][dataOntologyDepth])){
			    break breadth_first_data_search; // labelled break
			}
		    }
		}
		//System.err.println("common ontology depth is " + dataOntologyDepth);

		if(dataOntologyDepth >= maxDataOntologyDepth){
		    // Must separate by name
		    sortServicesByName(unsortedServices);
		    for(int j = 0; j < unsortedServices.length-1;j += MAX_SERVICES_PER_SUBMENU){
			 MobyService[] services = null;
			 if(j+MAX_SERVICES_PER_SUBMENU < unsortedServices.length){ //full slot
			     services = new MobyService[MAX_SERVICES_PER_SUBMENU];
			     System.arraycopy(unsortedServices, j, services, 0, MAX_SERVICES_PER_SUBMENU);
			 }
			 else{
			     services = new MobyService[unsortedServices.length-j];
			     System.arraycopy(unsortedServices, j, services, 0, unsortedServices.length-j);
			 }

			 JMenu newMenu = addNameDivSubMenu(submenu, services);
			 MobyDataServiceAssocInstance newMsadi = null;
			 if(msadi instanceof MobyDataXref){
			     newMsadi = new MobyDataXref((MobyDataObject) msadi, services[0]);
			 }
			 else if(msadi instanceof MobyDataObjectSAI){
			     newMsadi = new MobyDataObjectSAI((MobyDataObject) msadi, services);
			 }
			 else if(msadi instanceof MobyDataObjectSetSAI){
			     newMsadi = new MobyDataObjectSetSAI((MobyDataObjectSet) msadi, services);
			 }
			 addServicesToSubMenu(newMenu, newMsadi, handler);
		    }
		}

		else{
		// How many submenus do we need?
		Map<MobyDataType,Vector<MobyService>> outputType2Services = 
		    new TreeMap<MobyDataType,Vector<MobyService>>();
		for(int i = 0; i < outputLineages.length; i++){
		    if(outputLineages[i] == null){
			continue;
		    }
		    MobyDataType type = null;
		    // Branch
		    if(outputLineages[i].length > dataOntologyDepth){
			type = outputLineages[i][dataOntologyDepth];
		    }
		    // Leaf
		    else if(dataOntologyDepth >= 1){
			type = outputLineages[i][outputLineages[i].length-1];
		    }
		    // ?? rootless node?
		    else{
			type = new MobyDataType("Object (no details)");
		    }
		    MobyService service = unsortedServices[i];
		    // type exists already
		    if(outputType2Services.containsKey(type)){
			outputType2Services.get(type).add(service);
		    }
		    // otherwise first service of this type
		    else{
			Vector<MobyService> serviceItems = new Vector<MobyService>();
			serviceItems.add(service);
			outputType2Services.put(type, serviceItems);
		    }
		}

		for(MobyDataType type: outputType2Services.keySet()){

		    JMenu newMenu = addOutputTypeSubMenu(submenu, type);

		    Vector<MobyService> serviceVector = outputType2Services.get(type);
		    MobyService[] services = serviceVector.toArray(new MobyService[serviceVector.size()]);
		    MobyDataServiceAssocInstance newMsadi = null;
		    if(msadi instanceof MobyDataXref){
			newMsadi = new MobyDataXref((MobyDataObject) msadi, services[0]);
		    }
		    else if(msadi instanceof MobyDataObjectSAI){
			newMsadi = new MobyDataObjectSAI((MobyDataObject) msadi, services);
		    }
		    else if(msadi instanceof MobyDataObjectSetSAI){
			newMsadi = new MobyDataObjectSetSAI((MobyDataObjectSet) msadi, services);
		    }
		    addServicesToSubMenu(newMenu, newMsadi, handler);
		}}		
	    }

	    else{
		// How many submenus do we need?
		Map<MobyServiceType,Vector<MobyService>> serviceType2Services = 
		    new TreeMap<MobyServiceType,Vector<MobyService>>();
		for(int i = 0; i < serviceLineages.length; i++){
		    if(serviceLineages[i] == null){
			continue;
		    }
		    MobyServiceType type = null;
		    // Branch
		    if(serviceLineages[i].length > ontologyDepth){
			type = serviceLineages[i][ontologyDepth];
		    }
		    // Leaf
		    else if(ontologyDepth >= 1){
			type = serviceLineages[i][serviceLineages[i].length-1];
		    }
		    // ?? rootless node?
		    else{
			type = new MobyServiceType("Service (no details)");
		    }
		    MobyService service = unsortedServices[i];
		    // type exists already
		    if(serviceType2Services.containsKey(type)){
			serviceType2Services.get(type).add(service);
		    }
		    // otherwise first service of this type
		    else{
			Vector<MobyService> serviceItems = new Vector<MobyService>();
			serviceItems.add(service);
			serviceType2Services.put(type, serviceItems);
		    }
		}
		
		// Recursively call this method for each type in the ontology at the given depth
		for(MobyServiceType type: serviceType2Services.keySet()){

		    JMenu newMenu = addServiceTypeSubMenu(submenu, type);

		    Vector<MobyService> serviceVector = serviceType2Services.get(type);
		    MobyService[] services = serviceVector.toArray(new MobyService[serviceVector.size()]);
		    MobyDataServiceAssocInstance newMsadi = null;
		    if(msadi instanceof MobyDataXref){
			newMsadi = new MobyDataXref((MobyDataObject) msadi, services[0]);
		    }
		    else if(msadi instanceof MobyDataObjectSAI){
			newMsadi = new MobyDataObjectSAI((MobyDataObject) msadi, services);
		    }
		    else if(msadi instanceof MobyDataObjectSetSAI){
			newMsadi = new MobyDataObjectSetSAI((MobyDataObjectSet) msadi, services);
		    }
		    addServicesToSubMenu(newMenu, newMsadi, handler);
		}
	    }

	    // Causes resizing based on new items
	    if(submenu.isPopupMenuVisible()){
		submenu.setPopupMenuVisible(false);
		submenu.remove(getWaitItem(submenu));
		submenu.setPopupMenuVisible(true);
	    }
	    else{
		submenu.remove(getWaitItem(submenu));
	    }
	    // If recursed to create more submenus, do not execute the code below,
	    // which would add the items to the top level submenu too. 
	    return;
		
	} 

	addHandler(handler);

	// Don't sort the original list (who knows who's reading it at the moment)
	MobyService[] services = new MobyService[unsortedServices.length];
	System.arraycopy(unsortedServices, 0, services, 0, unsortedServices.length);
	sortServicesByName(services);
	msadi.setServices(services);

	for(int i = 0; i < services.length; i++){
	    MobyService service = services[i];

	    // If we are a top-level menu, the service index is the same as our loop iterator
	    int serviceIndex = i;
	    // If we are a recursive call to a submenu, we need to map our service to the top-level
	    // sub menu's service array value (i.e. the one in ms[])
	    if(isSubSubMenu){
		MobyService[] referenceServices = ms[dataIndex].getServices();
		for(serviceIndex = 0; serviceIndex < referenceServices.length; serviceIndex++){
		    if(referenceServices[serviceIndex].equals(service)){
			break; // found service index in base service list for the object the top-level submenu represents
		    }
		}
		if(serviceIndex == referenceServices.length){
		    logger.warn("Cannot add service option, cannot find " +
				"parent moby object service array index for service " + 
				service.getName());
		    continue;
		}
	    }

	    // Visual indicate secondary input usage in the service with an ellipsis at the end of the name
	    JMenuItem mobyItem = new JMenuItem("Run " + service.getName() + 
					       (hasSecondaryInput(service) ? "..." : "")+(service.isAsynchronous() ? "(async)":""));
	    mobyItem.setActionCommand("MOBY:"+dataIndex+":"+serviceIndex+(handler == null ? "" : ":"+handler.hashCode()));
	    mobyItem.addActionListener(this);
	    String sdesc = "No description provided";
	    String serviceDesc = service.getDescription();
	    String serviceAuthority = service.getAuthority();
	    if(serviceDesc != null && serviceDesc.length() > 0){
		if(serviceAuthority != null && serviceAuthority.length() > 0){
		    serviceDesc += " [" + serviceAuthority + "]";
		}
		serviceDesc = HTMLUtils.htmlifyToolTipText(serviceDesc, MAX_SERVICE_DESC_LEN);
		sdesc = serviceDesc;
	    }
	    if(sdesc != null){
		mobyItem.setToolTipText(sdesc);
	    }
	    submenu.add(mobyItem);
	}
    }

    protected void refreshMenu(JMenu submenu){
	// Causes resizing based on new items
	if(submenu.isPopupMenuVisible()){
	    submenu.setPopupMenuVisible(false);
	    submenu.remove(getWaitItem(submenu));
	    submenu.setPopupMenuVisible(true);
	}
	else{
	    submenu.remove(getWaitItem(submenu));
	}
    }

    public JMenu addServiceTypeSubMenu(JMenu parentMenu, MobyServiceType type){
	JMenu menu = new JMenu("Service type: " + type.getName());

	MobyServiceType[] typeLineage = type.getLineage();
	String commonAncestorDesc = "";
	for(int i = 0; i < typeLineage.length; i++){
	    if(typeLineage == null){
		commonAncestorDesc = "No details available";
		break;
	    }
	    commonAncestorDesc += typeLineage[i].getName() + " > ";
	}

	String menuToolTip = commonAncestorDesc + type.getDescription();
	menuToolTip = HTMLUtils.htmlifyToolTipText(menuToolTip,  MAX_SERVICE_DESC_LEN);
	menu.setToolTipText(menuToolTip);
	parentMenu.add(menu);

	// The following is to keep track of nested menu parents, which
	// you can't do directly in Swing (see
	// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4103931) 
	while(service2submenu.containsKey(parentMenu)){
	    parentMenu = (JMenu) service2submenu.get(parentMenu);
	}

	// All menus, no matter how nested, point to top submenu that corresponds to the data object
	service2submenu.put(menu, parentMenu);  

	return menu;
    }

    protected String getNameStart(MobyService service){
	String serviceName = service.getName();
	if(serviceName.length() <= 9){
	    return serviceName;
	}
	else{
	    return serviceName.substring(0, 8)+"...";
	}
    }

    public JMenu addNameDivSubMenu(JMenu parentMenu, MobyService[] services){
	if(services == null || services.length == 0){
	    return null;
	}

	JMenu menu = new JMenu("Service name "+getNameStart(services[0]) + "-"+
			       getNameStart(services[services.length-1])+":");
	parentMenu.add(menu);

	// The following is to keep track of nested menu parents, which
	// you can't do directly in Swing (see
	// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4103931) 
	while(service2submenu.containsKey(parentMenu)){
	    parentMenu = (JMenu) service2submenu.get(parentMenu);
	}

	// All menus, no matter how nested, point to top submenu that corresponds to the data object
	service2submenu.put(menu, parentMenu);  

	return menu;
    }

    public JMenu addOutputTypeSubMenu(JMenu parentMenu, MobyDataType type){
	JMenu menu = new JMenu("Result type: " + (type.getName().equals(MobyTags.MOBYOBJECT) ? "ID" : type.getName()));

	MobyDataType[] typeLineage = type.getLineage();
	String commonAncestorDesc = "";
	for(int i = 0; i < typeLineage.length; i++){
	    if(typeLineage == null){
		commonAncestorDesc = "No details available";
		break;
	    }
	    commonAncestorDesc += typeLineage[i].getName() + " > ";
	}

	String desc = type.getDescription();
	if(desc == null || desc.length() == 0){
	    desc = type.getComment();
	}
	String menuToolTip = commonAncestorDesc + desc;
	menuToolTip = HTMLUtils.htmlifyToolTipText(menuToolTip, MAX_SERVICE_DESC_LEN);
	menu.setToolTipText(menuToolTip);
	parentMenu.add(menu);

	// The following is to keep track of nested menu parents, which
	// you can't do directly in Swing (see
	// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4103931) 
	while(service2submenu.containsKey(parentMenu)){
	    parentMenu = (JMenu) service2submenu.get(parentMenu);
	}

	// All menus, no matter how nested, point to top submenu that corresponds to the data object
	service2submenu.put(menu, parentMenu);  

	return menu;
    }

    /**
     * Template object may be one of MobyPrimaryData, or in case of deferred content loading a 
     * MobyNamespace, MobyDataType, or MobyPrimaryDataSet.
     */
    public JMenu createObjectSubMenu(Object templateObject, String extraMenuText){
        String id = null;
        MobyDataType mobyDataType = null;
        String name = null;
	MobyNamespace[] namespaces = null;
	if(templateObject instanceof MobyPrimaryData){
	    MobyPrimaryData targetData = (MobyPrimaryData) templateObject;
	    mobyDataType = targetData.getDataType();
	    namespaces = targetData.getNamespaces();
	    id = targetData.getId();
	    if(id == null){
	        id = "";
	    }
	    else if(id.length() > MAX_ID_LEN+3){
	        id = id.substring(0, MAX_ID_LEN)+"...";
	    }
	    if(MobyTags.MOBYOBJECT.equals(mobyDataType.getName()) &&
               namespaces != null && namespaces.length > 0 && namespaces[0] != null){
		name = namespaces[0].getName();
            }
            else{
                name = mobyDataType.getName();
            }
        }
        else if(templateObject instanceof MobyNamespace){
            namespaces = new MobyNamespace[]{(MobyNamespace) templateObject};
            mobyDataType = MobyDataType.getDataType(MobyTags.MOBYOBJECT, SeahawkOptions.getRegistry());
            name = ((MobyNamespace) templateObject).getName();
        }
        else if(templateObject instanceof MobyDataType){
            mobyDataType = (MobyDataType) templateObject;
            name = ((MobyDataType) templateObject).getName();
        }
        else{
            logger.error("Could not create submenu for object of unaccepted type "+templateObject.getClass().getName());
        }
	
	String desc = null;
	String datatype = null;
	if(mobyDataType != null){
	    datatype = mobyDataType.getName();
	    desc = mobyDataType.getDescription();
	    if(desc == null || desc.length() == 0){
		desc = mobyDataType.getComment();  //Eddie seems to have put the decsription here for some reason
	    }
	}
	if(datatype != null && datatype.indexOf("objectclass:") != -1){ 
	    //LSID type URN, truncate the prefix and keep the part a user would care about (last part, the class name)
	    datatype = datatype.substring(datatype.indexOf("objectclass:")+12);
	}

	JMenu submenu = null;
	// If not defined, derive a description from the Data Namespace
	if(MobyTags.MOBYOBJECT.equals(mobyDataType.getName())){
	    String mobydesc = "?";
	    if(namespaces != null){
		// Take the longest description
		for (int j = 0; j < namespaces.length; j++){
		    if(namespaces[j] != null && namespaces[j].getDescription() != null &&
		       namespaces[j].getDescription().length() > mobydesc.length()){
			mobydesc = namespaces[j].getDescription();
			datatype = namespaces[j].getName();
		    }
		}
	    }
	    desc = "Record Identifier - " + mobydesc;
	    if(templateObject instanceof MobyDataObjectSet){
		datatype += " collection";
	    }
	    submenu = new JMenu("<html>Services for " + (extraMenuText == null ? "" : extraMenuText+" ") + 
                                "<font color='red'>"+datatype + "</font>" + (id == null ? "" : ":" + id)+"</html>");
	    assignMenuDataIndex(submenu);
	}
	else if(MobyTags.MOBYSTRING.equals(mobyDataType.getName())){
	    String sample = "";
            if(templateObject instanceof MobyDataInstance){
                sample = ((MobyDataInstance) templateObject).getName();
                if(sample == null){
		    sample = "\""+((MobyDataInstance) templateObject).toString()+"\"";
	        }
            }
	    if(templateObject instanceof MobyDataObjectSet){
		sample += " collection";
	    }
	    if(sample.length() > MAX_ID_LEN+3){
		sample = "\""+sample.substring(0, MAX_ID_LEN)+"...\"";
	    }
	    desc = "A piece of text";
	    submenu = new JMenu("<html>Services for "+(extraMenuText == null ? "" : extraMenuText+" ")+
				"<font color='red'>String</font> " + sample+"</html>");
	    assignMenuDataIndex(submenu);
	}
	// Complex object
	else{
	    String objectLabel = datatype;
	    if(namespaces != null && namespaces.length > 0 && namespaces[0] != null){
		objectLabel += ":" + namespaces[0].getName();
		String namespaceDesc = namespaces[0].getDescription();
		if(namespaceDesc != null && namespaceDesc.length() > 0){
		    desc += "["+namespaceDesc+"]";
		}
	    }
	    if(templateObject instanceof MobyDataObject && id != null && id.length() > 0){
		objectLabel = "<font color='red'>"+objectLabel+"</font>:" + id;
	    }
	    else if(templateObject instanceof MobyDataObjectSet){
		objectLabel = "<font color='red'>"+objectLabel+" collection</font>";
	    }
	    else{
		objectLabel = "<font color='red'>"+objectLabel+"</font>";
	    }
	    submenu = new JMenu("<html>Services for " + (extraMenuText == null ? "" : extraMenuText+" ")+objectLabel+"</html>");
	    assignMenuDataIndex(submenu);
	}
	desc = "Input data: " + desc;
	desc = HTMLUtils.htmlifyToolTipText(desc, MAX_SERVICE_DESC_LEN);
	submenu.setToolTipText(desc);
	submenu.setName(SERVICE_SUBMENU_NAME);
	return submenu;
    }

    public void serviceSelected(ServiceSearchDialog dialog, javax.swing.JMenuItem selectedServiceItem){
	actionPerformed(new ActionEvent(selectedServiceItem, 1, selectedServiceItem.getActionCommand()));
	removePopupOptions();
    }

    public void selectionCanceled(ServiceSearchDialog dialog){
	removePopupOptions();  //cleanup stored data
    }

    private synchronized int assignMenuDataIndex(JMenu submenu){
	// Make the services array bigger, then copy over existing data, 
	// leaving a blank at the end to be filled in by addServicesToSubMenu
	synchronized(ms){
	    MobyDataServiceAssocInstance[] mss = new MobyDataServiceAssocInstance[ms.length+1];
	    if(ms != null){
		System.arraycopy(ms, 0, mss, 0, ms.length);
	    }
	    ms = mss;
	    // Let everybody know what slot this submenu's data instance should go in to...
	    submenu2msIndex.put(submenu, ms.length-1);
	    submenu2msIndex.put(submenu.getPopupMenu(), ms.length-1); // Because JPopupMenu is actual component, 
	                                                              // JMenu just encapsulates it
	    return ms.length-1;
	}
    }

    /**
     * The first time it's called for a given menu, a wait item is created.
     * In subsequent calls the same item is returned.
     */
    private JMenuItem getWaitItem(JMenu menu){
	if(menu == null){
	    logger.warn("Cannot get wait menu item, passed in menu was null");
	    return null;
	}

	if(submenu2waitItem.containsKey(menu)){
	    return (JMenuItem) submenu2waitItem.get(menu);
	}
	else{
	    JMenuItem waitMsg = new JMenuItem("Retrieving Web services, please wait...");
	    submenu2waitItem.put(menu, waitMsg);
	    return waitMsg;
	}
    }

    /**
     * Removes the popup menu and replaces it with a window that has a filter widget
     * over the list of services from the original popup.
     */
    protected void setupSearchData(JMenu objectMenu, MobyDataInstance mobyData){
	// Setup the chooser dialog
	ServiceSearchDialog searchDialog = new ServiceSearchDialog(null, 
								   objectMenu.getText().replaceFirst("Services for", "Run "), 
								   this);
	addMenuOptionsToSearch(searchDialog, objectMenu);

	searchDialog.initializeList();
	searchDialog.pack();
	searchDialog.setVisible(true);
    }

    private void addMenuOptionsToSearch(ServiceSearchDialog searchDialog, MenuElement menuElement){
	if(menuElement instanceof JPopupMenu || menuElement instanceof JMenu){
	    for(MenuElement subMenuElement: menuElement.getSubElements()){
		addMenuOptionsToSearch(searchDialog, subMenuElement);
	    }
	}
	else if(menuElement instanceof JMenuItem){ // leaf defining a service
	    JMenuItem serviceItem = (JMenuItem) menuElement;
	    String label = serviceItem.getText();
	    if(label.equals(SEARCH_LABEL) || label.equals(CLIPBOARD_LABEL)){
		return; // not real services
	    }
	    serviceItem.setText(label.replaceFirst("Run ", ""));
	    searchDialog.addOption(serviceItem, false);
	}
	else{
	    System.err.println("Got dead end with "+menuElement.getClass().getName());
	}
    }

    public void addSearchItem(JMenu menu, int location){
	if(menu == null){
	    logger.warn("Cannot add service search option, menu reference passed in was null");
	    return;
	}

	if(!submenu2msIndex.containsKey(menu)){
	    logger.warn("Cannot add service search option, cannot find " +
			"moby object list index for menu " + 
			menu.getText());
	    return;
	}


	int dataIndex = ((Integer) submenu2msIndex.get(menu)).intValue();
	JMenuItem searchItem = new JMenuItem(SEARCH_LABEL);
	searchItem.setActionCommand("MOBY:"+dataIndex+":"+SERVICESEARCH_CMD);
	searchItem.addActionListener(this);
	menu.add(searchItem, location);	
	search2submenu.put(searchItem, menu);
    }

    public void addClipboardItem(JMenu menu, MobyDataInstance mobyData){
	if(menu == null){
	    logger.warn("Cannot add clipboard option, menu reference passed in was null");
	    return;
	}

	if(clipboard == null){
	    logger.warn("No clipboard has been registered yet, " +
			       "cannot add clipboard option for menu " + 
			       menu.getText());
	    return;
	}

	if(!submenu2msIndex.containsKey(menu)){
	    logger.warn("Cannot add clipboard option, cannot find " +
			       "moby object list index for menu " + 
			       menu.getText());
	    return;
	}

	// Set the data item, but unless no services are available, it will eventually
	// be replaced in addOptionsToSubMenu by the service assoc. instance.
	int dataIndex = ((Integer) submenu2msIndex.get(menu)).intValue();

	if(mobyData instanceof MobyDataObjectSet){
	    synchronized(ms){
		ms[dataIndex] = new MobyDataObjectSetSAI((MobyDataObjectSet) ((MobyDataObjectSet) mobyData).clone(), 
							 (MobyService[]) null);
	    }
	}
	else if(mobyData instanceof MobyDataObject){
	    synchronized(ms){
		ms[dataIndex] = new MobyDataObjectSAI((MobyDataObject) ((MobyDataObject) mobyData).clone(), 
						      (MobyService[]) null);
	    }
	}
	else{
	    logger.warn("Clipboard option for objects other than MobyDataObject " +
			"and MobyDataObjectSet are not yet supported:"+mobyData);
	    return;
	}

	JMenuItem clipItem = new JMenuItem(CLIPBOARD_LABEL);
	clipItem.setActionCommand("MOBY:"+dataIndex+":"+CLIPBOARD_CMD);
	clipItem.addActionListener(this);
	menu.add(clipItem);
    }

    public void sortServicesByName(MobyService[] services){
	try{
	    java.util.Arrays.sort(services, this);
	}
	catch(ClassCastException cce){
	    logger.error("ClassCastException: Warning: Could not alphabetically sort services by name," +
			"got exception (some input was not of type MobyService):" + cce);
	}
    }

    public int compare(MobyService o1, MobyService o2){
	return textCollator.compare(o1.getName(), o2.getName());
    }

    public boolean equals(Object obj){
	return textCollator.equals(obj);
    }
}
