/* @(#)SpinningAnimation.java   20 November 2006 */

/* Useful imports */

import edu.neu.ccs.*;
import edu.neu.ccs.gui.*;
import edu.neu.ccs.codec.*;
import edu.neu.ccs.console.*;
import edu.neu.ccs.filter.*;
import edu.neu.ccs.jpf.*;
import edu.neu.ccs.parser.*;
import edu.neu.ccs.pedagogy.*;
import edu.neu.ccs.quick.*;
import edu.neu.ccs.util.*;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.font.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.*;
import java.io.*;
import java.util.*;
import java.math.*;
import java.beans.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.regex.*;
import java.text.ParseException;

public class SpinningAnimation
    extends DisplayPanel
    implements WindowConstants
{
    /** The frame for the animation. */
    private JPTFrame frame = null;
    
    /** The main panel. */
    private TablePanel mainPanel = null;
    
    /** The common panel gap. */
    private int gap = 10;
    
    
    /** The paintable to spin. */
    Paintable paintable = null;
    
    /** The tile to hold the paintable. */
    Tile tile = null;
    
    /** The tile width. */
    int width = 0;
    
    /** The tile height. */
    int height = 0;
    
    /** The paintable component to hold the tile. */
    PaintableComponent component = null;
    
    
    /** Whether or not the animation is running. */
    private 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> 
     */
    private Object animationLock = new Object();
    
    /** The maximum animation delay in milliseconds. */
    private int maxDelay = 1000;
    
    /** The minimum animation delay in milliseconds. */
    private int minDelay = 10;
    
    
    /** The action to run the animation in a separate thread. */
    private ThreadedAction runAction =
        new ThreadedAction
            (new SimpleAction("Run Animation") {
                public void perform()
                { runAnimation(); }
    });
    
    
    /** The action to stop the animation. */
    private SimpleAction stopAction =
        new SimpleAction("Stop Animation") {
            public void perform() { stopAnimation(); }
    };
    
    
    /** The sliding action for the step slider. */
    private SimpleAction stepSliding =
        new SimpleAction("Step Sliding") {
            public void perform() { stepSliding(); }
    };
    
    
    /** The sliding action for the speed slider. */
    private SimpleAction speedSliding =
        new SimpleAction("Speed Sliding") {
            public void perform() { speedSliding(); }
    };
    
    
    /**
     * <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>
     */
    private SimpleAction closingAction
        = new SimpleAction("Window Closing Action") {
            public void perform() { closingAction(); }
    };
    
    
    /** The slider for the animation step size. */
    private SliderView stepSlider =
        new SliderView(HORIZONTAL, 0, 10, 5);
    
    
    /** The slider for the animation speed in steps per second. */
    private SliderView speedSlider =
        new SliderView(HORIZONTAL, 0, 100, 50);
    
    
    /** The label that echoes the animation step. */
    private Annotation stepLabel =
        new Annotation("5");
    
    
    /** The label that echoes the animation speed. */
    private Annotation speedLabel =
        new Annotation("50");
    
    
    /** The panel with the sliders and the labels. */
    private TablePanel sliderPanel = null;
    
    
    /** The direction labels. */
    private String[] directionLabels =
        { "Clockwise", "Counterclockwise" };
    
    /** The direction radio button layout. */
    private TableLayout directionLayout =
        new TableLayout(1, 2, gap, gap, CENTER);
    
    /** The direction radio button panel. */
    private RadioPanel directionPanel =
        new RadioPanel(directionLabels, directionLayout);
    
    
    /**
     * <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 100.</p>
     * 
     * <p>The object when converted to a paintable will be
     * automatically centered in the tile.</p>
     */
    public SpinningAnimation(Object paintable) {
        this(paintable, 100, 100);
    }
    
    
    /**
     * 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 100 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 = ComponentFactory.makePaintable(paintable);
        
        if (this.paintable == null) {
            String message =
                "Null Paintable in SpinningAnimation constructor";
            
            addObject(message);
            
            return;
        }
        
        this.width  = (width  >= 100) ? width  : 100;
        this.height = (height >= 100) ? height : 100;
        
        createMainPanel();
        createTile();
        decorateSliders();
        populateMainPanel();
        createFrame();
    }
    
    
    /** Create the main panel. */ 
    private void createMainPanel() {
        int rows = 4;
        int cols = 1;
        
        mainPanel = new TablePanel(rows, cols, gap, gap, CENTER);
        
        mainPanel.emptyBorder(gap);
    }
    
    
    /** Create the animation tile. */
    private 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);
    }
    
    
    /**
     * Places the sliders into a panel with labels
     * and perform other slider settings.
     */
    private void decorateSliders() {
        stepSlider.setMajorTickSpacing(1);
        stepSlider.installStandardLabels();
        stepSlider.addSlidingAction(stepSliding);
        
        speedSlider.setMajorTickSpacing(10);
        speedSlider.installStandardLabels();
        speedSlider.addSlidingAction(speedSliding);
        
        Object[][] sliderStuff = {
            { "Step Size",        stepSlider,  stepLabel  },
            { "Steps Per Second", speedSlider, speedLabel }
        };
        
        sliderPanel = new TablePanel
            (sliderStuff, gap, gap, CENTER);
        
        int width = TextFieldView.getSampleWidth("0000");
        sliderPanel.setMinimumColumnWidth(2, width);
    }
    
    
    /** Place the GUI elements into the main panel. */
    private void populateMainPanel() {
        // populate the main panel
        mainPanel.addObject(component, 0, 0);
        
        mainPanel.addObject(sliderPanel, 1, 0);
        
        mainPanel.addObject(directionPanel, 2, 0);
        
        
        Object[] actions = { runAction, stopAction };
        HTable buttons = new HTable(actions, gap, gap, CENTER);
        
        mainPanel.addObject(buttons, 3, 0);
        
        // add the main panel to this panel object
        add(mainPanel);
    }
    
    
    /**
     * Returns the step size
     * and guarantees that the step size is positive.
     */
    private int getStep() {
        int s = stepSlider.getValue();
        
        return (s >= 1) ? s : 1;
    }
    
    /**
     * Returns the speed
     * and guarantees that the speed is positive.
     */
    private int getSpeed() {
        int s = speedSlider.getValue();
        
        return (s >= 1) ? s : 1;
    }
    
    
    /**
     * Returns the thread delay
     * and guarantees that the thread delay is at least minDelay.
     */
    private int getDelay() {
        int delay = maxDelay / getSpeed();
        
        return (delay > minDelay) ? delay : minDelay;
    }
    
    
    /** Perform one animation step. */
    private 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.
     */
    private 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.
     */
    private 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>
     */
    private 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>
     */
    private void setStopState() {
        synchronized (animationLock) {
            running = false;
            
            stopAction.setEnabled(false);
            runAction. setEnabled(true);
        }
    }
    
    
    /** The step sliding method. */
    private void stepSliding() {
        String s = "" + getStep();
        stepLabel.setText(s);
        stepLabel.repaint();
    }
    
    
    /** The speed sliding method. */
    private void speedSliding() {
        String s = "" + getSpeed();
        speedLabel.setText(s);
        speedLabel.repaint();
    }
    
    
    /**
     * <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>
     */
    private void closingAction() {
        synchronized (animationLock) {
            running = false;
            
            if (frame != null)
                frame.dispose();
        }        
    }
    
    
    /**
     * The method to create the application frame and
     * install the window closing behavior.
     */
    private void createFrame() {
        frame = frame("Spinning Animation");
        setWindowClosingAction();
    }
    
    
    /** The method to set the window closing action. */
    private void setWindowClosingAction() {
        // remove default closing operation
        frame.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
        
        // set special closing operation
        WindowActionAdapter adapter = new WindowActionAdapter(frame);
        adapter.addWindowClosingAction(closingAction);
    }
    
    
    public static void main(String[] args) {
        new SpinningMethods().frame("", WEST);
    }
    
}

