/*
 * @(#)BufferedPanel.java    2.3.3  10 December 2004
 *
 * 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.gui;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.swing.*;

/**
 * <P>A panel that maintains a persistent graphics state 
 * by repainting itself from a stored 
 * <CODE>{@link BufferedImage BufferedImage}</CODE> object.</P>  
 *
 * <P>This behavior differs from the double-buffering 
 * provided in <CODE>{@link JComponent JComponent}</CODE> 
 * in that its intention is not necessarily to provide 
 * flicker-free repaints.  Its goal is to retain the effects of
 * method calls on its buffer graphics context regardless of
 * whether or not those calls are in a <CODE>paint</CODE> method.</P>
 *
 * <P>To ensure proper performance, the user should paint 
 * to the buffer graphics context, 
 * which is available through the 
 * <CODE>{@link #getBufferGraphics() getBufferGraphics}</CODE>
 * method, rather than to a component graphics context 
 * provided as an argument to the 
 * <CODE>{@link #paint(Graphics) paint}</CODE> method
 * or returned by the 
 * <CODE>{@link #getGraphics() getGraphics}</CODE> method.</P>
 *
 * @author  Richard Rasala
 * @author  Jeff Raab
 * @version 2.3.3
 * @since   1.0
 */
public class BufferedPanel extends DisplayPanel {

    ///////////////
    // Constants //
    ///////////////
    
    /** Default background color for a buffered panel. */
    public static final Color DEFAULT_BUFFER_BACKGROUND = Color.white;
    
    
    /////////////////
    // Member Data //
    /////////////////
    
    /** The buffered image that maintains the persistent graphics state. */
    protected BufferedImage buffer = null;
    
    /** The internal painter panel. */
    protected BufferedPanel.Painter painter = null;
    
    /** The background paint for this buffered panel. */
    protected Paint bufferBackground = DEFAULT_BUFFER_BACKGROUND;
    
    /** The mouse action adapter for this buffered panel. */
    protected MouseActionAdapter mouseActions = null;

    /** The key action adapter for this buffered panel. */
    protected KeyActionAdapter keyActions = null;
    
    
    //////////////////
    // Constructors //
    //////////////////

    /**
     * <P>Constructs a BufferedPanel containing a buffered image
     * with the given width and height,
     * and a white background.</P>
     *
     * <P>If the given width or height is less than 1 pixel,
     * that value is set to 1 pixel.</P>
     *
     * <P>Though the component itself may grow arbitrarily large,
     * the buffered image painted to the buffer will
     * remain the size specified in this constructor
     * unless the size is reset using the
     * <CODE>setBufferSize</CODE> method.</P>
     *
     * @param width  the width  of the buffered image
     * @param height the height of the buffered image
     * @see #BufferedPanel(Dimension)
     * @see #BufferedPanel(int, int, Paint)
     * @see #BufferedPanel(Dimension, Paint)
     * @see #setBufferSize(int, int)
     * @see #setBufferSize(Dimension)
     */
    public BufferedPanel(int width, int height) {
        this(width, height, DEFAULT_BUFFER_BACKGROUND);
    }
	
	
    /**
     * <P>Constructs a BufferedPanel containing a buffered image
     * with the given <CODE>Dimension</CODE>,
     * and a white background.</P>
     *
     * <P>If the given <CODE>Dimension</CODE> is <CODE>null</CODE>,
     * then the buffer width and height are both set to 1 pixel.</P>
     *
     * <P>If the width or height of the given <CODE>Dimension</CODE>
     * is less than 1 pixel, that value is set to 1 pixel.</P>
     *
     * <P>Though the component itself may grow arbitrarily large,
     * the buffered image painted to the buffer will
     * remain the size specified in this constructor
     * unless the size is reset using the
     * <CODE>setBufferSize</CODE> method.</P>
     *
     * @param dimension the dimension of the buffered image
     * @see #BufferedPanel(int, int)
     * @see #BufferedPanel(int, int, Paint)
     * @see #BufferedPanel(Dimension, Paint)
     * @see #setBufferSize(int, int)
     * @see #setBufferSize(Dimension)
     */
    public BufferedPanel(Dimension dimension) {
        this(
            (dimension == null) ? 1 : dimension.width,
            (dimension == null) ? 1 : dimension.height,
            DEFAULT_BUFFER_BACKGROUND);
    }
	
	
    /**
     * <P>Constructs a BufferedPanel containing a buffered image
     * with the given width and height, 
     * and the given background color or <CODE>Paint</CODE>
     * for the image.</P>
     *
     * <P>If the given width or height is less than 1 pixel,
     * that value is set to 1 pixel.</P>
     *
     * <P>Though the component itself may grow arbitrarily large,
     * the buffered image painted to the buffer will
     * remain the size specified in this constructor
     * unless the size is reset using the
     * <CODE>setBufferSize</CODE> method.</P>
     *
     * @param width      the width  of the buffered image
     * @param height     the height of the buffered image
     * @param background the background color or <CODE>Paint</CODE>
     *      for the buffered image
     * @see #BufferedPanel(int, int)
     * @see #BufferedPanel(Dimension)
     * @see #BufferedPanel(Dimension, Paint)
     * @see #setBufferSize(int, int)
     * @see #setBufferSize(Dimension)
     */
    public BufferedPanel(int width, int height, Paint background) {
        super(new CenterLayout());
        setBufferBackground(background);

        // build the buffered image as a side effect
        setBufferSize(width, height);
        
        // build the painter panel using the inner Painter class
        painter = new BufferedPanel.Painter(this);
        
        // install the painter panel
		add(painter);
		
        // build the automatically installed event handlers
        mouseActions = new MouseActionAdapter(painter);
        keyActions = new KeyActionAdapter(painter);
    }
    
	
    /**
     * <P>Constructs a BufferedPanel containing a buffered image
     * with the given <CODE>Dimension</CODE>,
     * and the given background color or <CODE>Paint</CODE>
     * for the image.</P>
     *
     * <P>If the given <CODE>Dimension</CODE> is <CODE>null</CODE>,
     * then the buffer width and height are both set to 1 pixel.</P>
     *
     * <P>If the width or height of the given <CODE>Dimension</CODE>
     * is less than 1 pixel, that value is set to 1 pixel.</P>
     *
     * <P>Though the component itself may grow arbitrarily large,
     * the buffered image painted to the buffer will
     * remain the size specified in this constructor
     * unless the size is reset using the
     * <CODE>setBufferSize</CODE> method.</P>
     *
     * @param dimension the dimension of the buffered image
     * @param background the background color or <CODE>Paint</CODE>
     *      for the buffered image
     * @see #BufferedPanel(int, int)
     * @see #BufferedPanel(Dimension)
     * @see #BufferedPanel(int, int, Paint)
     * @see #setBufferSize(int, int)
     * @see #setBufferSize(Dimension)
     */
    public BufferedPanel(Dimension dimension, Paint background) {
        this(
            (dimension == null) ? 1 : dimension.width,
            (dimension == null) ? 1 : dimension.height,
            background);
    }
	
	
	////////////////
    // Public API //
    ////////////////

    /**
     * <P>Returns a <CODE>Graphics2D</CODE> object that permits
     * painting to the internal buffered image for this panel.</P>
     *
     * <P>The user should always use this object to paint to the
     * buffer and thus indirectly modify this buffered panel.</P>
     *
     * <P>To make painting changes to the buffer visible, the
     * <CODE>repaint()</CODE> method must explicitly be called.
     * This allows a number of painting operations to be done
     * prior to screen repaint.</P>
     *
     * @return the graphics context of the internal buffered image
     * @see #getBuffer()
     * @see #getInnerPanel()
     */
    public Graphics2D getBufferGraphics() {
        return buffer.createGraphics();
    }
	
	
    /**
     * <P>Returns the internal buffered image for this panel.</P>
     *
     * @return the internal buffered image
     * @see #getBufferGraphics()
     * @see #getInnerPanel()
     */
    public BufferedImage getBuffer() {
        return buffer;
    }
    
    
    /**
     * <P>Returns the internal panel for this buffered panel,
     * that is, the panel that paints the buffered image
     * and handles the mouse and key adapters.</P>
     *
     * <P>This panel may be used when access to the panel on
     * which the graphics is drawn is needed.</P>
     *
     * <P>Do not set a border on this internal panel.  Set a
     * border on the outer <CODE>BufferedPanel</CODE> object.</P>
     *
     * @return the internal panel for this buffered panel
     * @see #getBufferGraphics()
     * @see #getBuffer()
     * @since 1.1
     */
    public DisplayPanel getInnerPanel() {
        return painter;
    }
    
    
    /**
     * <P>Repaints the buffered panel directly without a call to the generic
     * <CODE>repaint</CODE> method in <CODE>Component</CODE>.</P>
     *
     * <P>Instead, this method directly asks the inner panel to paint itself
     * which is done by repainting the screen from the buffer.</P>
     *
     * <P>This method is supplied for performance reasons to support the use
     * of animation in a buffered panel.</P>
     *
     * <P>This method does nothing if the buffered panel is not installed in
     * a window or dialog box.</P>
     *
     * @since 2.3
     */
    public void quickRepaint() {
        DisplayPanel panel = getInnerPanel();
        Graphics graphics = panel.getGraphics();
        
        if (graphics == null)
            return;
            
        panel.paint(graphics);
    }
    
    
    /**
     * <P>Sets the background color or <CODE>Paint</CODE>
     * to the given color or <CODE>Paint</CODE>.</P>
     *
     * <P>If the given color or <CODE>Paint</CODE> is <CODE>null</CODE>,
     * the current background is not changed.</P>
     *
     * @param background the new background color or <CODE>Paint</CODE>
     * @see #getBufferBackground()
     */
    public void setBufferBackground(Paint background) {
        if (background == null)
            return;
            
        bufferBackground = background;
    }
    
    
    /**
     * <P>Returns the background color or <CODE>Paint</CODE>
     * for this buffered panel.</P>
     *
     * @return the current background color or <CODE>Paint</CODE>
     * @see #setBufferBackground(Paint)
     */
    public Paint getBufferBackground() {
        return bufferBackground;
    }
    
    
    /**
     * <P>Fills this buffered panel 
     * with its background color or <CODE>Paint</CODE>.</P>
     *
     * @see #fillPanel(Paint)
     */
    public void clearPanel() {
        fillPanel(getBufferBackground());
    }
    
    
    /**
     * <P>Fills this buffered panel
     * with the given color or <CODE>Paint</CODE>.</P>
     *
     * <P>If the given color or <CODE>Paint</CODE> is <CODE>null</CODE>,
     * the buffered panel is not changed.</P>
     *
     * @param fill the color or <CODE>Paint</CODE> used to fill
     * @see #clearPanel()
     */
    public void fillPanel(Paint fill) {
        if (fill == null)
            return;
        
        Graphics2D g2 = getBufferGraphics();
        g2.setPaint(fill);
        g2.fillRect(0, 0, getBufferWidth(), getBufferHeight());
    }


    /**
     * <P>Sets the background color for the panel that wraps the buffer
     * to the given color.</P>
     *
     * @param background the new background color
     */
    public void setBackground(Color background) {
        if (painter != null)
            painter.setBackground(background);
            
        super.setBackground(background);
    }
    
    
    /**
     * <P>Sets the size of the buffered image 
     * to the given <CODE>Dimension</CODE>.</P>
     *
     * <P>If the given <CODE>Dimension</CODE> is <CODE>null</CODE>,
     * the current image size is not changed.</P>
     *
     * <P>If the width or height of the given <CODE>Dimension</CODE> 
     * is less than 1 pixel, it is set to 1 pixel.</P>
     *
     * <P>Any image area gained by an size increase in either direction
     * will be painted with the current background color.</P>
     *
     * <P>Any image area lost by a size decrease in either direction
     * will be clipped on the right and/or bottom of the image.</P>
     *
     * <P>For a short time both the image of the previous size 
     * and an image of the new size are maintained in memory.</P>
     *
     * @param size the new size for the image
     * @see #setBufferSize(int, int)
     */
    public synchronized void setBufferSize(Dimension size) {
        if (size == null)
            return;
        
        setBufferSize(size.width, size.height);
    }
    
    
    /**
     * <P>Sets the size of the buffered image 
     * to the given height and width.</P>
     *
     * <P>If the given width or height 
     * is less than 1 pixel, it is set to 1 pixel.</P>
     *
     * <P>Any image area gained by an size increase in either direction
     * will be painted with the current background color.</P>
     *
     * <P>Any image area lost by a size decrease in either direction
     * will be clipped on the right and/or bottom of the image.</P>
     *
     * <P>For a short time both the image of the previous size 
     * and an image of the new size are maintained in memory.</P>
     *
     * @param width  the new width  for the image
     * @param height the new height for the image
     * @see #setBufferSize(Dimension)
     */
    public synchronized void setBufferSize(int width, int height) {

        // ensure positive width and height
        width  = (int)Math.max(width,  1);
        height = (int)Math.max(height, 1);
        
        // save current buffer so we can later paint onto new buffer
        BufferedImage oldBuffer = buffer;
        
        // build the new buffered image
        buffer =
            new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        
        // clear the new buffered image
        clearPanel();
        
        // paint the old image to the new buffer if necessary
        if (oldBuffer != null) {
            Graphics2D g2 = getBufferGraphics();
            g2.drawImage(oldBuffer, 0, 0, this);
        }
        
        // refresh the enclosing window to account for the new buffer size
        refreshComponent();
    }
    
    
    /**
     * Returns the width of the buffered image.
     *
     * @return the width of the buffered image
     */
    public int getBufferWidth() {
        return buffer.getWidth();
    }
	
	
    /**
     * Returns the height of the buffered image.
     *
     * @return the height of the buffered image
     */
    public int getBufferHeight() {
        return buffer.getHeight();
    }
    
    
    /**
     * <P>Sets the mouse action adapter for the buffer
     * to the given adapter.</P>
     *
     * <P>If <CODE>null</CODE>, the current adapter
     * is not changed.</P>
     *
     * @param adapter the new mouse action adapter
     * @see #getMouseActionAdapter()
     */
    public void setMouseActionAdapter(MouseActionAdapter adapter) {
        if (adapter == null)
            return;
            
        painter.removeMouseListener(mouseActions);
        painter.removeMouseMotionListener(mouseActions);

        mouseActions = adapter;

        painter.addMouseListener(mouseActions);
        painter.addMouseMotionListener(mouseActions);
    }
    
    
    /**
     * Returns the mouse action adapter for the buffer.
     *
     * @return the mouse action adapter for the buffer
     * @see #setMouseActionAdapter(MouseActionAdapter)
     */
    public MouseActionAdapter getMouseActionAdapter() {
        return mouseActions;
    }    
    
    
    /**
     * <P>Sets the key action adapter for the buffer
     * to the given adapter.</P>
     *
     * <P>If <CODE>null</CODE>, the current adapter
     * is not changed.</P>
     *
     * @param adapter the new key action adapter
     * @see #getKeyActionAdapter()
     */
    public void setKeyActionAdapter(KeyActionAdapter adapter) {
        if (adapter == null)
            return;
            
        painter.removeKeyListener(keyActions);

        keyActions = adapter;

        painter.addKeyListener(keyActions);
    }
    
    
    /**
     * Returns the key action adapter for the buffer.
     *
     * @return the key action adapter for the buffer
     * @see #setKeyActionAdapter(KeyActionAdapter)
     */
    public KeyActionAdapter getKeyActionAdapter() {
        return keyActions;
    }
    
    
    /**
     * <P>Override this <CODE>paintOver</CODE> method to add additional
     * painting actions after the default buffer repaint is done during
     * a <CODE>repaint()</CODE> call.</P>
     *
     * <P>The intention of this facility is to enable algorithmic
     * painting to be done via the <CODE>paintOver</CODE> method on
     * top of the default painting of the buffer image on the panel.
     * This makes the buffer appear to be the background and what is
     * painted via the <CODE>paintOver</CODE> method to be painted in
     * the foreground.</P>
     *
     * <P>The default implementation of the <CODE>paintOver</CODE>
     * method is to do nothing.  This enables overrides as desired.</P>
     * 
     * @param g2 the <CODE>Graphics2D</CODE> context for the buffer
     *           repaint operation
     * @see Painter#paint(Graphics)
     * @since 1.0.1
     */
    public void paintOver(Graphics2D g2) {
        // intentionally left empty to allow for overrides
    }
    
    
    ///////////////////
    // Inner classes //
    ///////////////////
    
    /**
     * <P>Panel that paints the internal <CODE>BufferedImage</CODE>
     * that maintains the persistent graphics state 
     * of a <CODE>BufferedPanel</CODE>.</P>
     *
     * @author  Jeff Raab
     * @author  Richard Rasala
     * @version 2.3
     * @since   1.0
     */
    protected static class Painter extends DisplayPanel {
    
        /**
         * Reference to the <CODE>BufferedPanel</CODE> that created this
         * <CODE>Painter</CODE>.
         */
        protected BufferedPanel panel = null;
        
        
        /**
         * Contructor that should only be called by a
         * <CODE>BufferedPanel</CODE>.
         *
         * @param panel the <CODE>BufferedPanel</CODE> used to construct
         *        this <CODE>Painter</CODE>
         */
        protected Painter(BufferedPanel panel) {
            this.panel = panel;
        }
        
        
        /**
         * <P>Returns the size of the buffer as the size of this panel.</P>
         *
         * <P>Ignores any calls to <CODE>setPreferredSize</CODE>.</P>
         *
         * @return the size of the buffer as the size of this panel
         */
        public Dimension getPreferredSize() {
            return new Dimension(
                panel.getBufferWidth(), panel.getBufferHeight());
        }
        
        
        /**
         * Paints the image buffer of the buffered panel in this panel.
         *
         * @param g the standard graphics state for this panel
         */
        protected void paintComponent(Graphics g) {
            Insets in = getInsets();
            g.drawImage(panel.getBuffer(), in.left, in.top, this);
        }
        
        
        /**
         * Paints the component and then adds the work done by the
         * paintOver function.
         *
         * @see #paintOver(Graphics2D)
         */
        public void paint(Graphics g) {
            // this call will call paintComponent to paint the buffer
            super.paint(g);
            
            // set up for paintOver
            Insets in = getInsets();
            
            Graphics2D g2 = (Graphics2D) g;

            g2.transform(
                AffineTransform.
                    getTranslateInstance(in.left, in.top));
            
            // call paintOver
            panel.paintOver(g2);
        }
    }
}
