
package org.openspml.browser;

import java.net.URL;
import java.util.List;
import java.util.ArrayList;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.font.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import javax.swing.table.*;
import javax.swing.text.JTextComponent;

public class SwingUtil {

    //////////////////////////////////////////////////////////////////////
    //
    // Misc
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Add a damn component to a scroll pane, this is always what
     * you want, but its so obscure I never remember.
     */
    public static void add(JScrollPane scroll, JComponent comp) {

	scroll.getViewport().add(comp, null);
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Navigation
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Search up the hierarchy for the nearest JFrame.
     * If we're already one one return it.  Is this ok, can	
     * you have a JFrame inside another?
     * 
     * NOTE: For determining dialog parentage, this is relatively useless
     * since the component could be inside a modal JDialog or a JFrame. 
     * So you really need to call getOwner, test the type, and have
     * redundant dialog constructors.
     */
    public static JFrame getParentFrame(Component component) {
	
	JFrame frame = null;

	if (component instanceof JFrame) 
	    frame = (JFrame)component;
	else {
	    while (component != null) {
		component = component.getParent();
		if (component instanceof JFrame) {
		    frame = (JFrame)component;
		    break;
		}
	    }
	}

	return frame;
    }

    /**
     * Search up the hierarchy for the nearest JDialog.
     */
    public static JDialog getParentDialog(Component component) {
	
	JDialog dialog = null;

	if (component instanceof JDialog) 
	    dialog = (JDialog)component;
	else {
	    while (component != null) {
		component = component.getParent();
		if (component instanceof JDialog) {
		    dialog = (JDialog)component;
		    break;
		}
	    }
	}

	return dialog;
    }

    /**
     * Search up the hierarchy for the nearest Window that would
     * serve as the owner for a dialog.  This will either be a JFrame
     * or a JDialog.
     */
    public static Window getOwner(Component component) {
	
	Window owner = null;

	if (component instanceof Window)
	    owner = (Window)component;
	else {
	    while (component != null) {
		component = component.getParent();
		if (component instanceof Window) {
		    owner = (Window)component;
		    break;
		}
	    }
	}

	return owner;
    }

    /**
     * Search downward looking for a subcomponent of the given class.
     */
    public static Component findComponent(Component component, Class c) {
	
	Component found = null;

	if (component != null) {
	    if (component.getClass() == c) 
		found = component;
	    else if (component instanceof Container) {
		Container container = (Container)component;
		int max = container.getComponentCount();
		for (int i = 0 ; i < max && found == null ; i++) {
		    Component child = container.getComponent(i);
		    found = findComponent(child, c);
		}
	    }
	}

	return found;
    }

    /**
     * Determine the location of a component relative to another.
     * This has to exist somewhere, but I couldn't find it.
     */
    public static Point getRelativeLocation(Component parent, 
					    Component child) {

	Point p = new Point();

	Container cont = (Container)child.getParent();
	if (cont != null) {
	    do {
		Point ploc = cont.getLocation();
		p.x += ploc.x;
		p.y += ploc.y;
		cont = cont.getParent();
	    }
	    while (cont != null && cont != parent);
	}

	return p;
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Events
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Test to see if the mouse event represents an action by
     * the right mouse button.
     * <p>
     * isPopupTrigger seems to always return false so use
     * the modifier bits instead.
     */
    public static boolean isRightButton(MouseEvent e) {

	boolean right = false;

	int mods = e.getModifiers();
	right = ((mods & InputEvent.BUTTON3_MASK) != 0);

	return right;
    }

    /**
     * Fire actionPerformed events for a list of listeners.
     */
    public static void fireActionPerformed(List listeners, ActionEvent e) {
	if (listeners != null && e != null) {
	    for (int i = 0 ; i < listeners.size() ; i++) {
		ActionListener al = (ActionListener)listeners.get(i);
		al.actionPerformed(e);
	    }
	}
    }

    /**
     * Request focus for a component.  This is actually quite complicated.
     * Component.requstFocus will only have an effect if the component
     * is visible.  This means that if you call this before a dialog
     * is displayed it has no effect.  If the dialog is not modal, 
     * you can call this after the show() but not if it is modal.
     * You can try SwingUtilities.invokeLater, but that is not 
     * guaranteed to run after the component is visible.
     * A similar problem exists in dialogs where we are dynamically
     * adding new fields (typically JTextFields) and want to 
     * move the focus to them.
     * 
     * This solution was found on the web, 
     * we issue a FOCUS_GAINED event which apparently
     * triggers the necessary machinery.  This will be processed
     * in the event queue, always after the component has
     * been visualized.
     *
     * The source must be passed in, you'll get a NullPointer if not
     *
     * This doesn't really work either, the JTextField gets a cursor
     * and thinks its alive, but the original field still has focus.
     */
    public static void requestFocus(final Component source, 
				    final Component target) {

	SwingUtilities.invokeLater(new Runnable() {
		public void run() {
		    FocusEvent ev = 
			new FocusEvent(source, FocusEvent.FOCUS_GAINED);  
		    target.dispatchEvent(ev);
		}
	    });
    }

    /**
     * Trick to get the focus set to a particular component
     * once a window is visible.
     */
    public static void requestFocus(Window parent, final Component target) {

	parent.addWindowListener(new WindowAdapter() {
		public void windowOpened(WindowEvent e) {
		    SwingUtilities.invokeLater(new Runnable() {
			    public void run() {
				target.requestFocus();
			    }
			});
		}
	    });
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Text
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Get the value of a text component, collapsing empty strings to null.
     */
    public static String getText(JTextComponent c, boolean trim) {

	String s = c.getText();
	if (s != null) {
	    if (trim)
		s = s.trim();
	    if (s.length() == 0)
		s = null;
	}	
	return s;
    }

    /**
     * Get the value of a text component, trimming whitespace.
     */
    public static String getText(JTextComponent c) {
	return getText(c, true);
    }

    /**
     * Get the effective Graphics object for a component.
     */
    public static Graphics getGraphics(JComponent comp) {

	Graphics g = comp.getGraphics();
	if (g == null) {
	    JFrame frame = getParentFrame(comp);
	    if (frame != null)
		g = frame.getGraphics();
	}
	return g;
    }

    /**
     * Given a component and a text string, return the domension of the 
     * string as rendered in that component.
     */
    public static Dimension getStringDimension(JComponent comp, Font font, 
					       String string) {
	
	return getStringDimension(getGraphics(comp), font, string);
    }

    /**
     * Given a Graphics and a text string, return the domension of the 
     * string as rendered in that component.
     * <p>
     * It seems like there ought to be an easier way to do this...
     */
    public static Dimension getStringDimension(Graphics g, Font font, 
					       String string) {
	
	int width = 0;
	int height = 0;

	// can't pass a null FontRenderContext, so better be a Graphics2D
	if (g instanceof Graphics2D) {
	    Graphics2D g2 = (Graphics2D)g;

	    if (font == null)
		font = g.getFont();

	    FontRenderContext context = g2.getFontRenderContext();
	    Rectangle2D rec = font.getStringBounds(string, context);
	    
	    width = (int)rec.getWidth();
	    height = (int)rec.getHeight();

	    // System.out.println("Calculated string width " + 
	    // Util.itoa(width));

	}
	else {
	    System.out.println("WARNING: getStringDimension called" +
			       " without Graphics2D");
	}

	return new Dimension(width, height);
    }

    /**
     * Convert a string containing newlines into an <html> document
     * suitable for use in buidling a multi-line JLabel.
     */
    public static String getHtmlLabel(String src) {

	String html = null;
	if (src != null) {
	    src = src.trim();
	    StringBuffer b = new StringBuffer();
	    b.append("<html>");
	    for (int i = 0 ; i < src.length() ; i++) {
		char ch = src.charAt(i);
		if (ch == '\n')
		    b.append("<p>");
		else
		    b.append(ch);
	    }
	    b.append("</html>");
	    html = b.toString();
	}

	return html;
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Trees
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Calculate the TreePath to a node.
     */
    public static TreePath getPath(JTree tree, TreeNode node) {
		
	TreePath path = null;

	TreeModel m = tree.getModel();
	if (m instanceof DefaultTreeModel) {
	    DefaultTreeModel model = (DefaultTreeModel)m;
	    TreeNode[] nodes = model.getPathToRoot(node);
	    if (nodes != null)
		path = new TreePath(nodes);
	}

	return path;
    }

    /**
     * Given a JTree and a TreeNode within it, expand the tree down 
     * to that node.
     */
    public static void showNode(JTree tree, TreeNode node) {

	if (tree != null && node != null) {
	    TreePath path = getPath(tree, node);
	    if (path != null) {

		// this seems to expand the node as well, which 
		// turns out to be what we've been expecting in 
		// some places so leave it in
		tree.expandPath(path);

		tree.scrollPathToVisible(path);
	    }
	}
    }

    /**
     * Given a JTree and a TreeNode within it, expand the tree down 
     * to that node.
     */
    public static void expandNode(JTree tree, TreeNode node) {

	if (tree != null && node != null) {
	    TreePath path = getPath(tree, node);
	    if (path != null)
		tree.expandPath(path);
	}
    }

    /**
     * Given a JTree and an object assumed to be a TreeNode, select the node.
     * Assumes the model is DefaultTreeModel.
     */
    public static void selectNode(JTree tree, TreeNode node) {

	if (tree != null && node != null) {
	    TreePath path = getPath(tree, node);
	    if (path != null) {
		tree.setSelectionPath(path);
		tree.scrollPathToVisible(path);
	    }
	}
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Dialogs
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Repack the dialog that contains a component.
     */
    public static void packDialog(Component c) {
	JDialog d = getParentDialog(c);
	if (d != null)
	    d.pack();
    }

    /**
     * Center a window relative to the screen.
     */
    public static void centerWindow(Window window) {

	Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
	Dimension frameSize = window.getSize();
	if (frameSize.height > screenSize.height)
	    frameSize.height = screenSize.height;
	if (frameSize.width > screenSize.width)
	    frameSize.width = screenSize.width;

	window.setLocation((screenSize.width - frameSize.width) / 2, 
			   (screenSize.height - frameSize.height) / 2);
    }


    /**
     * Center a window over the screen, and display it.
     * This should be usable for JWindow, JFrame, and JDialog.
     */
    public static void showWindow(Window window) {

	centerWindow(window);
	window.show();
    }

    /**
     * Center a dialog box over a frame.
     */
    public static void centerDialog(JFrame frame, JDialog window) {
	
	if (frame != null && window != null) {
	    
	    Dimension d = frame.getSize();
	    int x = (int)((d.getWidth() - window.getWidth()) / 2);
	    int y = (int)((d.getHeight() - window.getHeight()) / 2);

	    window.setLocation(new Point(x, y));
	    window.setLocationRelativeTo(frame);
	}
    }

    /**
     * Center a dialog box over the frame and show it.
     */
    public static void showDialog(Component comp, JDialog dialog) {
	showDialog(getParentFrame(comp), dialog);
    }

    /**
     * Center a dialog box over the frame and show it.
     */
    public static void showDialog(JFrame frame, JDialog dialog) {
	
	if (frame != null)
	    centerDialog(frame, dialog);
	else
	    centerWindow(dialog);

	dialog.show();
    }

    /**
     * Having lots of trouble positioning dialogs relative to a component.
     * This does it, though it would seem setLocationRelativeTo 
     * should work.
     */
    public static void showDialog(JComponent parent, int x, int y, 
				  JDialog dialog) {

	JFrame frame = getParentFrame(parent);
	if (frame == null)
	    System.out.println("Warning: no frame");

	// future note: setLocationRelativeTo appears 
	// to be counter intuitive
	// this little wart doesn't appear to work, the location
	// seems to always end up in the center
	// td.setLocation(new Point(_popupX, _popupY));
	// td.setLocationRelativeTo(this);
	    
	Point p1 = frame.getLocation();
	Point p2 = getRelativeLocation(frame, parent);
	Point p3 = new Point(p1.x + p2.x + x, p1.y + p2.y + y);

	dialog.setLocation(p3);
	dialog.show();
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Notifications
    //
    //////////////////////////////////////////////////////////////////////

    public static void displayError(String msg, Throwable t) {

	String text = msg + "\n" + t.toString();
	
	JOptionPane.showMessageDialog(null, text, "Exception", 
				      JOptionPane.ERROR_MESSAGE);

    }

    public static void displayError(Throwable t) {

	JOptionPane.showMessageDialog(null, t.toString(), "Exception", 
				      JOptionPane.ERROR_MESSAGE);

    }

    public static void displayError(String msg) {

	JOptionPane.showMessageDialog(null, msg, "Error", 
				      JOptionPane.ERROR_MESSAGE);

    }

    public static void displayMessage(String title, String msg) {

	JOptionPane.showMessageDialog(null, msg, title,
				      JOptionPane.INFORMATION_MESSAGE);

    }

    //////////////////////////////////////////////////////////////////////
    //
    // Graphics
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Get an image, waiting as necessary.
     */
    public static Image getImage(URL url) {

	Image image = null;
	if (url != null) {
	    Toolkit tk = Toolkit.getDefaultToolkit();
	    image = tk.getImage(url);

	    // this seems to be enough to force it into memory?
	    // I haven't had to mess with ImageObserver yet...
	    // UPDATE: this seems to work most of the time, but
	    // I've seen a few cases where an image doesn't display
	    // initially, probably need to use an observer
	    
	    boolean status = tk.prepareImage(image, -1, -1, null);
	}

	return image;
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Tables
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Adjust the preferred size of each table column according to the
     * maximum width of the cells in the column.
     */
    public static void sizeColumns(JTable table) {

	TableColumnModel tcm = table.getColumnModel();
	int ncols = tcm.getColumnCount();
	for (int c = 0 ; c < ncols ; c++) {
	    TableColumn col = tcm.getColumn(c);
	    
	    // calculate header width
	    int headerWidth = 0;
	    TableCellRenderer rend = col.getHeaderRenderer();
	    if (rend != null) {
		Component comp = 
		    rend.getTableCellRendererComponent(table, 
						       col.getHeaderValue(),
						       false, false, 
						       0, 0);
		if (comp != null)
		    headerWidth = comp.getPreferredSize().width;
	    }

	    // find widest data cell
	    int width = 0;
	    int max = 0;
	    for (int r = 0 ; r < table.getRowCount() ; r++) {
		rend = table.getCellRenderer(r, c);
		if (rend != null) {
		    Component comp = 
			rend.getTableCellRendererComponent(table, table.getValueAt(r, c),
							   false, false, 
							   r, c);
		    if (comp != null) {
			width = comp.getPreferredSize().width;
			max = (width > max) ? width : max;
		    }
		}
	    }

	    max = (headerWidth > max) ? headerWidth : max;

	    // set min size rather than preferred size, if the area
	    // is wider than the preferred sizes, it won't expand to fill
	    col.setMinWidth(max);
	}
    }

    /**
     * In theory we would have to render every cell, but assume
     * that the 0,0 cell is representative of the others.
     */
    public static void sizeHeight(JTable table) {
	
	int width = 0;
	int height = 0;

	TableModel model = table.getModel();
	int rows = model.getRowCount();

	// do not include the header height, JScrollPane does that itself
	if (rows > 0) {
	    TableCellRenderer rend = table.getCellRenderer(0, 0);
	    Component comp = 
		rend.getTableCellRendererComponent(table, 
						   table.getValueAt(0, 0),
						   false, false, 0, 0);
	    int cellHeight = comp.getPreferredSize().height;

	    // System.out.println("sizeHeight: cellHeight = " +
	    // Util.itoa(cellHeight) + 
	    // " rows = " + 
	    // Util.itoa(rows));

	    // hmm, for empty cells this seems to come back 2, 
	    // for cells with text in them 19
	    // 17 seems to be exactly the right height for the 
	    // default font, need to figure out the right way 
	    // to calculate this!
	    cellHeight = 17;

	    // clamp this
	    if (rows > 10) rows = 10;

	    height += (cellHeight * rows);
	}

	Dimension d = new Dimension(width, height);
	table.setPreferredScrollableViewportSize(d);
    }

    /**
     * Awful kludge, we need to get the font height so we can set the preferred
     * size of the scroll pane.  Unfortunately, this requires that we be able
     * to obtain a Graphics2D object, and I'm not sure how to do that without
     * passing in the root JFrame.  
     * Try to avoid having to pass that in for now, this appears to be the
     * calculated font hight for the default font.
     */
    public static final int FONT_HEIGHT = 17;

    /**
     * Calculate the compact table height.
     * This is the original implementation from InfoTable do we need this?
     * This is way to kludgey.
     * It uses the JFrame to try to calculate string dimensions, 
     * try rewriting this using cell renderers instead.
     */
    public static void sizeHeightOld(JTable table, JFrame frame) {
	
	int width = 400;
	int height = 0;

	TableModel model = table.getModel();
	int rows = model.getRowCount();

	// do not include the header height, JScrollPane does that itself
	if (rows > 0) {
	    int fh = FONT_HEIGHT;
	    if (frame != null) {
		Graphics g = frame.getGraphics();
		Dimension d = SwingUtil.getStringDimension(g, null, "X");
		fh = d.height;
	    }

	    // was going to add cell padding, but the calculated 
	    // dimension seems to be about 1 or 2 pixels to large anyway, 
	    // this is too magic...
	    fh--;

	    // clamp this
	    if (rows > 10) rows = 10;

	    height += (fh * rows);
	}

	Dimension d = new Dimension(width, height);
	table.setPreferredScrollableViewportSize(d);
    }

    /**
     * Resize the table (and its containing Dialog if it has one)
     * after a structural change to the model.
     * Is all of this necessary?
     */
    public static void modelChanged(JTable table) {
	
	// calcuate optimial column sizes
	sizeColumns(table);

	// calculate optimal table height, you must do this otherwise
	// the preferred default height is 450
	sizeHeight(table);

	// for some odd reason, you need to call this in order to 
	// get the columns resized after changing the model, otherwise
	// you get a table with equal sized columns, looks like
	// a default of about 10 characters

	table.sizeColumnsToFit(-1);

	// invalidate the scroll pane
	table.getParent().invalidate();
	table.getParent().repaint();

	// invaliding this or our immediate parent doesn't work, have
	// to invalidate from the JDialog on down, probably could avoid passing
	// in JDialog by being smarter about traversing backwards in
	// the parent chain?

	JDialog dialog = getParentDialog(table);
	if (dialog != null) {
	    dialog.pack();
	}
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Menus
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Search a JPopupMenu for a sub JMenu with the given name.
     */
    public static JMenu findMenu(JPopupMenu popup, String name) {

	JMenu menu = null;

	int max = popup.getComponentCount();
	for (int i = 0 ; i < max ; i++) {
	    Component c = popup.getComponent(i);
	    if (c instanceof JMenu) {
		JMenu m = (JMenu)c;
		if (name.equals(m.getText())) {
		    menu = m;
		    break;
		}
	    }
	}

	return menu;
    }

    /**
     * Search a JPopupMenu for a JMenuitem with the given name.
     */
    public static JMenuItem findItem(JPopupMenu popup, String name) {

	JMenuItem item = null;

	int max = popup.getComponentCount();
	for (int i = 0 ; i < max ; i++) {
	    Component c = popup.getComponent(i);
	    if (c instanceof JMenuItem) {
		JMenuItem it = (JMenuItem)c;
		if (name.equals(it.getText())) {
		    item = it;
		    break;
		}
	    }
	}

	return item;
    }

    /**
     * Remove a JMenuItem from a JPopupMenu.
     */
    public static void removeItem(JPopupMenu popup, String name) {

	JMenuItem item = findItem(popup, name);
	if (item != null)
	    popup.remove(item);
    }

    //////////////////////////////////////////////////////////////////////
    //
    // JTabbedPane
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Determine which JTabbedPane tab is under a certain location.
     * Useful if you want to tell the difference between a menu
     * over a tab, and one over blank space.
     * Can also be used to popup tab specific menus, but since right
     * click selects the tab you can just use the current tab.
     */
    public static int getTabIndex(JTabbedPane tabs, Point point) {

	int index = -1;
	for (int i = 0 ; i < tabs.getTabCount() ; i++) {
	    Rectangle rec = tabs.getBoundsAt(i);
	    if (rec.contains(point)) {
		index = i;
		break;
	    }
	}
	return index;
    }



}
