/*
 * $Id: AbstractHighlighter.java,v 1.10 2008/10/14 22:31:38 rah003 Exp $
 *
 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.decorator;

import java.awt.Component;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.jdesktop.swingx.event.WeakEventListenerList;

/**
 * Abstract <code>Highlighter</code> implementation which manages change 
 * notification and supports conditional highlighting. 
 * Subclasses are required to fire ChangeEvents on internal changes which might
 * effect the highlight. The HighlightPredicate controls whether or not
 * a highlight should be applied for the given ComponentAdapter, 
 * subclasses must guarantee to respect its decision. 
 * <p>
 * 
 * Concrete custom implementations should focus on a single (or few) visual
 * attribute to highlight. This allows easy re-use by composition.  F.i. a custom
 * FontHighlighter:
 * 
 * <pre><code>
 * public static class FontHighlighter extends AbstractHighlighter {
 * 
 *     private Font font;
 * 
 *     public FontHighlighter(HighlightPredicate predicate, Font font) {
 *         super(predicate);
 *         setFont(font);
 *     }
 * 
 *     &#64;Override
 *     protected Component doHighlight(Component component,
 *             ComponentAdapter adapter) {
 *         component.setFont(font);
 *         return component;
 *     }
 *     
 *     public final void setFont(Font font) {
 *        if (equals(font, this.font)) return;
 *        this.font = font;
 *        fireStateChanged();
 *     }
 *     
 * 
 * }
 * 
 * </code></pre>
 * 
 * Client code can combine the effect with a f.i. Color decoration, and use a
 * shared HighlightPredicate to apply both for the same condition.
 * 
 * <pre><code>
 * HighlightPredicate predicate = new HighlightPredicate() {
 *     public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
 *         Object value = adapter.getFilteredValueAt(adapter.row, adapter.column);
 *         return (value instanceof Number) &amp;&amp; ((Number) value).intValue() &lt; 0;
 *     }
 * };
 * table.setHighlighters(
 *         new ColorHighlighter(predicate, Color.RED, null),
 *         new FontHighlighter(predicate, myBoldFont));
 * </code></pre>
 * 
 * @author Jeanette Winzenburg
 * 
 * @see HighlightPredicate
 * @see org.jdesktop.swingx.renderer.ComponentProvider
 */
public abstract class AbstractHighlighter implements Highlighter {

    /**
     * Only one <code>ChangeEvent</code> is needed per model instance since the
     * event's only (read-only) state is the source property.  The source
     * of events generated here is always "this".
     */
    private transient ChangeEvent changeEvent;

    /** The listeners waiting for model changes. */
    protected WeakEventListenerList listenerList = new WeakEventListenerList();

    /** the HighlightPredicate to use. */
    private HighlightPredicate predicate;

    /**
     * Instantiates a Highlighter with default HighlightPredicate.
     *
     * @see #setHighlightPredicate(HighlightPredicate)
     */
    public AbstractHighlighter() {
        this(null);
    }

    /**
     * Instantiates a Highlighter with the given 
     * HighlightPredicate.<p>
     * 
     * @param predicate the HighlightPredicate to use.
     * 
     * @see #setHighlightPredicate(HighlightPredicate)
     */
    public AbstractHighlighter(HighlightPredicate predicate) {
        setHighlightPredicate(predicate);
    }

    /**
     * Set the HighlightPredicate used to decide whether a cell should
     * be highlighted. If null, sets the predicate to HighlightPredicate.ALWAYS.
     * 
     * The default value is HighlightPredicate.ALWAYS. 
     * 
     * @param predicate the HighlightPredicate to use. 
     */
    public void setHighlightPredicate(HighlightPredicate predicate) {
        if (predicate == null) {
            predicate = HighlightPredicate.ALWAYS;
        }
        if (areEqual(predicate, getHighlightPredicate())) return;
        this.predicate = predicate;
        fireStateChanged();
    }

    /**
     * Returns the HighlightPredicate used to decide whether a cell 
     * should be highlighted. Guaranteed to be never null.
     * 
     * @return the HighlightPredicate to use, never null.
     */
    public HighlightPredicate getHighlightPredicate() {
        return predicate;
    }

    //----------------------- implement predicate respecting highlight

    /**
     * {@inheritDoc}
     * 
     * This calls doHighlight to apply the decoration if both HighlightPredicate
     * isHighlighted and canHighlight return true. Returns the undecorated component otherwise.
     * 
     * @param component the cell renderer component that is to be decorated
     * @param adapter the ComponentAdapter for this decorate operation
     * 
     * @see #canHighlight(Component, ComponentAdapter)
     * @see #doHighlight(Component, ComponentAdapter)
     * @see #getHighlightPredicate()
     */
    public Component highlight(Component component, ComponentAdapter adapter) {
        if (canHighlight(component, adapter) && 
                getHighlightPredicate().isHighlighted(component, adapter)) {
            component = doHighlight(component, adapter);
        }
        return component;
    }

    /**
     * Subclasses may override to further limit the highlighting based
     * on Highlighter state, f.i. a PainterHighlighter can only be applied
     * to PainterAware components. <p>
     * 
     * This implementation returns true always.
     * 
     * @param component
     * @param adapter
     * @return a boolean indication if the adapter can be highlighted based
     *   general state. This implementation returns true always.
     */
    protected boolean canHighlight(Component component, ComponentAdapter adapter) {
        return true;
    }


    /**
     * Apply the highlights. 
     * 
     * @param component the cell renderer component that is to be decorated
     * @param adapter the ComponentAdapter for this decorate operation
     * 
     * @see #highlight(Component, ComponentAdapter)
     */
    protected abstract Component doHighlight(Component component,
            ComponentAdapter adapter);


    /**
     * Returns true if the to objects are either both null or equal
     * each other.
     * 
     * @param oneItem one item
     * @param anotherItem another item 
     * @return true if both are null or equal other, false otherwise.
     */
    protected boolean areEqual(Object oneItem, Object anotherItem) {
        if ((oneItem == null) && (anotherItem == null)) return true;
        if (anotherItem != null) {
            return anotherItem.equals(oneItem);
        }
        return false;
    }

    //------------------------ implement Highlighter change notification

    /**
     * Adds a <code>ChangeListener</code>. ChangeListeners are
     * notified after changes of any attribute. 
     *
     * @param l the ChangeListener to add
     * @see #removeChangeListener
     */
    public final void addChangeListener(ChangeListener l) {
        listenerList.add(ChangeListener.class, l);
    }

    /**
     * Removes a <code>ChangeListener</code>e. 
     *
     * @param l the <code>ChangeListener</code> to remove
     * @see #addChangeListener
     */
    public final void removeChangeListener(ChangeListener l) {
        listenerList.remove(ChangeListener.class, l);
    }

    /**
     * Returns an array of all the change listeners
     * registered on this <code>Highlighter</code>.
     *
     * @return all of this model's <code>ChangeListener</code>s 
     *         or an empty
     *         array if no change listeners are currently registered
     *
     * @see #addChangeListener
     * @see #removeChangeListener
     *
     * @since 1.4
     */
    public final ChangeListener[] getChangeListeners() {
        return (ChangeListener[]) listenerList
                .getListeners(ChangeListener.class);
    }

    /** 
     * Notifies registered <code>ChangeListener</code>s about
     * state changes.<p>
     * 
     * Note: subclasses should be polite and implement any property
     * setters to fire only if the property is really changed. 
     *  
     */
    protected final void fireStateChanged() {
        Object[] listeners = listenerList.getListenerList();
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == ChangeListener.class) {
                if (changeEvent == null) {
                    changeEvent = new ChangeEvent(this);
                }
                ((ChangeListener) listeners[i + 1]).stateChanged(changeEvent);
            }
        }
    }

}
