//******************************************************************************
//
// File:    Bullet.java
// Package: edu.rit.draw.item
// Unit:    Class edu.rit.draw.item.Bullet
//
// This Java source file is copyright (C) 2006 by Alan Kaminsky. All rights
// reserved. For further information, contact the author, Alan Kaminsky, at
// ark@cs.rit.edu.
//
// This Java source file is part of the Parallel Java Library ("PJ"). PJ 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 3 of the License, or (at your option) any later version.
//
// PJ 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.
//
// Linking this library statically or dynamically with other modules is making a
// combined work based on this library. Thus, the terms and conditions of the
// GNU General Public License cover the whole combination.
//
// As a special exception, the copyright holders of this library give you
// permission to link this library with independent modules to produce an
// executable, regardless of the license terms of these independent modules, and
// to copy and distribute the resulting executable under terms of your choice,
// provided that you also meet, for each linked independent module, the terms
// and conditions of the license of that module. An independent module is a
// module which is not derived from or based on this library. If you modify this
// library, you may extend this exception to your version of the library, but
// you are not obligated to do so. If you do not wish to do so, delete this
// exception statement from your version.
//
// A copy of the GNU General Public License is provided in the file gpl.txt. You
// may also obtain a copy of the GNU General Public License on the World Wide
// Web at http://www.gnu.org/licenses/gpl.html.
//
//******************************************************************************

package edu.rit.draw.item;

import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Stroke;

import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;

import java.io.Externalizable;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

/**
 * Class Bullet provides various kinds of bullets that can be added to a
 * {@linkplain Text} item. The bullet is always added to the left of the text
 * item, with a specified distance from the left side of the bullet to the left
 * side of the text item.
 *
 * @author  Alan Kaminsky
 * @version 10-Jul-2006
 */
public class Bullet
	implements Externalizable
	{

// Hidden helper classes.

	/**
	 * Class Bullet.Info is a record containing information needed to draw a
	 * bullet. <TT>theArea</TT> draws the bullet in "standard position" -- with
	 * its lower left corner at (0,0), for a text font size of 1. If
	 * <TT>theArea</TT> is null, the bullet is not drawn. <TT>isFilled</TT>
	 * tells whether the interior of the bullet is filled.
	 * <TT>theOutlineWidth</TT> gives the width of the outline stroke for the
	 * bullet; if 0, the outline is not drawn.
	 *
	 * @author  Alan Kaminsky
	 * @version 10-Jul-2006
	 */
	private static class Info
		{
		public Area theArea;
		public boolean isFilled;
		public float theOutlineWidth;

		public Info
			(Area theArea,
			 boolean isFilled,
			 float theOutlineWidth)
			{
			this.theArea = theArea;
			this.isFilled = isFilled;
			this.theOutlineWidth = theOutlineWidth;
			}
		}

// Hidden constants.

	/**
	 * A kind of bullet with no shape.
	 */
	private static final int BULLET_KIND_NONE = 0;

	/**
	 * A kind of bullet in the shape of a medium sized dot.
	 */
	private static final int BULLET_KIND_DOT = 1;

	/**
	 * A kind of bullet in the shape of a medium sized circle.
	 */
	private static final int BULLET_KIND_CIRCLE = 2;

// Exported constants.

	/**
	 * The normal bullet offset, 36.0 (1/2 inch).
	 */
	public static final double NORMAL_OFFSET = 36.0;

	/**
	 * A nonexistent bullet.
	 */
	public static final Bullet NONE = null;

	/**
	 * A bullet in the shape of a medium sized dot, offset the normal offset
	 * (36.0, or 1/2 inch) from the text item.
	 */
	public static final Bullet DOT =
		new Bullet (BULLET_KIND_DOT, NORMAL_OFFSET);

	/**
	 * A bullet in the shape of a medium sized circle, offset the normal offset
	 * (36.0, or 1/2 inch) from the text item.
	 */
	public static final Bullet CIRCLE =
		new Bullet (BULLET_KIND_CIRCLE, NORMAL_OFFSET);

// Hidden data members.

	private static final long serialVersionUID = 1799559688305790072L;

	// Table of bullet information records, indexed by bullet kind.
	private static final Info[] theBulletInfo;
	static
		{
		theBulletInfo = new Info [3];

		theBulletInfo[0] = new Info (null, false, 0.0f);

		theBulletInfo[1] =
			new Info
				(new Area (new Ellipse2D.Double (0.0, -0.625, 0.5, 0.5)),
				 true,
				 0.0f);

		theBulletInfo[2] =
			new Info
				(new Area (new Ellipse2D.Double (0.0, -0.625, 0.5, 0.5)),
				 false,
				 1.0f);
		}

	// Kind of bullet.
	private int myKind;

	// Offset from left end of bullet to left end of text box.
	private double myOffset = theDefaultOffset;

	// The default offset.
	private static double theDefaultOffset = NORMAL_OFFSET;

// Exported constructors.

	/**
	 * Construct a new nonexistent bullet.
	 */
	public Bullet()
		{
		}

	/**
	 * Construct a new bullet of the same kind as the given bullet, but with the
	 * default offset.
	 *
	 * @param  theBullet  Bullet.
	 *
	 * @exception  NullPointerException
	 *     (unchecked exception) Thrown if <TT>theBullet</TT> is null.
	 */
	public Bullet
		(Bullet theBullet)
		{
		myKind = theBullet.myKind;
		myOffset = theDefaultOffset;
		}

// Hidden constructors.

	/**
	 * Construct a new bullet of the given kind with the given offset.
	 *
	 * @param  theKind    Kind of bullet (one of the <TT>BULLET_KIND_XXX</TT>
	 *                    values).
	 * @param  theOffset  Offset from left side of bullet to left side of text.
	 */
	private Bullet
		(int theKind,
		 double theOffset)
		{
		myKind = theKind;
		myOffset = theOffset;
		}

// Exported operations.

	/**
	 * Returns the default offset for bullets. The offset is the distance
	 * between the left side of the bullet and the left side of the text.
	 *
	 * @return  Default offset.
	 */
	public static double defaultOffset()
		{
		return theDefaultOffset;
		}

	/**
	 * Set the default offset for bullets. The offset is the distance between
	 * the left side of the bullet and the left side of the text.
	 *
	 * @param  theOffset  Default offset.
	 *
	 * @exception  IllegalArgumentException
	 *     (unchecked exception) Thrown if <TT>theOffset</TT> is less than 0.
	 */
	public static void defaultOffset
		(double theOffset)
		{
		if (theOffset < 0.0) throw new IllegalArgumentException();
		theDefaultOffset = theOffset;
		DOT.myOffset = theOffset;
		CIRCLE.myOffset = theOffset;
		}

	/**
	 * Returns this bullet's offset. The offset is the distance between the left
	 * side of the bullet and the left side of the text.
	 *
	 * @return  Offset.
	 */
	public double offset()
		{
		return myOffset;
		}

	/**
	 * Set this bullet's offset. The offset is the distance between the left
	 * side of the bullet and the left side of the text.
	 *
	 * @param  theOffset  Offset.
	 *
	 * @return  This bullet.
	 *
	 * @exception  IllegalArgumentException
	 *     (unchecked exception) Thrown if <TT>theOffset</TT> is less than 0.
	 */
	public Bullet offset
		(double theOffset)
		{
		if (theOffset < 0.0) throw new IllegalArgumentException();
		myOffset = theOffset;
		return this;
		}

	/**
	 * Write this bullet to the given object output stream.
	 *
	 * @param  out  Object output stream.
	 *
	 * @exception  IOException
	 *     Thrown if an I/O error occurred.
	 */
	public void writeExternal
		(ObjectOutput out)
		throws IOException
		{
		out.writeInt (myKind);
		out.writeDouble (myOffset);
		}

	/**
	 * Read this bullet from the given object input stream.
	 *
	 * @param  in  Object input stream.
	 *
	 * @exception  IOException
	 *     Thrown if an I/O error occurred.
	 */
	public void readExternal
		(ObjectInput in)
		throws IOException
		{
		myKind = in.readInt();
		myOffset = in.readDouble();
		if (0 > myKind || myKind >= theBulletInfo.length)
			{
			throw new InvalidObjectException
				("Invalid bullet (kind = " + myKind + ")");
			}
		}

	/**
	 * Draw this bullet in the given graphics context. It assumes the graphics
	 * context's paint is already set to the correct value. The bullet is scaled
	 * to match the <TT>ascent</TT>. The bullet's left side is placed relative
	 * to the coordinates of the left end of the text baseline <TT>(x,y)</TT>.
	 *
	 * @param  g2d     2-D graphics context.
	 * @param  ascent  Text font ascent.
	 * @param  x       X coordinate of the left end of the text baseline.
	 * @param  y       Y coordinate of the left end of the text baseline.
	 */
	public void draw
		(Graphics2D g2d,
		 double ascent,
		 double x,
		 double y)
		{
		Info info =
			0 <= myKind && myKind < theBulletInfo.length ?
				theBulletInfo[myKind] :
				null;
		if (info != null && info.theArea != null)
			{
			AffineTransform areaTransform = new AffineTransform();
			areaTransform.translate (x-myOffset, y);
			areaTransform.scale (ascent, ascent);
			Area transformedArea =
				info.theArea.createTransformedArea (areaTransform);
			if (info.isFilled)
				{
				g2d.fill (transformedArea);
				}
			if (info.theOutlineWidth > 0.0f)
				{
				Stroke oldStroke = g2d.getStroke();
				g2d.setStroke (new BasicStroke (info.theOutlineWidth));
				g2d.draw (transformedArea);
				g2d.setStroke (oldStroke);
				}
			}
		}

	}
