/*
 * Copyright (c) 2005-2007 Laf-Widget Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *  o Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer. 
 *     
 *  o Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution. 
 *     
 *  o Neither the name of Laf-Widget Kirill Grouchnikov nor the names of 
 *    its contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 *     
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
package org.jvnet.lafwidget.list;

import java.awt.*;
import java.awt.datatransfer.Transferable;
import java.awt.event.*;
import java.util.*;
import java.util.List;

import javax.swing.*;

import org.jvnet.lafwidget.LafWidgetUtilities;
import org.jvnet.lafwidget.utils.FadeTracker;
import org.jvnet.lafwidget.utils.FadeTracker.FadeKind;

/**
 * Animates moving list cells out of the way for a potential drop. Thanks to
 * Neil Cochran/keilly for a base visualization:
 * http://jroller.com/page/swinguistuff?entry=animated_jlist
 * 
 * Contributed under BSD license by Timothy Wall. See <a
 * href="http://abbot.sourceforge.net/demo/">http://abbot.sourceforge.net/demo/</a>
 * for the original code.
 * 
 * Implementation changed to use {@link FadeTracker} API.
 * 
 * @author Timothy Wall
 * @author Kirill Grouchnikov
 */
public class SmoothListDrop {

	public static final FadeTracker.FadeKind SMOOTH_LIST_DROP_FADE_KIND = new FadeTracker.FadeKind(
			"lafwidget.internal.smoothListDrop");

	/**
	 * This decorator completely over-paints the target JList, optionally
	 * painting a dragged item and animating creation of a space for the dragged
	 * item to be dropped.
	 */
	static abstract class DropSmoother extends AbstractComponentDecorator {
		/**
		 * Animation repaint interval. Make this larger to slow down the
		 * animation.
		 */
		// private static final int INTERVAL = 1000 / 24;
		// private static Timer timer = new Timer(true);
		private long fadeInstanceId;

		/** Simple decorator to provide the ghosted drag image. */
		private final class GhostedDragImage extends AbstractComponentDecorator {
			private int index;
			private Point location;
			private Point offset;

			public GhostedDragImage(int cellIndex, Point origin) {
				super(list);
				this.index = cellIndex;
				Rectangle b = list.getCellBounds(index, index);
				location = origin;
				this.offset = new Point(0, origin.y - b.y);
			}

			public void setLocation(Point where) {
				this.location = where;
				repaint();
			}

			public void paint(Graphics g) {
				Rectangle b = list.getCellBounds(index, index);
				Point origin = new Point(0, location.y - offset.y);
				origin.y = Math.max(0, origin.y);
				origin.y = Math.min(origin.y, list.getHeight() - b.height);
				g = g.create(origin.x, origin.y, b.width, b.height);
				((Graphics2D) g).translate(-b.x, -b.y);
				((Graphics2D) g).setComposite(AlphaComposite.getInstance(
						AlphaComposite.SRC_OVER, 0.5f));
				list.paint(g);
			}
		}

		// private final class Counter extends TimerTask {
		// public void run() {
		// synchronized (DropSmoother.this) {
		// if (reposition()) {
		// repaint();
		// }
		// }
		// }
		// }
		//
		// private Counter counter;
		/** Index of insertion point. */
		private int insertionIndex = -1;
		/** Index of object being dragged, if any. */
		private int draggedIndex = -1;
		private JList list;
		private Map<Integer, Rectangle> bounds = new TreeMap<Integer, Rectangle>();
		private GhostedDragImage dragImage;
		private Point origin;

		public DropSmoother(final JList list) {
			super(list);
			this.list = list;

			this.fadeInstanceId = FadeTracker.getInstance().trackFadeLooping(
					SMOOTH_LIST_DROP_FADE_KIND,
					// AnimationKind.SLOW,
					LafWidgetUtilities.getAnimationKind(list), list, null,
					false, new FadeTracker.FadeTrackerCallback() {
						double prevFadeCycle = 0.0;

						public void fadeEnded(FadeKind fadeKind) {
						}

						public void fadePerformed(FadeKind fadeKind,
								float fadeCycle10) {
							// System.out.println(fadeCycle10);
							synchronized (DropSmoother.this) {
								// if ((int) prevFadeCycle != (int) fadeCycle10)
								// {
								if (reposition()) {
									// System.out.println("repaint");
									repaint();
								}
								// }
								prevFadeCycle = fadeCycle10;
							}
						}
					}, false);
			//			
			// counter = new Counter();
			// timer.schedule(counter, INTERVAL, INTERVAL);
		}

		public void stop() {
			FadeTracker.getInstance().cancelFadeInstance(this.fadeInstanceId);
		}

		protected Object getPlaceholder() {
			return "";
		}

		protected abstract void move(int fromIndex, int toIndex);

		protected abstract void init(int index);

		protected void drop(Transferable t, int index) {
		}

		private void initialize(Point where) {
			insertionIndex = draggedIndex = -1;
			origin = where;
			int size = list.getModel().getSize();
			bounds.clear();
			for (int i = 0; i < size; i++) {
				bounds.put(i, getCellBoundsAfterInsertion(i));
			}
		}

		/** Track a drag which originated somewhere else. */
		public synchronized void startDragOver(Point where) {
			initialize(where);
			insertionIndex = getIndex(where, false);
		}

		/** Stop tracking an external drag. */
		public synchronized void endDragOver(Point where, Transferable t) {
			int idx = getIndex(where, false);
			if (idx != -1) {
				drop(t, idx);
			}
		}

		/** Start an internal drag. */
		public synchronized void startDrag(Point where) {
			// list.setSelectedIndex(-1);
			initialize(where);
			draggedIndex = insertionIndex = getIndex(where, true);
			list.setSelectedIndex(draggedIndex);
			dragImage = new GhostedDragImage(draggedIndex, origin);

			init(draggedIndex);
		}

		/** End an internal drag. */
		public synchronized void endDrag(Point where) {
			int toIndex = getIndex(where, true);
			int fromIndex = draggedIndex;
			dragImage.dispose();
			dragImage = null;
			draggedIndex = insertionIndex = -1;
			if (toIndex != -1 && toIndex != fromIndex) {
				Map newBounds = new TreeMap();
				newBounds.put(toIndex, bounds.get(new Integer(fromIndex)));
				if (fromIndex < toIndex) {
					for (int i = fromIndex + 1; i <= toIndex; i++) {
						newBounds.put(i - 1, bounds.get(i));
					}
				} else {
					for (int i = toIndex; i < fromIndex; i++) {
						newBounds.put(i + 1, bounds.get(i));
					}
				}
				bounds.putAll(newBounds);
				move(fromIndex, toIndex);
			}
		}

		private boolean reposition() {
			boolean changed = false;
			for (Integer x : bounds.keySet()) {
				Rectangle current = getCurrentCellBounds(x.intValue());
				Rectangle end = getCellBoundsAfterInsertion(x.intValue());
				if (current.x != end.x || current.y != end.y) {
					int xdelta = (end.x - current.x) / 2;
					int ydelta = (end.y - current.y) / 2;
					if (xdelta == 0)
						current.x = end.x;
					else
						current.x += xdelta;
					if (ydelta == 0)
						current.y = end.y;
					else
						current.y += ydelta;
					bounds.put(x, current);
					changed = true;
				}
			}
			return changed;
		}

		private int getIndex(Point where, boolean restrict) {
			int idx = list.locationToIndex(where);
			if (!restrict) {
				int size = list.getModel().getSize();
				// Assumes the list considers points below the last item
				// be within last item
				Rectangle last = list.getCellBounds(size - 1, size - 1);
				if (idx == size - 1 && where.y > last.y + last.height) {
					idx = size;
				}
			}
			return idx;
		}

		public synchronized void setInsertionLocation(Point where) {
			// Avoid painting focus and/or selection bgs, kind of a hack
			getPainter().requestFocus();
			list.clearSelection();
			setInsertionIndex(getIndex(where, draggedIndex != -1));
			dragImage.setLocation(where);
		}

		public synchronized void setInsertionIndex(int idx) {
			if (idx != insertionIndex) {
				insertionIndex = idx;
				repaint();
			}
		}

		private Rectangle getCellBoundsAfterInsertion(int index) {
			Rectangle r = list.getCellBounds(index, index);
			if (draggedIndex != -1) {
				if (index > draggedIndex) {
					if (index <= insertionIndex) {
						Rectangle r2 = list.getCellBounds(draggedIndex,
								draggedIndex);
						r.y -= r2.height;
					}
				} else if (index < draggedIndex) {
					if (index >= insertionIndex) {
						Rectangle r2 = list.getCellBounds(draggedIndex,
								draggedIndex);
						r.y += r2.height;
					}
				} else {
					Rectangle r2 = list.getCellBounds(insertionIndex,
							insertionIndex);
					r.y = r2.y;
				}
			} else if (insertionIndex != -1 && index > insertionIndex) {
				ListCellRenderer rnd = list.getCellRenderer();
				Component c = rnd.getListCellRendererComponent(list,
						getPlaceholder(), insertionIndex, false, false);
				r.y += c.getHeight();
			}
			return r;
		}

		private Rectangle getCurrentCellBounds(int cellIndex) {
			Rectangle r = getCellBoundsAfterInsertion(cellIndex);
			Rectangle r2 = bounds.get(cellIndex);
			if (r2 != null) {
				r.x = r2.x;
				r.y = r2.y;
			}
			return r;
		}

		public synchronized void paint(Graphics g) {
			boolean db = list.isDoubleBuffered();
			list.setDoubleBuffered(false);
			try {
				Rectangle b = getDecorationBounds();
				g.setColor(list.getBackground());
				g.fillRect(b.x, b.y, b.width, b.height);
				for (int i = 0; i < list.getModel().getSize(); i++) {
					if (i == draggedIndex)
						continue;
					Rectangle r = getCurrentCellBounds(i);
					Graphics g2 = g.create(r.x, r.y, r.width, r.height);
					Rectangle r2 = list.getCellBounds(i, i);
					((Graphics2D) g2).translate(0, -r2.y);
					list.paint(g2);
				}
			} finally {
				list.setDoubleBuffered(db);
			}
		}

	}

	/**
	 * Simple JList-local drag/drop handler. Invokes the smoother according to
	 * user input. A similar method could be used to accept drags originating
	 * outside of the JList.
	 */
	static class Listener extends MouseAdapter implements MouseMotionListener {
		private DropSmoother smoother;
		private boolean dragActive;
		private Point origin;

		public Listener(DropSmoother smoother) {
			this.smoother = smoother;
		}

		private boolean sufficientMove(Point where) {
			int dx = Math.abs(origin.x - where.x);
			int dy = Math.abs(origin.y - where.y);
			return (dx * dx + dy * dy) > 25;
		}

		public void mousePressed(MouseEvent e) {
			origin = e.getPoint();
		}

		public void mouseReleased(MouseEvent e) {
			if (dragActive) {
				smoother.endDrag(e.getPoint());
				dragActive = false;
			}
		}

		public void mouseDragged(MouseEvent e) {
			if (!dragActive) {
				if (sufficientMove(e.getPoint())) {
					smoother.startDrag(origin);
					dragActive = true;
				}
			}
			if (dragActive)
				smoother.setInsertionLocation(e.getPoint());
		}

		public void mouseExited(MouseEvent e) {
			if (dragActive)
				smoother.setInsertionIndex(-1);
		}

		public void mouseEntered(MouseEvent e) {
			if (dragActive)
				smoother.setInsertionLocation(e.getPoint());
		}

		public void mouseMoved(MouseEvent e) {
		}

		public void stop() {
			this.smoother.stop();
		}
	}

	public static class MyListModel extends AbstractListModel implements
			MutableListModel {
		private List DATA;

		public MyListModel() {
			this.DATA = new ArrayList();
			for (int i = 0; i < 3000; i++)
				this.DATA.add("item " + i);
		}

		public int getSize() {
			return DATA.size();
		}

		public Object getElementAt(int index) {
			return DATA.get(index);
		}

		public void insert(int index, Object value) {
			DATA.add(index, value);
		}

		public void remove(int index) {
			DATA.remove(index);
		}

		public void move(int fromIndex, int toIndex) {
			Object value = DATA.remove(fromIndex);
			DATA.add(toIndex, value);
		}

	}

	/**
	 * Throw up a frame to demonstrate the smoother at work. Funkify the list
	 * renderer to demonstrate that customized renderers are handled properly.
	 */
	public static void main(String[] args) {
		JFrame f = new JFrame("Smooth List Drop");
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		final MyListModel listModel = new MyListModel();
		final JList list = new JList(listModel);
		DropSmoother smoother = new DropSmoother(list) {
			protected void move(int fromIndex, int toIndex) {
				listModel.move(fromIndex, toIndex);
				// Object o = DATA.remove(fromIndex);
				// DATA.add(toIndex, o);
				list.revalidate();
				list.repaint();
			}

			@Override
			protected void init(int index) {
			}
		};
		Listener listener = new Listener(smoother);
		list.addMouseListener(listener);
		list.addMouseMotionListener(listener);
		// list.setCellRenderer(new DefaultListCellRenderer() {
		// public Component getListCellRendererComponent(JList list,
		// Object value, int index, boolean sel, boolean focus) {
		// Component c = super.getListCellRendererComponent(list, value,
		// index, sel, focus);
		// Color bg = c.getBackground();
		// if ((index % 2) == 0) {
		// int GRAY = 190;
		// c.setBackground(new Color((GRAY + bg.getRed() * 2) / 3,
		// (GRAY + bg.getGreen() * 2) / 3, (GRAY + bg
		// .getBlue() * 2) / 3));
		// ((JComponent) c).setOpaque(true);
		// } else if (c.getBackground().equals(list.getBackground())) {
		// ((JComponent) c).setOpaque(false);
		// }
		// if (value.toString().startsWith("t")) {
		// c.setForeground(Color.blue);
		// } else if (value.toString().startsWith("f")) {
		// Font font = list.getFont();
		// c.setFont(font.deriveFont(Font.ITALIC));
		// }
		// return c;
		// }
		// });
		f.getContentPane().add(new JScrollPane(list));
		f.pack();
		f.setVisible(true);
	}
}
