/**
 * $Id: mxCurveLabelShape.java,v 1.1 2012/11/15 13:26:44 gaudenz Exp $
 * Copyright (c) 2010, David Benson, Gaudenz Alder
 */
package com.mxgraph.shape;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.text.Bidi;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxCurve;
import com.mxgraph.util.mxLine;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxUtils;
import com.mxgraph.view.mxCellState;

/**
 * Draws the edge label along a curve derived from the curve describing
 * the edge's path
 */
public class mxCurveLabelShape implements mxITextShape
{
	/**
	 * Cache of the label text
	 */
	protected String lastValue;

	/**
	 * Cache of the label font
	 */
	protected Font lastFont;

	/**
	 * Cache of the last set of guide points that this label was calculated for
	 */
	protected List<mxPoint> lastPoints;

	/**
	 * Cache of the points between which drawing straight lines views as a
	 * curve
	 */
	protected mxCurve curve;

	/**
	 * Cache the state associated with this shape
	 */
	protected mxCellState state;

	/**
	 * Cache of information describing characteristics relating to drawing 
	 * each glyph of this label
	 */
	protected LabelGlyphCache[] labelGlyphs;

	/**
	 * Cache of the total length of the branch label
	 */
	protected double labelSize;

	/**
	 * Cache of the bounds of the label
	 */
	protected mxRectangle labelBounds;

	/**
	 * ADT to encapsulate label positioning information
	 */
	protected LabelPosition labelPosition = new LabelPosition();

	/**
	 * Buffer at both ends of the label
	 */
	public static double LABEL_BUFFER = 30;

	/**
	 * Factor by which text on the inside of curve is stretched
	 */
	public static double CURVE_TEXT_STRETCH_FACTOR = 20.0;

	/**
	 * Indicates that a glyph does not have valid drawing bounds, usually 
	 * because it is not visible
	 */
	public static mxRectangle INVALID_GLYPH_BOUNDS = new mxRectangle(0, 0, 0, 0);

	/**
	 * The index of the central glyph of the label that is visible
	 */
	public int centerVisibleIndex = 0;

	/**
	 * Specifies if image aspect should be preserved in drawImage. Default is true.
	 */
	public static Object FONT_FRACTIONALMETRICS = RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT;

	/**
	 * Cache of BIDI glyph vectors
	 */
	public GlyphVector[] rtlGlyphVectors;

	/**
	 * Shared FRC for font size calculations
	 */
	public static FontRenderContext frc = new FontRenderContext(null, false,
			false);

	/**
	 *
	 */
	protected boolean rotationEnabled = true;

	public mxCurveLabelShape(mxCellState state, mxCurve value)
	{
		this.state = state;
		this.curve = value;
	}

	/**
	 *
	 */
	public boolean getRotationEnabled()
	{
		return rotationEnabled;
	}

	/**
	 *
	 */
	public void setRotationEnabled(boolean value)
	{
		rotationEnabled = value;
	}

	/**
	 * 
	 */
	public void paintShape(mxGraphics2DCanvas canvas, String text,
			mxCellState state, Map<String, Object> style)
	{
		Rectangle rect = state.getLabelBounds().getRectangle();
		Graphics2D g = canvas.getGraphics();

		if (labelGlyphs == null)
		{
			updateLabelBounds(text, style);
		}

		if (labelGlyphs != null
				&& (g.getClipBounds() == null || g.getClipBounds().intersects(
						rect)))
		{
			// Creates a temporary graphics instance for drawing this shape
			float opacity = mxUtils.getFloat(style, mxConstants.STYLE_OPACITY,
					100);
			Graphics2D previousGraphics = g;
			g = canvas.createTemporaryGraphics(style, opacity, state);

			Font font = mxUtils.getFont(style, canvas.getScale());
			g.setFont(font);

			Color fontColor = mxUtils.getColor(style,
					mxConstants.STYLE_FONTCOLOR, Color.black);
			g.setColor(fontColor);

			g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
					RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

			g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
					FONT_FRACTIONALMETRICS);

			for (int j = 0; j < labelGlyphs.length; j++)
			{
				mxLine parallel = labelGlyphs[j].glyphGeometry;

				if (labelGlyphs[j].visible && parallel != null
						&& parallel != mxCurve.INVALID_POSITION)
				{
					mxPoint parallelEnd = parallel.getEndPoint();
					double x = parallelEnd.getX();
					double rotation = (Math.atan(parallelEnd.getY() / x));

					if (x < 0)
					{
						// atan only ranges from -PI/2 to PI/2, have to offset
						// for negative x values
						rotation += Math.PI;
					}

					final AffineTransform old = g.getTransform();
					g.translate(parallel.getX(), parallel.getY());
					g.rotate(rotation);
					Shape letter = labelGlyphs[j].glyphShape;
					g.fill(letter);
					g.setTransform(old);
				}
			}

			g.dispose();
			g = previousGraphics;
		}
	}

	/**
	 * Updates the cached position and size of each glyph in the edge label. 
	 * @param label the entire string of the label.
	 * @param style the edge style
	 */
	public mxRectangle updateLabelBounds(String label, Map<String, Object> style)
	{
		double scale = state.getView().getScale();
		Font font = mxUtils.getFont(style, scale);
		FontMetrics fm = mxUtils.getFontMetrics(font);
		int descent = 0;
		int ascent = 0;

		if (fm != null)
		{
			descent = fm.getDescent();
			ascent = fm.getAscent();
		}

		// Check that the size of the widths array matches 
		// that of the label size
		if (labelGlyphs == null || (!label.equals(lastValue)))
		{
			labelGlyphs = new LabelGlyphCache[label.length()];
		}

		if (!label.equals(lastValue) || !font.equals(lastFont))
		{
			char[] labelChars = label.toCharArray();
			ArrayList<LabelGlyphCache> glyphList = new ArrayList<LabelGlyphCache>();
			boolean bidiRequired = Bidi.requiresBidi(labelChars, 0,
					labelChars.length);

			labelSize = 0;

			if (bidiRequired)
			{
				Bidi bidi = new Bidi(label,
						Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);

				int runCount = bidi.getRunCount();

				if (rtlGlyphVectors == null
						|| rtlGlyphVectors.length != runCount)
				{
					rtlGlyphVectors = new GlyphVector[runCount];
				}

				for (int i = 0; i < bidi.getRunCount(); i++)
				{
					final String labelSection = label.substring(
							bidi.getRunStart(i), bidi.getRunLimit(i));
					rtlGlyphVectors[i] = font
							.layoutGlyphVector(mxCurveLabelShape.frc,
									labelSection.toCharArray(), 0,
									labelSection.length(),
									Font.LAYOUT_RIGHT_TO_LEFT);
				}

				int charCount = 0;

				for (GlyphVector gv : rtlGlyphVectors)
				{
					float vectorOffset = 0.0f;

					for (int j = 0; j < gv.getNumGlyphs(); j++)
					{
						Shape shape = gv.getGlyphOutline(j, -vectorOffset, 0);

						LabelGlyphCache qlyph = new LabelGlyphCache();
						glyphList.add(qlyph);
						qlyph.glyphShape = shape;
						mxRectangle size = new mxRectangle(gv.getGlyphLogicalBounds(j).getBounds2D());
						qlyph.labelGlyphBounds = size;
						labelSize += size.getWidth();
						vectorOffset += size.getWidth();

						charCount++;
					}
				}
			}
			else
			{
				rtlGlyphVectors = null;
				//String locale = System.getProperty("user.language");
				// Character iterator required where character is split over
				// string elements
				BreakIterator it = BreakIterator.getCharacterInstance(Locale.getDefault());
				it.setText(label);

				for (int i = 0; i < label.length();)
				{
					int next = it.next();
					int characterLen = 1;
					
					if (next != BreakIterator.DONE)
					{
						characterLen = next - i;
					}

					String glyph = label.substring(i, i + characterLen);
					
					LabelGlyphCache labelGlyph = new LabelGlyphCache();
					glyphList.add(labelGlyph);
					labelGlyph.glyph = glyph;
					GlyphVector vector = font.createGlyphVector(frc, glyph);
					labelGlyph.glyphShape = vector.getOutline();

					if (fm == null)
					{
						mxRectangle size = new mxRectangle(
								font.getStringBounds(glyph,
										mxCurveLabelShape.frc));
						labelGlyph.labelGlyphBounds = size;
						labelSize += size.getWidth();
					}
					else
					{
						double width = fm.stringWidth(glyph);
						labelGlyph.labelGlyphBounds = new mxRectangle(0, 0,
								width, ascent);
						labelSize += width;
					}

					i += characterLen;
					

				}
			}

			// Update values used to determine whether or not the label cache 
			// is valid or not
			lastValue = label;
			lastFont = font;
			lastPoints = curve.getGuidePoints();
			this.labelGlyphs = glyphList.toArray(new LabelGlyphCache[glyphList.size()]);
		}

		// Store the start/end buffers that pad out the ends of the branch so the label is
		// visible. We work initially as the start section being at the start of the
		// branch and the end at the end of the branch. Note that the actual label curve
		// might be reversed, so we allow for this after completing the buffer calculations,
		// otherwise they'd need to be constant isReversed() checks throughout
		labelPosition.startBuffer = LABEL_BUFFER * scale;
		labelPosition.endBuffer = LABEL_BUFFER * scale;

		calculationLabelPosition(style, label);

		if (curve.isLabelReversed())
		{
			double temp = labelPosition.startBuffer;
			labelPosition.startBuffer = labelPosition.endBuffer;
			labelPosition.endBuffer = temp;
		}

		double curveLength = curve.getCurveLength(mxCurve.LABEL_CURVE);
		double currentPos = labelPosition.startBuffer / curveLength;
		double endPos = 1.0 - (labelPosition.endBuffer / curveLength);

		mxRectangle overallLabelBounds = null;
		centerVisibleIndex = 0;

		double currentCurveDelta = 0.0;
		double curveDeltaSignificant = 0.3;
		double curveDeltaMax = 0.5;
		mxLine nextParallel = null;

		// TODO on translation just move the points, don't recalculate
		// Might be better than the curve is the only thing updated and
		// the curve shapes listen to curve events
		// !lastPoints.equals(curve.getGuidePoints())
		for (int j = 0; j < labelGlyphs.length; j++)
		{
			if (currentPos > endPos)
			{
				labelGlyphs[j].visible = false;
				continue;
			}

			mxLine parallel = nextParallel;

			if (currentCurveDelta > curveDeltaSignificant
					|| nextParallel == null)
			{
				parallel = curve.getCurveParallel(mxCurve.LABEL_CURVE,
						currentPos);

				currentCurveDelta = 0.0;
				nextParallel = null;
			}

			labelGlyphs[j].glyphGeometry = parallel;

			if (parallel == mxCurve.INVALID_POSITION)
			{
				continue;
			}

			// Get the four corners of the rotated rectangle bounding the glyph
			// The drawing bounds of the glyph is the unrotated rect that
			// just bounds those four corners
			final double w = labelGlyphs[j].labelGlyphBounds.getWidth();
			final double h = labelGlyphs[j].labelGlyphBounds.getHeight();
			final double x = parallel.getEndPoint().getX();
			final double y = parallel.getEndPoint().getY();
			// Bottom left
			double p1X = parallel.getX() - (descent * y);
			double minX = p1X, maxX = p1X;
			double p1Y = parallel.getY() + (descent * x);
			double minY = p1Y, maxY = p1Y;
			// Top left
			double p2X = p1X + ((h + descent) * y);
			double p2Y = p1Y - ((h + descent) * x);
			minX = Math.min(minX, p2X);
			maxX = Math.max(maxX, p2X);
			minY = Math.min(minY, p2Y);
			maxY = Math.max(maxY, p2Y);
			// Bottom right
			double p3X = p1X + (w * x);
			double p3Y = p1Y + (w * y);
			minX = Math.min(minX, p3X);
			maxX = Math.max(maxX, p3X);
			minY = Math.min(minY, p3Y);
			maxY = Math.max(maxY, p3Y);
			// Top right
			double p4X = p2X + (w * x);
			double p4Y = p2Y + (w * y);
			minX = Math.min(minX, p4X);
			maxX = Math.max(maxX, p4X);
			minY = Math.min(minY, p4Y);
			maxY = Math.max(maxY, p4Y);

			minX -= 2 * scale;
			minY -= 2 * scale;
			maxX += 2 * scale;
			maxY += 2 * scale;

			// Hook for sub-classers
			postprocessGlyph(curve, label, j, currentPos);

			// Need to allow for text on inside of curve bends. Need to get the 
			// parallel for the next section, if there is an excessive
			// inner curve, advance the current position accordingly

			double currentPosCandidate = currentPos
					+ (labelGlyphs[j].labelGlyphBounds.getWidth() + labelPosition.defaultInterGlyphSpace)
					/ curveLength;

			nextParallel = curve.getCurveParallel(mxCurve.LABEL_CURVE,
					currentPosCandidate);

			currentPos = currentPosCandidate;

			mxPoint nextVector = nextParallel.getEndPoint();
			double end2X = nextVector.getX();
			double end2Y = nextVector.getY();

			if (nextParallel != mxCurve.INVALID_POSITION
					&& j + 1 < label.length())
			{
				// Extend the current parallel line in its direction
				// by the length of the next parallel. Use the approximate
				// deviation to work out the angle change
				double deltaX = Math.abs(x - end2X);
				double deltaY = Math.abs(y - end2Y);

				// The difference as a proportion of the length of the next 
				// vector. 1 means a variation of 60 degrees.
				currentCurveDelta = Math
						.sqrt(deltaX * deltaX + deltaY * deltaY);
			}

			if (currentCurveDelta > curveDeltaSignificant)
			{
				// Work out which direction the curve is going in
				int ccw = Line2D.relativeCCW(0, 0, x, y, end2X, end2Y);

				if (ccw == 1)
				{
					// Text is on inside of curve
					if (currentCurveDelta > curveDeltaMax)
					{
						// Don't worry about excessive deltas, if they
						// are big the label curve will be screwed anyway
						currentCurveDelta = curveDeltaMax;
					}

					double textBuffer = currentCurveDelta
							* CURVE_TEXT_STRETCH_FACTOR / curveLength;
					currentPos += textBuffer;
					endPos += textBuffer;
				}
			}

			if (labelGlyphs[j].drawingBounds != null)
			{
				labelGlyphs[j].drawingBounds.setRect(minX, minY, maxX - minX,
						maxY - minY);
			}
			else
			{
				labelGlyphs[j].drawingBounds = new mxRectangle(minX, minY, maxX
						- minX, maxY - minY);
			}

			if (overallLabelBounds == null)
			{
				overallLabelBounds = (mxRectangle) labelGlyphs[j].drawingBounds
						.clone();
			}
			else
			{
				overallLabelBounds.add(labelGlyphs[j].drawingBounds);
			}

			labelGlyphs[j].visible = true;
			centerVisibleIndex++;
		}

		centerVisibleIndex /= 2;

		if (overallLabelBounds == null)
		{
			// Return a small rectangle in the center of the label curve
			// Null label bounds causes NPE when editing
			mxLine labelCenter = curve.getCurveParallel(mxCurve.LABEL_CURVE,
					0.5);
			overallLabelBounds = new mxRectangle(labelCenter.getX(),
					labelCenter.getY(), 1, 1);
		}

		this.labelBounds = overallLabelBounds;
		return overallLabelBounds;
	}

	/**
	 * Hook for sub-classers to perform additional processing on
	 * each glyph
	 * @param curve The curve object holding the label curve
	 * @param label the text label of the curve
	 * @param j the index of the label
	 * @param currentPos the distance along the label curve the glyph is
	 */
	protected void postprocessGlyph(mxCurve curve, String label, int j,
			double currentPos)
	{
	}

	/**
	 * Returns whether or not the rectangle passed in hits any part of this
	 * curve.
	 * @param rect the rectangle to detect for a hit
	 * @return whether or not the rectangle hits this curve
	 */
	public boolean intersectsRect(Rectangle rect)
	{
		// To save CPU, we can test if the rectangle intersects the entire
		// bounds of this label
		if ( (labelBounds != null
				&& (!labelBounds.getRectangle().intersects(rect)) )
				|| labelGlyphs == null )
		{
			return false;
		}

		for (int i = 0; i < labelGlyphs.length; i++)
		{
			if (labelGlyphs[i].visible
					&& rect.intersects(labelGlyphs[i].drawingBounds
							.getRectangle()))
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Hook method to override how the label is positioned on the curve
	 * @param style the style of the curve
	 * @param label the string label to be displayed on the curve
	 */
	protected void calculationLabelPosition(Map<String, Object> style,
			String label)
	{
		double curveLength = curve.getCurveLength(mxCurve.LABEL_CURVE);
		double availableLabelSpace = curveLength - labelPosition.startBuffer
				- labelPosition.endBuffer;
		labelPosition.startBuffer = Math.max(labelPosition.startBuffer,
				labelPosition.startBuffer + availableLabelSpace / 2 - labelSize
						/ 2);
		labelPosition.endBuffer = Math.max(labelPosition.endBuffer,
				labelPosition.endBuffer + availableLabelSpace / 2 - labelSize
						/ 2);
	}

	/**
	 * @return the curve
	 */
	public mxCurve getCurve()
	{
		return curve;
	}

	/**
	 * @param curve the curve to set
	 */
	public void setCurve(mxCurve curve)
	{
		this.curve = curve;
	}

	/**
	 * Utility class to describe the characteristics of each glyph of a branch
	 * branch label. Each instance represents one glyph
	 *
	 */
	public class LabelGlyphCache
	{
		/**
		 * Cache of the bounds of the individual element of the label of this 
		 * edge. Note that these are the unrotated values used to determine the 
		 * width of each glyph.
		 */
		public mxRectangle labelGlyphBounds;

		/**
		 * The un-rotated rectangle that just bounds this character
		 */
		public mxRectangle drawingBounds;

		/**
		 * The glyph being drawn
		 */
		public String glyph;

		/**
		 * A line parallel to the curve segment at which the element is to be
		 * drawn
		 */
		public mxLine glyphGeometry;

		/**
		 * The cached shape of the glyph
		 */
		public Shape glyphShape;

		/**
		 * Whether or not the glyph should be drawn
		 */
		public boolean visible;
	}

	/**
	 * Utility class that stores details of how the label is positioned
	 * on the curve
	 */
	public class LabelPosition
	{
		public double startBuffer = LABEL_BUFFER;

		public double endBuffer = LABEL_BUFFER;

		public double defaultInterGlyphSpace = 0;;
	}

	public mxRectangle getLabelBounds()
	{
		return labelBounds;
	}

	/**
	 * Returns the drawing bounds of the central indexed visible glyph
	 * @return the centerVisibleIndex
	 */
	public mxRectangle getCenterVisiblePosition()
	{
		return labelGlyphs[centerVisibleIndex].drawingBounds;
	}
}
