/*
 * @(#)Turtle.java    1.0  26 September 2002
 *
 * Copyright 2004
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */
 
package edu.neu.ccs.pedagogy;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.util.*;
import javax.swing.*;

import edu.neu.ccs.*;
import edu.neu.ccs.gui.*;

/**
 * A class to implement turtle graphics that will allow
 * one or more turtle objects to draw in a common
 * <CODE>{@link BufferedPanel BufferedPanel}</CODE>.
 * 
 * @author  Richard Rasala 
 * @version 2.2
 * @since 1.1
 */
public class Turtle {

    ///////////////
    // Constants //
    ///////////////
    
    /** The minimum size of the common turtle buffered panel. */
    public static final int MINIMUM_SIZE = 201;
    
    /** The preferred size of the common turtle buffered panel. */
    public static final int PREFERRED_SIZE = 401;
    
    /** The default pen paint. */
    public static final Paint DEFAULT_PAINT = Color.black;
    
    /** Turtle pen state: pen up. */
    public static final int PEN_UP = 0;
    
    /** Turtle pen state: pen down. */
    public static final int PEN_DOWN = 1;
    
    /** Degrees to radians conversion. */
    private static final double DEGREES_TO_RADIANS = Math.PI / 180.0;
        
    
    ////////////////////////
    // Static Member Data //
    ////////////////////////
    
    /** The common buffered panel for all turtle graphics. */
    protected static BufferedPanel turtleBufferPanel = null;
    
    /** The size of the common turtle buffered panel. */
    protected static int turtleBufferPanelSize = PREFERRED_SIZE;
    
    /** The affine transform used to scale all turtle drawing. */
    protected static AffineTransform transform = null;
    
    /** The list of turtle objects that have been created. */
    protected static Vector turtleList = new Vector();
    
    /////////////////
    // Member Data //
    /////////////////
    
    /** The turtle x position. */
    protected double xPosition = 0;
    
    /** The turtle y position. */
    protected double yPosition = 0;
    
    /**
     * The turtle direction in degrees measured counterclockwise
     * from the positive horizontal axis.
     */
    protected double direction = 0;
    
    /** The turtle pen paint. */
    protected Paint penPaint = DEFAULT_PAINT;
    
    /** The turtle pen state. */
    protected int penState = PEN_DOWN;
    
    /** Whether or not the turtle triangle is visible. */
    protected boolean showTurtle = true;
    
    /** Whether or not repaint occurs after each step. */
    protected boolean autoRepaint = true;
    
    /////////////////////
    // Private Helpers //
    /////////////////////
    
    /** The line to draw. */
    private Line2D line = new Line2D.Double();
    
    /** Endpoint 1 of the line. */
    private Point2D point1 = new Point2D.Double();
    
    /** Endpoint 2 of the line. */
    private Point2D point2 = new Point2D.Double();
    
    /** The path to outline the turtle triangle shape. */
    GeneralPath path = new GeneralPath(PathIterator.WIND_NON_ZERO, 4);
        
    /////////////////////////////
    // Static Member Functions //
    /////////////////////////////
    
    /**
     * Set the size of the common buffered panel for turtle graphics
     * (this must be called before the buffered panel is created).
     *
     * @param size the new turtle buffered panel size
     * @see #getTurtleBufferPanel()
     * @see #getTurtleBufferPanelSize()
     */
    public static void setTurtleBufferPanelSize(int size) {
        if (turtleBufferPanel == null) {
            if (size < MINIMUM_SIZE)
                size = MINIMUM_SIZE;
                
            turtleBufferPanelSize = size;
        }
    }
    
    /**
     * Return the size of the common buffered panel for turtle graphics.
     *
     * @see #getTurtleBufferPanel()
     * @see #setTurtleBufferPanelSize(int)
     */
    public static int getTurtleBufferPanelSize() {
        return turtleBufferPanelSize;
    }
    
    /**
     * Create and return the common buffered panel for turtle graphics.
     *
     * @see #setTurtleBufferPanelSize(int)
     * @see #getTurtleBufferPanelSize()
     */
    public static BufferedPanel getTurtleBufferPanel() {
        if (turtleBufferPanel == null) {
            turtleBufferPanel =
                new BufferedPanel
                    (turtleBufferPanelSize, turtleBufferPanelSize)
            {
                public void paintOver(Graphics2D g2) {
                    Turtle[] list = getTurtles();
                    
                    int size = list.length;
                    
                    if (size == 0)
                        return;
                    
                    for (int i = 0; i < size; i++)
                        if (list[i].showTurtle)
                            list[i].drawTurtle(g2);
                }
            };
            
            useCenterCoordinates();
        }
        
        return turtleBufferPanel;
    }
    
    /**
     * Use a coordinate system with the origin at the center
     * of the common turtle buffered panel and
     * with x increasing rightwards and y increasing upwards.
     *
     * @see #useCornerCoordinates()
     * @see #setCoordinatesViaBounds(Rectangle2D)
     * @see #setCoordinatesViaBounds(Rectangle2D, int)
     */
    public static void useCenterCoordinates() {
        if (turtleBufferPanel != null) {
            int center = (turtleBufferPanelSize + 1) / 2;
            
            transform =
                new AffineTransform(1, 0, 0, -1, center, center);
        }
    }
    
    /**
     * Use a coordinate system with the origin at the lower
     * left corner of the common turtle buffered panel and
     * with x increasing rightwards and y increasing upwards.
     *
     * @see #useCenterCoordinates()
     * @see #setCoordinatesViaBounds(Rectangle2D)
     * @see #setCoordinatesViaBounds(Rectangle2D, int)
     */
    public static void useCornerCoordinates() {
        if (turtleBufferPanel != null) {
            int xCorner = 0;
            int yCorner = turtleBufferPanelSize - 1;
            
            transform =
                new AffineTransform(1, 0, 0, -1, xCorner, yCorner);
        }
    }
    
    /**
     * Set the coordinate system in the common turtle buffered panel
     * using the given bounds in the world coordinate system to
     * define the affine coordinate transformation.
     *
     * @param world the bounds in world coordinates
     * @see #useCenterCoordinates()
     * @see #useCornerCoordinates()
     * @see #setCoordinatesViaBounds(Rectangle2D, int)
     */
    public static void setCoordinatesViaBounds(Rectangle2D world) {
        setCoordinatesViaBounds(world, 0);
    }
    
    /**
     * Set the coordinate system in the common turtle buffered panel
     * using the given bounds in the world coordinate system and the
     * given image inset to define the affine coordinate transformation.
     *
     * @param world the bounds in world coordinates
     * @param inset the inset of the turtle drawing within the image
     * @see #useCenterCoordinates()
     * @see #useCornerCoordinates()
     * @see #setCoordinatesViaBounds(Rectangle2D)
     */
    public static void setCoordinatesViaBounds
        (Rectangle2D world, int inset)
    {
        if (turtleBufferPanel != null && world != null) {
            Rectangle2D.Double image =
                new Rectangle2D.Double
                    (0, 0, turtleBufferPanelSize, turtleBufferPanelSize);
            
            PlotTool plottool =
                new PlotTool(world, image, true, inset);
            
            transform = plottool.getTransform();
        }
    }
    
    /** Return the graphics context for the common turtle buffered panel. */
    public static Graphics2D getTurtleBufferGraphics() {
        return getTurtleBufferPanel().getBufferGraphics();
    }
    
    /** Return the buffer in the common turtle buffered panel. */
    public static BufferedImage getTurtleBuffer() {
        return getTurtleBufferPanel().getBuffer();
    }
    
    /** Clear the common turtle buffered panel and repaint. */
    public static void clear() {
        getTurtleBufferPanel().clearPanel();
        repaint();
    }
    
    /** Repaint the common turtle buffered panel. */
    public static void repaint() {
        getTurtleBufferPanel().repaint();
    }
    
    /**
     * Return an array of all turtles that have been created.
     * If none have been created then the array returned will
     * have zero elements.
     */
    public static Turtle[] getTurtles() {
        int size = turtleList.size();
        
        Turtle[] list = new Turtle[size];
        
        for (int i = 0; i < size; i++)
            list[i] = (Turtle) turtleList.get(i);
        
        return list;
    }
    
    /** Show all turtle triangles. */
    public static void showAllTurtles() {
        Turtle[] list = getTurtles();
        
        int size = list.length;
        
        if (size == 0)
            return;
            
        for (int i = 0; i < size; i++)
            list[i].showTurtle = true;
        
        repaint();
    }
    
    /** Hide all turtle triangles. */
    public static void hideAllTurtles() {
        Turtle[] list = getTurtles();
        
        int size = list.length;

        if (size == 0)
            return;
            
        for (int i = 0; i < size; i++)
            list[i].showTurtle = false;
        
        repaint();
    }
    
    /////////////////
    // Constructor //
    /////////////////
    
    /**
     * The default turtle constructor creates the turtle buffered panel
     * if that buffered panel has not been created by an earlier turtle
     * and then adds this turtle to the list of all turtles.
     *
     * The default turtle settings are:
     * <UL>
     *   <LI> x position   = 0
     *   <LI> y position   = 0
     *   <LI> direction    = 0
     *   <LI> pen paint    = Color.black
     *   <LI> pen state    = PEN_DOWN
     *   <LI> show turtle  = true
     *   <LI> auto repaint = true
     * </UL>
     */
    public Turtle() {
        getTurtleBufferPanel();
        turtleList.add(this);
    }
    
    //////////////////////
    // Member Functions //
    //////////////////////
    
    /**
     * <P>Move the turtle from its current position by the given
     * distance in its current direction and if the current
     * pen state is PEN_DOWN then draw a line using the
     * current pen paint.</P>
     *
     * <P>The default is to set auto repaint to true.  This will
     * give immediate feedback for each step call but will be
     * slower if lots of step calls are made.</P>
     *
     * <P>To improve the speed of turtle graphics at the expense
     * of immediate feedback, make the call on a Turtle named T:</P>
     * 
     * <UL>
     *     <LI> T.setAutoRepaint(false);
     * </UL>
     *
     * <P>Then, to repaint the turtle buffered panel when the turtle
     * drawing is complete, make the static call:</P>
     *
     * <UL>
     *     <LI> Turtle.repaint();
     * </UL>
     *
     * <P>This design dramatically improves graphics performance.</P>
     *
     * @param distance the distance to step
     * @see #setAutoRepaint(boolean)
     * @see #getAutoRepaint()
     */
    public void step(double distance) {
        // compute cos and sin of the direction
        double cos = Math.cos(direction * DEGREES_TO_RADIANS);
        double sin = Math.sin(direction * DEGREES_TO_RADIANS);
        
        // compute the position displacement dx and dy
        double dx = distance * cos;
        double dy = distance * sin;
        
        // draw if the pen is down otherwise just move
        if (penState == PEN_DOWN) {
            point1.setLocation(xPosition, yPosition);
            xPosition += dx;
            yPosition += dy;
            point2.setLocation(xPosition, yPosition);
            
            transform.transform(point1, point1);
            transform.transform(point2, point2);
            
            line.setLine(point1, point2);
            
            Graphics2D g2 = getTurtleBufferGraphics();
            
            g2.setPaint((penPaint != null) ? penPaint: DEFAULT_PAINT);            
            g2.draw(line);
        }
        else {
            xPosition += dx;
            yPosition += dy;
        }
        
        if (autoRepaint)
            repaint();
    }
    
    /**
     * Increment the turtle direction by the given angle in degrees.
     *
     * @param angle the angle in degrees used to increment the direction
     * @see #setDirection(double)
     */
    public void turn(double angle) {
        setDirection(direction + angle);
    }
    
    /**
     * Show the turtle triangle.
     *
     * @see #hideTurtle()
     * @see #setVisible(boolean)
     * @see #isVisible()
     */
    public void showTurtle() {
        showTurtle = true;
        
        if (autoRepaint)
            repaint();
    }
    
    /**
     * Hide the turtle triangle.
     *
     * @see #showTurtle()
     * @see #setVisible(boolean)
     * @see #isVisible()
     */
    public void hideTurtle() {
        showTurtle = false;
        
        if (autoRepaint)
            repaint();
    }
    
    /**
     * Show or hide the turtle triangle using the given parameter to
     * decide.
     *
     * @param visible if true then show the turtle otherwise hide it
     * @see #showTurtle()
     * @see #hideTurtle()
     * @see #isVisible()
     */
    public void setVisible(boolean visible) {
        showTurtle = visible;
        
        if (autoRepaint)
            repaint();
    }
    
    /**
     * Return whether or not the turtle triangle is visible.
     *
     * @see #showTurtle()
     * @see #hideTurtle()
     * @see #setVisible(boolean)
     */
    public boolean isVisible() {
        return showTurtle;
    }
    
    /**
     * Set the turtle position.
     *
     * @param x the turtle x position
     * @param y the turtle y position
     * @see #setPosition(Point2D)
     * @see #getPosition()
     * @see #getX()
     * @see #getY()
     */
    public void setPosition(double x, double y) {
        xPosition = x;
        yPosition = y;
        
        if (autoRepaint)
            repaint();
    }
    
    /**
     * Set the turtle position.
     *
     * @param position the turtle position
     * @see #setPosition(double, double)
     * @see #getPosition()
     * @see #getX()
     * @see #getY()
     */
    public void setPosition(Point2D position) {
        if (position != null)
            setPosition(position.getX(), position.getY());
    }
    
    /**
     * Return the turtle position.
     *
     * @see #setPosition(double, double)
     * @see #setPosition(Point2D)
     * @see #getX()
     * @see #getY()
     */
    public Point2D getPosition() {
        return new Point2D.Double(xPosition, yPosition);
    }
    
    /**
     * Return the turtle x position.
     *
     * @see #setPosition(double, double)
     * @see #setPosition(Point2D)
     * @see #getPosition()
     * @see #getY()
     */
    public double getX() {
        return xPosition;
    }
    
    /**
     * Return the turtle y position.
     *
     * @see #setPosition(double, double)
     * @see #setPosition(Point2D)
     * @see #getPosition()
     * @see #getX()
     */
    public double getY() {
        return yPosition;
    }
    
    /**
     * Set the turtle direction in degrees counterclockwise from the
     * positive horizontal axis.
     *
     * @param direction the new turtle direction
     * @see #getDirection()
     * @see #turn(double)
     */
    public void setDirection(double direction) {
        direction %= 360.0;
        
        if (direction < 0.0)
            direction += 360.0;
        
        this.direction = direction;
        
        if (autoRepaint)
            repaint();
    }
    
    /**
     * Return the turtle direction.
     *
     * @see #setDirection(double)
     */
    public double getDirection() {
        return direction;
    }
    
    /**
     * Set the turtle pen paint.
     *
     * @param paint the paint to use for drawing this turtle
     * @see #getPaint()
     * @see #setPaint(int, int, int)
     * @see #setPaint(int, int, int, int)
     */
    public void setPaint(Paint paint) {
        penPaint = paint;
        
        if (autoRepaint)
            repaint();
    }
    
    /**
     * Set the turtle pen paint via r,g,b components.
     *
     * @param r the red   component of the Color to be used to paint
     * @param g the green component of the Color to be used to paint
     * @param b the blue  component of the Color to be used to paint
     * @see #getPaint()
     * @see #setPaint(Paint)
     * @see #setPaint(int, int, int, int)
     */
    public void setPaint(int r, int g, int b) {
        setPaint(new Color(r, g, b));
    }
    
    /**
     * Set the turtle pen paint via r, g, b, alpha components.
     *
     * @param r the red   component of the Color to be used to paint
     * @param g the green component of the Color to be used to paint
     * @param b the blue  component of the Color to be used to paint
     * @param alpha the alpha component of the Color to be used to paint
     * @see #getPaint()
     * @see #setPaint(Paint)
     * @see #setPaint(int, int, int)
     */
    public void setPaint(int r, int g, int b, int alpha) {
        setPaint(new Color(r, g, b, alpha));
    }
    
    /**
     * Return the turtle pen paint.
     *
     * @see #setPaint(Paint)
     */
    public Paint getPaint() {
        return penPaint;
    }
    
    /**
     * Set the turtle pen state.
     *
     * @param penState the pen state: PEN_UP or PEN_DOWN
     * @see #getPenState()
     * @see #PEN_UP
     * @see #PEN_DOWN
     */
    public void setPenState(int penState) {
        switch (penState) {
            case PEN_UP:
            case PEN_DOWN:
                this.penState = penState;
                break;
            
            default:
                break;
        }
    }
    
    /**
     * Return the turtle pen state.
     *
     * @see #setPenState(int)
     * @see #PEN_UP
     * @see #PEN_DOWN
     */
    public int getPenState() {
        return penState;
    }
    
    /**
     * Set auto repaint, that is, whether or not to repaint
     * after each step, to the given value.
     *
     * @param enabled the new auto repaint setting
     * @see #getAutoRepaint()
     * @see #step(double)
     */
    public void setAutoRepaint(boolean enabled) {
        autoRepaint = enabled;
    }
    
    /**
     * Return the current auto repaint setting.
     *
     * @see #setAutoRepaint(boolean)
     * @see #step(double)
     */
    public boolean getAutoRepaint() {
        return autoRepaint;
    }
    
    /**
     * Reset the turtle to its default initial state and then repaint.
     *
     * The default settings are:
     *
     * <UL>
     *   <LI> x position   = 0
     *   <LI> y position   = 0
     *   <LI> direction    = 0
     *   <LI> pen paint    = Color.black
     *   <LI> pen state    = PEN_DOWN
     *   <LI> show turtle  = true
     *   <LI> auto repaint = true
     * </UL>
     */
    public void reset() {
        xPosition   = 0;
        yPosition   = 0;
        direction   = 0;
        penPaint    = DEFAULT_PAINT;
        penState    = PEN_DOWN;
        autoRepaint = true;
        showTurtle  = true;
        
		repaint();
    }
    
    ////////////////////////////////
    // Protected Member Functions //
    ////////////////////////////////
    
    /**
     * <P>Draw the turtle triangle at the current turtle position in
     * the current turtle direction.</P>
     *
     * <P>This method is called by the <CODE>paintOver</CODE> method
     * of the common turtle buffered panel during repaint actions.</P>
     */
    protected void drawTurtle(Graphics2D g2) {
        // locate turtle in screen coordinates
        point1.setLocation(xPosition, yPosition);
        transform.transform(point1, point1);
        
        float x = (float) point1.getX();
        float y = (float) point1.getY();
        
        // compute cos and sin of the direction
        float cos = (float) Math.cos(direction * DEGREES_TO_RADIANS);
        float sin = (float) Math.sin(direction * DEGREES_TO_RADIANS);

        // compute the polygon for the triangle
        float size1 = 6;
        float size2 = 4;
        
        // Note that in image coordinates:
        //     The direction is along vector        V = (+cos, -sin)
        //     The perpendicular direction is along W = (+sin, +cos)
        //
        // If P is the turtle point then the triangle is computed as:
        //
        //     P + size1 * V
        //     P - size1 * V - size2 * W
        //     P - size1 * V + size2 * W
        
        path.reset();
        path.moveTo(x + size1*cos, y - size1*sin);
        path.lineTo(x - size1*cos - size2*sin, y + size1*sin - size2*cos);
        path.lineTo(x - size1*cos + size2*sin, y + size1*sin + size2*cos);
        path.closePath();
        
        g2.setPaint((penPaint != null) ? penPaint: DEFAULT_PAINT);            
        g2.fill(path);
    }

    //////////////////////
    // Static Self Test //
    //////////////////////
    
    /** A simple self test. */
    public static void selfTest() {
        // set the turtle buffered panel size to 501
        // this must be done before the turtle buffered panel is created!
        setTurtleBufferPanelSize(501);
        
        // open up a turtle window with the turtle buffered panel inside
        JPTFrame.createQuickJPTFrame(
            "Turtle Self Test",
            new DisplayWrapper(getTurtleBufferPanel()));
        
        // define the number of iteration steps in the tests
        int steps = 96;
        
        // define offset to be 1/4 of the turtle buffered panel size
        int offset = getTurtleBufferPanelSize() / 4;
                

        // Test 1: Test default turtle in center coordinates
        // using 90 degree angle.
    
        Turtle turtle1 = new Turtle();
        
        for (int i = 1; i <= steps; i++) {
            turtle1.step(i);
            turtle1.turn(90);
        }
        

        // Test 2: Test default turtle in center coordinates
        // offset by 1/4 of the turtle buffered panel size,
        // using 60 degree angle, and blue color.
        
        Turtle turtle2 = new Turtle();
        
        turtle2.setPosition(offset, offset);
        turtle2.setPaint(Color.blue);
        
        for (int i = 1; i <= steps; i++) {
            turtle2.step(i);
            turtle2.turn(60);
        }
        

        // Test 3: Test default turtle in corner coordinates
        // offset by 1/4 of the turtle buffered panel size,
        // using 60 degree angle,
        // and using colors that cycle every 6 times.
        // Turn off auto repaint and then repaint when the
        // entire test is done.
        
        Turtle.useCornerCoordinates();
        
        Turtle turtle3 = new Turtle();
        
        turtle3.setPosition(offset, offset);
        turtle3.setAutoRepaint(false);
        
        Color[] colors = new Color[]
            { Color.black,  Color.red,      Color.green,
              Color.blue,   Color.magenta,  Color.yellow};
            
        for (int i = 1; i <= steps; i++) {
            turtle3.setPaint(colors[i % 6]);
            turtle3.step(i);
            turtle3.turn(60);
        }
        
        Turtle.repaint();
        

        // Test 4: Use turtle in world coordinates with corners
        // at (-1, -1) and (+1, +1).  Set world position at
        // (0.5, -0.5), use 90 degree angle, and use paint that
        // is specified by an r,g,b value.
        
        Rectangle2D.Double world =
            new Rectangle2D.Double(-1, -1, 2, 2);
        
        Turtle.setCoordinatesViaBounds(world);
        
        Turtle turtle4 = new Turtle();
        
        turtle4.setPosition(0.5, -0.5);
        turtle4.setPaint(153, 51, 0);
        
        double denominator = getTurtleBufferPanelSize() / 2;
        
        for (int i = 1; i <= steps; i++) {
            turtle4.step(i / denominator);
            turtle4.turn(90);
        }
        
        
        // Test 5: Test extraction of a list of all turtles.  Go
        // back to center coordinates, offset to (-offset, offset),
        // use a different color for each turtle, and draw a set
        // of coordinate axes using the four turtles.
        
        Turtle.useCenterCoordinates();
        
        Turtle[] list = Turtle.getTurtles();
        int size = list.length;
        
        for (int i = 0; i < size; i++) {
            list[i].setAutoRepaint(true);
            list[i].setPosition(-offset, offset);
            list[i].setDirection(90 * i + 15);
            list[i].setPaint(colors[i]);
            list[i].step(offset/2);
        }
    }
}
