/**
 * GmailAssistant 1.1 (2008-03-16)
 * Copyright 2008 Zach Scrivena
 * zachscrivena@gmail.com
 * http://gmailassistant.sourceforge.net/
 *
 * Notifier for multiple Gmail accounts.
 *
 * TERMS AND CONDITIONS:
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * This program 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package gmailassistant;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import javax.swing.JWindow;
import javax.swing.Timer;


/**
 * Display a sliding animation for a window.
 */
class SlidingAnimator
        extends JWindow
        implements ActionListener
{
    /** interval between animation refreshes */
    private static final int ANIMATION_REFRESH_INTERVAL_MILLISECONDS = 20;

    /** source window to be animated */
    private final JWindow sourceWindow;

    /** direction of sliding animation */
    private final SlidingDirection dir;

    /** Swing timer for animation */
    private final Timer swingTimer;

    /** buffered off-screen image of the source window */
    private final BufferedImage img;

    /** is the animation running? */
    private volatile boolean running;

    /** is this the first step of the animation? */
    boolean firstStep;

    /** source window width */
    private final int sourceWidth;

    /** source window height */
    private final int sourceHeight;

    /** x-coordinate of the canvas */
    private int canvasX;

    /** y-coordinate of the canvas */
    private int canvasY;

    /** final x-coordinate of the window */
    private final int finalX;

    /** final y-coordinate of the window */
    private final int finalY;

    /** current x-coordinate of the window */
    private int currentX;

    /** current y-coordinate of the window */
    private int currentY;

    /** increment for the x-coordinate of the window */
    private final int deltaX;

    /** increment for the y-coordinate of the window */
    private final int deltaY;

    /** width of the animation window */
    private int windowWidth;

    /** height of the animation window */
    private int windowHeight;

    /** x-coordinate of the animation window */
    private int windowX;

    /** y-coordinate of the animation window */
    private int windowY;


    /**
    * Constructor. This method must run on the EDT.
    * The source window does not need to be visible when calling the function.
    *
    * @param sourceWindow
    *      source window to be animated
    * @param sourceWidth
    *      width of the source window
    * @param sourceHeight
    *      height of the source window
    * @param canvasX
    *      x-coordinate of the canvas on which to animate
    * @param canvasY
    *      y-coordinate of the canvas on which to animate
    * @param stepSize
    *      number of incremental pixels per animation step
    * @param dir
    *      direction of animation
    */
    SlidingAnimator(
            final JWindow sourceWindow,
            final int sourceWidth,
            final int sourceHeight,
            final int canvasX,
            final int canvasY,
            final int stepSize,
            final SlidingDirection dir)
    {
        this.sourceWindow = sourceWindow;
        this.sourceWidth = sourceWidth;
        this.sourceHeight = sourceHeight;
        this.canvasX = canvasX;
        this.canvasY = canvasY;
        this.dir = dir;
        this.setAlwaysOnTop(true);

        /* create an off-screen buffered image of the source window */
        img = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_INT_RGB);
        final Graphics2D g = img.createGraphics();

        sourceWindow.setVisible(true);
        sourceWindow.paintAll(g);

        if ((dir == SlidingDirection.UP_IN) ||
                (dir == SlidingDirection.DOWN_IN) ||
                (dir == SlidingDirection.LEFT_IN) ||
                (dir == SlidingDirection.RIGHT_IN))
        {
            sourceWindow.setVisible(false);
        }

        final int step = (stepSize == 0) ? 1 : Math.abs(stepSize);

        /* determine animation steps */
        switch (dir)
        {
            case UP_IN:
                currentX = canvasX;
                currentY = canvasY + sourceHeight;
                finalX = canvasX;
                finalY = canvasY;
                deltaX = 0;
                deltaY = -step;
                break;

            case DOWN_IN:
                currentX = canvasX;
                currentY = canvasY - sourceHeight;
                finalX = canvasX;
                finalY = canvasY;
                deltaX = 0;
                deltaY = step;
                break;

            case LEFT_IN:
                currentX = canvasX + sourceWidth;
                currentY = canvasY;
                finalX = canvasX;
                finalY = canvasY;
                deltaX = -step;
                deltaY = 0;
                break;

            case RIGHT_IN:
                currentX = canvasX - sourceWidth;
                currentY = canvasY;
                finalX = canvasX;
                finalY = canvasY;
                deltaX = step;
                deltaY = 0;
                break;

            case UP_OUT:
                currentX = canvasX;
                currentY = canvasY;
                finalX = canvasX;
                finalY = canvasY - sourceHeight;
                deltaX = 0;
                deltaY = -step;
                break;

            case DOWN_OUT:
                currentX = canvasX;
                currentY = canvasY;
                finalX = canvasX;
                finalY = canvasY + sourceHeight;
                deltaX = 0;
                deltaY = step;
                break;

            case LEFT_OUT:
                currentX = canvasX;
                currentY = canvasY;
                finalX = canvasX - sourceWidth;
                finalY = canvasY;
                deltaX = -step;
                deltaY = 0;
                break;

            case RIGHT_OUT:
                currentX = canvasX;
                currentY = canvasY;
                finalX = canvasX + sourceWidth;
                finalY = canvasY;
                deltaX = step;
                deltaY = 0;
                break;

            default:
                finalX = currentX;
                finalY = currentY;
                deltaX = 0;
                deltaY = 0;
        }

        swingTimer = new Timer(SlidingAnimator.ANIMATION_REFRESH_INTERVAL_MILLISECONDS, this);
    }


    /**
    * Perform the sliding animation, blocking until the animation is completed.
    * This method should NOT run on the EDT.
    */
    void animate()
    {
        running = true;
        firstStep = true;
        swingTimer.start();

        while (true)
        {
            if (!running)
            {
                break;
            }

            try
            {
                Thread.sleep(SlidingAnimator.ANIMATION_REFRESH_INTERVAL_MILLISECONDS);
            }
            catch (Exception e)
            {
                /* ignore */
            }
        }

        /* animation is completed */
        if ((dir == SlidingDirection.UP_IN) ||
                (dir == SlidingDirection.DOWN_IN) ||
                (dir == SlidingDirection.LEFT_IN) ||
                (dir == SlidingDirection.RIGHT_IN))
        {
            sourceWindow.setVisible(true);
        }
        else
        {
            sourceWindow.setVisible(false);
        }

        setVisible(false);
    }


    /**
    * Called by the Swing timer periodically.
    * This method runs on the EDT.
    */
    public void actionPerformed(ActionEvent e)
    {
        final int nextX = currentX + deltaX;
        final int nextY = currentY + deltaY;
        boolean proceed = true;

        if ((dir == SlidingDirection.UP_IN) ||
            (dir == SlidingDirection.UP_OUT))
        {
            if (nextY < finalY)
            {
                proceed = false;
            }
        }
        else if ((dir == SlidingDirection.DOWN_IN) ||
            (dir == SlidingDirection.DOWN_OUT))
        {
            if (nextY > finalY)
            {
                proceed = false;
            }
        }
        else if ((dir == SlidingDirection.LEFT_IN) ||
            (dir == SlidingDirection.LEFT_OUT))
        {
            if (nextX < finalX)
            {
                proceed = false;
            }
        }
        else if ((dir == SlidingDirection.RIGHT_IN) ||
            (dir == SlidingDirection.RIGHT_OUT))
        {
            if (nextX > finalX)
            {
                proceed = false;
            }
        }

        if (proceed)
        {
            currentX = nextX;
            currentY = nextY;
        }
        else
        {
            running = false;
            swingTimer.stop();
            return;
        }

        if (currentX >= canvasX)
        {
            windowX = currentX;
            windowWidth = sourceWidth - currentX + canvasX;
        }
        else
        {
            windowX = canvasX;
            windowWidth = currentX + sourceWidth - canvasX;
        }

        if (currentY >= canvasY)
        {
            windowY = currentY;
            windowHeight = sourceHeight - currentY + canvasY;
        }
        else
        {
            windowY = canvasY;
            windowHeight = currentY + sourceHeight - canvasY;
        }

        setSize(windowWidth, windowHeight);
        setLocation(windowX, windowY);

        if (firstStep)
        {
            setVisible(true);
            firstStep = false;

            if ((dir == SlidingDirection.UP_OUT) ||
                (dir == SlidingDirection.DOWN_OUT) ||
                (dir == SlidingDirection.LEFT_OUT) ||
                (dir == SlidingDirection.RIGHT_OUT))
            {
                sourceWindow.setVisible(false);
            }
        }
    }

    @Override
    public void update(Graphics g)
    {
        paint(g);
    }

    @Override
    public void paint(Graphics g)
    {
        g.drawImage(img.getSubimage(
                windowX - currentX,
                windowY - currentY,
                windowWidth,
                windowHeight),
                0,
                0,
                this);
    }

    /*****************
    * INNER CLASSES *
    *****************/

    /**
    * Sliding animation direction.
    */
    public enum SlidingDirection
    {
        UP_IN,
        UP_OUT,
        DOWN_IN,
        DOWN_OUT,
        LEFT_IN,
        LEFT_OUT,
        RIGHT_IN,
        RIGHT_OUT
    }
}