
package jp.riken.brain.ni.samuraigraph.figure.java2d;

import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

import jp.riken.brain.ni.samuraigraph.base.SGTuple2f;
import jp.riken.brain.ni.samuraigraph.base.SGUtility;
import jp.riken.brain.ni.samuraigraph.figure.SGDrawingElementArrow;
import jp.riken.brain.ni.samuraigraph.figure.SGDrawingElementLine;
import jp.riken.brain.ni.samuraigraph.figure.SGDrawingElementSymbol;


/**
 * An arrow class using Java2D.
 *
 */
public class SGDrawingElementArrow2D extends SGDrawingElementArrow
	implements SGIDrawingElementJava2D
{


	/**
	 * The default constructor.
	 */
	public SGDrawingElementArrow2D()
	{
		super();
	}


	protected SGDrawingElementLine getBodyInstance()
	{
		return new ArrowBody(this);
	}

	protected SGDrawingElementSymbol getHeadInstance()
	{
		return new ArrowHead(this);
	}

	private SGDrawingElementLine2D getLine2D()
	{
		return (SGDrawingElementLine2D)this.mLine;
	}
	
	private SGDrawingElementSymbol2D getStart2D()
	{
		return (SGDrawingElementSymbol2D)this.mStartHead;
	}

	private SGDrawingElementSymbol2D getEnd2D()
	{
		return (SGDrawingElementSymbol2D)this.mEndHead;
	}


	/**
	 * 
	 */
	public Rectangle2D getElementBounds()
	{
		SGDrawingElementLine2D line = this.getLine2D();
		SGDrawingElementSymbol2D start = this.getStart2D();
		SGDrawingElementSymbol2D end = this.getEnd2D();
		ArrayList list = new ArrayList();
		list.add( line.getElementBounds() );
		list.add( start.getElementBounds() );
		list.add( end.getElementBounds() );
		Rectangle2D rect = SGUtility.createUnion( list );
		return rect;
	}


	/**
	 * 
	 */
	public void paintElement( Graphics2D g2d )
	{
		if( this.isVisible() == false )
		{
			return;
		}

		this.getLine2D().paintElement(g2d);
		this.getStart2D().paintElement(g2d);
		this.getEnd2D().paintElement(g2d);
	}


	/**
	 * 
	 * @param g2d
	 * @param clipRect
	 */
	public void paint( Graphics2D g2d, Rectangle2D clipRect )
	{
		if( this.isVisible() == false )
		{
			return;
		}

		this.getLine2D().paint( g2d, clipRect );
		this.getStart2D().paint( g2d, clipRect );
		this.getEnd2D().paint( g2d, clipRect );
	}



	/**
	 * 
	 * @return
	 */
	public float getMagnitude()
	{
		final SGTuple2f start = this.getStart();
		final SGTuple2f end = this.getEnd();
		final float x = start.x - end.x;
		final float y = start.y - end.y;
		return (float)Math.sqrt( x*x + y*y );
	}



	/**
	 * 
	 */
	public boolean setStartX( final float x )
	{
		super.setStartX(x);
		this.updateHeadAngle();
		return true;
	}


	/**
	 * 
	 */
	public boolean setStartY( final float y )
	{
		super.setStartY(y);
		this.updateHeadAngle();
		return true;
	}


	/**
	 * 
	 */
	public boolean setEndX( final float x )
	{
		super.setEndX(x);
		this.updateHeadAngle();
		return true;
	}


	/**
	 * 
	 */
	public boolean setEndY( final float y )
	{
		super.setEndY(y);
		this.updateHeadAngle();
		return true;
	}


	/**
	 *
	 */
	public boolean setTermPoints( final SGTuple2f start, final SGTuple2f end )
	{
		super.setTermPoints( start, end );
		this.updateHeadAngle();
		return true;
	}


	private void updateHeadAngle()
	{
		final float pi = (float)Math.PI;
		final float angle = this.getLine2D().getAngle();
		this.mStartHead.setAngle( angle - 0.50f*pi );
		this.mEndHead.setAngle( angle + 0.50f*pi );
	}


	/**
	 * 
	 * @return
	 */
	private float getOpenAngleTangent()
	{
		return (float)Math.tan( this.getHeadOpenAngle() );
	}


	/**
	 * 
	 * @return
	 */
	private float getCloseAngleTangent()
	{
		return (float)Math.tan( this.getHeadCloseAngle() );
	}




	/**
	 * An inner class for arrow body.
	 */
	static class ArrowBody extends SGDrawingElementLine2D
	{
		private SGDrawingElementArrow mArrow;


		/**
		 *
		 */
		ArrowBody( SGDrawingElementArrow el )
		{
			super();
			this.mArrow = el;
		}

		/**
		 * 
		 */
		public Shape getShape()
		{
			final float open = mArrow.getHeadOpenAngle();
			final float close = mArrow.getHeadCloseAngle();
			final float tanOpen = (float)Math.tan(open);
			final float tanClose = (float)Math.tan(close);
			final float diff = ( open <= close ) ? this.getMagnification()*mArrow.getHeadSize()*( 1.0f - tanOpen/tanClose ) : 0.0f;
			final float len = this.getMagnitude();

			final int aType = SGDrawingElementArrow.SYMBOL_TYPE_ARROW_HEAD;
			final float yStart = ( mArrow.getStartHeadType()!=aType ) ? 0.0f : 0.50f*diff;
			final float yEnd = ( mArrow.getEndHeadType()!=aType ) ? len : ( len - 0.50f*diff );

			Line2D line = new Line2D.Float( 0.0f, yStart, 0.0f, yEnd );
			Shape shape = this.getAffineTransform().createTransformedShape( line );

			return shape;
		}


		/**
		 *
		 */
		private AffineTransform getAffineTransform()
		{
			AffineTransform af = new AffineTransform();

			// translate
			af.translate( this.getStart().x, this.getStart().y );

			// rotate
			final double angle = this.getAngle() - 0.50*Math.PI;
			af.rotate(angle);

			return af;		
		}

	}




	/**
	 * An inner class for arrow head.
	 */
	static class ArrowHead extends SGDrawingElementSymbol2D
	{

		private SGDrawingElementArrow mArrow;

		/**
		 * 
		 */
		ArrowHead( SGDrawingElementArrow el )
		{
			super();
			this.mArrow = el;
		}


		/**
		 * Overrode not to draw line in the symbol with finite area.
		 */
		protected void paintLine( Graphics2D g2d, Shape sh )
		{
			if( isLineTypeSymbol( this.getType() ) )
			{
				super.paintLine( g2d, sh );
			}
		}


		/**
		 * Overrode not to draw line in the symbol with finite area.
		 */
		protected void paintLine( Graphics2D g2d, Area clipArea, Shape sh )
		{
			if( isLineTypeSymbol( this.getType() ) )
			{
				super.paintLine( g2d, clipArea, sh );
			}
		}



		/**
		 * 
		 */
		public Shape getShape()
		{
			if( this.getType() == SGDrawingElementSymbol.SYMBOL_TYPE_VOID )
			{
				return null;
			}

			Shape sh = null;
			final int type = this.mType;

			// arrow head
			if( type == SYMBOL_TYPE_ARROW_HEAD )
			{
				final float open = this.mArrow.getHeadOpenAngle();
				final float close = this.mArrow.getHeadCloseAngle();
				if( close <= open )
				{
					return null;
				}
				
				final float tanOpen = (float)Math.tan(open);
				final float tanClose = (float)Math.tan(close);

				final float x = this.getX();
				final float y = this.getY();

				final float mag = this.getMagnification();
				final float headSize = mag*this.getSize();
				final float openSize = headSize*tanOpen;

				Point2D[] pointArray = new Point2D[4];
				pointArray[0] = new Point2D.Float(
					x,
					y
				);
				pointArray[1] = new Point2D.Float(
					x + openSize,
					y + headSize
				);
				pointArray[2] = new Point2D.Float(
					x,
					y + headSize - openSize/tanClose
				);
				pointArray[3] = new Point2D.Float(
					x - openSize,
					y + headSize
				);

				Shape[] pathArray = new Line2D[pointArray.length];
				for( int ii=0; ii<pathArray.length; ii++ )
				{
					pathArray[ii] = new Line2D.Float(
						pointArray[ii], pointArray[(ii+1)%pointArray.length] );
				}

				GeneralPath gp = new GeneralPath();
				for( int ii=0; ii<pathArray.length; ii++ )
				{
					gp.append( pathArray[ii], true );
				}

				sh = this.getAffineTransform().createTransformedShape( gp );
			}
			else
			{
				sh = super.getShape();
			}

			return sh;
		}

	}


}


