// License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.gui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collection;
import java.util.LinkedList;

import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.KeyStroke;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Segment;
import org.openstreetmap.josm.data.osm.Way;

/**
 * Manages the selection of a rectangle. Listening to left and right mouse button
 * presses and to mouse motions and draw the rectangle accordingly.
 * 
 * Left mouse button selects a rectangle from the press until release. Pressing
 * right mouse button while left is still pressed enable the rectangle to move
 * around. Releasing the left button fires an action event to the listener given
 * at constructor, except if the right is still pressed, which just remove the
 * selection rectangle and does nothing.
 * 
 * The point where the left mouse button was pressed and the current mouse 
 * position are two opposite corners of the selection rectangle.
 * 
 * It is possible to specify an aspect ratio (width per height) which the 
 * selection rectangle always must have. In this case, the selection rectangle
 * will be the largest window with this aspect ratio, where the position the left
 * mouse button was pressed and the corner of the current mouse position are at 
 * opposite sites (the mouse position corner is the corner nearest to the mouse
 * cursor). 
 * 
 * When the left mouse button was released, an ActionEvent is send to the 
 * ActionListener given at constructor. The source of this event is this manager.
 * 
 * @author imi
 */
public class SelectionManager implements MouseListener, MouseMotionListener, PropertyChangeListener {

	/**
	 * This is the interface that an user of SelectionManager has to implement
	 * to get informed when a selection closes.
	 * @author imi
	 */
	public interface SelectionEnded {
		/**
		 * Called, when the left mouse button was released.
		 * @param r The rectangle, that is currently the selection.
		 * @param alt Whether the alt key was pressed
		 * @param shift Whether the shift key was pressed
		 * @param ctrl Whether the ctrl key was pressed 
		 * @see InputEvent#getModifiersEx()
		 */
		public void selectionEnded(Rectangle r, boolean alt, boolean shift, boolean ctrl);
		/**
		 * Called to register the selection manager for "active" property.
		 * @param listener The listener to register
		 */
		public void addPropertyChangeListener(PropertyChangeListener listener);
		/**
		 * Called to remove the selection manager from the listener list 
		 * for "active" property.
		 * @param listener The listener to register
		 */
		public void removePropertyChangeListener(PropertyChangeListener listener);
	}
	/**
	 * The listener that receives the events after left mouse button is released.
	 */
	private final SelectionEnded selectionEndedListener;
	/**
	 * Position of the map when the mouse button was pressed.
	 * If this is not <code>null</code>, a rectangle is drawn on screen. 
	 */
	private Point mousePosStart;
	/**
	 * Position of the map when the selection rectangle was last drawn.
	 */
	private Point mousePos;
	/**
	 * The Component, the selection rectangle is drawn onto.
	 */
	private final NavigatableComponent nc;
	/**
	 * Whether the selection rectangle must obtain the aspect ratio of the 
	 * drawComponent.
	 */
	private boolean aspectRatio;

	/**
	 * Create a new SelectionManager.
	 *
	 * @param actionListener The action listener that receives the event when
	 * 		the left button is released.
	 * @param aspectRatio If true, the selection window must obtain the aspect
	 * 		ratio of the drawComponent.
	 * @param navComp The component, the rectangle is drawn onto.
	 */
	public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) {
		this.selectionEndedListener = selectionEndedListener;
		this.aspectRatio = aspectRatio;
		this.nc = navComp;
	}
	
	/**
	 * Register itself at the given event source.
	 * @param eventSource The emitter of the mouse events.
	 */
	public void register(NavigatableComponent eventSource) {
		eventSource.addMouseListener(this);
		eventSource.addMouseMotionListener(this);
		selectionEndedListener.addPropertyChangeListener(this);
		Main.contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "SelectionManager");
        Main.contentPane.getActionMap().put("SelectionManager", new AbstractAction(){
			public void actionPerformed(ActionEvent e) {
				if (mousePos != null && mousePosStart != null)
					paintRect();
				mousePosStart = null;
				mousePos = null;
            }
        });
        eventSource.addPropertyChangeListener("scale", new PropertyChangeListener(){
			public void propertyChange(PropertyChangeEvent evt) {
				if (mousePosStart != null) {
					paintRect();
					mousePos = mousePosStart = null;
				}
            }
        });
	}
	/**
	 * Unregister itself from the given event source. If a selection rectangle is
	 * shown, hide it first.
	 *
	 * @param eventSource The emitter of the mouse events.
	 */
	public void unregister(Component eventSource) {
		eventSource.removeMouseListener(this);
		eventSource.removeMouseMotionListener(this);
		selectionEndedListener.removePropertyChangeListener(this);
		Main.contentPane.getInputMap().remove(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
		Main.contentPane.getActionMap().remove("SelectionManager");
	}

	/**
	 * If the correct button, from the "drawing rectangle" mode
	 */
	public void mousePressed(MouseEvent e) {
		if (e.getButton() == MouseEvent.BUTTON1)
			mousePosStart = mousePos = e.getPoint();
	}

	/**
	 * If the correct button is hold, draw the rectangle.
	 */
	public void mouseDragged(MouseEvent e) {
		int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK); 

		
		if (buttonPressed != 0) {
			if (mousePosStart == null)
				mousePosStart = mousePos = e.getPoint();
			paintRect();
		}
		
		if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) {
			mousePos = e.getPoint();
			paintRect();
		} else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) {
			mousePosStart.x += e.getX()-mousePos.x;
			mousePosStart.y += e.getY()-mousePos.y;
			mousePos = e.getPoint();
			paintRect();
		}
	}

	/**
	 * Check the state of the keys and buttons and set the selection accordingly.
	 */
	public void mouseReleased(MouseEvent e) {
		if (e.getButton() != MouseEvent.BUTTON1)
			return;
		if (mousePos == null || mousePosStart == null)
			return; // injected release from outside
			
		// disable the selection rect
		paintRect();
		Rectangle r = getSelectionRectangle();
		mousePosStart = null;
		mousePos = null;

		boolean shift = (e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0;
		boolean alt = (e.getModifiersEx() & MouseEvent.ALT_DOWN_MASK) != 0;
		boolean ctrl = (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0;
		if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) == 0)
			selectionEndedListener.selectionEnded(r, alt, shift, ctrl);
	}


	/**
	 * Draw a selection rectangle on screen. If already a rectangle is drawn,
	 * it is removed instead.
	 */
	private void paintRect() {
		if (mousePos == null || mousePosStart == null || mousePos == mousePosStart)
			return;
		Graphics g = nc.getGraphics();
		g.setColor(Color.BLACK);
		g.setXORMode(Color.WHITE);

		Rectangle r = getSelectionRectangle();
		g.drawRect(r.x,r.y,r.width,r.height);
	}

	/**
	 * Calculate and return the current selection rectangle
	 * @return A rectangle that spans from mousePos to mouseStartPos
	 */
	private Rectangle getSelectionRectangle() {
		int x = mousePosStart.x;
		int y = mousePosStart.y;
		int w = mousePos.x - mousePosStart.x;
		int h = mousePos.y - mousePosStart.y;
		if (w < 0) {
			x += w;
			w = -w;
		}
		if (h < 0) {
			y += h;
			h = -h;
		}
		
		if (aspectRatio) {
			// keep the aspect ration by shrinking the rectangle
			double aspectRatio = (double)nc.getWidth()/nc.getHeight();
			if ((double)w/h > aspectRatio) {
				int neww = (int)(h*aspectRatio);
				if (mousePos.x < mousePosStart.x)
					x += w-neww;
				w = neww;
			} else {
				int newh = (int)(w/aspectRatio);
				if (mousePos.y < mousePosStart.y)
					y += h-newh;
				h = newh;
			}
		}
		
		return new Rectangle(x,y,w,h);
	}

	/**
	 * If the action goes inactive, remove the selection rectangle from screen
	 */
	public void propertyChange(PropertyChangeEvent evt) {
		if (evt.getPropertyName().equals("active") && !(Boolean)evt.getNewValue() && mousePosStart != null) {
			paintRect();
			mousePosStart = null;
			mousePos = null;
		}
	}

	/**
	 * Return a list of all objects in the rectangle, respecting the different
	 * modifier.
	 * @param alt Whether the alt key was pressed, which means select all objects
	 * 		that are touched, instead those which are completly covered. Also 
	 * 		select whole ways instead of segments.
	 */
	public Collection<OsmPrimitive> getObjectsInRectangle(Rectangle r, boolean alt) {
		Collection<OsmPrimitive> selection = new LinkedList<OsmPrimitive>();

		// whether user only clicked, not dragged.
		boolean clicked = r.width <= 2 && r.height <= 2;
		Point center = new Point(r.x+r.width/2, r.y+r.height/2);

		if (clicked) {
			OsmPrimitive osm = nc.getNearest(center, alt);
			if (osm != null)
				selection.add(osm);
		} else {
			// nodes
			for (Node n : Main.ds.nodes) {
				if (!n.deleted && r.contains(nc.getPoint(n.eastNorth)))
					selection.add(n);
			}
			
			// pending segments
			for (Segment s : Main.ds.segments)
				if (!s.deleted && rectangleContainSegment(r, alt, s))
					selection.add(s);

			// ways
			for (Way w : Main.ds.ways) {
				if (w.deleted)
					continue;
				boolean someSelectableSegment = false;
				boolean wholeWaySelected = true;
				for (Segment s : w.segments) {
					if (s.incomplete)
						continue;
					someSelectableSegment = true;
					if (!rectangleContainSegment(r, alt, s)) {
						wholeWaySelected = false;
						break;
					}
				}
				if (someSelectableSegment && wholeWaySelected)
					selection.add(w);
			}
		}
		return selection;
	}

	/**
	 * Decide whether the segment is in the rectangle Return 
	 * <code>true</code>, if it is in or false if not.
	 * 
	 * @param r			The rectangle, in which the segment has to be.
	 * @param alt		Whether user pressed the Alt key
	 * @param ls		The segment.
	 * @return <code>true</code>, if the Segment was added to the selection.
	 */
	private boolean rectangleContainSegment(Rectangle r, boolean alt, Segment ls) {
		if (ls.incomplete)
			return false;
		if (alt) {
			Point p1 = nc.getPoint(ls.from.eastNorth);
			Point p2 = nc.getPoint(ls.to.eastNorth);
			if (r.intersectsLine(p1.x, p1.y, p2.x, p2.y))
				return true;
		} else {
			if (r.contains(nc.getPoint(ls.from.eastNorth))
					&& r.contains(nc.getPoint(ls.to.eastNorth)))
				return true;
		}
		return false;
	}
	
	public void mouseClicked(MouseEvent e) {}
	public void mouseEntered(MouseEvent e) {}
	public void mouseExited(MouseEvent e) {}
	public void mouseMoved(MouseEvent e) {}
}
