package com.michaelbaranov.microba.jgrpah.birdview;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.UIManager;

import org.jgraph.JGraph;
import org.jgraph.event.GraphModelEvent;
import org.jgraph.event.GraphModelListener;
import org.jgraph.graph.GraphLayoutCache;

/**
 * A JFC/Swing component that displays a bird-eyes (thumbnail) view of a
 * {@link JGraph} in combination with {@link JScrollPane}. Also allows to pan
 * view with the mouse.
 * 
 * 
 * @author Michael Baranov
 * 
 */
public class Birdview extends JPanel {

	/**
	 * Color constant
	 */
	public static final Color PAN_RECT_COLOR = Color.black;

	/**
	 * Color constant
	 */
	public static final Color BACKGROUND_COLOR = UIManager.getColor("Panel.background");

	/**
	 * a {@link JGraph} component used as main display
	 */
	private JGraph displayGraph;

	/**
	 * a {@link JScrollPane} to track
	 */
	private JScrollPane peerScroller;

	/**
	 * a {@link JGraph} to tracl
	 */
	private JGraph peerGraph;

	/**
	 * a rect, subrect of the component area, that matches the tracked view
	 */
	private Rectangle2D paintRect;

	/**
	 * a rect, a subrect of the component area, that matches the tracked
	 * viewport
	 */
	private Rectangle2D panRect;

	/**
	 * the scale applied to {@link #displayGraph} to fit into {@link #paintRect}
	 */
	private double scale = 1.0;

	/**
	 * A listener
	 */
	private ScrollerListener scrollerListener = new ScrollerListener();

	/**
	 * A listener
	 */
	private SelfMouseListener selfMouseListener = new SelfMouseListener();

	/**
	 * A listener
	 */
	private GraphPropertyChangeListener graphPropertyChangeListener = new GraphPropertyChangeListener();
	
	/**
	 * Constructor.
	 * 
	 * @param doc
	 *            document to track. May be <code>null</code>
	 */
	public Birdview() {

		displayGraph = new JGraph();
		displayGraph.setEnabled(false);
		displayGraph.setAntiAliased(true);
		displayGraph.addMouseListener(selfMouseListener);
		displayGraph.addMouseMotionListener(selfMouseListener);

		this.setLayout(null);
		this.addComponentListener(new SelfResizeListener());
		this.add(displayGraph);

	}

	/**
	 * Makes this component track the provided graph and scroller. Set
	 * parameters to <code>null</code> to unbind thie component.
	 * 
	 * @param graph
	 *            the graph component. May be <code>null</code>
	 * @param scroller
	 *            the croller, usually the one that holds the graph. May be
	 *            <code>null</code>
	 */
	public void setTrackingFor(JGraph graph, JScrollPane scroller) {
		if (this.peerGraph != null) {
			this.peerGraph.getModel().removeGraphModelListener(scrollerListener);
			this.peerGraph.removePropertyChangeListener(graphPropertyChangeListener);
		}

		this.peerGraph = graph;

		if (this.peerGraph != null) {
			this.peerGraph.getModel().addGraphModelListener(scrollerListener);
			this.peerGraph.addPropertyChangeListener(graphPropertyChangeListener);

			this.displayGraph.setGraphLayoutCache(peerGraph.getGraphLayoutCache());

		} else {
			this.displayGraph.setGraphLayoutCache(new GraphLayoutCache());
		}

		//
		if (this.peerScroller != null) {
			this.peerScroller.getHorizontalScrollBar().removeAdjustmentListener(
					scrollerListener);
			this.peerScroller.getVerticalScrollBar().removeAdjustmentListener(
					scrollerListener);
		}

		this.peerScroller = scroller;

		if (this.peerScroller != null) {
			this.peerScroller.getHorizontalScrollBar().addAdjustmentListener(
					scrollerListener);
			this.peerScroller.getVerticalScrollBar().addAdjustmentListener(
					scrollerListener);
		}

		update();
		repaint();

	}

	/**
	 * Calculates {@link #paintRect}, {@link #panRect}, {@link #scale} based
	 * on observations of peerScroller.
	 * 
	 */
	private void update() {
		if (peerScroller != null) {
			Dimension viewSize = peerScroller.getViewport().getViewSize();
			double viewAspect = viewSize.getHeight() / viewSize.getWidth();

			Dimension panelSize = this.getSize();
			double panelAspect = panelSize.getHeight() / panelSize.getWidth();

			if (panelAspect < viewAspect) {
				// panel is wider, height is primary
				double desiredPanelWidth = panelSize.getHeight() / viewAspect;
				double blankWidth = panelSize.getWidth() - desiredPanelWidth;
				double gap = blankWidth / 2;
				paintRect = new Rectangle2D.Double(gap, 0, desiredPanelWidth,
						panelSize.height);
				scale = panelSize.getHeight() / viewSize.getHeight();
				scale *= peerGraph.getScale();
			} else {
				// panel is heigher, width is primary
				double desiredPanelHeight = panelSize.getWidth() * viewAspect;
				double blankHeight = panelSize.getHeight() - desiredPanelHeight;
				double gap = blankHeight / 2;
				paintRect = new Rectangle2D.Double(0, gap, panelSize.getWidth(),
						desiredPanelHeight);
				scale = panelSize.getWidth() / viewSize.getWidth();
				scale *= peerGraph.getScale();
			}

			Rectangle viewRect = peerScroller.getViewport().getViewRect();
			double shiftX = viewRect.getX() / viewSize.getWidth();
			double shiftY = viewRect.getY() / viewSize.getHeight();

			double sizeX = viewRect.getWidth() / viewSize.getWidth();
			double sizeY = viewRect.getHeight() / viewSize.getHeight();

			panRect = new Rectangle2D.Double(paintRect.getX() + paintRect.getWidth()
					* shiftX, paintRect.getY() + paintRect.getHeight() * shiftY,
					paintRect.getWidth() * sizeX, paintRect.getHeight() * sizeY);

		} else {
			panRect = null;
			paintRect = null;
			scale = 1;
		}
	}

	/**
	 * Addjusts {@link #panRect} to be postitionsed with the centter at a given
	 * point if possible. This methods ensures that {@link #panRect} fits
	 * entirely in {@link #paintRect}.
	 * 
	 * @param point
	 *            the desired panRect center
	 */
	private void panRectTo(Point point) {

		Dimension viewSize = peerScroller.getViewport().getViewSize();
		Rectangle viewRect = peerScroller.getViewport().getViewRect();

		double panHalfWidth = panRect.getWidth() / 2;
		double panHalfHeight = panRect.getHeight() / 2;

		Point2D panOrigin = new Point2D.Double(point.x - panHalfWidth, point.y
				- panHalfHeight);
		double xk = panOrigin.getX() / paintRect.getWidth();
		double yk = panOrigin.getY() / paintRect.getHeight();

		Point viewPos = new Point((int) (viewSize.getWidth() * xk), (int) (viewSize
				.getHeight() * yk));

		// make sure we do not pan past the bounds:
		if (viewPos.x < 0)
			viewPos.x = 0;
		if (viewPos.y < 0)
			viewPos.y = 0;

		int wd = (viewPos.x + viewRect.width) - viewSize.width;
		int hd = (viewPos.y + viewRect.height) - viewSize.height;

		if (wd > 0)
			viewPos.x -= wd;
		if (hd > 0)
			viewPos.y -= hd;

		// pan it
		peerScroller.getViewport().setViewPosition(viewPos);

		update();
		repaint();
	}

	public void paint(Graphics g) {
		Graphics2D g2 = (Graphics2D) g;

		// fill background
		g2.setColor(BACKGROUND_COLOR);
		g2.fillRect(0, 0, this.getWidth(), this.getHeight());

		if (peerGraph != null) {
			// paint the graph display
			displayGraph.setBounds(paintRect.getBounds());
			displayGraph.setScale(scale);
			displayGraph.setBackground(peerGraph.getBackground());
			paintChildren(g);
		}

		if (panRect != null) {
			// draw pan rect
			g2.setColor(PAN_RECT_COLOR);
			g2.draw(panRect.getBounds());
		}
	}

	/**
	 * A listener to watch for scrollbar movement.
	 * 
	 * @author Michael Baranov
	 * 
	 */
	private class ScrollerListener implements AdjustmentListener, GraphModelListener {

		public void adjustmentValueChanged(AdjustmentEvent e) {
			update();
			repaint();
		}

		public void graphChanged(GraphModelEvent e) {
			update();
			repaint();

		}

	}

	/**
	 * A listener to watch for sizing of the component itself.
	 * 
	 * @author Michael Baranov
	 * 
	 */
	private class SelfResizeListener implements ComponentListener {

		public void componentHidden(ComponentEvent e) {
		}

		public void componentMoved(ComponentEvent e) {
		}

		public void componentResized(ComponentEvent e) {
			update();
			repaint();
		}

		public void componentShown(ComponentEvent e) {
			update();
			repaint();
		}

	}

	/**
	 * A listener to watch for mouse for the component itself.
	 * 
	 * @author Michael Baranov
	 * 
	 */
	private class SelfMouseListener implements MouseListener, MouseMotionListener {

		public void mouseClicked(MouseEvent e) {
		}

		public void mouseEntered(MouseEvent e) {
		}

		public void mouseExited(MouseEvent e) {
		}

		public void mousePressed(MouseEvent e) {

		}

		public void mouseReleased(MouseEvent e) {
			if (paintRect != null)
				panRectTo(e.getPoint());

		}

		public void mouseDragged(MouseEvent e) {
			if (paintRect != null)
				panRectTo(e.getPoint());

		}

		public void mouseMoved(MouseEvent e) {
		}

	}

	/**
	 * A listener to watch for graph property change, most important "scale".
	 * 
	 * @author Michael Baranov
	 * 
	 */
	private class GraphPropertyChangeListener implements PropertyChangeListener {

		public void propertyChange(PropertyChangeEvent evt) {
			if ("scale".equals(evt.getPropertyName())) {
				update();
				repaint();
			}

		}

	}

}