/*
 Copyright (C) 2006 EBI
 
 This library is free software; you can redistribute it and/or
 modify it under the terms of the GNU Lesser General Public
 License as published by the Free Software Foundation; either
 version 2.1 of the License, or (at your option) any later version.
 
 This library is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the itmplied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 Lesser General Public License for more details.
 
 You should have received a copy of the GNU Lesser General Public
 License along with this library; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.biomart.configurator.view.gui.diagrams.components;

import java.awt.AWTEvent;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collections;

import javax.swing.JComponent;
import javax.swing.JPopupMenu;

import org.biomart.builder.model.Relation;
import org.biomart.builder.view.gui.diagrams.Diagram;
import org.biomart.common.resources.Resources;
import org.biomart.common.utils.McBeanMap;
import org.biomart.common.utils.Transaction;
import org.biomart.common.utils.Transaction.TransactionEvent;
import org.biomart.common.utils.Transaction.TransactionListener;
import org.biomart.configurator.view.gui.diagrams.contexts.DiagramContext;


/**
 * This component represents a relation between two keys, in the form of a line.
 * The path of the line is defined by one of the layout managers provided with
 * MartBuilder.
 * 
 * @author Richard Holland <holland@ebi.ac.uk>
 * @version $Revision: 1.37 $, $Date: 2007/12/20 18:43:46 $, modified by
 *          $Author: arek $
 * @since 0.5
 */
public class RelationComponent extends JComponent implements DiagramComponent,
		TransactionListener {

	/**
	 * Subclasses use this if the component needs repainting.
	 */
	protected boolean needsRepaint = false;

	/**
	 * Subclasses use this if the component needs recalculating.
	 */
	protected boolean needsRecalc = false;

	private boolean changed = false;
	private boolean mouseEventEnabled = true;

	private static final float RELATION_DASHSIZE = 6.0f; // 72 = 1 inch

	private static final float RELATION_DOTSIZE = 2.0f; // 72 = 1 inch

	private static final float RELATION_LINEWIDTH = 1.0f; // 72 = 1 inch

	private static final float RELATION_MITRE_TRIM = 10.0f; // 72 = 1 inch

	private static final Stroke ONE_MANY = new BasicStroke(
			RelationComponent.RELATION_LINEWIDTH, BasicStroke.CAP_ROUND,
			BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM);

	private static final Stroke ONE_ONE = new BasicStroke(
			RelationComponent.RELATION_LINEWIDTH * 2.0f, BasicStroke.CAP_ROUND,
			BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM);

	private static final Stroke ONE_MANY_DOTTED = new BasicStroke(
			RelationComponent.RELATION_LINEWIDTH, BasicStroke.CAP_ROUND,
			BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM,
			new float[] { RelationComponent.RELATION_DOTSIZE,
					RelationComponent.RELATION_DOTSIZE }, 0);

	private static final Stroke ONE_ONE_DOTTED = new BasicStroke(
			RelationComponent.RELATION_LINEWIDTH * 2.0f, BasicStroke.CAP_ROUND,
			BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM,
			new float[] { RelationComponent.RELATION_DOTSIZE,
					RelationComponent.RELATION_DOTSIZE }, 0);

	private static final Stroke ONE_MANY_DASHED = new BasicStroke(
			RelationComponent.RELATION_LINEWIDTH, BasicStroke.CAP_ROUND,
			BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM,
			new float[] { RelationComponent.RELATION_DASHSIZE,
					RelationComponent.RELATION_DASHSIZE }, 0);

	private static final Stroke ONE_ONE_DASHED = new BasicStroke(
			RelationComponent.RELATION_LINEWIDTH * 2.0f, BasicStroke.CAP_ROUND,
			BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM,
			new float[] { RelationComponent.RELATION_DASHSIZE,
					RelationComponent.RELATION_DASHSIZE }, 0);

	private static final Stroke ONE_MANY_DOTTED_DASHED = new BasicStroke(
			RelationComponent.RELATION_LINEWIDTH, BasicStroke.CAP_ROUND,
			BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM,
			new float[] { RelationComponent.RELATION_DASHSIZE,
					RelationComponent.RELATION_DOTSIZE,
					RelationComponent.RELATION_DOTSIZE,
					RelationComponent.RELATION_DOTSIZE }, 0);

	private static final Stroke ONE_ONE_DOTTED_DASHED = new BasicStroke(
			RelationComponent.RELATION_LINEWIDTH * 2.0f, BasicStroke.CAP_ROUND,
			BasicStroke.JOIN_ROUND, RelationComponent.RELATION_MITRE_TRIM,
			new float[] { RelationComponent.RELATION_DASHSIZE,
					RelationComponent.RELATION_DOTSIZE,
					RelationComponent.RELATION_DOTSIZE,
					RelationComponent.RELATION_DOTSIZE }, 0);

	private static final Stroke OUTLINE = new BasicStroke();

	private static final long serialVersionUID = 1;

	/**
	 * Constant referring to modified relation colour.
	 */
	public static Color MODIFIED_COLOUR = Color.BLUE;

	/**
	 * Constant referring to handmade relation colour.
	 */
	public static Color HANDMADE_COLOUR = Color.GREEN;

	/**
	 * Constant referring to incorrect relation colour.
	 */
	public static Color INCORRECT_COLOUR = Color.RED;

	/**
	 * Constant referring to masked relation colour.
	 */
	public static Color MASKED_COLOUR = Color.RED;

	/**
	 * Constant referring to normal relation colour.
	 */
	public static Color NORMAL_COLOUR = Color.DARK_GRAY;

	/**
	 * Constant referring to subclassed relation colour.
	 */
	public static Color SUBCLASS_COLOUR = Color.RED;

	/**
	 * Constant referring to unrolled relation colour.
	 */
	public static Color UNROLLED_COLOUR = Color.CYAN;

	private boolean restricted = false;

	private boolean compounded = false;

	private boolean loopback = false;

	private Diagram diagram;

	private Shape lineShape;

	private Shape outline;

	private Relation object;

	private RenderingHints renderHints;

	private Object state;

	private Stroke stroke;

	private final PropertyChangeListener repaintListener = new PropertyChangeListener() {
		public void propertyChange(final PropertyChangeEvent e) {
			RelationComponent.this.needsRepaint = true;
		}
	};

	/**
	 * The constructor constructs a component around a given relation, and
	 * associates the component with the given diagram.
	 * 
	 * @param relation
	 *            the relation to show in the component.
	 * @param diagram
	 *            the diagram to show this component in.
	 */
	public RelationComponent(final Relation relation, final Diagram diagram) {
		super();

		// Remember settings.
		this.object = relation;
		this.diagram = diagram;

		// Turn on the mouse.
		this.enableEvents(AWTEvent.MOUSE_EVENT_MASK);
		this.setDoubleBuffered(true); // Stop flicker.

		// Make sure we're transparent.
		this.setOpaque(false);

		// Set-up rendering hints.
		this.renderHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		this.renderHints.put(RenderingHints.KEY_RENDERING,
				RenderingHints.VALUE_RENDER_QUALITY);

		// Draw our contents, as we don't have any child classes that
		// do this for us unfortunately.
		this.recalculateDiagramComponent();

		Transaction.addTransactionListener(this);

		// Repaint events.
		relation.addPropertyChangeListener(Resources.get("PCDIRECTMODIFIED"),
				this.repaintListener);
		relation.getFirstKey().addPropertyChangeListener("status",
				this.repaintListener);
		relation.getFirstKey().getTable().addPropertyChangeListener("masked",
				this.repaintListener);
		relation.getFirstKey().getTable().addPropertyChangeListener(
				"dimensionMasked", this.repaintListener);
		relation.getSecondKey().addPropertyChangeListener("status",
				this.repaintListener);
		relation.getSecondKey().getTable().addPropertyChangeListener("masked",
				this.repaintListener);
		relation.getSecondKey().getTable().addPropertyChangeListener(
				"dimensionMasked", this.repaintListener);

		this.changed = this.getObject().isVisibleModified();
	}

	public void setDirectModified(final boolean modified) {
		// Ignore, for now.
	}


	public void setVisibleModified(final boolean modified) {
		// Ignore, for now.
	}

	public boolean isVisibleModified() {
		return false;
	}

	public void transactionResetDirectModified() {
		// Ignore, for now.
	}

	public void transactionResetVisibleModified() {
		// Ignore, for now.
	}

	public void transactionStarted(final TransactionEvent evt) {
		// Ignore, for now.
	}

	public void transactionEnded(final TransactionEvent evt) {
		final boolean visMod = this.getObject().isVisibleModified();
		this.needsRepaint |= this.changed ^ visMod;
		this.changed = visMod;
		if (this.needsRecalc)
			this.recalculateDiagramComponent();
		else if (this.needsRepaint)
			this.repaintDiagramComponent();
		this.needsRecalc = false;
		this.needsRepaint = false;
	}

	protected void paintComponent(final Graphics g) {
		final Graphics2D g2d = (Graphics2D) g;
		g2d.setRenderingHints(this.renderHints);
		if (this.changed) {
			g2d.setColor(DiagramComponent.GLOW_COLOUR);
			g2d.setStroke(new BasicStroke(DiagramComponent.GLOW_WIDTH));
			g2d.draw(this.outline);
		}
		g2d.setColor(this.getForeground());
		g2d.setStroke(this.stroke);
		g2d.draw(this.loopback ? this.outline : this.lineShape);
	}

	protected void processMouseEvent(final MouseEvent evt) {
		boolean eventProcessed = false;

		if (evt.getButton() != 0)
			this.getDiagram().deselectAll();

		// Is it a right-click?
		if (evt.isPopupTrigger() && this.mouseEventEnabled) {
			// Build the basic menu.
			final JPopupMenu contextMenu = this.getContextMenu();

			// Customise it using the diagram context.
			if (this.getDiagram().getDiagramContext() != null)
				this.getDiagram().getDiagramContext().populateContextMenu(
						contextMenu, this.getObject());

			// Display.
			if (contextMenu.getComponentCount() > 0)
				contextMenu.show(this, evt.getX(), evt.getY());

			// We have successfully handled the event.
			eventProcessed = true;
		}

		// Pass the event on up if we're not interested.
		if (!eventProcessed)
			super.processMouseEvent(evt);
	}

	public boolean contains(final int x, final int y) {
		// Clicks are on us if they are within a certain distance
		// of the outline shape.
		return this.outline != null
				&& this.outline.intersects(new Rectangle2D.Double(x
						- RelationComponent.RELATION_LINEWIDTH * 2, y
						- RelationComponent.RELATION_LINEWIDTH * 2,
						RelationComponent.RELATION_LINEWIDTH * 4,
						RelationComponent.RELATION_LINEWIDTH * 4));
	}

	public JPopupMenu getContextMenu() {
		final JPopupMenu contextMenu = new JPopupMenu();
		// No additional entries for us yet.
		// Return it.
		return contextMenu;
	}

	public JPopupMenu getMultiContextMenu() {
		final JPopupMenu contextMenu = new JPopupMenu();
		// No additional entries for us yet.
		// Return it.
		return contextMenu;
	}

	public Diagram getDiagram() {
		return this.diagram;
	}

	/**
	 * Returns the diagram component representing the first key of this
	 * relation.
	 * 
	 * @return the diagram component for the first key.
	 */
	public KeyComponent getFirstKeyComponent() {
		return (KeyComponent) this.diagram.getDiagramComponent(((Relation)this.getObject())
				.getFirstKey());
	}

	public TransactionListener getObject() {
		return this.object;
	}

	/**
	 * Returns the diagram component representing the second key of this
	 * relation.
	 * 
	 * @return the diagram component for the second key.
	 */
	public KeyComponent getSecondKeyComponent() {
		return (KeyComponent) this.diagram.getDiagramComponent(((Relation)this.getObject())
				.getSecondKey());
	}

	public Object getState() {
		return this.state;
	}

	public McBeanMap getSubComponents() {
		// We have no sub-components.
		return new McBeanMap(Collections.EMPTY_MAP);
	}

	public void recalculateDiagramComponent() {
		// Nothing to do here.
	}

	public void repaintDiagramComponent() {
		this.updateAppearance();
		this.repaint();
	}

	/**
	 * Sets the shape for us to display the outline of. This will usually be a
	 * line, however it's up to the layout manager entirely.
	 * 
	 * @param shape
	 *            the shape this relation should take on screen.
	 */
	public void setLineShape(final Shape shape) {
		// Only change if the shape has changed.
		if (this.lineShape != shape || this.lineShape != null
				&& !this.lineShape.equals(shape)) {
			this.lineShape = shape;
			// Update the outline of the relation shape accordingly.
			if (this.lineShape != null)
				this.outline = RelationComponent.OUTLINE
						.createStrokedShape(this.lineShape);
		}
		// Update our appearance.
		this.updateAppearance();
	}

	public void setState(final Object state) {
		this.state = state;
	}

	/**
	 * If this is set to <tt>true</tt> then the component will appear with a
	 * dashed outline. Otherwise, it appears with a solid outline.
	 * 
	 * @param restricted
	 *            <tt>true</tt> if the component is to appear with a dashed
	 *            outline. The default is <tt>false</tt>.
	 */
	public void setRestricted(final boolean restricted) {
		this.restricted = restricted;
	}

	/**
	 * If this is set to <tt>true</tt> then the component will appear with a
	 * dotted outline. Otherwise, it appears with a solid outline.
	 * 
	 * @param compounded
	 *            <tt>true</tt> if the component is to appear with a dotted
	 *            outline. The default is <tt>false</tt>.
	 */
	public void setCompounded(final boolean compounded) {
		this.compounded = compounded;
	}

	/**
	 * If this is set to <tt>true</tt> then the component will appear with a
	 * double outline. Otherwise, it appears with a single outline.
	 * 
	 * @param loopback
	 *            <tt>true</tt> if the component is to appear with a double
	 *            outline. The default is <tt>false</tt>.
	 */
	public void setLoopback(final boolean loopback) {
		this.loopback = loopback;
	}

	public void updateAppearance() {
		// Use the context to alter us first.
		final DiagramContext mod = this.getDiagram().getDiagramContext();
		if (mod != null)
			mod.customiseAppearance(this, this.getObject());
		// Work out what style to draw the relation line.
		final Stroke oldStroke = this.stroke;
		if (((Relation)this.getObject()).isOneToOne())
			this.stroke = this.restricted ? this.compounded ? RelationComponent.ONE_ONE_DOTTED_DASHED
					: RelationComponent.ONE_ONE_DASHED
					: this.compounded ? RelationComponent.ONE_ONE_DOTTED
							: RelationComponent.ONE_ONE;
		else
			this.stroke = this.restricted ? this.compounded ? RelationComponent.ONE_MANY_DOTTED_DASHED
					: RelationComponent.ONE_MANY_DASHED
					: this.compounded ? RelationComponent.ONE_MANY_DOTTED
							: RelationComponent.ONE_MANY;
		this.setVisible(true);
		// Force repaint of area if stroke changed.
		if (oldStroke != this.stroke) {
			this.revalidate();
			this.repaint(this.getBounds());
		}
		if (this.getObject() != null)
			this.setToolTipText(this.getObject().toString());
	}

	public void setMouseEventEnabled(boolean b) {
		this.mouseEventEnabled = b;
	}
}
