/*
 * $Id: TableColumnExt.java 3927 2011-02-22 16:34:11Z kleopatra $
 *
 * Copyright 2004 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.table;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Comparator;
import java.util.Hashtable;

import javax.swing.DefaultCellEditor;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;

import org.jdesktop.swingx.decorator.CompoundHighlighter;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.plaf.UIDependent;
import org.jdesktop.swingx.renderer.AbstractRenderer;

/**
 * <code>TableColumn</code> extension for enhanced view column configuration.
 * The general drift is to strengthen the TableColumn abstraction as <b>the</b>
 * place to configure and dynamically update view column properties, covering a
 * broad range of customization requirements. Using collaborators are expected
 * to listen to property changes and update themselves accordingly.
 * <p>
 * 
 * A functionality enhancement is the notion of column visibility:
 * <code>TableColumnModelExt</code> manages sets of visible/hidden
 * <code>TableColumnExt</code>s controlled by the columns'
 * <code>visible</code> property. Typically, users can toggle column
 * visibility at runtime, f.i. through a dedicated control in the upper trailing
 * corner of a <code>JScrollPane</code>.
 * <p>
 * 
 * A prominent group of properties allows fine-grained, per-column control of
 * corresponding Table/-Header features.
 * 
 * <ul>
 * <li><b>Sorting</b>: <code>sortable</code> controls whether this column
 * should be sortable by user's sort gestures; <code>Comparator</code> can
 * hold a column specific type.
 * 
 * <li><b>Editing</b>: <code>editable</code> controls whether cells of this
 * column should be accessible to in-table editing.
 * 
 * <li><b>Tooltip</b>: <code>toolTipText</code> holds the column tooltip
 * which is shown when hovering over the column's header.
 * 
 * <li><b>Highlighter</b>: <code>highlighters</code> holds the column
 * highlighters; these are applied to the renderer after the table highlighters.
 * Any modification of the list of contained <code>Highlighter</code>s
 * (setting them, adding one or removing one) will result in a
 * {@code PropertyChangeEvent} being fired for "highlighters". State changes on
 * contained <code>Highlighter</code>s will result in a PropertyChangeEvent
 * for "highlighterStateChanged".
 * </ul>
 * 
 * 
 * Analogous to <code>JComponent</code>, this class supports per-instance
 * "client" properties. They are meant as a small-scale extension mechanism.
 * They are similar to regular bean properties in that registered
 * <code>PropertyChangeListener</code>s are notified about changes. TODO:
 * example?
 * <p>
 * 
 * A <code>TableColumnExt</code> implements UIDependent, that is it takes over
 * responsibility to update LAF dependent properties of contained elements when
 * messaged with updateUI. This implementation updates its <code>Highlighter</code>s,
 * Cell-/HeaderRenderer and CellEditor. <p>
 * 
 * TODO: explain prototype (sizing, collaborator-used-by ColumnFactory (?))
 * <p>
 * 
 * @author Ramesh Gupta
 * @author Amy Fowler
 * @author Jeanette Winzenburg
 * @author Karl Schaefer
 * 
 * @see TableColumnModelExt
 * @see ColumnFactory
 * @see org.jdesktop.swingx.plaf.UIDependent
 * @see javax.swing.JComponent#putClientProperty
 */
public class TableColumnExt extends TableColumn implements UIDependent {
    
    /** visible property. Initialized to <code>true</code>.*/
    protected boolean visible = true;
    /** hideable property. Initialized to <code>true</code>.*/
    protected boolean hideable = true;
    
    /** prototype property. */
    protected Object prototypeValue;


    /** per-column comparator  */
    protected Comparator<?> comparator;
    /** per-column sortable property. Initialized to <code>true</code>. */
    protected boolean sortable = true;
    /** per-column editable property. Initialized to <code>true</code>.*/
    protected boolean editable = true;
    /** per-column tool tip text. */
    private String toolTipText;
    
    /** storage for client properties. */
    protected Hashtable<Object, Object> clientProperties;

    /**
     * The compound highlighter for the column.
     */
    protected CompoundHighlighter compoundHighlighter;
    
    private ChangeListener highlighterChangeListener;

    private boolean ignoreHighlighterStateChange;

    
    /**
     * Creates new table view column with a model index = 0.
     */
    public TableColumnExt() {
        this(0);
    }

    /**
     * Creates new table view column with the specified model index.
     * @param modelIndex index of table model column to which this view column
     *        is bound.
     */
    public TableColumnExt(int modelIndex) {
        this(modelIndex, 75);    // default width taken from javax.swing.table.TableColumn
    }

    /**
     * Creates new table view column with the specified model index and column width.
     * @param modelIndex index of table model column to which this view column
     *        is bound.
     * @param width pixel width of view column
     */
    public TableColumnExt(int modelIndex, int width) {
        this(modelIndex, width, null, null);
    }

    /**
     * Creates new table view column with the specified model index, column
     * width, cell renderer and cell editor.
     * @param modelIndex index of table model column to which this view column
     *        is bound.
     * @param width pixel width of view column
     * @param cellRenderer the cell renderer which will render all cells in this
     *        view column
     * @param cellEditor the cell editor which will edit cells in this view column
     */
    public TableColumnExt(int modelIndex, int width,
                          TableCellRenderer cellRenderer, TableCellEditor cellEditor) {
        super(modelIndex, width, cellRenderer, cellEditor);
    }

    /**
     * Instantiates a new table view column with all properties copied from the 
     * given original.
     * 
     * @param columnExt the column to copy properties from
     * @see #copyFrom(TableColumnExt)
     */
    public TableColumnExt(TableColumnExt columnExt) {
        this(columnExt.getModelIndex(), columnExt.getWidth(), columnExt
                .getCellRenderer(), columnExt.getCellEditor());
        copyFrom(columnExt);
    }

    
    /**
     * Sets the <code>Highlighter</code>s to the table, replacing any old settings.
     * None of the given Highlighters must be null.<p>
     * 
     * This is a bound property. <p> 
     * 
     * Note: as of version #1.257 the null constraint is enforced strictly. To remove
     * all highlighters use this method without param.
     * 
     * @param highlighters zero or more not null highlighters to use for renderer decoration.
     * @throws NullPointerException if array is null or array contains null values.
     * 
     * @see #getHighlighters()
     * @see #addHighlighter(Highlighter)
     * @see #removeHighlighter(Highlighter)
     * 
     */
    public void setHighlighters(Highlighter... highlighters) {
        ignoreHighlighterStateChange = true;
        Highlighter[] old = getHighlighters();
        getCompoundHighlighter().setHighlighters(highlighters);
        firePropertyChange("highlighters", old, getHighlighters());
        ignoreHighlighterStateChange = false;
    }

    /**
     * Returns the <code>Highlighter</code>s used by this table.
     * Maybe empty, but guarantees to be never null.
     * 
     * @return the Highlighters used by this table, guaranteed to never null.
     * @see #setHighlighters(Highlighter[])
     */
    public Highlighter[] getHighlighters() {
        return getCompoundHighlighter().getHighlighters();
    }
    /**
     * Appends a <code>Highlighter</code> to the end of the list of used
     * <code>Highlighter</code>s. The argument must not be null. 
     * <p>
     * 
     * @param highlighter the <code>Highlighter</code> to add, must not be null.
     * @throws NullPointerException if <code>Highlighter</code> is null.
     * 
     * @see #removeHighlighter(Highlighter)
     * @see #setHighlighters(Highlighter[])
     */
    public void addHighlighter(Highlighter highlighter) {
        ignoreHighlighterStateChange = true;
        Highlighter[] old = getHighlighters();
        getCompoundHighlighter().addHighlighter(highlighter);
        firePropertyChange("highlighters", old, getHighlighters());
        ignoreHighlighterStateChange = false;
    }

    /**
     * Removes the given Highlighter. <p>
     * 
     * Does nothing if the Highlighter is not contained.
     * 
     * @param highlighter the Highlighter to remove.
     * @see #addHighlighter(Highlighter)
     * @see #setHighlighters(Highlighter...)
     */
    public void removeHighlighter(Highlighter highlighter) {
        ignoreHighlighterStateChange = true;
        Highlighter[] old = getHighlighters();
        getCompoundHighlighter().removeHighlighter(highlighter);
        firePropertyChange("highlighters", old, getHighlighters());
        ignoreHighlighterStateChange = false;
    }
    
    /**
     * Returns the CompoundHighlighter assigned to the table, null if none.
     * PENDING: open up for subclasses again?.
     * 
     * @return the CompoundHighlighter assigned to the table.
     */
    protected CompoundHighlighter getCompoundHighlighter() {
        if (compoundHighlighter == null) {
            compoundHighlighter = new CompoundHighlighter();
            compoundHighlighter.addChangeListener(getHighlighterChangeListener());
        }
        return compoundHighlighter;
    }

    /**
     * Returns the <code>ChangeListener</code> to use with highlighters. Lazily 
     * creates the listener.
     * 
     * @return the ChangeListener for observing changes of highlighters, 
     *   guaranteed to be <code>not-null</code>
     */
    protected ChangeListener getHighlighterChangeListener() {
        if (highlighterChangeListener == null) {
            highlighterChangeListener = createHighlighterChangeListener();
        }
        return highlighterChangeListener;
    }

    /**
     * Creates and returns the ChangeListener observing Highlighters.
     * <p>
     * Here: repaints the table on receiving a stateChanged.
     * 
     * @return the ChangeListener defining the reaction to changes of
     *         highlighters.
     */
    protected ChangeListener createHighlighterChangeListener() {
        return new ChangeListener() {
            @Override
            public void stateChanged(ChangeEvent e) {
                if (ignoreHighlighterStateChange) return;
                firePropertyChange("highlighterStateChanged", false, true);
            }
        };
    }

    /** 
     * Returns true if the user <i>can</i> resize the TableColumn's width, 
     * false otherwise. This is a usability override: it takes into account
     * the case where it's principally <i>allowed</i> to resize the column
     * but not possible because the column has fixed size.
     * 
     * @return a boolean indicating whether the user can resize this column.
     */
    @Override
    public boolean getResizable() {
        // TODO JW: resizable is a bound property, so to be strict
        // we'll need to override setMin/MaxWidth to fire resizable
        // property change.
        return super.getResizable() && (getMinWidth() < getMaxWidth());
    }

    /**
     * Sets the editable property. This property allows to mark all cells in a
     * column as read-only, independent of the per-cell editability as returned
     * by the <code>TableModel.isCellEditable</code>. If the cell is
     * read-only in the model layer, this property will have no effect.
     * 
     * @param editable boolean indicating whether or not the user may edit cell
     *        values in this view column
     * @see #isEditable
     * @see org.jdesktop.swingx.JXTable#isCellEditable(int, int)
     * @see javax.swing.table.TableModel#isCellEditable
     */
    public void setEditable(boolean editable) {
        boolean oldEditable = this.editable;
        this.editable = editable;
        firePropertyChange("editable",
                           Boolean.valueOf(oldEditable),
                           Boolean.valueOf(editable));
    }

    /**
     * Returns the per-column editable property.
     * The default is <code>true</code>.
     * 
     * @return boolean indicating whether or not the user may edit cell
     *        values in this view column
     * @see #setEditable
     */
    public boolean isEditable() {
        return editable;
    }

    /**
     * Sets the prototypeValue property.  The value should be of a type
     * which corresponds to the column's class as defined by the table model.
     * If non-null, the JXTable instance will use this property to calculate
     * and set the initial preferredWidth of the column.  Note that this
     * initial preferredWidth will be overridden if the user resizes columns
     * directly.
     * 
     * @param value Object containing the value of the prototype to be used
     *         to calculate the initial preferred width of the column
     * @see #getPrototypeValue
     * @see org.jdesktop.swingx.JXTable#getPreferredScrollableViewportSize
     */
    public void setPrototypeValue(Object value) {
        Object oldPrototypeValue = this.prototypeValue;
        this.prototypeValue = value;
        firePropertyChange("prototypeValue",
                           oldPrototypeValue,
                           value);

    }

    /**
     * Returns the prototypeValue property.
     * The default is <code>null</code>.
     * 
     * @return Object containing the value of the prototype to be used
     *         to calculate the initial preferred width of the column
     * @see #setPrototypeValue
     */
    public Object getPrototypeValue() {
        return prototypeValue;
    }


    /**
     * Sets the comparator to use for this column.
     * <code>JXTable</code> sorting api respects this property by passing it on
     * to the <code>SortController</code>. 
     * 
     * @param comparator a custom comparator to use in interactive
     *    sorting.
     * @see #getComparator
     * @see org.jdesktop.swingx.sort.SortController
     * @see org.jdesktop.swingx.decorator.SortKey
     */
    public void setComparator(Comparator<?> comparator) {
        Comparator<?> old = getComparator();
        this.comparator = comparator;
        firePropertyChange("comparator", old, getComparator());
    }
    
    /**
     * Returns the comparator to use for the column. 
     * The default is <code>null</code>.
     * 
     * @return <code>Comparator</code> to use for this column
     * @see #setComparator
     */
    public Comparator<?> getComparator() {
        return comparator;
    }

    /**
     * Sets the sortable property. <code>JXTable</code> sorting api respects this
     * property by disabling interactive sorting on this column if false. 
     * 
     * @param sortable boolean indicating whether or not this column can
     *        be sorted in the table
     * @see #isSortable 
     */
    public void setSortable(boolean sortable) {
        boolean old = isSortable();
        this.sortable = sortable;
        firePropertyChange("sortable", old, isSortable());
    }
 
    /**
     * Returns the sortable property.
     * The default value is <code>true</code>.
     * 
     * @return boolean indicating whether this view column is sortable
     * @see #setSortable
     */
    public boolean isSortable() {
        return sortable;
    }
    
    /**
     * Registers the text to display in the column's tool tip. 
     * Typically, this is used by <code>JXTableHeader</code> to
     * display when the mouse cursor lingers over the column's
     * header cell.
     * 
     * @param toolTipText text to show.
     * @see #setToolTipText(String)
     */
    public void setToolTipText(String toolTipText) {
        String old = getToolTipText();
        this.toolTipText = toolTipText;
        firePropertyChange("toolTipText", old, getToolTipText());
    }
    
    /**
     * Returns the text of to display in the column's tool tip. 
     * The default is <code>null</code>. 
     * 
     * @return the text of the column ToolTip.
     * @see #setToolTipText(String)
     */
    public String getToolTipText() {
        return toolTipText;
    }
    
    
    /**
     * Sets the title of this view column.  This is a convenience
     * wrapper for <code>setHeaderValue</code>.
     * @param title String containing the title of this view column
     */
    public void setTitle(String title) {
        setHeaderValue(title);                // simple wrapper
    }

    /**
     * Convenience method which returns the headerValue property after
     * converting it to a string. 
     * @return String containing the title of this view column or null if
     *   no headerValue is set.
     */
    public String getTitle() {
        Object header = getHeaderValue();
        return header != null ? header.toString() : null; // simple wrapper
    }

    /**
     * Sets the visible property.  This property controls whether or not
     * this view column is currently visible in the table.
     * 
     * @param visible boolean indicating whether or not this view column is
     *        visible in the table
     * @see #setVisible
     */
    public void setVisible(boolean visible) {
        boolean oldVisible = isVisible();
        this.visible = visible;
        firePropertyChange("visible", oldVisible, isVisible());
    }

    /**
     * Returns a boolean indicating whether or not this column is visible. 
     * The bare property value is constrained by this column's hideable setting,
     * that is a not hideable column is always visible, irrespective of the
     * property setting. 
     * <p>
     * The default is <code>true</code>.
     * 
     * @return boolean indicating whether or not this view column is
     *        visible in the table
     * @see #setVisible
     */
    public boolean isVisible() {
        if (!isHideable()) return true;
        return visible;
    }

    /**
     * Sets the hideable property. This property controls whether the column can
     * be hidden. This is a bound property. If the column's visibilty is affected, 
     * listeners are notified about that change as well..
     * <p>
     * 
     * The default value is true.
     * 
     * @param hideable
     */
    public void setHideable(boolean hideable) {
        boolean old = isHideable();
        boolean oldVisible = isVisible();
        this.hideable = hideable;
        firePropertyChange("visible", oldVisible, isVisible());
        firePropertyChange("hideable", old, isHideable());
    }
    
    /**
     * Returns the hideable property.
     * 
     * @return the hideable property.
     * 
     * @see #setHideable(boolean)
     */
    public boolean isHideable() {
        return hideable;
    }
    
    /**
     * Sets the client property "key" to <code>value</code>. 
     * If <code>value</code> is <code>null</code> this method will remove the property. 
     * Changes to
     * client properties are reported with <code>PropertyChange</code> events.
     * The name of the property (for the sake of PropertyChange events) is
     * <code>key.toString()</code>.
     * <p>
     * The <code>get/putClientProperty</code> methods provide access to a
     * per-instance hashtable, which is intended for small scale extensions of
     * TableColumn.
     * <p>
     * 
     * @param key Object which is used as key to retrieve value
     * @param value Object containing value of client property
     * @throws IllegalArgumentException if key is <code>null</code>
     * @see #getClientProperty
     * @see javax.swing.JComponent#putClientProperty
     */
    public void putClientProperty(Object key, Object value) {
        if (key == null)
            throw new IllegalArgumentException("null key");

        if ((value == null) && (getClientProperty(key) == null)) {
            return;
        }

        Object old = getClientProperty(key);
        if (value == null) {
            getClientProperties().remove(key);
        }
        else {
            getClientProperties().put(key, value);
        }

        firePropertyChange(key.toString(), old, value);
        /* Make all fireXXX methods in TableColumn protected instead of private */
    }

    /**
     * Returns the value of the property with the specified key. Only properties
     * added with <code>putClientProperty</code> will return a non-<code>null</code>
     * value.
     * 
     * @param key Object which is used as key to retrieve value
     * @return Object containing value of client property or <code>null</code>
     * 
     * @see #putClientProperty
     */
    public Object getClientProperty(Object key) {
        return ((key == null) || (clientProperties == null)) ?
                null : clientProperties.get(key);
    }

    private Hashtable<Object, Object> getClientProperties() {
        if (clientProperties == null) {
            clientProperties = new Hashtable<Object, Object>();
        }
        return clientProperties;
    }


     /**
      * Copies properties from original. Handles all properties except
      * modelIndex, width, cellRenderer, cellEditor. Called from copy 
      * constructor.
      *  
      * @param original the tableColumn to copy from
      * 
      * @see #TableColumnExt(TableColumnExt)
      */
     protected void copyFrom(TableColumnExt original) {
             setEditable(original.isEditable());
             setHeaderValue(original.getHeaderValue());    // no need to copy setTitle();
             setToolTipText(original.getToolTipText());
             setIdentifier(original.getIdentifier());
             setMaxWidth(original.getMaxWidth());
             setMinWidth(original.getMinWidth());
             setPreferredWidth(original.getPreferredWidth());
             setPrototypeValue(original.getPrototypeValue());
             // JW: isResizable is overridden to return a calculated property!
             setResizable(original.isResizable);
             setVisible(original.isVisible());
             setSortable(original.isSortable());
             setComparator(original.getComparator());
             copyClientPropertiesFrom(original);
             
             if (original.compoundHighlighter != null) {
                 setHighlighters(original.getHighlighters());
             }
         
     }
     
     /**
      * Copies all clientProperties of this <code>TableColumnExt</code>
      * to the target column.
      * 
      * @param original the target column.
      */
     protected void copyClientPropertiesFrom(TableColumnExt original) {
        if (original.clientProperties == null) return;
        for(Object key: original.clientProperties.keySet()) {
            putClientProperty(key, original.getClientProperty(key));
        }
    }


    /**
     * Notifies registered <code>PropertyChangeListener</code>s 
     * about property changes. This method must be invoked internally
     * whe any of the enhanced properties changed.
     * <p>
     * Implementation note: needed to replicate super 
     * functionality because super's field <code>propertyChangeSupport</code> 
     * and method <code>fireXX</code> are both private.
     * 
     * @param propertyName  name of changed property
     * @param oldValue old value of changed property
     * @param newValue new value of changed property
     */ 
    protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
        if ((oldValue != null && !oldValue.equals(newValue)) ||
              oldValue == null && newValue != null) {
             PropertyChangeListener pcl[] = getPropertyChangeListeners();
             if (pcl != null && pcl.length != 0) {
                 PropertyChangeEvent pce = new PropertyChangeEvent(this,
                     propertyName,
                     oldValue, newValue);

                 for (int i = 0; i < pcl.length; i++) {
                     pcl[i].propertyChange(pce);
                 }
             }
         }
     }

//---------------- implement UIDependent
    
    /**
     * Update ui of owned ui-dependent parts. This implementation
     * updates the contained highlighters.
     * 
     */
    @Override
    public void updateUI() {
        updateHighlighterUI();
        updateRendererUI(getCellRenderer());
        updateRendererUI(getHeaderRenderer());
        updateEditorUI(getCellEditor());
    }

    /**
     * @param editor 
     * 
     */
    private void updateEditorUI(TableCellEditor editor) {
        if (editor == null) return;
        // internal knowledge of core table - already updated
        if ((editor instanceof JComponent)
                || (editor instanceof DefaultCellEditor))
            return;
        try {
            Component comp = editor
                    .getTableCellEditorComponent(null, null, false, -1, -1);
            if (comp != null) {
                SwingUtilities.updateComponentTreeUI(comp);
            }
        } catch (Exception e) {
            // can't do anything - renderer can't cope with off-range cells
        }
    }

    /**
     * @param tableCellRenderer 
     * 
     */
    private void updateRendererUI(TableCellRenderer renderer) {
        if (renderer == null) return;
        // internal knowledge of core table - already updated
        if (renderer instanceof JComponent) {
            return;
        }
        Component comp = null;
        if (renderer instanceof AbstractRenderer) {
            comp = ((AbstractRenderer) renderer).getComponentProvider().getRendererComponent(null);
        } else {
            try {
                comp = renderer
                .getTableCellRendererComponent(null, null, false, false,
                        -1, -1);
              
            } catch (Exception e) {
                // can't do anything - renderer can't cope with off-range cells
            }
        }
        if (comp != null) {
            SwingUtilities.updateComponentTreeUI(comp);
        }
    }

    /**
     * 
     */
    private void updateHighlighterUI() {
        if (compoundHighlighter == null) return;
        compoundHighlighter.updateUI();
    }
}
