/*
 * @(#)RepaintManager.java	1.60 04/02/18
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package javax.swing;


import java.awt.*;
import java.awt.event.*;
import java.awt.peer.ComponentPeer;
import java.awt.peer.ContainerPeer;
import java.awt.image.VolatileImage;
import java.util.*;
import java.applet.*;

import sun.security.action.GetPropertyAction;


/**
 * This class manages repaint requests, allowing the number
 * of repaints to be minimized, for example by collapsing multiple 
 * requests into a single repaint for members of a component tree.
 *
 * @version 1.60 02/18/04
 * @author Arnaud Weber
 */
public class RepaintManager 
{
    /**
     * Maps from GraphicsConfiguration to VolatileImage.
     */
    private Map volatileMap = new HashMap(1);
     
    Hashtable dirtyComponents = new Hashtable();
    Hashtable tmpDirtyComponents = new Hashtable();
    Vector    invalidComponents;

    boolean   doubleBufferingEnabled = true;

    private Dimension doubleBufferMaxSize;

    // Support for both the standard and volatile offscreen buffers exists to
    // provide backwards compatibility for the [rare] programs which may be
    // calling getOffScreenBuffer() and not expecting to get a VolatileImage.
    // Swing internally is migrating to use *only* the volatile image buffer.

    // Support for standard offscreen buffer
    //
    DoubleBufferInfo standardDoubleBuffer;

    private static final Object repaintManagerKey = RepaintManager.class;

    // Whether or not a VolatileImage should be used for double-buffered painting
    static boolean volatileImageBufferEnabled = true;

    // The maximum number of times Swing will attempt to use the VolatileImage
    // buffer during a paint operation.
    static final int VOLATILE_LOOP_MAX = 2;

    static {
	String vib = (String) java.security.AccessController.doPrivileged(
               new GetPropertyAction("swing.volatileImageBufferEnabled"));
	volatileImageBufferEnabled = (vib == null || vib.equals("true"));
    }

    /** 
     * Return the RepaintManager for the calling thread given a Component.
     * 
     * @param c a Component -- unused in the default implementation, but could
     *          be used by an overridden version to return a different RepaintManager
     *          depending on the Component
     * @return the RepaintManager object
     */
    public static RepaintManager currentManager(Component c) {
        // Note: SystemEventQueueUtilities.ComponentWorkRequest passes
        // in null as the component, so if component is ever used to 
        // determine the current RepaintManager, SystemEventQueueUtilities
        // will need to be modified accordingly.
        RepaintManager result = (RepaintManager) SwingUtilities.appContextGet(repaintManagerKey);
        if(result == null) {
            result = new RepaintManager();
            SwingUtilities.appContextPut(repaintManagerKey, result);
        }
	return result;
    }
    
    /**
     * Return the RepaintManager for the calling thread given a JComponent.
     * <p>
    * Note: This method exists for backward binary compatibility with earlier
     * versions of the Swing library. It simply returns the result returned by
     * {@link #currentManager(Component)}. 
     *
     * @param c a JComponent -- unused
     * @return the RepaintManager object
     */
    public static RepaintManager currentManager(JComponent c) {
	return currentManager((Component)c);
    }


    /**
     * Set the RepaintManager that should be used for the calling 
     * thread. <b>aRepaintManager</b> will become the current RepaintManager
     * for the calling thread's thread group.
     * @param aRepaintManager  the RepaintManager object to use
     */
    public static void setCurrentManager(RepaintManager aRepaintManager) {
        if (aRepaintManager != null) {
            SwingUtilities.appContextPut(repaintManagerKey, aRepaintManager);
        } else {
            SwingUtilities.appContextRemove(repaintManagerKey);
        }
    }

    /** 
     * Create a new RepaintManager instance. You rarely call this constructor.
     * directly. To get the default RepaintManager, use 
     * RepaintManager.currentManager(JComponent) (normally "this").
     */
    public RepaintManager() {
	Object dbe = java.security.AccessController.doPrivileged(
               new GetPropertyAction("awt.nativeDoubleBuffering"));
        boolean nativeDoubleBuffering = (dbe != null) ?
                      Boolean.valueOf(dbe.toString()).booleanValue() : false;
        // If native doublebuffering is being used, do NOT use
        // Swing doublebuffering.
        doubleBufferingEnabled = !nativeDoubleBuffering;
    }


    /**
     * Mark the component as in need of layout and queue a runnable
     * for the event dispatching thread that will validate the components
     * first isValidateRoot() ancestor. 
     * 
     * @see JComponent#isValidateRoot
     * @see #removeInvalidComponent
     */
    public synchronized void addInvalidComponent(JComponent invalidComponent) 
    {
        Component validateRoot = null;

	/* Find the first JComponent ancestor of this component whose
	 * isValidateRoot() method returns true.  
	 */
        for(Component c = invalidComponent; c != null; c = c.getParent()) {
	    if ((c instanceof CellRendererPane) || (c.getPeer() == null)) {
		return;
	    }
	    if ((c instanceof JComponent) && (((JComponent)c).isValidateRoot())) {
		validateRoot = c;
		break;
	    }
	}
        
	/* There's no validateRoot to apply validate to, so we're done.
	 */
	if (validateRoot == null) {
	    return;
	}

	/* If the validateRoot and all of its ancestors aren't visible
	 * then we don't do anything.  While we're walking up the tree
	 * we find the root Window or Applet.
	 */
	Component root = null;
	
	for(Component c = validateRoot; c != null; c = c.getParent()) {
	    if (!c.isVisible() || (c.getPeer() == null)) {
		return;
	    }
	    if ((c instanceof Window) || (c instanceof Applet)) {
		root = c;
		break;
	    }
	}

	if (root == null) {
	    return;
	}
	   
	/* Lazily create the invalidateComponents vector and add the
	 * validateRoot if it's not there already.  If this validateRoot
	 * is already in the vector, we're done.
	 */
	if (invalidComponents == null) {
	    invalidComponents = new Vector();
	}
	else {
	    int n = invalidComponents.size();
	    for(int i = 0; i < n; i++) {
		if(validateRoot == (Component)(invalidComponents.elementAt(i))) {
		    return;
		}
	    }
	}
	invalidComponents.addElement(validateRoot);

	/* Queues a Runnable that calls RepaintManager.validateInvalidComponents() 
	 * and RepaintManager.paintDirtyRegions() with SwingUtilities.invokeLater().
	 */
	SystemEventQueueUtilities.queueComponentWorkRequest(root);
    }


    /** 
     * Remove a component from the list of invalid components.
     * 
     * @see #addInvalidComponent
     */
    public synchronized void removeInvalidComponent(JComponent component) {
        if(invalidComponents != null) {
            int index = invalidComponents.indexOf(component);
            if(index != -1) {
                invalidComponents.removeElementAt(index);
            }
        }
    }


    /** 
     * Add a component in the list of components that should be refreshed.
     * If <i>c</i> already has a dirty region, the rectangle <i>(x,y,w,h)</i> 
     * will be unioned with the region that should be redrawn. 
     * 
     * @see JComponent#repaint
     */
    public void addDirtyRegion(JComponent c, int x, int y, int w, int h) 
    {
	/* Special cases we don't have to bother with.
	 */
        if ((w <= 0) || (h <= 0) || (c == null)) {
            return;
        }

	if ((c.getWidth() <= 0) || (c.getHeight() <= 0)) {
	    return;
	}

        if (extendDirtyRegion(c, x, y, w, h)) {
            // Component was already marked as dirty, region has been
            // extended, no need to continue.
            return;
        }

	/* Make sure that c and all it ancestors (up to an Applet or
	 * Window) are visible.  This loop has the same effect as 
	 * checking c.isShowing() (and note that it's still possible 
	 * that c is completely obscured by an opaque ancestor in 
	 * the specified rectangle).
	 */
	Component root = null;

        // Note: We can't synchronize around this, Frame.getExtendedState
        // is synchronized so that if we were to synchronize around this
        // it could lead to the possibility of getting locks out
        // of order and deadlocking.
	for (Container p = c; p != null; p = p.getParent()) {
	    if (!p.isVisible() || (p.getPeer() == null)) {
		return;
	    }
	    if ((p instanceof Window) || (p instanceof Applet)) {
                // Iconified frames are still visible!
                if (p instanceof Frame &&
                        (((Frame)p).getExtendedState() & Frame.ICONIFIED) ==
                                    Frame.ICONIFIED) {
                    return;
                }
		root = p;
		break;
	    }
	}

	if (root == null) return;

        synchronized(this) {
            if (extendDirtyRegion(c, x, y, w, h)) {
                // In between last check and this check another thread
                // queued up runnable, can bail here.
                return;
            }
            dirtyComponents.put(c, new Rectangle(x, y, w, h));
        }

	/* Queues a Runnable that calls validateInvalidComponents() and
	 * rm.paintDirtyRegions() with SwingUtilities.invokeLater().
	 */
	SystemEventQueueUtilities.queueComponentWorkRequest(root);
    }

    /**
     * Extends the dirty region for the specified component to include
     * the new region.
     *
     * @return false if <code>c</code> is not yet marked dirty.
     */
    private synchronized boolean extendDirtyRegion(
        Component c, int x, int y, int w, int h) {
        Rectangle r = (Rectangle)dirtyComponents.get(c);
        if (r != null) {
            // A non-null r implies c is already marked as dirty,
            // and that the parent is valid. Therefore we can
            // just union the rect and bail.
            SwingUtilities.computeUnion(x, y, w, h, r);
            return true;
        }
        return false;
    }

    /** Return the current dirty region for a component.
     *  Return an empty rectangle if the component is not
     *  dirty.
     */
    public Rectangle getDirtyRegion(JComponent aComponent) {
	Rectangle r = null;
	synchronized(this) {
	    r = (Rectangle)dirtyComponents.get(aComponent);
	}
	if(r == null)
	    return new Rectangle(0,0,0,0);
	else
	    return new Rectangle(r);
    }

    /** 
     * Mark a component completely dirty. <b>aComponent</b> will be
     * completely painted during the next paintDirtyRegions() call.
     */
    public void markCompletelyDirty(JComponent aComponent) {
	addDirtyRegion(aComponent,0,0,Integer.MAX_VALUE,Integer.MAX_VALUE);
    }
	    
    /** 
     * Mark a component completely clean. <b>aComponent</b> will not
     * get painted during the next paintDirtyRegions() call.
     */
    public void markCompletelyClean(JComponent aComponent) {
	synchronized(this) {
		dirtyComponents.remove(aComponent);
	}
    }

    /** 
     * Convenience method that returns true if <b>aComponent</b> will be completely
     * painted during the next paintDirtyRegions(). If computing dirty regions is
     * expensive for your component, use this method and avoid computing dirty region
     * if it return true.
     */
    public boolean isCompletelyDirty(JComponent aComponent) {
	Rectangle r;
	
	r = getDirtyRegion(aComponent);
	if(r.width == Integer.MAX_VALUE &&
	   r.height == Integer.MAX_VALUE)
	    return true;
	else
	    return false;
    }


    /** 
     * Validate all of the components that have been marked invalid.
     * @see #addInvalidComponent
     */
    public void validateInvalidComponents() {
        Vector ic;
        synchronized(this) {
            if(invalidComponents == null) {
                return;
	    }
            ic = invalidComponents;
            invalidComponents = null;
        }
	int n = ic.size();
        for(int i = 0; i < n; i++) {
            ((Component)ic.elementAt(i)).validate();
        }
    }
    

    /**
     * Paint all of the components that have been marked dirty.
     * 
     * @see #addDirtyRegion
     */
    public void paintDirtyRegions() {
        int i, count;
	Vector roots;
        JComponent dirtyComponent;

	synchronized(this) {  // swap for thread safety
	    Hashtable tmp = tmpDirtyComponents;
	    tmpDirtyComponents = dirtyComponents;
	    dirtyComponents = tmp;
	    dirtyComponents.clear();
	}

        count = tmpDirtyComponents.size();
        if (count == 0) {
            return;
        } 

        Rectangle rect;
        int localBoundsX = 0;
        int localBoundsY = 0;
        int localBoundsH = 0;
        int localBoundsW = 0;	
        Enumeration keys;

        roots = new Vector(count);
        keys = tmpDirtyComponents.keys();

        while(keys.hasMoreElements()) {
            dirtyComponent = (JComponent) keys.nextElement();
            collectDirtyComponents(tmpDirtyComponents, dirtyComponent, roots);
        }

        count = roots.size();
        //        System.out.println("roots size is " + count);
        for(i=0 ; i < count ; i++) {
            dirtyComponent = (JComponent) roots.elementAt(i);
            rect = (Rectangle) tmpDirtyComponents.get(dirtyComponent);
            //            System.out.println("Should refresh :" + rect);
            localBoundsH = dirtyComponent.getHeight();
            localBoundsW = dirtyComponent.getWidth();

            SwingUtilities.computeIntersection(localBoundsX, 
					       localBoundsY,
					       localBoundsW,
					       localBoundsH,
					       rect);
            // System.out.println("** paint of " + dirtyComponent + rect);
            if (rect.x == 0 && rect.y == 0 &&
                         rect.width == dirtyComponent.getWidth() &&
                         rect.height == dirtyComponent.getHeight()) {
                Container parent = dirtyComponent.getParent();
                ComponentPeer parentPeer;
                if (parent != null && !parent.isLightweight() &&
                    (parentPeer = parent.getPeer()) != null) {
                    // Cancel any pending paints on the heavy weight peer.
                    // This avoid duplicate painting.
                    ((ContainerPeer)parentPeer).cancelPendingPaint(
                                    dirtyComponent.getX(),
                                    dirtyComponent.getY(),
                                    rect.width, rect.height);
                }
            }
            dirtyComponent.paintImmediately(rect.x,rect.y,rect.width,rect.height);
        }
	tmpDirtyComponents.clear();
    }


    Rectangle tmp = new Rectangle();

    void collectDirtyComponents(Hashtable dirtyComponents,
				JComponent dirtyComponent,
				Vector roots) {
        int dx, dy, rootDx, rootDy;
        Component component, rootDirtyComponent,parent;
	//Rectangle tmp;
        Rectangle cBounds;

        // Find the highest parent which is dirty.  When we get out of this
        // rootDx and rootDy will contain the translation from the
        // rootDirtyComponent's coordinate system to the coordinates of the
        // original dirty component.  The tmp Rect is also used to compute the
        // visible portion of the dirtyRect.

        component = rootDirtyComponent = dirtyComponent;

        int x = dirtyComponent.getX();
        int y = dirtyComponent.getY();
        int w = dirtyComponent.getWidth();
        int h = dirtyComponent.getHeight();

        dx = rootDx = 0;
        dy = rootDy = 0;
        tmp.setBounds((Rectangle) dirtyComponents.get(dirtyComponent));

        // System.out.println("Collect dirty component for bound " + tmp + 
        //                                   "component bounds is " + cBounds);;
        SwingUtilities.computeIntersection(0,0,w,h,tmp);

        if (tmp.isEmpty()) {
            // System.out.println("Empty 1");
            return;
        } 

        for(;;) {
            parent = component.getParent();
            if(parent == null) 
                break;

            if(!(parent instanceof JComponent))
                break;

            component = parent;

            dx += x;
            dy += y;
            tmp.setLocation(tmp.x + x, tmp.y + y);

            x = component.getX();
            y = component.getY();
            w = component.getWidth();
            h = component.getHeight();
            tmp = SwingUtilities.computeIntersection(0,0,w,h,tmp);

            if (tmp.isEmpty()) {
                // System.out.println("Empty 2");
                return;
            }

            if (dirtyComponents.get(component) != null) {
                rootDirtyComponent = component;
                rootDx = dx;
                rootDy = dy;
            }
        } 

        if (dirtyComponent != rootDirtyComponent) {
	    Rectangle r;
            tmp.setLocation(tmp.x + rootDx - dx,
			    tmp.y + rootDy - dy);
	    r = (Rectangle)dirtyComponents.get(rootDirtyComponent);
	    SwingUtilities.computeUnion(tmp.x,tmp.y,tmp.width,tmp.height,r);
        }

        // If we haven't seen this root before, then we need to add it to the
        // list of root dirty Views.

        if (!roots.contains(rootDirtyComponent)) 
            roots.addElement(rootDirtyComponent);	
    }


    /**
     * Returns a string that displays and identifies this
     * object's properties.
     *
     * @return a String representation of this object
     */
    public synchronized String toString() {
	StringBuffer sb = new StringBuffer();
	if(dirtyComponents != null) 
	    sb.append("" + dirtyComponents);
        return sb.toString();
    }


   /**
     * Return the offscreen buffer that should be used as a double buffer with 
     * the component <code>c</code>.
     * By default there is a double buffer per RepaintManager.
     * The buffer might be smaller than <code>(proposedWidth,proposedHeight)</code>
     * This happens when the maximum double buffer size as been set for the receiving
     * repaint manager.
     */
    public Image getOffscreenBuffer(Component c,int proposedWidth,int proposedHeight) {
	return _getOffscreenBuffer(c, proposedWidth, proposedHeight);
    }

  /**
   * Return a volatile offscreen buffer that should be used as a
   * double buffer with the specified component <code>c</code>.
   * The image returned will be an instance of VolatileImage, or null
   * if a VolatileImage object could not be instantiated.
   * This buffer might be smaller than <code>(proposedWidth,proposedHeight)</code>.
   * This happens when the maximum double buffer size has been set for this
   * repaint manager.
   *
   * @see java.awt.image.VolatileImage
   * @since 1.4
   */
    public Image getVolatileOffscreenBuffer(Component c, 
					    int proposedWidth,int proposedHeight) {
        GraphicsConfiguration config = c.getGraphicsConfiguration();
        if (config == null) {
            config = GraphicsEnvironment.getLocalGraphicsEnvironment().
                            getDefaultScreenDevice().getDefaultConfiguration();
        }
	Dimension maxSize = getDoubleBufferMaximumSize();
	int width = proposedWidth < 1 ? 1 :
            (proposedWidth > maxSize.width? maxSize.width : proposedWidth);
        int height = proposedHeight < 1 ? 1 :
            (proposedHeight > maxSize.height? maxSize.height : proposedHeight);
        VolatileImage image = (VolatileImage)volatileMap.get(config);
        if (image == null || image.getWidth() < width ||
                             image.getHeight() < height) {
            if (image != null) {
                image.flush();
            }
            image = config.createCompatibleVolatileImage(width, height);
            volatileMap.put(config, image);
        }
	return image;
    }

    private Image _getOffscreenBuffer(Component c, int proposedWidth, int proposedHeight) {
	Dimension maxSize = getDoubleBufferMaximumSize();
	DoubleBufferInfo doubleBuffer = null;
        int width, height;

        if (standardDoubleBuffer == null) {
            standardDoubleBuffer = new DoubleBufferInfo();
        }
        doubleBuffer = standardDoubleBuffer;
	    
	width = proposedWidth < 1? 1 : 
	          (proposedWidth > maxSize.width? maxSize.width : proposedWidth);
        height = proposedHeight < 1? 1 : 
                  (proposedHeight > maxSize.height? maxSize.height : proposedHeight);

        if (doubleBuffer.needsReset || (doubleBuffer.image != null &&
                                        (doubleBuffer.size.width < width ||
                                         doubleBuffer.size.height < height))) {
            doubleBuffer.needsReset = false;
            if (doubleBuffer.image != null) {
                doubleBuffer.image.flush();
                doubleBuffer.image = null;
            }
            width = Math.max(doubleBuffer.size.width, width);
            height = Math.max(doubleBuffer.size.height, height);
        }

	Image result = doubleBuffer.image;

	if (doubleBuffer.image == null) {
            result = c.createImage(width , height);
            doubleBuffer.size = new Dimension(width, height);
	    if (c instanceof JComponent) {
		((JComponent)c).setCreatedDoubleBuffer(true);
		doubleBuffer.image = result;
	    }
	    // JComponent will inform us when it is no longer valid
	    // (via removeNotify) we have no such hook to other components,
	    // therefore we don't keep a ref to the Component
	    // (indirectly through the Image) by stashing the image.
	}
        return result;
    }


    /** Set the maximum double buffer size. **/
    public void setDoubleBufferMaximumSize(Dimension d) {
        doubleBufferMaxSize = d;
        if (standardDoubleBuffer != null && standardDoubleBuffer.image != null) {
            if (standardDoubleBuffer.image.getWidth(null) > d.width || 
		standardDoubleBuffer.image.getHeight(null) > d.height) {
		standardDoubleBuffer.image = null;
	    }
        }
        // Clear out the VolatileImages
        Iterator gcs = volatileMap.keySet().iterator();
        while (gcs.hasNext()) {
            GraphicsConfiguration gc = (GraphicsConfiguration)gcs.next();
            VolatileImage image = (VolatileImage)volatileMap.get(gc);
            if (image.getWidth() > d.width || image.getHeight() > d.height) {
                image.flush();
                gcs.remove();
	    }
	}	    
    }

    /**
     * Returns the maximum double buffer size.
     *
     * @return a Dimension object representing the maximum size
     */
    public Dimension getDoubleBufferMaximumSize() {
	if (doubleBufferMaxSize == null) {
	    try {
	        doubleBufferMaxSize = Toolkit.getDefaultToolkit().getScreenSize();
	    } catch (HeadlessException e) {
		doubleBufferMaxSize = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
	    }
	}
        return doubleBufferMaxSize;
    }

    /**
     * Enables or disables double buffering in this RepaintManager.
     * CAUTION: The default value for this property is set for optimal
     * paint performance on the given platform and it is not recommended
     * that programs modify this property directly.
     *
     * @param aFlag  true to activate double buffering
     * @see #isDoubleBufferingEnabled
     */
    public void setDoubleBufferingEnabled(boolean aFlag) {
        doubleBufferingEnabled = aFlag;
    }

    /**
     * Returns true if this RepaintManager is double buffered.
     * The default value for this property may vary from platform
     * to platform.  On platforms where native double buffering
     * is supported in the AWT, the default value will be <code>false</code>
     * to avoid unnecessary buffering in Swing.
     * On platforms where native double buffering is not supported,
     * the default value will be <code>true</code>.
     *
     * @return true if this object is double buffered
     */
    public boolean isDoubleBufferingEnabled() {
        return doubleBufferingEnabled;
    }

    /**
     * This resets the double buffer. Actually, it marks the double buffer
     * as invalid, the double buffer will then be recreated on the next
     * invocation of getOffscreenBuffer.
     */
    void resetDoubleBuffer() {
	if (standardDoubleBuffer != null) {
	    standardDoubleBuffer.needsReset = true;
	}
    }

    /**
     * This resets the volatile double buffer. 
     */
    void resetVolatileDoubleBuffer(GraphicsConfiguration gc) {
        Image image = (Image)volatileMap.remove(gc);
        if (image != null) {
            image.flush();
        }
    }

    /**
     * Returns true if we should use the <code>Image</code> returned
     * from <code>getVolatileOffscreenBuffer</code> to do double buffering.
     */
    boolean useVolatileDoubleBuffer() {
        return volatileImageBufferEnabled;
    }

    private class DoubleBufferInfo {
        public Image image;
        public Dimension size;
        public boolean needsReset = false;
    }
}
