/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.draw.tools;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.swing.Icon;

import com.cburch.draw.actions.ModelMoveHandleAction;
import com.cburch.draw.actions.ModelRemoveAction;
import com.cburch.draw.actions.ModelTranslateAction;
import com.cburch.draw.canvas.Canvas;
import com.cburch.draw.canvas.Selection;
import com.cburch.draw.model.CanvasModel;
import com.cburch.draw.model.CanvasObject;
import com.cburch.draw.model.Handle;
import com.cburch.draw.model.HandleGesture;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.Bounds;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.util.GraphicsUtil;
import com.cburch.logisim.util.Icons;

public class SelectTool extends AbstractTool {
	private static final int IDLE = 0;
	private static final int MOVE_ALL = 1;
	private static final int RECT_SELECT = 2;
	private static final int RECT_TOGGLE = 3;
	private static final int MOVE_HANDLE = 4;
	
	private static final int DRAG_TOLERANCE = 2;
	private static final int HANDLE_SIZE = 8;
	
	private static final Color RECT_SELECT_BACKGROUND = new Color(0, 0, 0, 32);
	
	private int curAction;
	private List<CanvasObject> beforePressSelection;
	private Handle beforePressHandle;
	private Location dragStart;
	private Location dragEnd;
	private boolean dragEffective;
	private int lastMouseX;
	private int lastMouseY;
	private HandleGesture curGesture;
	
	public SelectTool() {
		curAction = IDLE;
		dragStart = Location.create(0, 0);
		dragEnd = dragStart;
		dragEffective = false;
	}
	
	@Override
	public Icon getIcon() {
		return Icons.getIcon("select.gif");
	}

	@Override
	public Cursor getCursor(Canvas canvas) {
		return Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
	}
	
	@Override
	public List<Attribute<?>> getAttributes() {
		return Collections.emptyList();
	}
	
	@Override
	public void toolSelected(Canvas canvas) {
		curAction = IDLE;
		canvas.getSelection().clearSelected();
		repaintArea(canvas);
	}
	
	@Override
	public void toolDeselected(Canvas canvas) {
		curAction = IDLE;
		canvas.getSelection().clearSelected();
		repaintArea(canvas);
	}
	
	private int getHandleSize(Canvas canvas) {
		double zoom = canvas.getZoomFactor();
		return (int) Math.ceil(HANDLE_SIZE / Math.sqrt(zoom));
	}
	
	@Override
	public void mousePressed(Canvas canvas, MouseEvent e) {
		beforePressSelection = new ArrayList<CanvasObject>(canvas.getSelection().getSelected());
		beforePressHandle = canvas.getSelection().getSelectedHandle();
		int mx = e.getX();
		int my = e.getY();
		boolean shift = (e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0;
		dragStart = Location.create(mx, my);
		dragEffective = false;
		dragEnd = dragStart;
		lastMouseX = mx;
		lastMouseY = my;
		Selection selection = canvas.getSelection();
		selection.setHandleSelected(null);
		
		// see whether user is pressing within an existing handle
		int halfSize = getHandleSize(canvas) / 2;
		CanvasObject clicked = null;
		for (CanvasObject shape : selection.getSelected()) {
			List<Handle> handles = shape.getHandles(null);
			for (Handle han : handles) {
				int dx = han.getX() - mx;
				int dy = han.getY() - my;
				if (dx >= -halfSize && dx <= halfSize
						&& dy >= -halfSize && dy <= halfSize) {
					if (shape.canMoveHandle(han)) {
						curAction = MOVE_HANDLE;
						curGesture = new HandleGesture(han, 0, 0,
								e.getModifiersEx());
						repaintArea(canvas);
						return;
					} else if (clicked == null) {
						clicked = shape;
					}
				}
			}
		}

		// see whether the user is clicking within a shape
		if (clicked == null) {
			clicked = getObjectAt(canvas.getModel(), e.getX(), e.getY(), false);
		}
		if (clicked != null) {
			if (shift && selection.isSelected(clicked)) {
				selection.setSelected(clicked, false);
				curAction = IDLE;
			} else {
				if (!shift && !selection.isSelected(clicked)) {
					selection.clearSelected();
				}
				selection.setSelected(clicked, true);
				selection.setMovingShapes(selection.getSelected(), 0, 0);
				curAction = MOVE_ALL;
			}
			repaintArea(canvas);
			return;
		}
		
		clicked = getObjectAt(canvas.getModel(), e.getX(), e.getY(), true);
		if (clicked != null && selection.isSelected(clicked)) {
			if (shift) {
				selection.setSelected(clicked, false);
				curAction = IDLE;
			} else {
				selection.setMovingShapes(selection.getSelected(), 0, 0);
				curAction = MOVE_ALL;
			}
			repaintArea(canvas);
			return;
		}

		if (shift) {
			curAction = RECT_TOGGLE;
		} else {
			selection.clearSelected();
			curAction = RECT_SELECT;
		}
		repaintArea(canvas);
	}
	
	@Override
	public void cancelMousePress(Canvas canvas) {
		List<CanvasObject> before = beforePressSelection;
		Handle handle = beforePressHandle;
		beforePressSelection = null;
		beforePressHandle = null;
		if (before != null) {
			curAction = IDLE;
			Selection sel = canvas.getSelection();
			sel.clearDrawsSuppressed();
			sel.setMovingShapes(Collections.<CanvasObject>emptySet(), 0, 0);
			sel.clearSelected();
			sel.setSelected(before, true);
			sel.setHandleSelected(handle);
			repaintArea(canvas);
		}
	}
	
	@Override
	public void mouseDragged(Canvas canvas, MouseEvent e) {
		setMouse(canvas, e.getX(), e.getY(), e.getModifiersEx());
	}
	
	@Override
	public void mouseReleased(Canvas canvas, MouseEvent e) {
		beforePressSelection = null;
		beforePressHandle = null;
		setMouse(canvas, e.getX(), e.getY(), e.getModifiersEx());
		
		CanvasModel model = canvas.getModel();
		Selection selection = canvas.getSelection();
		Set<CanvasObject> selected = selection.getSelected();
		int action = curAction;
		curAction = IDLE;
		
		if (!dragEffective) {
			Location loc = dragEnd;
			CanvasObject o = getObjectAt(model, loc.getX(), loc.getY(), false);
			if (o != null) {
				Handle han = o.canDeleteHandle(loc);
				if (han != null) {
					selection.setHandleSelected(han);
				} else {
					han = o.canInsertHandle(loc);
					if (han != null) {
						selection.setHandleSelected(han);
					}
				}
			}
		}
		
		Location start = dragStart;
		int x1 = e.getX();
		int y1 = e.getY();
		switch (action) {
		case MOVE_ALL:
			Location moveDelta = selection.getMovingDelta();
			if (dragEffective && !moveDelta.equals(Location.create(0, 0))) {
				canvas.doAction(new ModelTranslateAction(model, selected,
						moveDelta.getX(), moveDelta.getY()));
			}
			break;
		case MOVE_HANDLE:
			HandleGesture gesture = curGesture;
			curGesture = null;
			if (dragEffective && gesture != null) {
				ModelMoveHandleAction act;
				act = new ModelMoveHandleAction(model, gesture);
				canvas.doAction(act);
				Handle result = act.getNewHandle();
				if (result != null) {
					Handle h = result.getObject().canDeleteHandle(result.getLocation());
					selection.setHandleSelected(h);
				}
			}
			break;
		case RECT_SELECT:
			if (dragEffective) {
				Bounds bds = Bounds.create(start).add(x1, y1);
				selection.setSelected(canvas.getModel().getObjectsIn(bds), true);
			} else {
				CanvasObject clicked;
				clicked = getObjectAt(model, start.getX(), start.getY(), true);
				if (clicked != null) {
					selection.clearSelected();
					selection.setSelected(clicked, true);
				}
			}
			break;
		case RECT_TOGGLE:
			if (dragEffective) {
				Bounds bds = Bounds.create(start).add(x1, y1);
				selection.toggleSelected(canvas.getModel().getObjectsIn(bds));
			} else {
				CanvasObject clicked;
				clicked = getObjectAt(model, start.getX(), start.getY(), true);
				selection.setSelected(clicked, !selected.contains(clicked));
			}
			break;
		}
		selection.clearDrawsSuppressed();
		repaintArea(canvas);
	}
	
	@Override
	public void keyPressed(Canvas canvas, KeyEvent e) {
		int code = e.getKeyCode();
		if ((code == KeyEvent.VK_SHIFT || code == KeyEvent.VK_CONTROL
				|| code == KeyEvent.VK_ALT) && curAction != IDLE) {
			setMouse(canvas, lastMouseX, lastMouseY, e.getModifiersEx());
		}
	}
	
	@Override
	public void keyReleased(Canvas canvas, KeyEvent e) {
		keyPressed(canvas, e);
	}
	
	@Override
	public void keyTyped(Canvas canvas, KeyEvent e) {
		char ch = e.getKeyChar();
		Selection selected = canvas.getSelection();
		if ((ch == '\u0008' || ch == '\u007F') && !selected.isEmpty()) {
			ArrayList<CanvasObject> toRemove = new ArrayList<CanvasObject>();
			for (CanvasObject shape : selected.getSelected()) {
				if (shape.canRemove()) {
					toRemove.add(shape);
				}
			}
			if (!toRemove.isEmpty()) {
				e.consume();
				CanvasModel model = canvas.getModel();
				canvas.doAction(new ModelRemoveAction(model, toRemove));
				selected.clearSelected();
				repaintArea(canvas);
			}
		} else if (ch == '\u001b' && !selected.isEmpty()) {
			selected.clearSelected();
			repaintArea(canvas);
		}
	}
	
	
	private void setMouse(Canvas canvas, int mx, int my, int mods) {
		lastMouseX = mx;
		lastMouseY = my;
		boolean shift = (mods & MouseEvent.SHIFT_DOWN_MASK) != 0;
		boolean ctrl = (mods & InputEvent.CTRL_DOWN_MASK) != 0;
		Location newEnd = Location.create(mx, my);
		dragEnd = newEnd;

		Location start = dragStart;
		int dx = newEnd.getX() - start.getX();
		int dy = newEnd.getY() - start.getY();
		if (!dragEffective) {
			if (Math.abs(dx) + Math.abs(dy) > DRAG_TOLERANCE) {
				dragEffective = true;
			} else {
				return;
			}
		}

		switch (curAction) {
		case MOVE_HANDLE:
			HandleGesture gesture = curGesture;
			if (ctrl) {
				Handle h = gesture.getHandle(); 
				dx = canvas.snapX(h.getX() + dx) - h.getX();
				dy = canvas.snapY(h.getY() + dy) - h.getY();
			}
			curGesture = new HandleGesture(gesture.getHandle(), dx, dy, mods);
			canvas.getSelection().setHandleGesture(curGesture);
			break;
		case MOVE_ALL:
			if (ctrl) {
				int minX = Integer.MAX_VALUE;
				int minY = Integer.MAX_VALUE;
				for (CanvasObject o : canvas.getSelection().getSelected()) {
					for (Handle handle : o.getHandles(null)) {
						int x = handle.getX();
						int y = handle.getY();
						if (x < minX) minX = x;
						if (y < minY) minY = y;
					}
				}
				dx = canvas.snapX(minX + dx) - minX;
				dy = canvas.snapY(minY + dy) - minY;
			}
			if (shift) {
				if (Math.abs(dx) > Math.abs(dy)) {
					dy = 0;
				} else {
					dx = 0;
				}
			}
			canvas.getSelection().setMovingDelta(dx, dy);
			break;
		}
		repaintArea(canvas);
	}

	private void repaintArea(Canvas canvas) {
		canvas.repaint();
	}
	
	@Override
	public void draw(Canvas canvas, Graphics g) {
		Selection selection = canvas.getSelection();
		int action = curAction;

		Location start = dragStart;
		Location end = dragEnd;
		HandleGesture gesture = null;
		boolean drawHandles;
		switch (action) {
		case MOVE_ALL:
			drawHandles = !dragEffective;
			break;
		case MOVE_HANDLE:
			drawHandles = !dragEffective;
			if (dragEffective) gesture = curGesture;
			break;
		default:
			drawHandles = true;
		}

		CanvasObject moveHandleObj = null;
		if (gesture != null) moveHandleObj = gesture.getHandle().getObject();
		if (drawHandles) {
			// unscale the coordinate system so that the stroke width isn't scaled
			double zoom = 1.0;
			Graphics gCopy = g.create();
			if (gCopy instanceof Graphics2D) {
				zoom = canvas.getZoomFactor();
				if (zoom != 1.0) {
					((Graphics2D) gCopy).scale(1.0 / zoom, 1.0 / zoom);
				}
			}
			GraphicsUtil.switchToWidth(gCopy, 1);

			int size = (int) Math.ceil(HANDLE_SIZE * Math.sqrt(zoom));
			int offs = size / 2;
			for (CanvasObject obj : selection.getSelected()) {
				List<Handle> handles;
				if (action == MOVE_HANDLE && obj == moveHandleObj) {
					handles = obj.getHandles(gesture);
				} else {
					handles = obj.getHandles(null);
				}
				for (Handle han : handles) {
					int x = han.getX();
					int y = han.getY();
					if (action == MOVE_ALL && dragEffective) {
						Location delta = selection.getMovingDelta();
						x += delta.getX();
						y += delta.getY();
					}
					x = (int) Math.round(zoom * x);
					y = (int) Math.round(zoom * y);
					gCopy.clearRect(x - offs, y - offs, size, size);
					gCopy.drawRect(x - offs, y - offs, size, size);
				}
			}
			Handle selHandle = selection.getSelectedHandle();
			if (selHandle != null) {
				int x = selHandle.getX();
				int y = selHandle.getY();
				if (action == MOVE_ALL && dragEffective) {
					Location delta = selection.getMovingDelta();
					x += delta.getX();
					y += delta.getY();
				}
				x = (int) Math.round(zoom * x);
				y = (int) Math.round(zoom * y);
				int[] xs = { x - offs, x, x + offs, x };
				int[] ys = { y, y - offs, y, y + offs };
				gCopy.setColor(Color.WHITE);
				gCopy.fillPolygon(xs, ys, 4);
				gCopy.setColor(Color.BLACK);
				gCopy.drawPolygon(xs, ys, 4);
			}
		}
		
		switch (action) {
		case RECT_SELECT:
		case RECT_TOGGLE:
			if (dragEffective) {
				// find rectangle currently to show
				int x0 = start.getX();
				int y0 = start.getY();
				int x1 = end.getX();
				int y1 = end.getY();
				if (x1 < x0) { int t = x0; x0 = x1; x1 = t; }
				if (y1 < y0) { int t = y0; y0 = y1; y1 = t; }

				// make the region that's not being selected darker
				int w = canvas.getWidth();
				int h = canvas.getHeight();
				g.setColor(RECT_SELECT_BACKGROUND);
				g.fillRect(0, 0, w, y0);
				g.fillRect(0, y0, x0, y1 - y0);
				g.fillRect(x1, y0, w - x1, y1 - y0);
				g.fillRect(0, y1, w, h - y1);

				// now draw the rectangle
				g.setColor(Color.GRAY);
				g.drawRect(x0, y0, x1 - x0, y1 - y0);
			}
			break;
		}
	}

	private static CanvasObject getObjectAt(CanvasModel model, int x, int y,
			boolean assumeFilled) {
		Location loc = Location.create(x, y);
		for (CanvasObject o : model.getObjectsFromTop()) {
			if (o.contains(loc, assumeFilled)) return o;
		}
		return null;
	}
}
