/*
 * org.openmicroscopy.shoola.util.ui.drawingtools.figures.RotateEllipseFigure 
 *
  *------------------------------------------------------------------------------
 *  Copyright (C) 2006-2007 University of Dundee. All rights reserved.
 *
 *
 * 	This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 *------------------------------------------------------------------------------
 */
package org.openmicroscopy.shoola.util.ui.drawingtools.figures;



//Java imports
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.LinkedList;

//Third-party libraries
import org.jhotdraw.draw.AbstractAttributedFigure;
import org.jhotdraw.draw.AttributeKey;
import org.jhotdraw.draw.AttributeKeys;
import org.jhotdraw.draw.ConnectionFigure;
import org.jhotdraw.draw.Connector;
import org.jhotdraw.draw.Handle;
import org.jhotdraw.draw.ResizeHandleKit;
import org.jhotdraw.draw.TransformHandleKit;
import org.jhotdraw.geom.Geom;
import org.jhotdraw.samples.svg.Gradient;
import org.jhotdraw.samples.svg.SVGAttributeKeys;

//Application-internal dependencies

/** 
 * Version of an EllipseFigure that the user can rotate.
 *
 * @author  Jean-Marie Burel &nbsp;&nbsp;&nbsp;&nbsp;
 * 	<a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a>
 * @author	Donald MacDonald &nbsp;&nbsp;&nbsp;&nbsp;
 * 	<a href="mailto:donald@lifesci.dundee.ac.uk">donald@lifesci.dundee.ac.uk</a>
 * @version 3.0
 * <small>
 * (<b>Internal version:</b> $Revision: $Date: $)
 * </small>
 * @since OME3.0
 */
public class RotateEllipseFigure  
	extends AbstractAttributedFigure  
{	

	/** The geometry of the ellipse figure. */
	protected Ellipse2D.Double ellipse;
	
	/**
	 * This is used to perform faster drawing and hit testing.
	 */
	protected Shape cachedTransformedShape;
	    
	/** Creates a new instance. */
	public RotateEllipseFigure() 
	{
		this(0, 0, 0, 0);
	}
	
	/**
	 * Created a new rotated Ellipse with position x, y and width and height.
	 * 
	 * @param x 		The x-coordinate of the top-left corner.
	 * @param y 		The y-coordinate of the top-left corner.
	 * @param width 	The width of the ellipse.
	 * @param height 	The height of the ellipse.
	 */
	public RotateEllipseFigure(double x, double y, double width, double height)
	{
		ellipse = new Ellipse2D.Double(x, y, width, height);
		AffineTransform transform = new AffineTransform();
		transform.setToIdentity();
		AttributeKeys.TRANSFORM.set(this, transform);
	}
		    
	/**
	 *  Sets the attribute key to the value newValue on the figure. If the 
	 *  attribute is an AffineTransform then we need to redraw the image.
	 *  
	 *  @param key The attribute key.
	 *  @param newValue The new value of the attribute. 
	 */
	public void setAttribute(AttributeKey key, Object newValue)
	{
		if (key == AttributeKeys.TRANSFORM) {
			invalidate();
		} 
		super.setAttribute(key, newValue);
	} 

	/**
	 * Returns the bounds of the ellipse before applying the affine transform.
	 * 
	 * @return See above. 
	 */
	public Rectangle2D.Double getBounds()
	{
		return new Rectangle2D.Double(ellipse.getBounds2D().getX(),
			ellipse.getBounds2D().getY(), ellipse.getBounds2D().getWidth(),
			ellipse.getBounds2D().getHeight());
	}
	
	/**
	 * Overridden to return the drawing area, taking into account the 
	 * affine transformation.
	 * @see AbstractAttributedFigure#getDrawingArea()
	 */
	public Rectangle2D.Double getDrawingArea()
	{
		Rectangle2D rx = getTransformedShape().getBounds2D();
		Rectangle2D.Double r =
				(rx instanceof Rectangle2D.Double) ? (Rectangle2D.Double) 
						rx : new Rectangle2D.Double(rx.getX(), rx.getY(), 
							rx.getWidth(), rx.getHeight());
		AffineTransform object = AttributeKeys.TRANSFORM.get(this);
		if (object == null)
		{
			double g = SVGAttributeKeys.getPerpendicularHitGrowth(this)*2;
			Geom.grow(r, g, g);
		}
		else
		{
			double strokeTotalWidth = AttributeKeys.getStrokeTotalWidth(this);
			double width = strokeTotalWidth/2d;
			width *= Math.max(object.getScaleX(), object.getScaleY());
			Geom.grow(r, width, width);
		}
		return r;
	}
	
	public boolean containsMapped(double oX, double oY)
	{
		AffineTransform t = AttributeKeys.TRANSFORM.get(this);
		double x = oX-ellipse.getCenterX();
		double y = oY-ellipse.getCenterY();
		double[] matrix = new double[6];
		t.getMatrix(matrix);
		matrix[4] = 0;
		matrix[5] = 0;
		
		// Create a new transform with rotation only, 
		AffineTransform aT = new AffineTransform(matrix);
		Point2D.Double startPt = new Point2D.Double(1,0);
		Point2D.Double a = (Point2D.Double)aT.transform(startPt, null);
		// Calculate the starting rotation point.
		double thetaStart = Math.acos(startPt.y/Math.sqrt
				(startPt.x*startPt.x+startPt.y*startPt.y));
		
		// Calculate the current rotation the ellipse has undergone.
		double rotation = Math.acos(a.y/Math.sqrt(a.x*a.x+a.y*a.y));
		// Normalise to compensate for rotations > 180'
		double theta = rotation-Math.floor(rotation/Math.PI)*Math.PI-thetaStart;
		AffineTransform newT = new AffineTransform();
		newT.setToRotation(theta);
		Point2D rotatedPoint = new Point2D.Double();
		newT.transform(new Point2D.Double(x, y), rotatedPoint);
		if(containsEllipseAlgorithm(rotatedPoint.getX(), rotatedPoint.getY(),
				0,0, ellipse.getWidth(), ellipse.getHeight()))
			return true;
		return false;
	}
	
	private boolean containsEllipseAlgorithm(double x, double y, 
							 double cx, double cy, 
							 double w, double h)
	{
		double wr = w/2;
		double hr = h/2;
		double xx = x-cx;
		double yy = y-cy;
		double dist = (xx*xx)/(wr*wr)+(yy*yy)/(hr*hr);
		if(dist<1)
			return true;
		return false;
	}

	
	/**
	 * Checks if a Point2D.Double is inside the figure.
	 * @see AbstractAttributedFigure#contains(Point2D.Double)
	 */
	public boolean contains(Point2D.Double p)
	{
		// XXX - This does not take the stroke width into account!
		return getTransformedShape().contains(p);
	}

	/**
	 * Returns the ellipse after the affine transform has been applied to it.
	 * This will return the ellipse with the correct width and height, but the 
	 * x, y coords will be the lead.x, lead.y of the transformed shape.
	 * 
	 * @return see above.
	 */
	public Ellipse2D.Double getTransformedEllipse()
	{
		AffineTransform t = AttributeKeys.TRANSFORM.get(this);
		if (t == null) return ellipse;
		
		Ellipse2D.Double e = new Ellipse2D.Double(0,0,0,0);
		
		Point2D.Double startW = (Point2D.Double) t.transform(
				new Point2D.Double(0, ellipse.getCenterY()), null);
		Point2D.Double endW = (Point2D.Double) t.transform(
				new Point2D.Double(ellipse.getWidth(), 
						ellipse.getCenterY()), null);
		Point2D.Double startH = (Point2D.Double) t.transform(
				new Point2D.Double(ellipse.getCenterX(), 0), null);
		Point2D.Double endH = (Point2D.Double) t.transform(
				new Point2D.Double(ellipse.getCenterX(), ellipse.getHeight()),
				null);
		Point2D.Double lead = (Point2D.Double) t.transform(
				new Point2D.Double(0,0), null);
		e.width = Math.round(startW.distance(endW));
		e.height = Math.round(startH.distance(endH));
		e.x = Math.round(lead.getX());
		e.y = Math.round(lead.getY());
		return e;
	}
	
	/**
	 * Returns the Transformed ellipse as a transformedShape, of type Shape.
	 * 
	 * @return See above.
	 */
	protected Shape getTransformedShape()
	{
		AffineTransform t = AttributeKeys.TRANSFORM.get(this);
		if (t == null) cachedTransformedShape = ellipse;
		else cachedTransformedShape = t.createTransformedShape(ellipse);
		return cachedTransformedShape; 
	}
	
	/**
	 * Returns the Transformed ellipse as a transformedShape, of type Shape.
	 * 
	 * @return See above.
	 */
	protected Shape getTransformedShape(double i, double j)
	{
		AffineTransform t = AttributeKeys.TRANSFORM.get(this);
		if (t == null) cachedTransformedShape = ellipse;
		else
		{
			Ellipse2D.Double newEllipse = new Ellipse2D.Double(
					ellipse.x-i, ellipse.y-j, ellipse.width+i, ellipse.height+j);
			cachedTransformedShape = t.createTransformedShape(newEllipse);
		}
		return cachedTransformedShape; 
	}
	
	/**
	 * Returns the ellipse.
	 * 
	 * @return See above.
	 */
	public Ellipse2D.Double getEllipse() { return ellipse; }
	
	/**
	 * Overridden to return the correct handles.
	 * @see AbstractAttributedFigure#createHandles(int)
	 */
	public Collection<Handle> createHandles(int detailLevel)
	{
		LinkedList<Handle> handles = new LinkedList<Handle>();
		switch (detailLevel%2)
		{
			case 0:
				ResizeHandleKit.addResizeHandles(this, handles);
				break;
			case 1:
				TransformHandleKit.addTransformHandles(this, handles);
				break;
			default:
				break;
		}
		return handles;
	} 
	
	/**
	 * Sets the ellipse geometry.
	 * @param x see above.
	 * @param y see above.
	 * @param width see above.
	 * @param height see above.
	 */
	public void setEllipse(double x, double y, double width, double height)
	{
		ellipse.x = x;
		ellipse.y = y;
		ellipse.width = width;
		ellipse.height = height;
		invalidate();
	}
	
	/**
	 * Sets the bounds of the ellipse from the anchor to lead. 
	 * 
	 * @param anchor The start point of the drawing action.
	 * @param lead The end point the drawing action.
	 * 
	 */
	public void setBounds(Point2D.Double anchor, Point2D.Double lead)
	{
		ellipse.x = Math.min(anchor.x, lead.x);
		ellipse.y = Math.min(anchor.y, lead.y);
		ellipse.width = Math.max(0.1, Math.abs(lead.x-anchor.x));
		ellipse.height = Math.max(0.1, Math.abs(lead.y-anchor.y));
		invalidate();
	}

	/**
	 * Returns the height of the transformed ellipse.
	 * 
	 * @return See above.
	 */
	public double getHeight()
	{
		return getTransformedEllipse().getHeight();
	}

	/**
	 * Returns the width of the transformed ellipse.
	 * 
	 * @return See above.
	 */
	public double getWidth()
	{
		return getTransformedEllipse().getWidth();
	}
	
	/**
	 * Set the width of the ellipse to the newWidth, the new ellipse will 
	 * still be centered around the same point as the original ellipse.
	 * 
	 * @param newWidth see above.
	 */
	public void setWidth(double newWidth)
	{
		AffineTransform t = AttributeKeys.TRANSFORM.get(this);
		if (t == null) return;
		
		double centreX = getCentreX();
		double centreY = getCentreY();
		double height = getHeight();
		double[] matrix = new double[6];
		t.getMatrix(matrix);
		matrix[4] = 0;
		matrix[5] = 0;
		AffineTransform aT = new AffineTransform(matrix);
		Point2D.Double a = (Point2D.Double) aT.transform(
				new Point2D.Double(1,0), null);
		double theta = Math.asin(a.y/Math.sqrt(a.x*a.x+a.y*a.y));
		AffineTransform newT = new AffineTransform();
		newT.setToRotation(theta);
		
		Ellipse2D.Double newEllipse = new Ellipse2D.Double(0, 0,
									newWidth, height);
		
		Shape rotatedShape = newT.createTransformedShape(newEllipse);
		double rotatedShapeCentreX = rotatedShape.getBounds2D().getCenterX();
		double rotatedShapeCentreY = rotatedShape.getBounds2D().getCenterY();
		
		double diffX = centreX-rotatedShapeCentreX;
		double diffY = centreY-rotatedShapeCentreY;
	
		Point2D.Double lead = new Point2D.Double(diffX, diffY);
		
		AffineTransform rT = AffineTransform.getTranslateInstance(lead.x, 
							lead.y);
		rT.concatenate(newT);
		
		ellipse = newEllipse;
		AttributeKeys.TRANSFORM.set(this, rT);
		invalidate();
	}
	
	/**
	 * Returns the height of the ellipse to the newHieght, the new ellipse will 
	 * still be centered around the same point as the original ellipse.
	 * 
	 * @param newHeight see above.
	 */
	public void setHeight(double newHeight)
	{
		AffineTransform t = AttributeKeys.TRANSFORM.get(this);
		if (t == null) return;
		
		double centreX = getCentreX();
		double centreY = getCentreY();
		double width = getWidth();
		double[] matrix = new double[6];
		t.getMatrix(matrix);
		matrix[4] = 0;
		matrix[5] = 0;
		AffineTransform aT = new AffineTransform(matrix);
		Point2D.Double a = (Point2D.Double) aT.transform(
					new Point2D.Double(1, 0), null);
		double theta = Math.asin(a.y/Math.sqrt(a.x*a.x+a.y*a.y));
		AffineTransform newT = new AffineTransform();
		newT.setToRotation(theta);
		
		Ellipse2D.Double newEllipse = new Ellipse2D.Double(0, 0, width, 
									newHeight);
		
		Shape rotatedShape = newT.createTransformedShape(newEllipse);
		double rotatedShapeCentreX = rotatedShape.getBounds2D().getCenterX();
		double rotatedShapeCentreY = rotatedShape.getBounds2D().getCenterY();
		
		double diffX = centreX-rotatedShapeCentreX;
		double diffY = centreY-rotatedShapeCentreY;
	
		Point2D.Double lead = new Point2D.Double(diffX, diffY);
		
		AffineTransform rT = AffineTransform.getTranslateInstance(lead.x, 
									lead.y);
		rT.concatenate(newT);
		
		ellipse = newEllipse;
		AttributeKeys.TRANSFORM.set(this, rT);
		invalidate();
	}
	
	/** 
	 * Returns the x coordinate of the figure. 
	 * 
	 * @return See above.
	 */
	public double getCentreX()
	{
		AffineTransform t = AttributeKeys.TRANSFORM.get(this);
		if (t == null) return ellipse.getCenterX();
		Point2D src = new Point2D.Double(ellipse.getCenterX(), 
									ellipse.getCenterY());
		Point2D dest = new Point2D.Double();
		t.transform(src, dest);
		return dest.getX();
	}
	
	/** 
	 * Returns the y coordinate of the figure. 
	 * 
	 * @return See above.
	 */
	public double getCentreY()
	{
		AffineTransform t = AttributeKeys.TRANSFORM.get(this);
		if (t == null) return ellipse.getCenterY();
		
		Point2D src = new Point2D.Double(ellipse.getCenterX(), 
					ellipse.getCenterY());
		Point2D dest = new Point2D.Double();
		t.transform(src, dest);
		return dest.getY();
	}
	
	/**
	 * Transforms the figure.
	 *
	 * @param tx the transformation.
	 */
	public void transform(AffineTransform tx)
	{
		if (AttributeKeys.TRANSFORM.get(this) == null)
		{
			AttributeKeys.TRANSFORM.basicSetClone(this, tx);
		}
		else
		{
			AffineTransform t = AttributeKeys.TRANSFORM.getClone(this);
			t.preConcatenate(tx);
			AttributeKeys.TRANSFORM.basicSet(this, t);
		}
	}
	
	
	/*
	 * UNDO/REDO methods.
	 * @see org.jhotdraw.draw.Figure#restoreTransformTo(java.lang.Object)
	 */
	public void restoreTransformTo(Object geometry)
	{
		Object[] restoreData = (Object[]) geometry;
		ellipse=(Ellipse2D.Double) ((Ellipse2D.Double) restoreData[0]).clone();
		SVGAttributeKeys.TRANSFORM.basicSetClone(this, (AffineTransform) restoreData[1]);
		SVGAttributeKeys.FILL_GRADIENT.basicSetClone(this, (Gradient) restoreData[2]);
		SVGAttributeKeys.STROKE_GRADIENT.basicSetClone(this, (Gradient) restoreData[3]);
		invalidate();
	}
	
	/*
	 * UNDO/REDO methods.
	 * @see org.jhotdraw.draw.Figure#getTransformRestoreData(java.lang.Object)
	 */
	public Object getTransformRestoreData()
	{
		return new Object[] 
		      { 
				ellipse.clone(),
				SVGAttributeKeys.TRANSFORM.getClone(this),
				SVGAttributeKeys.FILL_GRADIENT.getClone(this),
				SVGAttributeKeys.STROKE_GRADIENT.getClone(this), 
		      };
	}
	
	
	/**
	 * Clones the figure.
	 */
	public RotateEllipseFigure clone()
	{
		RotateEllipseFigure that = (RotateEllipseFigure) super.clone();
		that.ellipse = (Ellipse2D.Double) this.ellipse.clone();
		that.cachedTransformedShape = null;
		return that;
	}
	
	// EVENT HANDLING
	public boolean isEmpty()
	{
		Rectangle2D.Double b = getBounds();
		return b.width <= 0|| b.height <= 0;
	}
	
	/**
	 * Invalidate the figure and remove the cachedTransformedShape, this means
	 * that the figures geometry has changed and it should be redrawn.
	 * @see AbstractAttributedFigure#invalidate()
	 */
	public void invalidate()
	{			
		super.invalidate();
		cachedTransformedShape = null;
	}
	
	/*
	 * Drawing code.
	 */
	/**
	 * Draws the figure 
	 * @see AbstractAttributedFigure#draw(Graphics2D)
	 */
	public void draw(Graphics2D g) { drawFigure(g); }
	
	/**
	 * This method is invoked before the rendered image of the figure is
	 * a composite figure.
	 * 
	 * @param g The graphics context.
	 */
	private void drawFigure(Graphics2D g)
	{
		Paint paint = SVGAttributeKeys.getFillPaint(this);
		if (paint != null)
		{
			g.setPaint(paint);
			drawFill(g);
		}
		paint = SVGAttributeKeys.getStrokePaint(this);
		if (paint != null && AttributeKeys.STROKE_WIDTH.get(this) > 0)
		{
			g.setPaint(paint);
			g.setStroke(AttributeKeys.getStroke(this));
			drawStroke(g);
		}
	}
	
	/**
	 * Draws the fill of the ellipse.
	 */
	protected void drawFill(Graphics2D g)
	{
		g.fill(getTransformedShape());
	}
	
	/**
	 * Draws the stroke of the ellipse.
	 */
	protected void drawStroke(Graphics2D g)
	{
		g.draw(getTransformedShape());
	}

	/*
	 * Connecting to the figure. Since this figure does not allow connections
	 * it will always return NULL.
	 */
	
	/**
	 * Return false as no connections can exist.
	 */
	public boolean canConnect() { return false; }
	
	/**
	 * Return null as no connections can exist.
	 */
	public Connector findConnector(Point2D.Double p, ConnectionFigure prototype)
	{
		return null; 
	}
	
	/**
	 * Return null as no connections can exist.
	 */
	public Connector findCompatibleConnector(Connector c,
			boolean isStartConnector)
	{
		return null; 
	}

}


