/*
 * @(#)VertexView.java	1.0 03-JUL-04
 * 
 * Copyright (c) 2001-2004 Gaudenz Alder
 *  
 */
package org.jgraph.graph;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.Serializable;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;

import org.jgraph.JGraph;
import org.jgraph.plaf.GraphUI;
import org.jgraph.plaf.basic.BasicGraphUI;

/**
 * The default implementation of a vertex view.
 *
 * @version 1.0 1/1/02
 * @author Gaudenz Alder
 */

public class VertexView extends AbstractCellView {

	/** Renderer for the class. */
	public static VertexRenderer renderer = new VertexRenderer();

	/** Reference to the bounds attribute */
	protected Rectangle2D bounds;

	/** Cached bounds of all children if vertex is a group */
	protected Rectangle2D groupBounds = DefaultGraphCell.defaultBounds;

	/**
	 * Constructs a vertex view for the specified model object
	 * and the specified child views.
	 *
	 * @param cell reference to the model object
	 */
	public VertexView(Object cell, JGraph graph, CellMapper mapper) {
		super(cell, graph, mapper);
	}

	//
	// CellView Interface
	//

	/**
	 * Overrides the parent method to udpate the cached points.
	 */
	public void update() {
		super.update();
		bounds = GraphConstants.getBounds(allAttributes);
		groupBounds = null;
	}

	public void childUpdated() {
		super.childUpdated();
		groupBounds = null;
	}

	/**
	 * Returns the cached bounds for the vertex.
	 */
	public Rectangle2D getBounds() {
		if (!isLeaf()) {
			if (groupBounds == null)
				updateGroupBounds();
			return groupBounds;
		}
		return bounds;
	}

	public Rectangle2D getCachedBounds() {
		return bounds;
	}

	public void setCachedBounds(Rectangle2D bounds) {
		this.bounds = bounds;
	}

	protected void updateGroupBounds() {
		// Note: Prevent infinite recursion by removing
		// child edges that point to their parent.
		CellView[] childViews = getChildViews();
		LinkedList result = new LinkedList();
		for (int i = 0; i < childViews.length; i++)
			if (includeInGroupBounds(childViews[i]))
				result.add(childViews[i]);
		childViews = new CellView[result.size()];
		result.toArray(childViews);
		groupBounds = getBounds(childViews);
	}

	private boolean includeInGroupBounds(CellView view) {
		if (view instanceof EdgeView) {
			GraphModel model = graph.getModel();
			EdgeView edgeView = (EdgeView) view;
			if (edgeView.getCell() instanceof DefaultMutableTreeNode) {
				DefaultMutableTreeNode edge =
					(DefaultMutableTreeNode) edgeView.getCell();
				if (model.getSource(edge) instanceof TreeNode) {
					TreeNode source = (TreeNode) model.getSource(edge);
					if (((DefaultMutableTreeNode) source.getParent())
						.isNodeDescendant(edge)) {
						return false;
					}
				}
				if (model.getTarget(edge) instanceof TreeNode) {
					TreeNode target = (TreeNode) model.getTarget(edge);
					if (((DefaultMutableTreeNode) target.getParent())
						.isNodeDescendant(edge)) {
						return false;
					}
				}
			}
		}
		return true;
	}

	/**
	 * Returns a renderer for the class.
	 */
	public CellViewRenderer getRenderer() {
		return renderer;
	}

	/**
	 * Returns a cell handle for the view, if the graph and the view
	 * are sizeable.
	 */
	public CellHandle getHandle(GraphContext context) {
		if (GraphConstants.isSizeable(getAllAttributes())
			&& context.getGraph().isSizeable())
			return new SizeHandle(this, context);
		return null;
	}

	//
	// Special Methods
	//

	/**
	 * Returns the center of this vertex.
	 */
	public Point2D getCenterPoint() {
		Rectangle2D r = getBounds();
		return getAttributes().createPoint(r.getCenterX(), r.getCenterY());
	}

	/**
	 * Returns the intersection of the bounding rectangle and the
	 * straight line between the source and the specified point p.
	 * The specified point is expected not to intersect the bounds.
	 * Note: You must override this method if you use a different
	 * renderer. This is because this method relies on the
	 * VertexRenderer interface, which can not be safely assumed
	 * for subclassers.
	 */
	public Point2D getPerimeterPoint(Point2D source, Point2D p) {
		return renderer.getPerimeterPoint(this, source, p);
	}

	/** Array that holds the cursors for the different control points. */
	public static transient int[] defaultCursors =
		new int[] {
			Cursor.NW_RESIZE_CURSOR,
			Cursor.N_RESIZE_CURSOR,
			Cursor.NE_RESIZE_CURSOR,
			Cursor.W_RESIZE_CURSOR,
			Cursor.E_RESIZE_CURSOR,
			Cursor.SW_RESIZE_CURSOR,
			Cursor.S_RESIZE_CURSOR,
			Cursor.SE_RESIZE_CURSOR };
	
	/** Array that holds the cursors for the different control points. */
	public static transient int[] xCursors =
		new int[] {
			Cursor.W_RESIZE_CURSOR,
			0,
			Cursor.E_RESIZE_CURSOR,
			Cursor.W_RESIZE_CURSOR,
			Cursor.E_RESIZE_CURSOR,
			Cursor.W_RESIZE_CURSOR,
			0,
			Cursor.E_RESIZE_CURSOR };
	
	/** Array that holds the cursors for the different control points. */
	public static transient int[] yCursors =
		new int[] {
			Cursor.N_RESIZE_CURSOR,
			Cursor.N_RESIZE_CURSOR,
			Cursor.N_RESIZE_CURSOR,
			0,
			0,
			Cursor.S_RESIZE_CURSOR,
			Cursor.S_RESIZE_CURSOR,
			Cursor.S_RESIZE_CURSOR };
	
	public static class SizeHandle implements CellHandle, Serializable {

		// Double Buffer
		protected transient Image offscreen;
		protected transient Graphics offgraphics;
		protected transient boolean firstDrag = true;

		protected transient JGraph graph;

		/* Reference to the temporary view for this handle. */
		protected transient VertexView vertex;

		protected transient CellView[] portViews;

		protected transient Rectangle2D cachedBounds;

		/* Reference to the context for the specified view. */
		protected transient GraphContext context;

		protected transient Rectangle2D initialBounds;

		protected transient CellView[] contextViews;

		/* Index of the active control point. -1 if none is active. */
		protected transient int index = -1;

		/* Array of control points represented as rectangles. */
		protected transient Rectangle2D[] r = new Rectangle2D[8];

		protected boolean firstOverlayInvocation = true;

		/** Array that holds the cursors for the different control points. */
		public transient int[] cursors = null;

		public SizeHandle(VertexView vertexview, GraphContext ctx) {
			graph = ctx.getGraph();
			vertex = vertexview;
			int sizeableAxis = GraphConstants.getSizeableAxis(vertex.getAllAttributes());
			if (sizeableAxis == GraphConstants.X_AXIS)
				cursors = xCursors;
			else if (sizeableAxis == GraphConstants.Y_AXIS)
				cursors = yCursors;
			else
				cursors = defaultCursors;
			// PortView Preview
			portViews = ctx.createTemporaryPortViews();
			initialBounds = (Rectangle2D) vertex.getBounds().clone();
			context = ctx;
			for (int i = 0; i < r.length; i++)
				r[i] =  vertex.getAttributes().createRect();
			invalidate();
		}

		public boolean isConstrainedSizeEvent(MouseEvent e) {
			GraphUI ui = graph.getUI();
			if (ui instanceof BasicGraphUI)
				return ((BasicGraphUI) ui).isConstrainedMoveEvent(e);
			return false;
		}
		
		public void paint(Graphics g) {
			invalidate();
			g.setColor(graph.getHandleColor());
			for (int i = 0; i < r.length; i++) {
				if (cursors[i] != 0)
					g.fill3DRect(
						(int) r[i].getX(),
						(int) r[i].getY(),
						(int) r[i].getWidth(),
						(int) r[i].getHeight(),
						true);
			}
		}

		// Double Buffers by David Larsson
		protected void initOffscreen() {
			try {
				Rectangle rect = graph.getBounds();
				//RepaintManager repMan = RepaintManager.currentManager(graph);
				//offscreen = repMan.getVolatileOffscreenBuffer(getGraph(), (int) rect.getWidth(), (int) rect.getHeight());
				offscreen =
					new BufferedImage(
						rect.width,
						rect.height,
						BufferedImage.TYPE_INT_RGB);
				offgraphics = offscreen.getGraphics();
				offgraphics.setClip(0, 0, rect.width, rect.height);
				offgraphics.setColor(graph.getBackground());
				offgraphics.fillRect(0, 0, rect.width, rect.height);
				graph.getUI().paint(offgraphics, graph);
			} catch (Error e) {
				offscreen = null;
				offgraphics = null;
			}
		}

		public void overlay(Graphics g) {
			if (!firstOverlayInvocation) {
				if (cachedBounds != null) {
					g.setColor(Color.black);
					Rectangle2D tmp =
						graph.toScreen((Rectangle2D) cachedBounds.clone());
					g.drawRect(
						(int) tmp.getX(),
						(int) tmp.getY(),
						(int) tmp.getWidth() - 2,
						(int) tmp.getHeight() - 2);
				} else if (!initialBounds.equals(vertex.getBounds())) {
					Graphics2D g2 = (Graphics2D) g;
					AffineTransform oldTransform = g2.getTransform();
					g2.scale(graph.getScale(), graph.getScale());
					graph.getUI().paintCell(
						g,
						vertex,
						vertex.getBounds(),
						true);
					if (contextViews != null)
						for (int i = 0; i < contextViews.length; i++) {
							graph.getUI().paintCell(
								g,
								contextViews[i],
								contextViews[i].getBounds(),
								true);
						}
					g2.setTransform(oldTransform);
					if (portViews != null && graph.isPortsVisible())
						graph.getUI().paintPorts(g, portViews);
				}
			}
			firstOverlayInvocation = false;
		}

		/**
		 * Invoked when the mouse pointer has been moved on a component
		 * (with no buttons down).
		 */
		public void mouseMoved(MouseEvent event) {
			if (vertex != null) {
				for (int i = 0; i < r.length; i++) {
					if (r[i].contains(event.getPoint())) {
						graph.setCursor(new Cursor(cursors[i]));
						event.consume();
						return;
					}
				}
			}
		}

		/** Process mouse pressed event. */
		public void mousePressed(MouseEvent event) {
			if (!graph.isSizeable())
				return;
			for (int i = 0; i < r.length; i++) {
				if (r[i].contains(event.getPoint()) && cursors[i] != 0) {
					Set set = new HashSet();
					set.add(vertex.getCell());
					contextViews = context.createTemporaryContextViews(set);
					Object[] all =
						AbstractCellView.getDescendantViews(
							new CellView[] { vertex });
					if (all.length
						>= org.jgraph.plaf.basic.BasicGraphUI.MAXHANDLES)
						cachedBounds = (Rectangle2D) initialBounds.clone();
					event.consume();
					index = i;
					return;
				}
			}
		}

		/** Process mouse dragged event. */
		public void mouseDragged(MouseEvent event) {
			if (firstDrag
				&& graph.isDoubleBuffered()
				&& cachedBounds == null) {
				initOffscreen();
				firstDrag = false;
			}
			Rectangle2D dirty = null;
			Graphics g =
				(offgraphics != null) ? offgraphics : graph.getGraphics();
			if (index == -1)
				return;
			Rectangle2D newBounds = computeBounds(event);
			g.setColor(graph.getForeground());
			g.setXORMode(graph.getBackground().darker());
			overlay(g);
			if (offgraphics != null) {
				dirty =
					graph.toScreen((Rectangle2D) vertex.getBounds().clone());
				Rectangle2D t =
					graph.toScreen(AbstractCellView.getBounds(contextViews));
				if (t != null)
					dirty.add(t);
			}
			if (cachedBounds != null)
				cachedBounds = newBounds;
			else {
				// Reset old Bounds
				CellView[] all =
					AbstractCellView.getDescendantViews(
						new CellView[] { vertex });
				for (int i = 0; i < all.length; i++) {
					CellView orig =
						graph.getGraphLayoutCache().getMapping(
							all[i].getCell(),
							false);
					AttributeMap origAttr =
						(AttributeMap) orig.getAllAttributes().clone();
					all[i].setAttributes(origAttr);
					all[i].refresh(false);
				}
				vertex.setBounds(newBounds);
				if (vertex != null)
					graph.getGraphLayoutCache().update(vertex);
				if (contextViews != null)
					graph.getGraphLayoutCache().update(contextViews);
			}
			overlay(g);
			if (offscreen != null) {
				dirty.add(
					graph.toScreen((Rectangle2D) vertex.getBounds().clone()));
				Rectangle2D t =
					graph.toScreen(AbstractCellView.getBounds(contextViews));
				if (t != null)
					dirty.add(t);
				int size = 2;
				dirty.setFrame(
					dirty.getX() - size,
					dirty.getY() - size,
					dirty.getWidth() + size * 2,
					dirty.getHeight() + size * 2);
				double sx1 = Math.max(0, dirty.getX());
				double sy1 = Math.max(0, dirty.getY());
				double sx2 = sx1 + dirty.getWidth();
				double sy2 = sy1 + dirty.getHeight();
				graph.getGraphics().drawImage(
					offscreen,
					(int) sx1,
					(int) sy1,
					(int) sx2,
					(int) sy2,
					(int) sx1,
					(int) sy1,
					(int) sx2,
					(int) sy2,
					graph);
			}
		}

		protected Rectangle2D computeBounds(MouseEvent event) {
			double left = initialBounds.getX();
			double right = initialBounds.getX() + initialBounds.getWidth() - 1;
			double top = initialBounds.getY();
			double bottom =
				initialBounds.getY() + initialBounds.getHeight() - 1;
			Point2D p =
				graph.fromScreen(
					graph.snap((Point2D) event.getPoint().clone()));
			// Not into negative coordinates
			p.setLocation(Math.max(0, p.getX()), Math.max(0, p.getY()));
			// Bottom row
			if (index > 4)
				bottom = p.getY();
			// Top row
			else if (index < 3)
				top = p.getY();
			// Left col
			if (index == 0 || index == 3 || index == 5)
				left = p.getX();
			// Right col
			else if (index == 2 || index == 4 || index == 7)
				right = p.getX();
			double width = right - left;
			double height = bottom - top;
			if (isConstrainedSizeEvent(event)) {
				if (index == 3 || index == 4 || index == 5)
					height = width;
				else if (index == 1 || index == 6 || index == 2 || index == 7)
					width = height;
				else {
					height = width;
					top = bottom - height;
				}
			}
			if (width < 0) { // Flip over left side
				left += width;
				width = Math.abs(width);
			}
			if (height < 0) { // Flip over top side
				top += height;
				height = Math.abs(height);
			}
			return vertex.getAttributes().createRect(left, top, width + 1, height + 1);
		}

		// Dispatch the edit event
		public void mouseReleased(MouseEvent e) {
			if (index != -1) {
				cachedBounds = computeBounds(e);
				vertex.setBounds(cachedBounds);
				CellView[] views =
					AbstractCellView.getDescendantViews(
						new CellView[] { vertex });
				Map attributes = GraphConstants.createAttributes(views, null);
				graph.getGraphLayoutCache().edit(attributes, null, null, null);
			}
			e.consume();
			cachedBounds = null;
			initialBounds = null;
			firstDrag = true;
		}

		private void invalidate() {
			// Retrieve current bounds and set local vars
			Rectangle2D tmp = graph.getCellBounds(vertex.getCell());
			if (tmp != null) {
				tmp = (Rectangle2D) tmp.clone();
				graph.toScreen(tmp);
				int handlesize = graph.getHandleSize();
				int s2 = 2 * handlesize;
				double left = tmp.getX() - handlesize;
				double top = tmp.getY() - handlesize;
				double w2 = tmp.getX() + (tmp.getWidth() / 2) - handlesize;
				double h2 = tmp.getY() + (tmp.getHeight() / 2) - handlesize;
				double right = tmp.getX() + tmp.getWidth() - handlesize;
				double bottom = tmp.getY() + tmp.getHeight() - handlesize;
				// Update control point positions
				r[0].setFrame(left, top, s2, s2);
				r[1].setFrame(w2, top, s2, s2);
				r[2].setFrame(right, top, s2, s2);
				r[3].setFrame(left, h2, s2, s2);
				r[4].setFrame(right, h2, s2, s2);
				r[5].setFrame(left, bottom, s2, s2);
				r[6].setFrame(w2, bottom, s2, s2);
				r[7].setFrame(right, bottom, s2, s2);
			}
		}

	}

}
