/*
 * $Id: JXTableHeader.java 3960 2011-03-15 19:36:53Z kschaefe $
 *
 * 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;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Serializable;
import java.util.logging.Logger;

import javax.swing.JTable;
import javax.swing.SortOrder;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputListener;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import org.jdesktop.swingx.event.TableColumnModelExtListener;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.plaf.TableHeaderAddon;
import org.jdesktop.swingx.sort.SortController;
import org.jdesktop.swingx.table.TableColumnExt;

/**
 * TableHeader with extended functionality if associated Table is of
 * type JXTable.<p>
 * 
 * <h2> Extended user interaction </h2>
 * 
 * <ul>
 * <li> Supports column pack (== auto-resize to exactly fit the contents)
 *  on double-click in resize region.
 *  <li> Configurable to resort a column on the second click of a mouseClicked event
 *  (feature request #271-swingx)
 * <li> Does its best to not sort if the mouse click happens in the resize region.
 *  <li> Supports horizontal auto-scroll if a column is dragged outside visible rectangle. 
 *  This feature is enabled if the autoscrolls property is true. The default is false 
 *  (because of Issue #788-swingx which still isn't fixed for jdk1.6).
 * </ul>
 * 
 * Note: extended sort and resize related functionality is fully effective only if the header's
 * table is of type JXTable and has control over the row sorter, that is the row sorter
 * is of type SortController.
 * 
 * <h2> Extended functionality </h2>
 * 
 * <ul>
 * <li> Listens to TableColumn propertyChanges to update itself accordingly.
 * <li> Supports per-column header ToolTips. 
 * <li> Guarantees reasonable minimal height > 0 for header preferred height.
* </ul>
 * 
 * 
 * @author Jeanette Winzenburg
 * 
 * @see JXTable#toggleSortOrder(int)
 * @see JXTable#resetSortOrder()
 * @see SortGestureRecognizer
 */
public class JXTableHeader extends JTableHeader 
    implements TableColumnModelExtListener {

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger.getLogger(JXTableHeader.class
            .getName());
    
    static {
        LookAndFeelAddons.contribute(new TableHeaderAddon());
    }

    private transient PropertyChangeListener tablePropertyChangeListener;
    private boolean resortsOnDoubleClick;

    /**
     *  Constructs a <code>JTableHeader</code> with a default 
     *  <code>TableColumnModel</code>.
     *
     * @see #createDefaultColumnModel
     */
    public JXTableHeader() {
        super();
    }

    /**
     * Constructs a <code>JTableHeader</code> which is initialized with
     * <code>cm</code> as the column model. If <code>cm</code> is
     * <code>null</code> this method will initialize the table header with a
     * default <code>TableColumnModel</code>.
     * 
     * @param columnModel the column model for the table
     * @see #createDefaultColumnModel
     */
    public JXTableHeader(TableColumnModel columnModel) {
        super(columnModel);
    }


    /**
     * {@inheritDoc} <p>
     * Sets the associated JTable. Enables enhanced header
     * features if table is of type JXTable.<p>
     * 
     * PENDING: who is responsible for synching the columnModel?
     */
    @Override
    public void setTable(JTable table) {
        uninstallTable();
        super.setTable(table);
        installTable();
//        setColumnModel(table.getColumnModel());
        // the additional listening option makes sense only if the table
        // actually is a JXTable
        if (getXTable() != null) {
            installHeaderListener();
        } else {
            uninstallHeaderListener();
        }
    }

    /**
     * Installs the table. <p>
     * This implemenation synchs enabled state and installs the PropertyChangeListener. 
     */
    protected void installTable() {
        updateEnabledFromTable();
        if (getTable() == null) return;
        getTable().addPropertyChangeListener(getTablePropertyChangeListener());
    }
    
    /**
     * Synchs the header's enabled with the table's enabled property.
     */
    protected void updateEnabledFromTable() {
        setEnabled(getTable() != null ? getTable().isEnabled() : true);
    }

    /**
     * Uninstalls the table. <p>
     * This implementation uninstalls the PropertyChangeListener.
     */
    protected void uninstallTable() {
        if (getTable() == null) return;
        getTable().removePropertyChangeListener(getTablePropertyChangeListener());
    }


    /**
     * Implements TableColumnModelExt to allow internal update after
     * column property changes.<p>
     * 
     * This implementation triggers a resizeAndRepaint on every propertyChange which
     * doesn't already fire a "normal" columnModelEvent.
     * 
     * @param event change notification from a contained TableColumn.
     * @see #isColumnEvent(PropertyChangeEvent)
     * @see TableColumnModelExtListener
     * 
     * 
     */
    @Override
    public void columnPropertyChange(PropertyChangeEvent event) {
       if (isColumnEvent(event)) return;
       resizeAndRepaint(); 
    }
    
    
    /**
     * Returns a boolean indicating if a property change event received
     * from column changes is expected to be already broadcasted by the
     * core TableColumnModel. <p>
     * 
     * This implementation returns true for notification of width, preferredWidth
     * and visible properties, false otherwise.
     * 
     * @param event the PropertyChangeEvent received as TableColumnModelExtListener.
     * @return a boolean to decide whether the same event triggers a
     *   base columnModelEvent.
     */
    protected boolean isColumnEvent(PropertyChangeEvent event) {
        return "width".equals(event.getPropertyName()) || 
            "preferredWidth".equals(event.getPropertyName())
            || "visible".equals(event.getPropertyName());
    }

    /**
     * {@inheritDoc} <p>
     * 
     * Overridden to respect the column tooltip, if available. 
     * 
     * @return the column tooltip of the column at the mouse position 
     *   if not null or super if not available.
     */
    @Override
    public String getToolTipText(MouseEvent event) {
        String columnToolTipText = getColumnToolTipText(event);
        return columnToolTipText != null ? columnToolTipText : super.getToolTipText(event);
    }

    /**
     * Returns the column tooltip of the column at the position
     * of the MouseEvent, if a tooltip is available.
     * 
     * @param event the mouseEvent representing the mouse location.
     * @return the column tooltip of the column below the mouse location,
     *   or null if not available.
     */
    protected String getColumnToolTipText(MouseEvent event) {
	JXTable table = getXTable();
        if (table == null) return null;
        int column = columnAtPoint(event.getPoint());
        if (column < 0) return null;
        TableColumnExt columnExt = table.getColumnExt(column);
        return columnExt != null ? columnExt.getToolTipText() : null;
    }
    
    /**
     * Returns the associated table if it is of type JXTable, or null if not.
     * 
     * @return the associated table if of type JXTable or null if not.
     */
    public JXTable getXTable() {
        if (!(getTable() instanceof JXTable))
            return null;
        return (JXTable) getTable();
    }

    /**
     * Returns the resortsOnDoubleClick property. 
     * 
     * @return a flag indicating whether or not the second click in a mouseClicked
     * event should toggle the sort order again.
     * 
     * @see #setResortsOnDoubleClick(boolean)
     */
    public boolean getResortsOnDoubleClick() {
        return getXTable() != null && resortsOnDoubleClick;
    }
    
    /**
     * Sets the resortsOnDoubleClick property. If enabled, the second click
     * of a mouseClicked event will toggle the sort order again if the
     * column has been unsorted before. This is introduced to support
     * feature request #271-swingx. It is effective only if the coupled table
     * is of type JXTable and has full control about its RowSorter's properties.
     * 
     * The default value is false. 
     * 
     * @param resortsOnDoubleClick a boolean indicating whether or not the
     *    second click in a mouseClicked event should resort the column.
     *    
     *  @see #getResortsOnDoubleClick()  
     */
    public void setResortsOnDoubleClick(boolean resortsOnDoubleClick) {
        boolean old = getResortsOnDoubleClick();
        this.resortsOnDoubleClick = resortsOnDoubleClick;
        firePropertyChange("resortsOnDoubleClick", old, getResortsOnDoubleClick());
    }
    
    /**
     * Returns the TableCellRenderer to use for the column with the given index. This
     * implementation returns the column's header renderer if available or this header's
     * default renderer if not.
     * 
     * @param columnIndex the index in view coordinates of the column
     * @return the renderer to use for the column, guaranteed to be not null.
     */
    public TableCellRenderer getCellRenderer(int columnIndex) {
        TableCellRenderer renderer = getColumnModel().getColumn(columnIndex).getHeaderRenderer();
        return renderer != null ? renderer : getDefaultRenderer();
    }
    
    /**
     * {@inheritDoc} <p>
     * 
     * Overridden to adjust for a reasonable minimum height. Done to fix Issue 334-swingx,
     * which actually is a core issue misbehaving in returning a zero height
     * if the first column has no text. 
     * 
     * @see #getPreferredSize(Dimension)
     * @see #getMinimumHeight(int).
     * 
     */
    @Override
    public Dimension getPreferredSize() {
        Dimension pref = super.getPreferredSize();
        pref = getPreferredSize(pref);
        pref.height = getMinimumHeight(pref.height);
        return pref;
    }
    
    /**
     * Returns a preferred size which is adjusted to the maximum of all
     * header renderers' height requirement.
     * 
     * @param pref an initial preferred size
     * @return the initial preferred size with its height property adjusted 
     *      to the maximum of all renderers preferred height requirement. 
     *  
     *  @see #getPreferredSize()
     *  @see #getMinimumHeight(int)
     */
    protected Dimension getPreferredSize(Dimension pref) {
        int height = pref.height;
        for (int i = 0; i < getColumnModel().getColumnCount(); i++) {
            TableCellRenderer renderer = getCellRenderer(i);
            Component comp = renderer.getTableCellRendererComponent(table, 
                    getColumnModel().getColumn(i).getHeaderValue(), false, false, -1, i);
            height = Math.max(height, comp.getPreferredSize().height);
        }
        pref.height = height;
        return pref;
        
    }

    /**
     * Returns a reasonable minimal preferred height for the header. This is
     * meant as a last straw if all header values are null, renderers report 0 as
     * their preferred height.<p>
     * 
     * This implementation returns the default header renderer's preferred height as measured
     * with a dummy value if the input height is 0, otherwise returns the height
     * unchanged.
     * 
     * @param height the initial height.
     * @return a reasonable minimal preferred height.
     * 
     * @see #getPreferredSize()
     * @see #getPreferredSize(Dimension)
     */
    protected int getMinimumHeight(int height) {
        if ((height == 0)) {
//                && (getXTable() != null) 
//                && getXTable().isColumnControlVisible()){
            TableCellRenderer renderer = getDefaultRenderer();
            Component comp = renderer.getTableCellRendererComponent(getTable(), 
                        "dummy", false, false, -1, -1);
            height = comp.getPreferredSize().height;
        }
        return height;
    }
    

    /**
     * @inherited <p>
     * 
     * Overridden to fire a propertyChange for draggedColumn. 
     */
    @Override
    public void setDraggedColumn(TableColumn column) {
        if (getDraggedColumn() == column) return;
        TableColumn old = getDraggedColumn();
        super.setDraggedColumn(column);
        firePropertyChange("draggedColumn", old, getDraggedColumn());
    }

    
    /**
     * @inherited <p>
     * 
     * Overridden to fire a propertyChange for resizingColumn. 
     */
    @Override
    public void setResizingColumn(TableColumn aColumn) {
        if (getResizingColumn() == aColumn) return;
        TableColumn old = getResizingColumn();
        super.setResizingColumn(aColumn);
        firePropertyChange("resizingColumn", old, getResizingColumn());
    }
    
    
    
    /**
     * {@inheritDoc} <p>
     * 
     * Overridden to scroll the table to keep the dragged column visible.
     * This side-effect is enabled only if the header's autoscroll property is
     * <code>true</code> and the associated table is of type JXTable.<p> 
     * 
     * The autoscrolls is disabled by default. With or without - core 
     * issue #6503981 has weird effects (for jdk 1.6 - 1.6u3) on a plain 
     * JTable as well as a JXTable, fixed in 1.6u4.
     * 
     */
    @Override
    public void setDraggedDistance(int distance) {
        int old = getDraggedDistance();
        super.setDraggedDistance(distance);
        // fire because super doesn't
        firePropertyChange("draggedDistance", old, getDraggedDistance());
        if (!getAutoscrolls() || (getXTable() == null)) return;
        TableColumn column = getDraggedColumn();
        // fix for #788-swingx: don't try to scroll if we have no dragged column
        // as doing will confuse the horizontalScrollEnabled on the JXTable.
        if (column != null) {
            JXTable table = getXTable();
            if (table != null)
            	table.scrollColumnToVisible(getViewIndexForColumn(column));
        }
    }
    
    /**
     * Returns the the dragged column if and only if, a drag is in process and
     * the column is visible, otherwise returns <code>null</code>.
     * 
     * @return the dragged column, if a drag is in process and the column is
     *         visible, otherwise returns <code>null</code>
     * @see #getDraggedDistance
     */
    @Override
    public TableColumn getDraggedColumn() {
        return isVisible(draggedColumn) ? draggedColumn : null; 
    }

    /**
     * Checks and returns the column's visibility. 
     * 
     * @param column the <code>TableColumn</code> to check
     * @return a boolean indicating if the column is visible
     */
    private boolean isVisible(TableColumn column) {
        return getViewIndexForColumn(column) >= 0;
    }

    /**
     * Returns the (visible) view index for the table column
     * or -1 if not visible or not contained in this header's
     * columnModel.
     * 
     * 
     * @param aColumn the TableColumn to find the view index for
     * @return the view index of the given table column or -1 if not visible
     * or not contained in the column model.
     */
    private int getViewIndexForColumn(TableColumn aColumn) {
        if (aColumn == null)
            return -1;
        TableColumnModel cm = getColumnModel();
        for (int column = 0; column < cm.getColumnCount(); column++) {
            if (cm.getColumn(column) == aColumn) {
                return column;
            }
        }
        return -1;
    }
    
    /**
     * Returns the PropertyChangeListener to register on the owning table,
     * lazily created.
     * 
     * @return the PropertyChangeListener to use on the owning table.
     */
    protected PropertyChangeListener getTablePropertyChangeListener() {
        if (tablePropertyChangeListener == null) {
            tablePropertyChangeListener = createTablePropertyChangeListener();
        }
        return tablePropertyChangeListener;
    }

    /**
     * Creates and returns the PropertyChangeListener to register on the 
     * owning table.<p>
     * 
     * This implementation synchs the header's enabled properties with the 
     * table's enabled.
     * 
     * @return the PropertyChangeListener to register on the owning table.
     */
    protected PropertyChangeListener createTablePropertyChangeListener() {
        PropertyChangeListener l = new PropertyChangeListener() {
            
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if ("enabled".equals(evt.getPropertyName())) {
                    updateEnabledFromTable();
                }
            }
        };
        return l;
    }


    /**
     * Creates and installs header listeners to service the extended functionality.
     * This implementation creates and installs a custom mouse input listener.
     */
    protected void installHeaderListener() {
        if (headerListener == null) {
            headerListener = new HeaderListener();
            addMouseListener(headerListener);
            addMouseMotionListener(headerListener);
        }
    }

    /**
     * Uninstalls header listeners to service the extended functionality.
     * This implementation uninstalls a custom mouse input listener.
     */
    protected void uninstallHeaderListener() {
        if (headerListener != null) {
            removeMouseListener(headerListener);
            removeMouseMotionListener(headerListener);
            headerListener = null;
        }
    }

    private MouseInputListener headerListener;

    /**
     * A MouseListener implementation to support enhanced tableHeader functionality.
     * 
     * Supports column "packing" by double click in resize region. Works around
     * core issue #6862170 (must not sort column by click into resize region).
     * <p>
     * 
     * Note that the logic is critical, mostly because it must be independent of
     * sequence of listener notification. So we check whether or not a pressed
     * happens in the resizing region in both pressed and released, taking the
     * header's resizingColumn property as a marker. The inResize flag can only
     * be turned on in those. At the end of the released, we check if we are
     * in resize and disable core sorting - which happens in clicked - if appropriate.
     * In our clicked we hook the pack action (happens only on double click)
     * and reset the resizing region flag always. Pressed (and all other methods)
     * restore sorting enablement. 
     * <p>
     * 
     * Supports resort on double click if enabled in the JXTableHeader (Issue #271-swingx). 
     * 
     * Is fully effective only if JXTable has control over the row sorter, that is
     * if the row sorter is of type SortController.
     * 
     */
    private class HeaderListener implements MouseInputListener, Serializable {
        private TableColumn cachedResizingColumn;
        private SortOrder[] cachedSortOrderCycle;
        private int sortColumn = -1;
        
        /**
         * Packs column on double click in resize region. Resorts 
         * column on double click if enabled and not in resize region.
         */
        @Override
        public void mouseClicked(MouseEvent e) {
            if (shouldIgnore(e)) {
                return;
            }
            doResize(e);
            doDoubleSort(e);
            uncacheResizingColumn();
        }

        private void doDoubleSort(MouseEvent e) {
            if (!hasCachedSortColumn() || e.getClickCount() % 2 == 1) return;
            JXTable table = getXTable();
            if (table != null)
            	table.toggleSortOrder(sortColumn);
            uncacheSortColumn();
        }

        private boolean hasCachedSortColumn() {
            return sortColumn >= 0;
        }

        /**
         * Resets sort enablement always, set resizing marker if available.
         */
        @Override
        public void mousePressed(MouseEvent e) {
            resetToggleSortOrder(e);
            if (shouldIgnore(e)) {
                return;
            }
            cacheResizingColumn(e);
        }

        /** 
         * Sets resizing marker if available, disables table sorting if in 
         * resize region and sort gesture (aka: single click).
         */
        @Override
        public void mouseReleased(MouseEvent e) {
            if (shouldIgnore(e)) {
                return;
            }
            cacheResizingColumn(e);
            cacheSortColumn(e);
            if (isInResizeRegion(e) && e.getClickCount() % 2 == 1) {
                disableToggleSortOrder(e);
            } 
        }

        private void cacheSortColumn(MouseEvent e) {
            if (!canCacheSortColumn(e)) uncacheSortColumn();
            if (e.getClickCount() % 2 == 1) {
        	JXTable table = getXTable();
                if (table == null)
                    return;
                int column = columnAtPoint(e.getPoint());
                if (column >= 0) {
                    int primarySortIndex = table.getSortedColumnIndex();
                    if (primarySortIndex == column) {
                        column = -1;
                    }
                }
                sortColumn = column;
            }
            
        }

        private void uncacheSortColumn() {
            sortColumn = -1;
        }

        private boolean canCacheSortColumn(MouseEvent e) {
            if (hasSortController() && !isInResizeRegion(e) && getResortsOnDoubleClick()) {
                return true;
            }
            return false;
        }
        
        /**
         * Returns a boolean indication if the mouse event should be ignored.
         * Here: returns true if table not enabled or not an event from the left mouse
         * button.
         * 
         * @param e
         * @return
         */
        private boolean shouldIgnore(MouseEvent e) {
            return !SwingUtilities.isLeftMouseButton(e)
              || !table.isEnabled();
        }

        /**
         * Packs caches resizing column on double click, if available. Does nothing
         * otherwise.
         * 
         * @param e
         */
        private void doResize(MouseEvent e) {
            if (e.getClickCount() != 2)
                return;
            int column = getViewIndexForColumn(cachedResizingColumn);
            if (column >= 0) {
        	JXTable table = getXTable();
        	if (table != null)
        	    table.packColumn(column, 5);
            }
        }


        /**
         * 
         * @param e
         */
        private void disableToggleSortOrder(MouseEvent e) {
            JXTable table = getXTable();
            if (!hasSortController() || table == null) return;
            SortController<?> controller = (SortController<?>) table.getRowSorter();
            cachedSortOrderCycle = controller.getSortOrderCycle();
            controller.setSortOrderCycle();
        }

        /**
         * @return
         */
        private boolean hasSortController() {
            JXTable table = getXTable();
            if (table == null)
            	return false;
            return (table.getRowSorter() instanceof SortController<?>);
        }
        
        /**
         * 
         */
        private void resetToggleSortOrder(MouseEvent e) {
            JXTable table = getXTable();
            if (cachedSortOrderCycle == null || table == null) return;
            ((SortController<?>) table.getRowSorter()).setSortOrderCycle(cachedSortOrderCycle);
            cachedSortOrderCycle = null;
        }


        /**
         * Caches the resizing column if set. Does nothing if null.
         *     
         * @param e
         */
        private void cacheResizingColumn(MouseEvent e) {
            TableColumn column = getResizingColumn();
            if (column != null) {
                cachedResizingColumn = column;
            }
        }

        /**
         * Sets the cached resizing column to null.
         */
        private void uncacheResizingColumn() {
            cachedResizingColumn = null;
        }

        /**
         * Returns true if the mouseEvent happened in the resizing region.
         * 
         * @param e
         * @return
         */
        private boolean isInResizeRegion(MouseEvent e) {
            return cachedResizingColumn != null; // inResize;
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        /**
         * Resets all cached state.
         */
        @Override
        public void mouseExited(MouseEvent e) {
            uncacheSortColumn();
            uncacheResizingColumn();
            resetToggleSortOrder(e);
        }

        /**
         * Resets all cached state.
         */
        @Override
        public void mouseDragged(MouseEvent e) {
            uncacheSortColumn();
            uncacheResizingColumn();
            resetToggleSortOrder(e);
        }

        /**
         * Resets all cached state.
         */
        @Override
        public void mouseMoved(MouseEvent e) {
            uncacheSortColumn();
            resetToggleSortOrder(e);
        }
    }

    

}
