package lu.tudor.santec.gecamed.core.gui.widgets;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;

/**
 * @author jens.ferring@tudor.lu
 * 
 * This is a modification of the class BasicListUI.Handler.<br>
 * <br>
 * This class takes a JList and changes the selection mode in the way,<br>
 * that the selection is not removed, if clicking another entry. This<br>
 * is like having the CTRL-key pressed all the time.<br>
 * <br>
 * THIS CLASS IS NOT SUPPOSED TO BE USED WITH A JLIST HAVING DRAG ENABLED<br>
 * AND ANY OTHER SELECTION MODE, THAN MULTI_INTERVAL_SELECTION!!!
 */
public class KeepSelectionHandler implements MouseListener, MouseMotionListener
{
	/* ======================================== */
	// 		CONSTANTS
	/* ======================================== */
	
	private static final String BasicListUI_Handler_CLASS = "class javax.swing.plaf.basic.BasicListUI$Handler";
	
	/* ======================================== */
	// 		MEMBERS
	/* ======================================== */
	
	private MouseListener 		defaultMouseListener;
	private MouseMotionListener defaultMouseMotionListener;
	
	private JList list;
	
	private boolean dragging = false;
	
	
	
	/* ======================================== */
	// 		CONSTRUCTOR
	/* ======================================== */
	
	private KeepSelectionHandler (JList list)
	{
		this.list = list;
	}
	
	
	/**
	 * Use this method to the MouseListener and MouseMotionListener<br>
	 * of the given JList from the default to this class.
	 * 
	 * @param list the JList to be modified
	 */
	public static void keepSelection (JList list)
	{
		KeepSelectionHandler handler = new KeepSelectionHandler (list);
		
		MouseListener[] ml = list.getMouseListeners();
		for (MouseListener l : ml)
		{
			if (l != null && BasicListUI_Handler_CLASS.equals(l.getClass().toString()))
			{
				handler.defaultMouseListener = l;
				list.removeMouseListener(l);
				break;
			}
		}
		list.addMouseListener(handler);
		
		MouseMotionListener[] mml = list.getMouseMotionListeners();
		for (MouseMotionListener l : mml)
		{
			if (l != null && BasicListUI_Handler_CLASS.equals(l.getClass().toString()))
			{
				handler.defaultMouseMotionListener = l;
				list.removeMouseMotionListener((MouseMotionListener) l);
				break;
			}
		}
		
		if (ml == null || mml == null)
			throw new RuntimeException(new StringBuffer("Couldn't remove the default ")
					.append("Mouse").append(ml==null?"":"Motion").append("Listener")
					.append(" (").append(BasicListUI_Handler_CLASS).append(")")
					.toString());
		
		list.addMouseMotionListener(handler);
	}
	
	
	
	/* ======================================== */
	// 		ADEPTED METHODS
	/* ======================================== */
	
	private void adjustSelection(MouseEvent e) 
	{
		int row = loc2IndexFileList(list, e.getPoint());
		if (row < 0) 
			return;
		
		int 	anchorIndex = adjustIndex(list.getAnchorSelectionIndex(), list);
		boolean anchorSelected;
		
		if (anchorIndex == -1) 
		{
			anchorIndex = 0;
			anchorSelected = false;
		} 
		else 
			anchorSelected = list.isSelectedIndex(anchorIndex);

		if (e.isShiftDown()) 
		{
			if (anchorSelected) 
				 list.addSelectionInterval(anchorIndex, row);
			else list.removeSelectionInterval(anchorIndex, row);
		} 
		else if (list.isSelectedIndex(row)) 
			list.removeSelectionInterval(row, row);
		else 
			list.addSelectionInterval(row, row);
	}

	
	private int adjustIndex (int index, JList list) 
	{
		return index < list.getModel().getSize() ? index : -1;
	}
	
	
	/**
	 * @return <code>true</code>, if any settings made for the JList<br>
	 * are not compatible with the function of this class.
	 */
	private boolean checkBadListSettings ()
	{
		return list.getSelectionMode() != ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
				|| list.getDragEnabled();
	}
	

	
	/* ======================================== */
	// 		MOUSE (MOTION) EVENTS
	/* ======================================== */
	
	public void mousePressed(MouseEvent e)
	{
		if (checkBadListSettings())
		{
			// this class does not support dragging
			// use the default listener instead ...
			defaultMouseListener.mousePressed(e);
			return;
		}
		
		if (shouldIgnore(e, list)) 
			return;
		
		// When drag is enabled mouse drags won't change the selection
		// in the list, so we only set the isAdjusting flag when it's
		// not enabled
		list.setValueIsAdjusting(true);
		
		adjustFocus(list);
		adjustSelection(e);
	}
	

	public void mouseReleased(MouseEvent e)
	{
		if (checkBadListSettings())
		{
			// this class does not support dragging
			// use the default listener instead ...
			defaultMouseListener.mouseReleased(e);
			return;
		}
		
		if (shouldIgnore(e, list))
			return;

		list.setValueIsAdjusting(false);
		
		if (dragging)
			dragging = false;
	}


	public void mouseDragged(MouseEvent e)
	{
		if (checkBadListSettings())
		{
			// this class does not support dragging
			// use the default listener instead ...
			defaultMouseMotionListener.mouseDragged(e);
			return;
		}
		
		if (shouldIgnore(e, list)) {
			return;
		}
		
		dragging = true;
		
		int modifiers = e.getModifiersEx() | KeyEvent.SHIFT_DOWN_MASK;
		
		MouseEvent me = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), modifiers, 
				e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger(), e.getButton());
		
		adjustSelection(me);
	}


	public void mouseClicked(MouseEvent e) 
	{
		if (checkBadListSettings())
		{
			// this class does not support dragging
			// use the default listener instead ...
			defaultMouseListener.mouseClicked(e);
		}
	}
	
	public void mouseEntered(MouseEvent e) 
	{
		if (checkBadListSettings())
		{
			// this class does not support dragging
			// use the default listener instead ...
			defaultMouseListener.mouseEntered(e);
		}
	}
	
	public void mouseExited(MouseEvent e)
	{
		if (checkBadListSettings())
		{
			// this class does not support dragging
			// use the default listener instead ...
			defaultMouseListener.mouseExited(e);
		}
	}
	
	public void mouseMoved(MouseEvent e) 
	{
		if (checkBadListSettings())
		{
			// this class does not support dragging
			// use the default listener instead ...
			defaultMouseMotionListener.mouseMoved(e);
		}
	}
	
	
	public static void main(String[] args)
	{
		int i = 1;
		JFrame 	frame 	= new JFrame("KeepSelectionTest");
		JList 	list 	= new JList( new Object[] 
			{("entry "+i++), ("entry "+i++), ("entry "+i++), ("entry "+i++), ("entry "+i++), ("entry "+i++),
			 ("entry "+i++), ("entry "+i++), ("entry "+i++), ("entry "+i++), ("entry "+i++), ("entry "+i++)});
		
		keepSelection(list);
		frame.add(new JScrollPane(list));
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setSize(250, 253);
		frame.setLocationRelativeTo(null);
		frame.setVisible(true);
	}
	
	
	
	/* ======================================== */
	// 	COPIED FROM SwingUtilities2 FROM JDK 1.6
	/* ======================================== */
	
	/**
     * Ignore mouse events if the component is null, not enabled, or the event
     * is not associated with the left mouse button.
     */
    public static boolean shouldIgnore(MouseEvent me, JComponent c) 
    {
        return c == null || !c.isEnabled()
                         || !SwingUtilities.isLeftMouseButton(me);
    }
    
    
    /**
     * Request focus on the given component if it doesn't already have it
     * and <code>isRequestFocusEnabled()</code> returns true.
     */
    public static void adjustFocus(JComponent c) 
    {
        if (!c.hasFocus() && c.isRequestFocusEnabled()) 
            c.requestFocus();
    }
    
    
    /**
     * A variation of locationToIndex() which only returns an index if the
     * Point is within the actual bounds of a list item (not just in the cell)
     * and if the JList has the "List.isFileList" client property set.
     * Otherwise, this method returns -1.
     * This is used to make WindowsL&F JFileChooser act like native dialogs.
     */
    public static int loc2IndexFileList(JList list, Point point) 
    {
        int index = list.locationToIndex(point);
        if (index != -1) 
        {
            Object bySize = list.getClientProperty("List.isFileList");
            if (bySize instanceof Boolean && ((Boolean)bySize).booleanValue() &&
                !pointIsInActualBounds(list, index, point)) 
            {
                index = -1;
            }
        }
        return index;
    }
    
    
    /**
     * Returns true if the given point is within the actual bounds of the
     * JList item at index (not just inside the cell).
     */
    private static boolean pointIsInActualBounds(JList list, int index, Point point) 
    {
        ListCellRenderer 	renderer 	= list.getCellRenderer();
        ListModel 			dataModel 	= list.getModel();
        Object 				value 		= dataModel.getElementAt(index);
        Component 			item 		= renderer.getListCellRendererComponent(
        									list, value, index, false, false);
        Dimension 			itemSize 	= item.getPreferredSize();
        Rectangle 			cellBounds 	= list.getCellBounds(index, index);
        
		if (!item.getComponentOrientation().isLeftToRight()) 
		    cellBounds.x 	+= (cellBounds.width - itemSize.width);
		
        cellBounds.width 	= itemSize.width;
        cellBounds.height 	= itemSize.height;
        
		return cellBounds.contains(point);
    }
}
