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

package org.biomoby.service.dashboard;

import org.tulsoft.tools.gui.SwingUtils;
import org.tulsoft.shared.PrefsUtils;

import javax.swing.JTree;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.Icon;
import javax.swing.JPopupMenu;
import javax.swing.JOptionPane;
import javax.swing.JMenuItem;
import javax.swing.JSeparator;
import javax.swing.ToolTipManager;
import javax.swing.AbstractAction;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.tree.TreePath;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeSelectionEvent;

import java.awt.Component;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import java.util.Set;
import java.util.Enumeration;

/**
 * A common JTree re-used (and always slightly modified) by all
 * Biomoby registry trees. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: CommonTree.java,v 1.22 2008/03/02 12:45:26 senger Exp $
 */
public class CommonTree
    extends JTree
    implements DashboardProperties {

    // action commands for popup menu items
    protected final static String AC_SEARCH   = "ac-search";
    protected final static String AC_EXPAND   = "ac-expand";
    protected final static String AC_COLLAPSE = "ac-collapse";
    protected final static String AC_RELOAD   = "ac-reload";
    protected final static String AC_NSORT    = "ac-nsort";
    protected final static String AC_ASORT    = "ac-asort";

    // some shared constants
    final static protected int SORTED_AS_PREVIOUSLY   = -1;
    final static protected int SORTED_BY_NAME         = 0;
    final static protected int SORTED_BY_AUTHORITY    = 1;
    final static protected int SORTED_UNUSUAL         = 9;
    // all higher numbers indicates an unusual sorting order that we
    // will not keep in case of reloading
    final static protected int SORTED_BY_SERVICE_TYPE = 11;
    final static protected int SORTED_BY_INPUT_DATA   = 12;
    final static protected int SORTED_BY_OUTPUT_DATA  = 13;

    final static protected String PREF_KEY_SEARCH = "searchKey";

    // each tree has its identity (it helps in logs)
    private static int allTreeCounter = 0;
    protected int treeId;

    // tree components
    protected JPopupMenu popup;
    protected static String
    lastSearchText = PrefsUtils.getNode (Dashboard.class).get (PREF_KEY_SEARCH, "");

    protected int lastSorted = SORTED_BY_NAME;

    protected PropertyChannel propertyChannel;

    // shared icons
    static protected Icon searchIcon;
    static protected Icon menuSearchIcon, menuSearchIconDis;
    static protected Icon menuExpandIcon, menuExpandIconDis;
    static protected Icon menuCollapseIcon, menuCollapseIconDis;
    static protected Icon menuReloadIcon, menuReloadIconDis;
    static protected Icon smallNIcon, smallNIconDis;
    static protected Icon smallAIcon, smallAIconDis;
    static protected Icon smallTIcon, smallTIconDis;
    static protected Icon smallIIcon, smallIIconDis;
    static protected Icon smallOIcon, smallOIconDis;
    static protected Icon sLeafIcon, stLeafIcon, dtLeafIcon, nsLeafIcon;

    protected String rootNode;

    final static protected String ACCESS_ERROR_INTRO =
    "Check please values in text fields (in Registry Browser)\n"
    + "specifying registry endpoint and namespace, and in the\n"
    + "one specifying directory used as a local cache. Then\n"
    + "right-click in the data types area and select 'Reload'.\n\n";

    /*********************************************************************
     * Constructor
     ********************************************************************/
    public CommonTree (String rootNode) {
        super (new DefaultTreeModel (new DefaultMutableTreeNode (new CommonNode (rootNode))));
	this.rootNode = rootNode;
	treeId = ++allTreeCounter;

	// create an almost empty tree, only with a root node that
	// contains just a String holding the name of the root node;
        putClientProperty ("JTree.lineStyle", "Angled");
	setCellRenderer (new CommonTreeCellRenderer());
	getSelectionModel().setSelectionMode
	    (TreeSelectionModel.SINGLE_TREE_SELECTION);

	// listen for when the selection changes
	addTreeSelectionListener ( new TreeSelectionListener() {
		public void valueChanged (TreeSelectionEvent e) {
		    DefaultMutableTreeNode node =
			(DefaultMutableTreeNode)getLastSelectedPathComponent();
		    selected (node);
		}
	    });

	// enable tool tips
	ToolTipManager.sharedInstance().registerComponent (this);

	// load menu icons
	loadIcons();
    }

    /**************************************************************************
     * Keep shared storage of properties updated in various panels.
     **************************************************************************/
    public void setPropertyChannel (PropertyChannel propertyChannel) {
	this.propertyChannel = propertyChannel;
    }

    /*********************************************************************
     *
     ********************************************************************/
    class CommonTreeCellRenderer
	extends DefaultTreeCellRenderer {

	Set<String> toBeHighlighted;
	Icon leafImage;

	public void setLeafIcon (Icon icon) {
	    leafImage = icon;
	}

	public void setToBeHighlighted (Set<String> toBeHighlighted) {
	    this.toBeHighlighted = toBeHighlighted;
	}

	public Component getTreeCellRendererComponent (JTree tree,
						       java.lang.Object value,
						       boolean isSelected,
						       boolean expanded,
						       boolean leaf,
						       int row,
						       boolean hasAFocus) {
	    //
	    // I will reuse the default rendering done by my parent,
	    // except the change of an icon (for naming contexts)
	    //
	    Component c = super.getTreeCellRendererComponent (tree, value, isSelected,
							      expanded, leaf, row, hasAFocus);
	    // add a tool-tip for the root node
	    if (row == 0) {
		setToolTipText ("Right-click will show other options");
		setFont (new Font ("Dialog",  Font.BOLD, 12));
	    } else {
		setToolTipText (null);
		setFont (new Font ("Dialog",  Font.PLAIN, 10));
	    }

	    // root node
	    if (row == 0) {
		setText (rootNode);
	    }

	    // some icons
 	    if (leaf)
 		((JLabel)c).setIcon (leafImage);


	    // do highlight
	    Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
	    CommonNode nodeObject = (CommonNode)userObject;
	    if (toBeHighlighted != null && toBeHighlighted.contains (nodeObject.getValue()))
		setText ("<html><font color='red'>" + nodeObject + "</font></html>");

	    return c;
	}
    }

    /*********************************************************************
     *
     ********************************************************************/
    public void setLeafIcon (Icon icon) {
	CommonTreeCellRenderer r = (CommonTreeCellRenderer)getCellRenderer();
	r.setLeafIcon (icon);
    }

    /*********************************************************************
     * Put this tree in a scrollable pane.
     ********************************************************************/
    public JScrollPane scrollable() {
 	return new JScrollPane (this);
    }

    /*********************************************************************
     * Get data from a registry model and update the tree. Will be
     * overwritten by subclass.
     ********************************************************************/
    public void update (int howSorted, Object data) {
    }

    /*********************************************************************
     * Called when a tree node is selected. Does nothing here.
     ********************************************************************/
    protected void selected (DefaultMutableTreeNode node) {
    }

    /*********************************************************************
     * Load all menu icons.
     ********************************************************************/
    protected void loadIcons() {
	if (menuSearchIcon == null)
	    menuSearchIcon = 
		SwingUtils.createIcon ("images/smallSearch.gif", Dashboard.class);
	if (menuSearchIconDis == null)
	    menuSearchIconDis = 
		SwingUtils.createIcon ("images/smallSearch_dis.gif", Dashboard.class);

	if (menuExpandIcon == null)
	    menuExpandIcon = 
		SwingUtils.createIcon ("images/smallExpand.gif", Dashboard.class);
	if (menuExpandIconDis == null)
	    menuExpandIconDis = 
		SwingUtils.createIcon ("images/smallExpand_dis.gif", Dashboard.class);

	if (menuCollapseIcon == null)
	    menuCollapseIcon = 
		SwingUtils.createIcon ("images/smallCollapse.gif", Dashboard.class);
	if (menuCollapseIconDis == null)
	    menuCollapseIconDis = 
		SwingUtils.createIcon ("images/smallCollapse_dis.gif", Dashboard.class);

	if (menuReloadIcon == null)
	    menuReloadIcon = 
		SwingUtils.createIcon ("images/smallReload.gif", Dashboard.class);
	if (menuReloadIconDis == null)
	    menuReloadIconDis = 
		SwingUtils.createIcon ("images/smallReload_dis.gif", Dashboard.class);

	if (smallNIcon    == null) smallNIcon    = loadIcon ("images/smallSortAZ.gif");
	if (smallNIconDis == null) smallNIconDis = loadIcon ("images/smallSortAZ_dis.gif");
	if (smallAIcon    == null) smallAIcon    = loadIcon ("images/smallAuth.gif");
	if (smallAIconDis == null) smallAIconDis = loadIcon ("images/smallAuth_dis.gif");
	if (smallTIcon    == null) smallTIcon    = loadIcon ("images/smallInher.gif");
	if (smallTIconDis == null) smallTIconDis = loadIcon ("images/smallInher_dis.gif");
	if (smallIIcon    == null) smallIIcon    = loadIcon ("images/smallImport.gif");
	if (smallIIconDis == null) smallIIconDis = loadIcon ("images/smallImport_dis.gif");
	if (smallOIcon    == null) smallOIcon    = loadIcon ("images/smallExport.gif");
	if (smallOIconDis == null) smallOIconDis = loadIcon ("images/smallExport_dis.gif");

	if (nsLeafIcon == null) nsLeafIcon = loadIcon ("images/small_yellow.gif");
	if (stLeafIcon == null) stLeafIcon = loadIcon ("images/magentaDiamond.gif");
	if (sLeafIcon  == null) sLeafIcon  = loadIcon ("images/console.gif");
	if (dtLeafIcon == null) dtLeafIcon = loadIcon ("images/smallPage.gif");
    }

    //
    protected static Icon loadIcon (String path) {
	return AbstractPanel.loadIcon (path);
    }

    /*********************************************************************
     * Create a popup object with common items. Subclasses can (and
     * usually do) add more items, or re-created the whole popup. <p>
     *
     * @param title is given to the popups
     ********************************************************************/
    protected void createPopups (String title) {
	popup = new JPopupMenu (title);
	popup.add
	    (createMenuItem (new AbstractAction ("Search") {
		    public void actionPerformed (ActionEvent e) {
			String searchText = searchDialog();
			if (searchText != null)
			    search (searchText);
		    }
		}, AC_SEARCH, menuSearchIcon, menuSearchIconDis));
	popup.add
	    (createMenuItem (new AbstractAction ("Expand all nodes") {
		    public void actionPerformed (ActionEvent e) {
			expand();
		    }
		}, AC_EXPAND, menuExpandIcon, menuExpandIconDis));

	popup.add
	    (createMenuItem (new AbstractAction ("Collapse all nodes") {
		    public void actionPerformed (ActionEvent e) {
			collapse();
		    }
		}, AC_COLLAPSE, menuCollapseIcon, menuCollapseIconDis));

	popup.addSeparator();

	popup.add
	    (createMenuItem (new AbstractAction ("Reload") {
		    public void actionPerformed (ActionEvent e) {
			reload();
		    }
		}, AC_RELOAD, menuReloadIcon, menuReloadIconDis));

	// add listener to this tree to bring up popup menus
	MouseListener popupListener = new PopupListener();
	addMouseListener (popupListener);
    }

    class PopupListener extends MouseAdapter {
	public void mousePressed (MouseEvent e) {
	    maybeShowPopup (e);
	}
	public void mouseReleased (MouseEvent e) {
	    maybeShowPopup (e);
	}
	private void maybeShowPopup (MouseEvent e) {
	    if (e.isPopupTrigger() && popup.isEnabled()) {
		popup.show (e.getComponent(),
			e.getX(), e.getY());
	    }
	}
    }

    /*********************************************************************
     * Create a menu item.
     ********************************************************************/
    public static JMenuItem createMenuItem (AbstractAction action,
					    String actionCommand) {
	return AbstractPanel.createMenuItem (action, actionCommand);
    }

    /*********************************************************************
     * Create a menu item.
     ********************************************************************/
    public static JMenuItem createMenuItem (AbstractAction action,
					    String actionCommand,
					    Icon icon,
					    Icon disabledIcon) {
	return AbstractPanel.createMenuItem (action, actionCommand,
					     -1, icon, disabledIcon);
    }

    /*********************************************************************
     * Enable/disable the whole popup except the 'Reload' option (that
     * one remains enabled always).
     ********************************************************************/
    protected void setEnabledPopup (boolean enabled) {
	synchronized (popup) {
	    Component[] components = popup.getComponents();
	    for (int i = 0; i < components.length; i++) {
		if ( components[i] instanceof JMenuItem &&
		     ! AC_RELOAD.equals (((JMenuItem)components[i]).getActionCommand()) )
		    ((JMenuItem)components[i]).setEnabled (enabled);
	    }
	}
    }

    /*********************************************************************
     * Adding sorting items to the popup menu.
     ********************************************************************/
    protected void addSortingItems() {
	popup.addSeparator();
	popup.add
	    (createMenuItem (new AbstractAction ("Sort by names") {
		    public void actionPerformed (ActionEvent e) {
			update (lastSorted = SORTED_BY_NAME, null);
		    }
		}, AC_NSORT, smallNIcon, smallNIconDis));
	popup.add
	    (createMenuItem (new AbstractAction ("Sort by authorities") {
		    public void actionPerformed (ActionEvent e) {
			update (lastSorted = SORTED_BY_AUTHORITY, null);
		    }
		}, AC_ASORT, smallAIcon, smallAIconDis));
    }

    /*********************************************************************
     * Collapse all nodes, starting from the root. <p>
     ********************************************************************/
    protected void collapse() {
 	SwingUtils.collapseTree (this, (DefaultMutableTreeNode)getModel().getRoot());
    }

    /*********************************************************************
     * Expand all nodes, starting from the root. <p>
     ********************************************************************/
    protected void expand() {
	final SwingWorker worker = new SwingWorker() {
		public Object construct() {
		    SwingUtils.expandTree (CommonTree.this, (DefaultMutableTreeNode)getModel().getRoot());
		    return null;  // not used here
		}
		// runs on the event-dispatching thread.
		public void finished() {
		    repaint();
		}
	    };
	worker.start(); 
    }

    /*********************************************************************
     * Does nothing here - it is supposed to be overwritten bya
     * concrete tree.
     ********************************************************************/
    protected void reload() {
    }

    /*********************************************************************
     * Search underlying objects and highligh nodes corresponding to
     * the found objects. But it does nothing here - it is supposed to
     * be overwritten by a concrete tree.
     ********************************************************************/
    protected void search (String searchText) {
    }

    /*********************************************************************
     * Show a dialog asking for a search string, and return it. Or
     * return null if user cancel the dialog.
     ********************************************************************/
    protected String searchDialog() {
	if (searchIcon == null)
	    searchIcon = 
		SwingUtils.createIcon ("images/viewerButton.gif", Dashboard.class);

	String result = (String)JOptionPane.showInputDialog
	    (null,
	     "The search will highlight nodes in this tree that\n" +
	     "contain (or are related to) the text you enter here.\n"  +
	     "Text is treated as a regular expression. The case-\n" +
	     "insensitivity is, however, always added by default.\n \n" +
	     "For example, try: germplasm or mark|kawas.\n \n",
	     "Search",
	     JOptionPane.OK_CANCEL_OPTION,
	     searchIcon,
	     null,
	     lastSearchText);
	if (result != null) {
	    lastSearchText = result;
	    PrefsUtils.getNode (Dashboard.class).put (PREF_KEY_SEARCH, lastSearchText);
	}
	return result;
    }

    /*********************************************************************
     *
     ********************************************************************/
    protected void highlightAndJumpTo (Set<String> toBeHighlighted) {

	setSelectionPath (null);
	CommonTreeCellRenderer r = (CommonTreeCellRenderer)getCellRenderer();
	r.setToBeHighlighted (toBeHighlighted);
	collapse ();
	DefaultTreeModel tModel = (DefaultTreeModel)getModel();
	DefaultMutableTreeNode root = (DefaultMutableTreeNode)tModel.getRoot(); 
	Enumeration en = root.depthFirstEnumeration();
	while (en.hasMoreElements()) {
	    DefaultMutableTreeNode node = (DefaultMutableTreeNode)en.nextElement();
	    if (toBeHighlighted.contains ( ((CommonNode)node.getUserObject()).getValue())) {
		makeVisible (new TreePath (tModel.getPathToRoot (node)));
	    }
	}
    }

    /*********************************************************************
     * Display an error message.
     ********************************************************************/
    protected static void error (String prologue, Exception e) {
	AbstractPanel.error (prologue, e);
    }

    /*********************************************************************
     * Remove a menu item (given by an action command string) from a
     * popup menu.
     ********************************************************************/
    protected void removeFromPopups (String actionCommand) {
	if (popup == null) return;
	Component[] components = popup.getComponents();
	for (int i = 0; i < components.length; i++) {
	    if ( components[i] instanceof JMenuItem &&
		 actionCommand.equals (((JMenuItem)components[i]).getActionCommand()) ) {
		popup.remove (components[i]);
		return;
	    }
	}
    }

    /*********************************************************************
     * Remove a menu separator (that is after menu item given by an
     * action command string) from a popup menu.
     ********************************************************************/
    protected void removeSeparatorAfter (String actionCommand) {
	if (popup == null) return;
	Component[] components = popup.getComponents();
	boolean found = false;
	for (int i = 0; i < components.length; i++) {
	    if ( components[i] instanceof JSeparator && found) {
		popup.remove (components[i]);
		return;
	    }
	    found = ( components[i] instanceof JMenuItem &&
		      actionCommand.equals (((JMenuItem)components[i]).getActionCommand()) );
	}
    }

    /*********************************************************************
     * Enable/disable an item (indicated by an 'actionCommand') in the
     * popup menu.
     ********************************************************************/
    protected void setEnabledPopupItem (String actionCommand, boolean enabled) {
	AbstractPanel.setEnabledMenuItem (popup, actionCommand, enabled);
    }


}
