/*
 * @(#)AbstractPaintable.java    1.0  15 December 2003
 *
 * 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.geom.*;
import javax.swing.*;
import java.beans.*;

/**
 * <P>The abstract class <CODE>AbstractPaintable</CODE> defines objects that
 * implement <CODE>Paintable</CODE> and <CODE>SupportsPropertyChange</CODE>.</P>
 *
 * @author  Richard Rasala
 * @version 2.3
 * @since   2.3
 */
public abstract class AbstractPaintable
    implements Paintable, SupportsPropertyChange
{
    /** Bound property name for set default bounds2D. */
    public static final String SET_DEFAULT_BOUNDS2D = "set.default.bounds2d";
    
    /** Bound property name for set default center. */
    public static final String SET_DEFAULT_CENTER   = "set.default.center";
    
    /** Bound property name for set visible. */
    public static final String SET_VISIBLE          = "set.visible";
    
    /** Bound property name for set opacity. */
    public static final String SET_OPACITY          = "set.opacity";
    
    
    /** The default Bounds2D rectangle. */
    private Rectangle2D defaultBounds2D = null;
    
    /** The default center. */
    private Point2D defaultCenter = null;
    
    /** The visibility property of the paintable. */
    private boolean visible = true;
    
    /** The opacity for paintables that are partially transparent. */
    private float opacity = 1;
    
    
    /**
     * <P>The main listener for this AbstractPaintable object to implement
     * the interface <CODE>SupportsPropertyChange</CODE>.</P>
     *
     * <P>Note: In this implementation, <CODE>PropertyChangeSupport</CODE>
     * is used rather than <CODE>SwingPropertyChangeSupport</CODE> to
     * ensure thread safety.</P>
     */
    private final PropertyChangeSupport changeAdapter
        = new PropertyChangeSupport(this);
    
    
    /**
     * <P>The forwarding listener for this AbstractPaintable object to implement
     * the interface <CODE>SupportsPropertyChange</CODE>.</P>
     *
     * @see #getForwardingListener()
     */
    private final PropertyChangeForwardingListener forwardingListener
        = new PropertyChangeForwardingListener(this);
    

    /**
     * <P>Paints onto a <CODE>Graphics</CODE> context using information
     * from this object.</P>
     *
     * <P>When this method call is complete, the internal state of g
     * should be unchanged.</P>
     * 
     * @param g the graphics context on which to paint
     * @see #getPreparedGraphics2D(Graphics)
     */
    public abstract void paint(Graphics g);
    
    
    /**
     * <P>Returns a copy of the 2-dimensional bounds of the paint region
     * affected by the <CODE>paint</CODE> method.</P>
     *
     * @return a copy of the 2-dimensional bounds of the paint region
     * @see #setDefaultBounds2D(Rectangle2D)
     * @see #getDefaultBounds2D()
     */
    public abstract Rectangle2D getBounds2D();
    
    
    /**
     * <P>Returns a copy of the center of the paint region.</P>
     *
     * <P>In this class, the method is implemented to return the
     * center of the rectangle returned by the method
     * <CODE>getBounds2D</CODE>.</P>
     *
     * <P>However, a derived class may override this method if
     * the object geometry suggests a different result.</P>
     *
     * @return a copy of the center of the paint region
     * @see #setDefaultCenter(Point2D)
     * @see #getDefaultCenter()
     */
    public Point2D getCenter() {
        Rectangle2D bounds = getBounds2D();
        
        double x = bounds.getCenterX();
        double y = bounds.getCenterY();
        
        return new Point2D.Double(x, y);
    }
    
    
    /**
     * <P>Sets the default Bounds2D rectangle.</P>
     *
     * <P>If the default Bounds2D rectangle is non-<CODE>null</CODE>,
     * then a derived class may choose to use this default rather than
     * perform a computation to obtain more exact bounds.  Thus, the
     * use of this default is optional rather than mandatory.</P>
     *
     * <P>It is valid to set the default Bounds2D rectangle to
     * <CODE>null</CODE> to force a computation of the bounds.
     * Setting the default Bounds2D rectangle to a rectangle
     * with zero width or height is equivalent to setting it
     * to <CODE>null</CODE>.</P>
     *
     * <P>A derived class should specify whether it uses the default
     * Bounds2D rectangle in cases when it is non-<CODE>null</CODE>.
     *
     * <P>Fires property change: SET_DEFAULT_BOUNDS2D.</P>
     * 
     * @param rectangle the default Bounds2D rectangle
     * @see #getBounds2D()
     * @see #getDefaultBounds2D()
     */
    public final void setDefaultBounds2D(Rectangle2D rectangle) {
        if (rectangle == null) {
            if (defaultBounds2D == null)
                return;
            
            defaultBounds2D = null;
        }
        else {
            if (rectangle.equals(defaultBounds2D))
                return;
            
            double x = rectangle.getX();
            double y = rectangle.getY();
            double w = rectangle.getWidth();
            double h = rectangle.getHeight();
            
            defaultBounds2D = ((w <= 0) || (h <= 0))
                ? null
                : new Rectangle2D.Double(x, y, w, h);
        }
        
        firePropertyChange(SET_DEFAULT_BOUNDS2D, null, null);
    }
    
    
    /**
     * Returns a copy of the default Bounds2D rectangle.
     *
     * @return a copy of the default Bounds2D rectangle
     * @see #getBounds2D()
     * @see #setDefaultBounds2D(Rectangle2D)
     */
    public final Rectangle2D getDefaultBounds2D() {
        if (defaultBounds2D == null)
            return null;
        else {
            double x = defaultBounds2D.getX();
            double y = defaultBounds2D.getY();
            double w = defaultBounds2D.getWidth();
            double h = defaultBounds2D.getHeight();
            
            return new Rectangle2D.Double(x, y, w, h);
        }
    }
    
    
    /**
     * <P>Sets the default center.</P>
     *
     * <P>If the default center is non-<CODE>null</CODE>,
     * then a derived class may choose to use this default rather than
     * perform a computation to obtain a more exact center.  Thus, the
     * use of this default is optional rather than mandatory.</P>
     *
     * <P>It is valid to set the default center to
     * <CODE>null</CODE> to force a computation of the center.</P>
     *
     * <P>A derived class should specify whether it uses the default
     * center in cases when it is non-<CODE>null</CODE>.
     *
     * <P>Fires property change: SET_DEFAULT_CENTER.</P>
     * 
     * @param center the default center
     * @see #getCenter()
     * @see #getDefaultCenter()
     */
    public final void setDefaultCenter(Point2D center) {
        if (center == null) {
            if (defaultCenter == null)
                return;
            
            defaultCenter = null;
        }
        else {
            if (center.equals(defaultCenter))
                return;
            
            double x = center.getX();
            double y = center.getY();
            
            defaultCenter = new Point2D.Double(x, y);
        }
        
        firePropertyChange(SET_DEFAULT_CENTER, null, null);
    }
    
    
    /**
     * Returns a copy of the default center.
     *
     * @return a copy of the default center
     * @see #getCenter()
     * @see #setDefaultCenter(Point2D)
     */
    public final Point2D getDefaultCenter() {
        if (defaultCenter == null)
            return null;
        else {
            double x = defaultCenter.getX();
            double y = defaultCenter.getY();
            
            return new Point2D.Double(x, y);
        }
    }
    
    
    /**
     * <P>Tests if a specified point is inside the paintable.</P>
     *
     * <P>This method returns <CODE>false</CODE> if one or more of the following
     * conditions occurs:</P>
     *
     * <UL>
     *   <LI>The point is <CODE>null</CODE>.</LI>
     *   <LI>The point is not in the rectangle <CODE>getBounds2D</CODE>.</LI>
     *   <LI>The method <CODE>isVisible</CODE> returns <CODE>false</CODE>.</LI>
     * </UL>
     *
     * @param  p a specified <CODE>Point2D</CODE>
     * @return whether or not a specified point is inside the paintable
     */
    public final boolean contains(Point2D p) {
        if (p == null)
            return false;
        
        return contains(p.getX(), p.getY());
    }
    
    
    /**
     * <P>Tests if a point specified by coordinates is possibly inside the
     * paintable.</P>
     *
     * <P>This method returns <CODE>true</CODE> if and only if:</P>
     *
     * <UL>
     *   <LI>The point is in the rectangle <CODE>getBounds2D</CODE>.</LI>
     *   <LI>The method <CODE>isVisible</CODE> returns <CODE>true</CODE>.</LI>
     * </UL>
     *
     * <P>This is a helper method for implementing <CODE>contains</CODE>.</P>
     *
     * @param  x the x-coordinate of the point
     * @param  y the y-coordinate of the point
     * @return whether or not a specified point is possibly inside the paintable
     */
    protected final boolean possiblyContains(double x, double y) {
        return isVisible() && getBounds2D().contains(x, y);
    }
    
    
    /**
     * <P>Sets the visibility property of this paintable.</P>
     *
     * <P>The default for the visibility property should be <CODE>true</CODE></P>.
     *
     * <P>Fires property change: SET_VISIBLE.</P>
     * 
     * @param visible the visibility setting
     * @see #isVisible()
     * @see #setOpacity(float)
     * @see #getOpacity()
     */
    public final void setVisible(boolean visible) {
        if (this.visible != visible) {
            this.visible = visible;
            firePropertyChange(SET_VISIBLE, null, null);
        }
    }
    
    
    /**
     * Returns the current visibility property of this paintable.
     *
     * @see #setVisible(boolean)
     * @see #setOpacity(float)
     * @see #getOpacity()
     */
    public final boolean isVisible() {
        return visible;
    }
    
    
    /**
     * <P>Sets the opacity of this paintable to a value between 0 and 1.</P>
     *
     * <P>Note that an opacity of 0 will make the paintable invisible.  This
     * is not recommended.  Instead use <CODE>setVisible(false)</CODE>.</P>
     *
     * <P>Fires property change: SET_OPACITY.</P>
     * 
     * @param opacity the opacity of this paintable
     * @see #getOpacity()
     * @see #setVisible(boolean)
     * @see #isVisible()
     */
    public final void setOpacity(float opacity) {
        if (opacity < 0)
            opacity = 0;
        
        if (opacity > 1)
            opacity = 1;
        
        if (this.opacity != opacity) {
            this.opacity = opacity;
            firePropertyChange(SET_OPACITY, null, null);
        }
    }
    
    
    /**
     * Returns the opacity value of this paintable between 0 and 1.
     *
     * @return the opacity of this paintable
     * @see #setOpacity(float)
     * @see #setVisible(boolean)
     * @see #isVisible()
     */
    public final float getOpacity() {
        return opacity;
    }
    
    
    /**
     * <P>Applies the current opacity to calculate and set a net opacity.</P>
     *
     * <P>The following conditions must hold:</P>
     *
     * <UL>
     *   <LI>The current opacity of this paintable is less than 1;</LI>
     *   <LI>The given graphics context either has no <CODE>Composite</CODE> set
     *       or has a <CODE>Composite</CODE> of type <CODE>AlphaComposite</CODE>
     *       whose rule is <CODE>SRC_OVER</CODE>.
     * </UL>
     *
     * <P>In the case that the graphics context has a <CODE>Composite</CODE> set
     * of type <CODE>AlphaComposite</CODE> with rule <CODE>SRC_OVER</CODE>, then
     * the net opacity is the product of the current opacity of this object and
     * the alpha value of the <CODE>AlphaComposite</CODE>.</P>
     *
     * <P>Otherwise the net opacity equals the current opacity of this object.</P>
     *
     * @param h the graphics context whose opacity will be changed
     */
    public final void applyOpacity(Graphics2D h) {
        // return if there is no need to change opacity
        if (opacity >= 1.0f)
            return;
        
        // check the current composite for h2 and compute net opacity
        Composite composite = h.getComposite();
        float netOpacity = opacity;
        
        if (composite instanceof AlphaComposite) {
            AlphaComposite alphaComposite = (AlphaComposite) composite;
            
            // return if caller has set a compositing rule != SRC_OVER
            if (alphaComposite.getRule() != AlphaComposite.SRC_OVER)
                return;
            
            // set the net opacity via the product rule
            netOpacity *= alphaComposite.getAlpha();
        }
        else {
            if (composite != null)
                // return if caller has set a special Composite class
                return;
        }
        
        // set a new composite rule
        h.setComposite
            (AlphaComposite.getInstance(AlphaComposite.SRC_OVER, netOpacity));
    }
    
    
    /**
     * <P>Returns a copy of the given graphics context after modifying the copy
     * to set anti-aliasing on, to clip to within the bounds region, and to
     * apply the opacity of this paintable if needed.</P>
     *
     * <P>For convenience, the graphics context is returned as a
     * <CODE>Graphics2D</CODE> object.</P>
     *
     * @param  g the given graphics context to copy
     * @return a suitably prepared copy of the given graphics context
     */
    public final Graphics2D getPreparedGraphics2D(Graphics g) {
        Graphics2D h = (Graphics2D) g.create();
        
        h.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        
        h.clip(getBounds2D());
        
        applyOpacity(h);
        
        return h;
    }
    
    
    /** Property Change. */
    
    /**
     * <P>Add a <CODE>PropertyChangeListener</CODE> to the listener list.
     * The listener is registered for all properties.</P>
     *
     * @param listener the PropertyChangeListener to be added
     */
    public final void addPropertyChangeListener(
        PropertyChangeListener listener) 
    {
        changeAdapter.addPropertyChangeListener(listener);
    }

    /**
     * <P>Add a <CODE>PropertyChangeListener</CODE> to the listener list for a
     * specific property.  The listener will be invoked only when a call on
     * <CODE>firePropertyChange</CODE> names that specific property.</P>
     *
     * @param propertyName the name of the property to listen on 
     * @param listener the PropertyChangeListener to be added
     */
    public final void addPropertyChangeListener(
        String propertyName,
        PropertyChangeListener listener) 
    {
        changeAdapter.addPropertyChangeListener(propertyName, listener);
    }

    /**
     * <P>Add all items in the given <CODE>PropertyChangeListener</CODE> array
     * to the listener list.  These items are registered for all properties.</P>
     *
     * @param listeners the PropertyChangeListener array to be added
     */
    public final void addPropertyChangeListeners(PropertyChangeListener[] listeners)
    {
        if (listeners == null)
            return;
        
        int length = listeners.length;
        
        for (int i = 0; i < length; i++)
            addPropertyChangeListener(listeners[i]);
    }
    
    
    /**
     * <P>Add all items in the given <CODE>PropertyChangeListener</CODE> array
     * to the listener list for a specific property.  These items will be invoked
     * only when a call on <CODE>firePropertyChange</CODE> names that specific
     * property.</P>
     *
     * @param listeners the PropertyChangeListener array to be added
     */
    public final void addPropertyChangeListeners(
        String propertyName,
        PropertyChangeListener[] listeners)
    {
        if (listeners == null)
            return;
        
        int length = listeners.length;
        
        for (int i = 0; i < length; i++)
            addPropertyChangeListener(propertyName, listeners[i]);
    }
    
    
    /**
     * <P>Remove a <CODE>PropertyChangeListener</CODE> from the listener list.
     * This removes a <CODE>PropertyChangeListener</CODE> that was registered
     * for all properties.</P>
     *
     * @param listener the PropertyChangeListener to be removed
     */
    public final void removePropertyChangeListener(
        PropertyChangeListener listener) 
    {
        changeAdapter.removePropertyChangeListener(listener);
    }

    /**
     * <P>Remove a <CODE>PropertyChangeListener</CODE> for a specific property.</P>
     *
     * @param propertyName the name of the property that was listened on 
     * @param listener the PropertyChangeListener to be removed
     */
    public final void removePropertyChangeListener(
        String propertyName,
        PropertyChangeListener listener) 
    {
        changeAdapter.removePropertyChangeListener(propertyName, listener);
    }
    
    
    /**
     * <P>Returns an array of all listeners that were added to this object.</P>
     *
     * @return an array with all listeners
     */
    public final PropertyChangeListener[] getPropertyChangeListeners() {
        return changeAdapter.getPropertyChangeListeners();
    }
    
    
    /**
     * <P>Returns an array of all listeners that were added to this object
     * and associated with the named property.</P>
     *
     * @param propertyName the name of the property to seek 
     */
    public final PropertyChangeListener[]
        getPropertyChangeListeners(String propertyName)
    {
        return changeAdapter.getPropertyChangeListeners(propertyName);
    }
    
    
    /**
     * <P>Check if there are any listeners for a specific property.</P>
     *
     * @param propertyName the name of the property to check 
     */
    public final boolean hasListeners(String propertyName) {
        return changeAdapter.hasListeners(propertyName);
    }
    
    
    /**
     * <P>Report a bound property update to any registered listeners.
     * No event is fired if the old and new values are equal and non-null.</P>
     *
     * @param propertyName the programmatic name of the property that was changed
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    public final void firePropertyChange(
        String propertyName,
        Object oldValue,
        Object newValue)
    {
        changeAdapter.firePropertyChange(propertyName, oldValue, newValue);
    }
    
    
    /**
     * <P>Report a bound property update to any registered listeners.
     * No event is fired if the old and new values are equal.</P>
     *
     * @param propertyName the programmatic name of the property that was changed
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    public final void firePropertyChange(
        String propertyName,
        boolean oldValue,
        boolean newValue)
    {
        if (newValue != oldValue)
            changeAdapter.firePropertyChange(propertyName, oldValue, newValue);
    }
    
    
    /**
     * <P>Report a bound property update to any registered listeners.
     * No event is fired if the old and new values are equal.</P>
     *
     * @param propertyName the programmatic name of the property that was changed
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    public final void firePropertyChange(
        String propertyName,
        char oldValue,
        char newValue)
    {
        if (newValue != oldValue)
            changeAdapter.firePropertyChange
                (propertyName, new Character(oldValue), new Character(newValue));
    }
    
    
    /**
     * <P>Report a bound property update to any registered listeners.
     * No event is fired if the old and new values are equal.</P>
     *
     * @param propertyName the programmatic name of the property that was changed
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    public final void firePropertyChange(
        String propertyName,
        byte oldValue,
        byte newValue)
    {
        if (newValue != oldValue)
            changeAdapter.firePropertyChange
                (propertyName, new Byte(oldValue), new Byte(newValue));
    }
    
    
    /**
     * <P>Report a bound property update to any registered listeners.
     * No event is fired if the old and new values are equal.</P>
     *
     * @param propertyName the programmatic name of the property that was changed
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    public final void firePropertyChange(
        String propertyName,
        short oldValue,
        short newValue)
    {
        if (newValue != oldValue)
            changeAdapter.firePropertyChange
                (propertyName, new Short(oldValue), new Short(newValue));
    }
    
    
    /**
     * <P>Report a bound property update to any registered listeners.
     * No event is fired if the old and new values are equal.</P>
     *
     * @param propertyName the programmatic name of the property that was changed
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    public final void firePropertyChange(
        String propertyName,
        int oldValue,
        int newValue)
    {
        if (newValue != oldValue)
            changeAdapter.firePropertyChange(propertyName, oldValue, newValue);
    }
    
    
    /**
     * <P>Report a bound property update to any registered listeners.
     * No event is fired if the old and new values are equal.</P>
     *
     * @param propertyName the programmatic name of the property that was changed
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    public final void firePropertyChange(
        String propertyName,
        long oldValue,
        long newValue)
    {
        if (newValue != oldValue)
            changeAdapter.firePropertyChange
                (propertyName, new Long(oldValue), new Long(newValue));
    }
    
    
    /**
     * <P>Report a bound property update to any registered listeners.
     * No event is fired if the old and new values are equal.</P>
     *
     * @param propertyName the programmatic name of the property that was changed
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    public final void firePropertyChange(
        String propertyName,
        float oldValue,
        float newValue)
    {
        if (newValue != oldValue)
            changeAdapter.firePropertyChange
                (propertyName, new Float(oldValue), new Float(newValue));
    }
    
    
    /**
     * <P>Report a bound property update to any registered listeners.
     * No event is fired if the old and new values are equal.</P>
     *
     * @param propertyName the programmatic name of the property that was changed
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    public final void firePropertyChange(
        String propertyName,
        double oldValue,
        double newValue)
    {
        if (newValue != oldValue)
            changeAdapter.firePropertyChange
                (propertyName, new Double(oldValue), new Double(newValue));
    }
    
    
    /**
     * <P>Fire an existing <CODE>PropertyChangeEvent</CODE> to any registered
     * listeners.  No event is fired if the given event's old and new values
     * are equal and non-null.</P>
     *
     * @param evt the PropertyChangeEvent object
     */
    public final void firePropertyChange(PropertyChangeEvent evt) {
        changeAdapter.firePropertyChange(evt);
    }
    
    
    /**
     * <P>Returns the <CODE>PropertyChangeForwardingListener</CODE> that
     * will forward the property change events it receives to this object.</P>
     *
     * @return the forwarding listener
     */
    public final PropertyChangeForwardingListener getForwardingListener() {
        return forwardingListener;
    }
    

    /**
     * Add the forwarding listener as a property change listener
     * for the given object if the object supports property change.
     *
     * @param object the object that should add the forwarding listener
     */
    public final void addForwardingListener(Object object) {
        if (object instanceof SupportsPropertyChange) {
            SupportsPropertyChange spc = (SupportsPropertyChange) object;
            spc.addPropertyChangeListener(getForwardingListener());
        }
    }
    
    
    /**
     * Remove the forwarding listener as a property change listener
     * for the given object if the object supports property change.
     *
     * @param object the object that should remove the forwarding listener
     */
    public final void removeForwardingListener(Object object) {
        if (object instanceof SupportsPropertyChange) {
            SupportsPropertyChange spc = (SupportsPropertyChange) object;
            spc.removePropertyChangeListener(getForwardingListener());
        }
    }
    
    
    /**
     * Remove the forwarding listener as a property change listener
     * for the old object if the old object supports property change
     * and add the forwarding listener as a property change listener
     * for the new object if the new object supports property change.
     *
     * @param oldobject the old object that should remove the forwarding listener
     * @param newobject the new object that should add the forwarding listener
     */
    public final void removeAndAddForwardingListener(Object oldobject, Object newobject)
    {
        removeForwardingListener(oldobject);
        addForwardingListener(newobject);
    }    
    
}
