/* @(#)SpinningAnimation.java   1 September 2007 */

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

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

public class SpinningAnimation
    extends DisplayPanel
    implements WindowConstants
{
    /** The minimum animation component size: 100. */
    public static final int SIZE = 100;
    
    
    /** The common panel gap. */
    public static final int GAP = 10;
    
    
    /** The frame for the animation. */
    protected JPTFrame frame = null;
    
    
    /** The main panel. */
    protected TablePanel mainPanel = null;
    
    
    /** The paintable to spin. */
    protected Paintable paintable = null;
    
    
    /** The tile to hold the paintable. */
    protected Tile tile = null;
    
    
    /** The tile component width. */
    protected int width = 0;
    
    
    /** The tile component height. */
    protected int height = 0;
    
    
    /** The paintable component to hold the tile. */
    protected PaintableComponent component = null;
    
    
    /** The maximum animation delay in milliseconds. */
    protected int maxDelay = 1000;
    
    
    /** The minimum animation delay in milliseconds. */
    protected int minDelay = 10;
    
    
    /** Whether or not the animation is running. */
    protected volatile boolean running = false;
    
    
    /**
     * <p>The object for animation synchronization.</p>
     * 
     * <p>When a thread synchronizes on this object,
     * the thread can complete its tasks before
     * another thread can change state.</p> 
     */
    protected Object animationLock = new Object();
    
    
    /** The action to run the animation in a separate thread. */
    protected SimpleThreadedAction runAction =
        new SimpleThreadedAction("Run Animation") {
            public void perform() { runAnimation(); }
    };
    
    
    /** The action to stop the animation. */
    protected SimpleAction stopAction =
        new SimpleAction("Stop Animation") {
            public void perform() { stopAnimation(); }
    };
    
    
    /** The content for the animation actions panel. */
    protected Object[] actionStuff = { runAction, stopAction };
    
    
    /** The animation actions panel. */
    protected HTable actionPanel = new HTable(actionStuff, GAP, GAP, CENTER);

    
    /** The slider for the animation step size. */
    protected SliderView stepSlider =
        new SliderView(HORIZONTAL, 1, 10, 5, 180, 1, 0);
    
    
    /** The slider for the animation speed in steps per second. */
    protected SliderView speedSlider =
        new SliderView(HORIZONTAL, 10, 100, 50, 180, 10, 5);
    
    
    /** The content for the slider panel. */
    protected Object[][] sliderStuff = {
            { "Step Size",        stepSlider  },
            { "Steps Per Second", speedSlider }
        };
        
    /** The slider panel. */
    protected TablePanel sliderPanel =
        new TablePanel(sliderStuff, GAP, GAP, CENTER);
    
    
    /** The rotation direction labels. */
    protected String[] directionLabels =
        { "Clockwise", "Counterclockwise" };
    
    
    /** The rotation direction radio button layout. */
    protected TableLayout directionLayout =
        new TableLayout(1, 2, GAP, GAP, CENTER);
    
    
    /** The rotation direction radio button panel. */
    protected RadioPanel directionPanel =
        new RadioPanel(directionLabels, directionLayout);
    
    
    /**
     * <p>The window closing action.</p>
     *
     * <p>This action is required to properly terminate
     * the animation thread if the window is closed
     * while the animation is running.</p>
     */
    protected SimpleAction closingAction
        = new SimpleAction("Window Closing Action") {
            public void perform() { closingAction(); }
    };
    
    
    /** The title for the frame. */
    protected String frameTitle = "Spinning Animation";
    
    
    /**
     * <p>The constructor that requires an <code>Object</code>
     * that may be converted to a non-<code>null</code>
     * <code>Paintable</code> using the appropriate method
     * in <code>ComponentFactory</code> and which sets the
     * width and height of the animation tile to SIZE.</p>
     * 
     * <p>The object when converted to a paintable will be
     * automatically centered in the tile.</p>
     */
    public SpinningAnimation(Object paintable) {
        this(paintable, SIZE, SIZE, null);
    }
    
    
    /**
     * The constructor that requires an <code>Object</code>
     * that may be converted to a non-<code>null</code>
     * <code>Paintable</code> using the appropriate method
     * in <code>ComponentFactory</code> and which provides
     * the width and height of the animation tile.  The
     * width and height will be set to at least SIZE if
     * the given values are smaller.
     * 
     * <p>The object when converted to a paintable will be
     * automatically centered in the tile.</p>
     */
    public SpinningAnimation
        (Object paintable, int width, int height)
    {
        this(paintable, width, height, null);
    }
    
    
    public SpinningAnimation
        (Object paintable, int width, int height, String frameTitle)
    {
        this.paintable = ComponentFactory.makePaintable(paintable);
        
        if (this.paintable == null) {
            String message =
                "Null Paintable in SpinningAnimation constructor";
            
            addObject(message);
        }
        else {
            this.width  = (width  >= SIZE) ? width  : SIZE;
            this.height = (height >= SIZE) ? height : SIZE;
            
            createTile();
            createMainPanel();
        }
        
        if (frameTitle != null) {
            this.frameTitle = frameTitle;
        }
        
        createFrame();
    }
    
    
    /** Create the animation tile and its component. */
    protected void createTile() {
        paintable.moveCenterTo(width/2, height/2);
        
        tile = new Tile(paintable);
        
        Rectangle2D bounds = new XRect(0, 0, width, height);
        tile.setDefaultBounds2D(bounds);
        
        component = new PaintableComponent(tile);
        
        component.emptyBorder(4);
        component.lineBorder(Colors.black, 4);
    }
    
    
    /** Place the GUI elements into the main panel. */
    protected void createMainPanel() {
        mainPanel = new TablePanel(4, 1, GAP, GAP, CENTER);
        
        mainPanel.addObject(component,      0, 0);
        mainPanel.addObject(sliderPanel,    1, 0);
        mainPanel.addObject(directionPanel, 2, 0);
        mainPanel.addObject(actionPanel,    3, 0);
        
        mainPanel.emptyBorder(GAP);
        
        add(mainPanel);
    }
    
    
    /** Returns the step size. */
    protected int getStep() {
        return stepSlider.getValue();
    }
    
    
    /** Returns the speed. */
    protected int getSpeed() {
        return speedSlider.getValue();
    }
    
    
    /**
     * Returns the thread delay
     * and guarantees that the thread delay is at least minDelay.
     */
    protected int getDelay() {
        int delay = maxDelay / getSpeed();
        
        return (delay > minDelay) ? delay : minDelay;
    }
    
    
    /** Perform one animation step. */
    protected void animate() {
        int step = getStep();
        
        if (directionPanel.getSelectedIndex() == 1)
            step = -step;
        
        tile.rotate(step);
    }
    
    
    /**
     * The animation loop method that will
     * execute in a separate thread.
     */
    protected void runAnimation() {
        // set state for run
        setRunState();
        
        // perform animation
        while(true) {
            synchronized (animationLock) {
                if (!running)
                    return;
                
                animate();
            }
            
            JPTUtilities.pauseThread(getDelay());
        }
    }
    
    
    /**
     * The method to stop the animation loop.
     */
    protected void stopAnimation() {
        // set state for stop
        setStopState();
        
        // perform any desired cleanup activities
    }
    
    
    /**
     * <p>Set the enable/disable state of the actions
     * for the Run state.</p>
     * 
     * <p>Set boolean running to true.</p>
     */
    protected void setRunState() {
        synchronized (animationLock) {
            runAction. setEnabled(false);
            stopAction.setEnabled(true);
            
            running = true;
        }
    }
    
    
    /**
     * <p>Set boolean running to false.</p>
     * 
     * <p>Set the enable/disable state of the actions
     * for the Stop state.</p>
     */
    protected void setStopState() {
        synchronized (animationLock) {
            running = false;
            
            stopAction.setEnabled(false);
            runAction. setEnabled(true);
        }
    }
    
    
    /**
     * <p>The window closing method.</p>
     *
     * <p>This method is required to properly terminate
     * the animation thread if the window is closed
     * while the animation is running.</p>
     */
    protected void closingAction() {
        synchronized (animationLock) {
            running = false;
            
            if (frame != null)
                frame.dispose();
        }        
    }
    
    
    /** The method to set the window closing action. */
    protected void setWindowClosingAction() {
        // remove default closing operation
        frame.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
        
        // set special closing operation
        WindowActionAdapter adapter = new WindowActionAdapter(frame);
        adapter.addWindowClosingAction(closingAction);
    }
    
    
    /**
     * The method to create the application frame and
     * install the window closing behavior.
     */
    protected void createFrame() {
        frame = frame(frameTitle);
        setWindowClosingAction();
    }
    
    
    /**
     * For test purposes, launch a spinning animation using a
     * star-shaped paintable object.
     */
    public static void main(String[] args) {
        Shape star = RegularShape.star(0, 0, 100, 7, 3);
        
        ShapePaintable paintable =
            new ShapePaintable(star, PaintMode.FILL_DRAW, Color.red);
        
        new SpinningAnimation(paintable, 220, 220, "Star Animation");
    }
    
}

