/**
 * Copyright (c) 2005, Romain Guy <romain.guy@jext.org>
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice, 
 *       this list of conditions and the following disclaimer.
 *     * Neither the name of the Romain Guy nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.lyra.Widgets;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JComponent;

/**
 * An infinite progress panel displays a rotating figure and
 * a message to notice the user of a long, duration unknown
 * task. The shape and the text are drawn upon a white veil
 * which alpha level (or shield value) lets the underlying
 * component shine through. This panel is meant to be used
 * as a <i>glass pane</i> in the window performing the long
 * operation.
 * <p>
 * On the contrary to regular glass panes, you don't need to
 * set it visible or not by yourself. Once you've started the
 * animation all the mouse events are intercepted by this
 * panel, preventing them from being forwarded to the
 * underlying components.
 * <p>
 * The panel can be controlled by the <code>start()</code>,
 * <code>stop()</code> and <code>interrupt()</code> methods.
 * <p>
 * Example:
 * <br /><br />
 * <pre>InfiniteProgressPanel pane = new InfiniteProgressPanel();
 * frame.setGlassPane(pane);
 * pane.start()</pre>
 * <p>
 * Several properties can be configured at creation time. The
 * message and its font can be changed at runtime. Changing the
 * font can be done using <code>setFont()</code> and
 * <code>setForeground()</code>.
 * <p>
 * @author Romain Guy
 * @version 1.0.4, %Revision, 112%, %LastChangedDate, 25/04/11 11:31AM%
 */

public class InfiniteProgressPanel extends JComponent implements MouseListener
{
    /**
     *  Serial storage method
     */
	private static final long serialVersionUID = 1L;
	
	/** 
	 * Contains the bars composing the circular shape. 
	 */
    protected Area[]  ticker     = null;
    
    /** 
     * The animation thread is responsible for fade in/out and rotation. 
     */
    protected Thread  animation  = null;
    
    /** 
     * Notifies whether the animation is running or not. 
     */
    protected boolean started    = false;
    
    /** 
     * Alpha level of the veil, used for fade in/out. 
     */
    protected int     alphaLevel = 0;
    
    /** 
     * Duration of the veil's fade in/out. 
     */
    protected int     rampDelay  = 15;
    
    /** 
     * Alpha level of the veil. 
     */
    protected float   shield     = 0.70f;
    
    /** 
     * Message displayed below the circular shape. 
     */
    protected String  text       = "";
    
    /** 
     * Amount of bars composing the circular shape. 
     */
    protected int     barsCount  = 14;
    
    /** 
     * Amount of frames per second. Lowers this to save CPU. 
     */
    protected float   fps        = 15.0f;
    
    /** 
     * Rendering hints to set anti aliasing. 
     */
    protected RenderingHints hints = null;

    
    /**
     * Creates a new progress panel with default values:
     * <ul>
     * <li>No message</li>
     * <li>14 bars</li>
     * <li>Veil's alpha level is 70%</li>
     * <li>15 frames per second</li>
     * <li>Fade in/out last 300 ms</li>
     * </ul>
     */
    public InfiniteProgressPanel()
    {
        this("");
    }
    // Ends of Constructor Class
    

    /**
     * Creates a new progress panel with default values:
     * <ul>
     * <li>14 bars</li>
     * <li>Veil's alpha level is 70%</li>
     * <li>15 frames per second</li>
     * <li>Fade in/out last 300 ms</li>
     * </ul>
     * @param text The message to be displayed. Can be null or empty.
     */
    public InfiniteProgressPanel(String text)
    {
        this(text, 14);
    }
    // End of Constructor Method
    

    /**
     * Creates a new progress panel with default values:
     * <ul>
     * <li>Veil's alpha level is 70%</li>
     * <li>15 frames per second</li>
     * <li>Fade in/out last 300 ms</li>
     * </ul>
     * @param text The message to be displayed. Can be null or empty.
     * @param barsCount The amount of bars composing the circular shape
     */
    public InfiniteProgressPanel(String text, int barsCount)
    {
        this(text, barsCount, 0.70f);
    }
    // End of Constructor Method
    

    /**
     * Creates a new progress panel with default values:<br />
     * <ul>
     * <li>15 frames per second</li>
     * <li>Fade in/out last 300 ms</li>
     * </ul>
     * @param text The message to be displayed. Can be null or empty.
     * @param barsCount The amount of bars composing the circular shape.
     * @param shield The alpha level between 0.0 and 1.0 of the colored
     *               shield (or veil).
     */
    public InfiniteProgressPanel(String text, int barsCount, float shield)
    {
        this(text, barsCount, shield, 15.0f);
    }
    // End of Constructor Method

    
    /**
     * Creates a new progress panel with default values:<br />
     * <ul>
     * <li>Fade in/out last 300 ms</li>
     * </ul>
     * @param text The message to be displayed. Can be null or empty.
     * @param barsCount The amount of bars composing the circular shape.
     * @param shield The alpha level between 0.0 and 1.0 of the colored
     *               shield (or veil).
     * @param fps The number of frames per second. Lower this value to
     *            decrease CPU usage.
     */
    public InfiniteProgressPanel(String text, int barsCount, float shield, float fps)
    {
        this(text, barsCount, shield, fps, 300);
    }
    // End of Constructor Method
    

    /**
     * Creates a new progress panel.
     * @param text The message to be displayed. Can be null or empty.
     * @param barsCount The amount of bars composing the circular shape.
     * @param shield The alpha level between 0.0 and 1.0 of the coloured
     *               shield (or veil).
     * @param fps The number of frames per second. Lower this value to
     *            decrease CPU usage.
     * @param rampDelay The duration, in milliseconds, of the fade in and
     *                  the fade out of the veil.
     */
    public InfiniteProgressPanel(String text, int barsCount, float shield, float fps, int rampDelay)
    {
    	// Pass up the elements
        this.text 	   = text;
        this.rampDelay = rampDelay >= 0 ? rampDelay : 0;
        this.shield    = shield >= 0.0f ? shield : 0.0f;
        this.fps       = fps > 0.0f ? fps : 15.0f;
        this.barsCount = barsCount > 0 ? barsCount : 14;

        // Set rendering hints
        this.hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        this.hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        this.hints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    }
    // End of Constructor Method
    

    /**
     * Changes the displayed message at runtime.
     * <p>
     * @param text The message to be displayed. Can be null or empty.
     */
    public void setText(String text)
    {
    	// Adjust the message
        this.text = text;
    }
    // End of setText Method
    

    /**
     * Returns the current displayed message.
     * <p>
     * @return String containing displayed message.
     */
    public String getText()
    {
    	// What is currently being displayed
        return text;
    }
    // End of getText Method

    
    /**
     * Starts the waiting animation by fading the veil in, then
     * rotating the shapes. This method handles the visibility
     * of the glass pane.
     */
    public void start()
    {
    	// Capture mouse events
        addMouseListener(this);
        
        // Display this GlassPane and block other elements
        setVisible(true);
        
        // Generate a new ticker
        ticker = buildTicker();
        
        // Create a thread and start it
        animation = new Thread(new Animator(true));
        animation.start();
    }
    // End of start Method

    
    /**
     * Stops the waiting animation by stopping the rotation
     * of the circular shape and then by fading out the veil.
     * This methods sets the panel invisible at the end.
     */
    public void stop()
    {
    	// We can only stop what exists
        if (animation != null) {
        	
        	// Interrupt the tread
	        animation.interrupt();
	        
	        // Null the animator
	        animation = null;
	        
	        // Start a new process to wind down
	        animation = new Thread(new Animator(false));
	        animation.start();
        }
    }
    // End of end Method
    
    
    /**
     * Interrupts the animation, whatever its state is. You
     * can use it when you need to stop the animation without
     * running the fade out phase.<p>
     * This methods sets the panel invisible at the end.
     */
    public void interrupt()
    {
    	// Only proceed if the thread is active
        if (animation != null) {
        	
        	// Interrupt the thread
            animation.interrupt();
            animation = null;

            // Disable listener
            removeMouseListener(this);
            
            // Hide GlassPane
            setVisible(false);
        }
    }
    // End of interrupt Method
    

    /* (non-Javadoc)
     * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
     */
    public void paintComponent(Graphics g)
    {
        if (started)
        {
            int width  = getWidth();

            double maxY = 0.0; 

            Graphics2D g2 = (Graphics2D) g;
            g2.setRenderingHints(hints);
            
            g2.setColor(new Color(255, 255, 255, (int) (alphaLevel * shield)));
            g2.fillRect(0, 0, getWidth(), getHeight());

            for (int i = 0; i < ticker.length; i++)
            {
                int channel = 224 - 128 / (i + 1);
                g2.setColor(new Color(channel, channel, channel, alphaLevel));
                g2.fill(ticker[i]);

                Rectangle2D bounds = ticker[i].getBounds2D();
                if (bounds.getMaxY() > maxY)
                    maxY = bounds.getMaxY();
            }

            if (text != null && text.length() > 0)
            {
	            FontRenderContext context = g2.getFontRenderContext();
	            TextLayout layout = new TextLayout(text, getFont(), context);
	            Rectangle2D bounds = layout.getBounds();
	            g2.setColor(getForeground());
	            layout.draw(g2, (float) (width - bounds.getWidth()) / 2,
	                    		(float) (maxY + layout.getLeading() + 2 * layout.getAscent()));
            }
        }
    }
    // End of paintComponent Method
    

    /**
     * Builds the circular shape and returns the result as an array of
     * <code>Area</code>. Each <code>Area</code> is one of the bars
     * composing the shape.
     */
    private Area[] buildTicker()
    {
        Area[] ticker = new Area[barsCount];
        Point2D.Double center = new Point2D.Double((double) getWidth() / 2, (double) getHeight() / 2);
        double fixedAngle = 2.0 * Math.PI / ((double) barsCount);

        for (double i = 0.0; i < (double) barsCount; i++)
        {
            Area primitive = buildPrimitive();

            AffineTransform toCenter = AffineTransform.getTranslateInstance(center.getX(), center.getY());
            AffineTransform toBorder = AffineTransform.getTranslateInstance(45.0, -6.0);
            AffineTransform toCircle = AffineTransform.getRotateInstance(-i * fixedAngle, center.getX(), center.getY());

            AffineTransform toWheel = new AffineTransform();
            toWheel.concatenate(toCenter);
            toWheel.concatenate(toBorder);

            primitive.transform(toWheel);
            primitive.transform(toCircle);
            
            ticker[(int) i] = primitive;
        }

        return ticker;
    }
    // End of buildTicker Method
    

    /**
     * Builds a bar.
     */
    private Area buildPrimitive()
    {
        Rectangle2D.Double body = new Rectangle2D.Double(6, 0, 30, 12);
        Ellipse2D.Double   head = new Ellipse2D.Double(0, 0, 12, 12);
        Ellipse2D.Double   tail = new Ellipse2D.Double(30, 0, 12, 12);

        Area tick = new Area(body);
        tick.add(new Area(head));
        tick.add(new Area(tail));

        return tick;
    }
    // End of buildBar Method
    
    
    /*
     * MOUSE EVENTS
     * 
     * Mouse Events are captured at this point and ignored.   This essentially
     * prevents the user from doing anything that could potentially distrupt the
     * process.
     */
    
    /* (non-Javadoc)
     * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
     */
    public void mouseClicked(MouseEvent e) { }

    /* (non-Javadoc)
     * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
     */
    public void mousePressed(MouseEvent e) {}

    /* (non-Javadoc)
     * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
     */
    public void mouseReleased(MouseEvent e) {}

    /* (non-Javadoc)
     * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
     */
    public void mouseEntered(MouseEvent e) {}

    /* (non-Javadoc)
     * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
     */
    public void mouseExited(MouseEvent e) {}

    

    /**
     * Animation thread.
     */
    private class Animator implements Runnable
    {
    	// Are we going to be fading in the animation
        private boolean rampUp = true;

        
        /**
         * Basic constructor for the class, establishes if a Ramp Up process
         * will be taking place.
         * <p>
         * @param rampUp Boolean on Ramp Up.
         */
        protected Animator(boolean rampUp)
        {
        	// Pass up the value
            this.rampUp = rampUp;
        }
        // End of Consructor Class

        
        /* (non-Javadoc)
         * @see java.lang.Runnable#run()
         */
        public void run()
        {
            Point2D.Double center = new Point2D.Double((double) getWidth() / 2, (double) getHeight() / 2);
            double fixedIncrement = 2.0 * Math.PI / ((double) barsCount);
            AffineTransform toCircle = AffineTransform.getRotateInstance(fixedIncrement, center.getX(), center.getY());
    
            long start = System.currentTimeMillis();
            if (rampDelay == 0)
                alphaLevel = rampUp ? 255 : 0;

            started = true;
            boolean inRamp = rampUp;

            while (!Thread.interrupted())
            {
                if (!inRamp)
                {
                    for (int i = 0; i < ticker.length; i++)
                        ticker[i].transform(toCircle);
                }

                repaint();

                if (rampUp)
                {
                    if (alphaLevel < 255)
                    {
                        alphaLevel = (int) (255 * (System.currentTimeMillis() - start) / rampDelay);
                        if (alphaLevel >= 255)
                        {
                            alphaLevel = 255;
                            inRamp = false;
                        }
                    }
                } else if (alphaLevel > 0) {
                    alphaLevel = (int) (255 - (255 * (System.currentTimeMillis() - start) / rampDelay));
                    if (alphaLevel <= 0)
                    {
                        alphaLevel = 0;
                        break;
                    }
                }

                try
                {
                    Thread.sleep(inRamp ? 10 : (int) (1000 / fps));
                } catch (InterruptedException ie) {
                    break;
                }
                Thread.yield();
            }

            if (!rampUp)
            {
                started = false;
                repaint();

                setVisible(false);
                removeMouseListener(InfiniteProgressPanel.this);
            }
        }
        // End of Run Method
    }
    // End of Animator Class
    
}