/*
 * Copyright (c) 2005-2010 Substance Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of Substance Kirill Grouchnikov nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.pushingpixels.substance.internal.ui;

import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.BasicTableHeaderUI;
import javax.swing.table.*;

import org.pushingpixels.lafwidget.LafWidgetUtilities;
import org.pushingpixels.lafwidget.animation.AnimationConfigurationManager;
import org.pushingpixels.lafwidget.animation.AnimationFacet;
import org.pushingpixels.substance.api.*;
import org.pushingpixels.substance.api.renderers.SubstanceDefaultTableHeaderCellRenderer;
import org.pushingpixels.substance.internal.animation.StateTransitionMultiTracker;
import org.pushingpixels.substance.internal.animation.StateTransitionTracker;
import org.pushingpixels.substance.internal.animation.StateTransitionTracker.RepaintCallback;
import org.pushingpixels.substance.internal.painter.HighlightPainterUtils;
import org.pushingpixels.substance.internal.utils.*;
import org.pushingpixels.trident.Timeline.TimelineState;
import org.pushingpixels.trident.callback.TimelineCallback;
import org.pushingpixels.trident.callback.UIThreadTimelineCallbackAdapter;

/**
 * UI for table headers in <b>Substance</b> look and feel.
 * 
 * @author Kirill Grouchnikov
 */
public class SubstanceTableHeaderUI extends BasicTableHeaderUI {
	/**
	 * Repaints the header on column selection.
	 */
	protected TableHeaderListener substanceHeaderListener;

	/**
	 * The default renderer.
	 */
	protected TableCellRenderer defaultHeaderRenderer;

	/**
	 * Holds the list of currently selected indices.
	 */
	protected Map<Integer, Object> selectedIndices;

	/**
	 * Listener for transition animations on list selections.
	 */
	protected ListSelectionListener substanceFadeSelectionListener;

	private StateTransitionMultiTracker<Integer> stateTransitionMultiTracker;

	/**
	 * Property change listener.
	 */
	protected PropertyChangeListener substancePropertyChangeListener;

	/**
	 * Listener for table header.
	 * 
	 * @author Kirill Grouchnikov
	 */
	private static class TableHeaderListener implements ListSelectionListener {
		/**
		 * The associated table header UI.
		 */
		private SubstanceTableHeaderUI ui;

		/**
		 * Simple constructor.
		 * 
		 * @param ui
		 *            The associated table header UI
		 */
		public TableHeaderListener(SubstanceTableHeaderUI ui) {
			this.ui = ui;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.swing.event.ListSelectionListener#valueChanged(javax.swing.
		 * event.ListSelectionEvent)
		 */
		@Override
        public void valueChanged(ListSelectionEvent e) {
			if (ui.header == null)
				return;
			if (ui.header.isValid())
				ui.header.repaint();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent)
	 */
	public static ComponentUI createUI(JComponent comp) {
		SubstanceCoreUtilities.testComponentCreationThreadingViolation(comp);
		return new SubstanceTableHeaderUI();
	}

	/**
	 * Creates a new UI delegate.
	 */
	public SubstanceTableHeaderUI() {
		this.stateTransitionMultiTracker = new StateTransitionMultiTracker<Integer>();
		selectedIndices = new HashMap<Integer, Object>();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicTableHeaderUI#installListeners()
	 */
	@Override
	protected void installListeners() {
		super.installListeners();
		TableColumnModel columnModel = header.getColumnModel();
		if (columnModel != null) {
			ListSelectionModel lsm = columnModel.getSelectionModel();
			if (lsm != null) {
				// fix for defect 109 - memory leak on skin switch
				substanceHeaderListener = new TableHeaderListener(this);
				lsm.addListSelectionListener(substanceHeaderListener);
			}
		}

		// Add listener for the selection animation
		this.substanceFadeSelectionListener = new ListSelectionListener() {
			@Override
            public void valueChanged(ListSelectionEvent e) {
				if (header == null)
					return;

				if (!header.getColumnModel().getColumnSelectionAllowed())
					return;

				// fix for issue 367 - check that there is an associated
				// table and that it has a substance UI delegate
				JTable table = header.getTable();
				if (table == null)
					return;

				TableUI ui = table.getUI();
				if (!(ui instanceof SubstanceTableUI)) {
					stateTransitionMultiTracker.clear();
					return;
				}

				// optimization on large tables and large selections
				// and syncing the fade presence with the table
				// (issue 309)
				SubstanceTableUI tableUI = (SubstanceTableUI) ui;
				// System.out.println("Sel anim: "
				// + tableUI.hasSelectionAnimations());
				if (!tableUI._hasSelectionAnimations()) {
					stateTransitionMultiTracker.clear();
					return;
				}

				// Set<Long> initiatedFadeSequences = new HashSet<Long>();
				Set<StateTransitionTracker> initiatedTrackers = new HashSet<StateTransitionTracker>();
				boolean fadeCanceled = false;

				// if (SubstanceCoreUtilities.toBleedWatermark(list))
				// return;

				// FadeTracker fadeTrackerInstance = FadeTracker.getInstance();
				TableColumnModel columnModel = header.getColumnModel();
				int size = columnModel.getColumnCount();
				ListSelectionModel lsm = columnModel.getSelectionModel();
				for (int i = e.getFirstIndex(); i <= e.getLastIndex(); i++) {
					if (i >= size)
						continue;
					if (lsm.isSelectedIndex(i)) {
						// check if was selected before
						if (!selectedIndices.containsKey(i)) {
							// start fading in
							// System.out.println("Fade in on index " + i);

							if (!fadeCanceled) {
								StateTransitionTracker tracker = getTracker(i,
										getColumnState(i).isFacetActive(
												ComponentStateFacet.ROLLOVER),
										false);
								tracker.getModel().setSelected(true);
								initiatedTrackers.add(tracker);
								if (initiatedTrackers.size() > 15) {
									stateTransitionMultiTracker.clear();
									initiatedTrackers.clear();
									fadeCanceled = true;
								}
							}

							selectedIndices.put(i, columnModel.getColumn(i));
						}
					} else {
						// check if was selected before and still points to the
						// same element
						if (selectedIndices.containsKey(i)) {
							if (selectedIndices.get(i) == columnModel
									.getColumn(i)) {
								// start fading out
								// System.out.println("Fade out on index " + i);

								if (!fadeCanceled) {
									StateTransitionTracker tracker = getTracker(
											i,
											getColumnState(i)
													.isFacetActive(
															ComponentStateFacet.ROLLOVER),
											true);
									tracker.getModel().setSelected(false);
									initiatedTrackers.add(tracker);
									if (initiatedTrackers.size() > 15) {
										stateTransitionMultiTracker.clear();
										initiatedTrackers.clear();
										fadeCanceled = true;
									}
								}
							}
							selectedIndices.remove(i);
						}
					}
				}
			}
		};

		if (columnModel != null) {
			ListSelectionModel lsm = columnModel.getSelectionModel();
			if (lsm != null) {
				lsm.addListSelectionListener(substanceFadeSelectionListener);
			}
		}

		this.substancePropertyChangeListener = new PropertyChangeListener() {
			@Override
            public void propertyChange(PropertyChangeEvent evt) {
				if ("table".equals(evt.getPropertyName())) {
					// track changes to the table and re-register the
					// column model listener to the new table.
					TableColumnModel oldModel = (evt.getOldValue() instanceof JTable) ? ((JTable) evt
							.getOldValue()).getColumnModel()
							: null;
					TableColumnModel newModel = (evt.getNewValue() instanceof JTable) ? ((JTable) evt
							.getNewValue()).getColumnModel()
							: null;
					processColumnModelChangeEvent(oldModel, newModel);
				}
			}
		};
		this.header
				.addPropertyChangeListener(this.substancePropertyChangeListener);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicTableHeaderUI#installDefaults()
	 */
	@Override
	protected void installDefaults() {
		super.installDefaults();

		defaultHeaderRenderer = header.getDefaultRenderer();
		if (defaultHeaderRenderer instanceof UIResource) {
			header
					.setDefaultRenderer(new SubstanceDefaultTableHeaderCellRenderer());
		}

		for (int i = 0; i < header.getColumnModel().getColumnCount(); i++) {
			if (header.getColumnModel().getSelectionModel().isSelectedIndex(i)) {
				selectedIndices.put(i, header.getColumnModel().getColumn(i));
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicTableHeaderUI#uninstallListeners()
	 */
	@Override
	protected void uninstallListeners() {
		// fix for defect 109 - memory leak on skin switch
		TableColumnModel columnModel = header.getColumnModel();
		if (columnModel != null) {
			ListSelectionModel lsm = columnModel.getSelectionModel();
			if (lsm != null) {
				lsm.removeListSelectionListener(substanceHeaderListener);
				substanceHeaderListener = null;
			}
		}

		this.header
				.removePropertyChangeListener(this.substancePropertyChangeListener);
		this.substancePropertyChangeListener = null;

		super.uninstallListeners();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicTableHeaderUI#uninstallDefaults()
	 */
	@Override
	protected void uninstallDefaults() {
		super.uninstallDefaults();

		selectedIndices.clear();

		if (header.getDefaultRenderer() instanceof SubstanceDefaultTableHeaderCellRenderer) {
			header.setDefaultRenderer(defaultHeaderRenderer);
			if (defaultHeaderRenderer instanceof Component)
				SwingUtilities
						.updateComponentTreeUI((Component) defaultHeaderRenderer);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.ComponentUI#paint(java.awt.Graphics,
	 * javax.swing.JComponent)
	 */
	@Override
	public void paint(Graphics g, JComponent c) {
		if (header.getColumnModel().getColumnCount() <= 0) {
			return;
		}
		boolean ltr = header.getComponentOrientation().isLeftToRight();

		Rectangle clip = g.getClipBounds();
		Point left = clip.getLocation();
		Point right = new Point(clip.x + clip.width - 1, clip.y);

		TableColumnModel cm = header.getColumnModel();
		int[] selectedColumns = cm.getSelectedColumns();
		Set<Integer> selected = new HashSet<Integer>();
		for (int sel : selectedColumns)
			selected.add(sel);

		int cMin = header.columnAtPoint(ltr ? left : right);
		int cMax = header.columnAtPoint(ltr ? right : left);
		// This should never happen.
		if (cMin == -1) {
			cMin = 0;
		}
		// If the table does not have enough columns to fill the view we'll get
		// -1.
		// Replace this with the index of the last column.
		if (cMax == -1) {
			cMax = cm.getColumnCount() - 1;
		}

		TableColumn draggedColumn = header.getDraggedColumn();
		int columnWidth;
		Rectangle cellRect = header.getHeaderRect(ltr ? cMin : cMax);
		TableColumn aColumn;
		if (ltr) {
			for (int column = cMin; column <= cMax; column++) {
				aColumn = cm.getColumn(column);
				columnWidth = aColumn.getWidth();
				cellRect.width = columnWidth;
				if (aColumn != draggedColumn) {
					this.paintCell(g, cellRect, column, selected
							.contains(column));
				}
				cellRect.x += columnWidth;
			}
		} else {
			for (int column = cMax; column >= cMin; column--) {
				aColumn = cm.getColumn(column);
				columnWidth = aColumn.getWidth();
				cellRect.width = columnWidth;
				if (aColumn != draggedColumn) {
					this.paintCell(g, cellRect, column, selected
							.contains(column));
				}
				cellRect.x += columnWidth;
			}
		}

		this.paintGrid(g, c);

		// Paint the dragged column if we are dragging.
		if (draggedColumn != null) {
			int draggedColumnIndex = viewIndexForColumn(draggedColumn);
			Rectangle draggedCellRect = header
					.getHeaderRect(draggedColumnIndex);

			// Draw a gray well in place of the moving column.
			g.setColor(header.getParent().getBackground());
			g.fillRect(draggedCellRect.x, draggedCellRect.y,
					draggedCellRect.width, draggedCellRect.height);

			draggedCellRect.x += header.getDraggedDistance();

			// Fill the background.
			g.setColor(header.getBackground());
			g.fillRect(draggedCellRect.x, draggedCellRect.y,
					draggedCellRect.width, draggedCellRect.height);

			this.paintCell(g, draggedCellRect, draggedColumnIndex, selected
					.contains(draggedColumnIndex));
		}

		// Remove all components in the rendererPane.
		rendererPane.removeAll();
	}

	/**
	 * Retrieves renderer for the specified column header.
	 * 
	 * @param columnIndex
	 *            Column index.
	 * @return Renderer for the specified column header.
	 */
	private Component getHeaderRenderer(int columnIndex) {
		TableColumn aColumn = header.getColumnModel().getColumn(columnIndex);
		TableCellRenderer renderer = aColumn.getHeaderRenderer();
		if (renderer == null) {
			renderer = header.getDefaultRenderer();
		}
		return renderer.getTableCellRendererComponent(header.getTable(),
				aColumn.getHeaderValue(), false, false, -1, columnIndex);
	}

	/**
	 * 
	 * 
	 * @param g
	 * @param c
	 */
	protected void paintGrid(Graphics g, JComponent c) {
		boolean ltr = header.getComponentOrientation().isLeftToRight();

		Graphics2D g2d = (Graphics2D) g.create();

		Rectangle clip = g.getClipBounds();
		Point left = clip.getLocation();
		// tweak the points for issue 378 - making sure that the
		// grid lines are repainted correctly on scroll.
		int lineWeight = (int) Math.ceil(SubstanceSizeUtils
				.getBorderStrokeWidth(SubstanceSizeUtils
						.getComponentFontSize(c)));
		left = new Point(left.x - 2 * lineWeight, left.y);
		Point right = new Point(clip.x + clip.width + 2 * lineWeight, clip.y);

		TableColumnModel cm = header.getColumnModel();

		int cMin = header.columnAtPoint(ltr ? left : right);
		int cMax = header.columnAtPoint(ltr ? right : left);
		// This should never happen.
		if (cMin == -1) {
			cMin = 0;
		}

		Rectangle cellRect0 = header.getHeaderRect(cMin);
		// int top = cellRect0.y;
		int bottom = cellRect0.y + cellRect0.height;

		Color gridColor = getGridColor(this.header);

		float strokeWidth = SubstanceSizeUtils
				.getBorderStrokeWidth(SubstanceSizeUtils
						.getComponentFontSize(this.header));
		g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND,
				BasicStroke.JOIN_BEVEL));
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		g2d.setColor(gridColor);
		g2d.setComposite(LafWidgetUtilities.getAlphaComposite(this.header,
				0.7f, g));

		g2d.drawLine((int) left.getX(), (int) (bottom - strokeWidth / 2),
				(int) right.getX(), (int) (bottom - strokeWidth / 2));
		// If the table does not have enough columns to fill the view we'll
		// get -1. Replace this with the index of the last column.
		if (cMax == -1) {
			cMax = cm.getColumnCount() - 1;
		}

		TableColumn draggedColumn = this.header.getDraggedColumn();
		int columnWidth;
		Rectangle cellRect = this.header.getHeaderRect(ltr ? cMin : cMax);
		TableColumn aColumn;
		if (ltr) {
			for (int column = cMin; column <= cMax; column++) {
				aColumn = cm.getColumn(column);
				columnWidth = aColumn.getWidth();
				cellRect.width = columnWidth;

				if (aColumn != draggedColumn) {
					if (hasLeadingVerticalGridLine(header, cellRect, column)) {
						g2d
								.drawLine(cellRect.x, cellRect.y, cellRect.x,
										bottom);
					}
					if (hasTrailingVerticalGridLine(header, cellRect, column)) {
						g2d.drawLine(cellRect.x + cellRect.width - 1,
								cellRect.y, cellRect.x + cellRect.width - 1,
								bottom);
					}
				}

				cellRect.x += columnWidth;
			}
		} else {
			for (int column = cMax; column >= cMin; column--) {
				aColumn = cm.getColumn(column);
				columnWidth = aColumn.getWidth();
				cellRect.width = columnWidth;

				if (aColumn != draggedColumn) {
					if (hasLeadingVerticalGridLine(header, cellRect, column)) {
						g2d.drawLine(cellRect.x + cellRect.width - 1,
								cellRect.y, cellRect.x + cellRect.width - 1,
								bottom);
					}
					if (hasTrailingVerticalGridLine(header, cellRect, column)) {
						g2d
								.drawLine(cellRect.x, cellRect.y, cellRect.x,
										bottom);
					}
				}
				cellRect.x += columnWidth;
			}
		}

		g2d.dispose();
	}

	private boolean hasTrailingVerticalGridLine(JTableHeader tableHeader,
			Rectangle cellRect, int column) {
		boolean toDrawLine = (column != (tableHeader.getColumnModel()
				.getColumnCount() - 1));
		if (!toDrawLine) {
			Container parent = this.header.getParent();
			if (tableHeader.getComponentOrientation().isLeftToRight()) {
				toDrawLine = (parent != null)
						&& (parent.getWidth() > (cellRect.x + cellRect.width));
			} else {
				toDrawLine = (parent != null) && (cellRect.x > 0);
			}
		}
		return toDrawLine;
	}

	private boolean hasLeadingVerticalGridLine(JTableHeader tableHeader,
			Rectangle cellRect, int column) {
		if (column != 0) {
			return false;
		}
		Container parent = tableHeader.getParent();
		if (parent instanceof JViewport) {
			Container grand = parent.getParent();
			if (grand instanceof JScrollPane) {
				return (((JScrollPane) grand).getRowHeader() != null);
			}
		}
		return false;
	}

	/**
	 * Returns the grid color for the table header.
	 * 
	 * @param header
	 *            Table header.
	 * @return Grid color.
	 */
	protected static Color getGridColor(JTableHeader header) {
		boolean isEnabled = header.isEnabled();
		if (header.getTable() != null) {
			// fix for issue 472 - handle standalone table headers
			isEnabled = isEnabled && header.getTable().isEnabled();
		}
		ComponentState currState = isEnabled ? ComponentState.ENABLED
				: ComponentState.DISABLED_UNSELECTED;
		Color gridColor = SubstanceColorSchemeUtilities.getColorScheme(header,
				ColorSchemeAssociationKind.BORDER, currState).getLineColor();
		return gridColor;
	}

	/**
	 * Paints cell.
	 * 
	 * @param g
	 *            Graphic context.
	 * @param cellRect
	 *            Cell rectangle.
	 * @param columnIndex
	 *            Column index.
	 * @param isSelected
	 *            Selection indication.
	 */
	private void paintCell(Graphics g, Rectangle cellRect, int columnIndex,
			boolean isSelected) {
		Graphics2D g2d = (Graphics2D) g.create();
		g2d.setComposite(LafWidgetUtilities.getAlphaComposite(header, g));

		// paint default background
		Component component = getHeaderRenderer(columnIndex);

		StateTransitionTracker.ModelStateInfo modelStateInfo = getModelStateInfo(columnIndex);
		Map<ComponentState, StateTransitionTracker.StateContributionInfo> activeStates = ((modelStateInfo == null) ? null
				: modelStateInfo.getStateContributionMap());
		ComponentState currState = ((modelStateInfo == null) ? getColumnState(columnIndex)
				: modelStateInfo.getCurrModelState());

		boolean hasHighlights = false;
		if (activeStates != null) {
			for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> stateEntry : activeStates
					.entrySet()) {
				hasHighlights = (SubstanceColorSchemeUtilities
						.getHighlightAlpha(this.header, stateEntry.getKey())
						* stateEntry.getValue().getContribution() > 0.0f);
				if (hasHighlights)
					break;
			}
		} else {
			hasHighlights = (SubstanceColorSchemeUtilities.getHighlightAlpha(
					this.header, currState) > 0.0f);
		}

		// System.out.println(row + ":" + prevTheme.getDisplayName() + "["
		// + alphaForPrevBackground + "]:" + currTheme.getDisplayName()
		// + "[" + alphaForCurrBackground + "]");

		if (hasHighlights) {
			if (activeStates == null) {
				float alpha = SubstanceColorSchemeUtilities.getHighlightAlpha(
						this.header, currState);
				if (alpha > 0.0f) {
					SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities
							.getColorScheme(this.header,
									ColorSchemeAssociationKind.HIGHLIGHT,
									currState);
					SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities
							.getColorScheme(this.header,
									ColorSchemeAssociationKind.HIGHLIGHT,
									currState);
					g2d.setComposite(LafWidgetUtilities.getAlphaComposite(
							this.header, alpha, g));
					HighlightPainterUtils.paintHighlight(g2d,
							this.rendererPane, rendererPane, cellRect, 0.8f,
							null, fillScheme, borderScheme);
					g2d.setComposite(LafWidgetUtilities.getAlphaComposite(
							this.header, g));
				}
			} else {
				for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> stateEntry : activeStates
						.entrySet()) {
					ComponentState activeState = stateEntry.getKey();
					float alpha = SubstanceColorSchemeUtilities
							.getHighlightAlpha(this.header, activeState)
							* stateEntry.getValue().getContribution();
					if (alpha == 0.0f)
						continue;
					SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities
							.getColorScheme(this.header,
									ColorSchemeAssociationKind.HIGHLIGHT,
									activeState);
					SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities
							.getColorScheme(this.header,
									ColorSchemeAssociationKind.HIGHLIGHT,
									activeState);
					g2d.setComposite(LafWidgetUtilities.getAlphaComposite(
							this.header, alpha, g));
					HighlightPainterUtils.paintHighlight(g2d,
							this.rendererPane, rendererPane, cellRect, 0.8f,
							null, fillScheme, borderScheme);
					g2d.setComposite(LafWidgetUtilities.getAlphaComposite(
							this.header, g));
				}
			}
		}

		rendererPane.paintComponent(g2d, component, header, cellRect.x,
				cellRect.y, cellRect.width, cellRect.height, true);

		g2d.dispose();
	}

	/**
	 * Retrieves view index for the specified column.
	 * 
	 * @param aColumn
	 *            Table column.
	 * @return View index for the specified column.
	 */
	private int viewIndexForColumn(TableColumn aColumn) {
		TableColumnModel cm = header.getColumnModel();
		for (int column = 0; column < cm.getColumnCount(); column++) {
			if (cm.getColumn(column) == aColumn) {
				return column;
			}
		}
		return -1;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.ComponentUI#update(java.awt.Graphics,
	 * javax.swing.JComponent)
	 */
	@Override
	public void update(Graphics g, JComponent c) {
		// fix for issue 175 - table header under resize mode off
		// was painted in color scheme-agnostic (gray) color.
		boolean isEnabled = this.header.isEnabled();
		if (this.header.getTable() != null) {
			// fix for issue 472 - handle standalone table headers
			isEnabled = isEnabled && this.header.getTable().isEnabled();
		}
		ComponentState backgroundState = isEnabled ? ComponentState.ENABLED
				: ComponentState.DISABLED_UNSELECTED;

		// fix for issue 360 - respect the clip bounds of the
		// table header
		Rectangle clip = g.getClipBounds();
		if (clip == null)
			clip = c.getBounds();

		// do not use the highlight scheme for painting the
		// table header background
		SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities
				.getColorScheme(c, backgroundState);
		SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities
				.getColorScheme(c, ColorSchemeAssociationKind.BORDER,
						backgroundState);

		HighlightPainterUtils.paintHighlight(g, null, c, clip, 0.0f, null,
				fillScheme, borderScheme);
		paint(g, c);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.swing.plaf.basic.BasicTableHeaderUI#uninstallUI(javax.swing.JComponent
	 * )
	 */
	@Override
	public void uninstallUI(JComponent c) {
		for (int i = 0; i < header.getColumnModel().getColumnCount(); i++) {
			TableColumn aColumn = header.getColumnModel().getColumn(i);
			TableCellRenderer renderer = aColumn.getHeaderRenderer();
			if (renderer == null) {
				renderer = header.getDefaultRenderer();
			}
			Component rendComp = renderer.getTableCellRendererComponent(header
					.getTable(), aColumn.getHeaderValue(), false, false, -1, i);
			SwingUtilities.updateComponentTreeUI(rendComp);
		}
		super.uninstallUI(c);
	}

	/**
	 * Returns the current state for the specified cell.
	 * 
	 * @param columnIndex
	 *            Column index.
	 * @return The current state for the specified column.
	 */
	public ComponentState getColumnState(int columnIndex) {
		// ButtonModel synthModel = new DefaultButtonModel();
		boolean toEnable = header.isEnabled();

		// get the rollover column index from the table UI delegate
		JTable table = this.header.getTable();
		if (table != null) {
			toEnable = toEnable && table.isEnabled();
		}

		StateTransitionTracker tracker = this.stateTransitionMultiTracker
				.getTracker(columnIndex);
		if (tracker == null) {
			boolean isRollover = false;
			TableColumnModel columnModel = header.getColumnModel();
			boolean isSelected = false;
			if (columnModel.getColumnSelectionAllowed()) {
				isSelected = columnModel.getSelectionModel().isSelectedIndex(
						columnIndex);
				if ((table != null)
						&& (table.getUI() instanceof SubstanceTableUI)) {
					SubstanceTableUI tableUI = (SubstanceTableUI) table.getUI();
					int rolledOverIndex = tableUI.getRolloverColumnIndex();
					isRollover = (rolledOverIndex >= 0)
							&& (rolledOverIndex == columnIndex);
					boolean hasSelectionAnimations = tableUI
							.hasSelectionAnimations();
					if (hasSelectionAnimations
							&& AnimationConfigurationManager.getInstance()
									.isAnimationAllowed(
											AnimationFacet.SELECTION, table))
						isSelected = this.selectedIndices
								.containsKey(columnIndex);
				}
			}
			return ComponentState.getState(toEnable, isRollover, isSelected);
		} else {
			ComponentState fromTracker = tracker.getModelStateInfo()
					.getCurrModelState();
			return ComponentState.getState(toEnable, fromTracker
					.isFacetActive(ComponentStateFacet.ROLLOVER), fromTracker
					.isFacetActive(ComponentStateFacet.SELECTION));
		}
	}

	public StateTransitionTracker.ModelStateInfo getModelStateInfo(
			int columnIndex) {
		if (this.stateTransitionMultiTracker.size() == 0)
			return null;
		StateTransitionTracker tracker = this.stateTransitionMultiTracker
				.getTracker(columnIndex);
		if (tracker == null) {
			return null;
		} else {
			return tracker.getModelStateInfo();
		}
	}

	public StateTransitionTracker getStateTransitionTracker(int columnIndex) {
		return this.stateTransitionMultiTracker.getTracker(columnIndex);
	}

	/**
	 * Returns the scroll pane corner filler component. This method is used in
	 * {@link SubstanceScrollPaneUI} to put a consistent filler for tables.
	 * 
	 * @return Scroll pane corner filler.
	 */
	public JComponent getScrollPaneCornerFiller() {
		return new ScrollPaneCornerFiller(this.header);
	}

	/**
	 * Corner filler for tables wrapped in scroll panes.
	 * 
	 * @author Kirill Grouchnikov
	 */
	protected static class ScrollPaneCornerFiller extends JComponent implements
			UIResource {
		/**
		 * Associated table header.
		 */
		protected JTableHeader header;

		/**
		 * Creates a new corner filler.
		 * 
		 * @param header
		 *            Table header.
		 */
		public ScrollPaneCornerFiller(JTableHeader header) {
			this.header = header;
		}

		@Override
		protected void paintComponent(Graphics g) {
			Graphics2D g2d = (Graphics2D) g.create();
			// System.err.println("Painting " + this.hashCode() + " from "
			// + ((header == null) ? "null" : header.hashCode()));

			boolean ltr = header.getComponentOrientation().isLeftToRight();
			final ComponentState backgroundState = (header.isEnabled() && header
					.getTable().isEnabled()) ? ComponentState.ENABLED
					: ComponentState.DISABLED_UNSELECTED;

			SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities
					.getColorScheme(this.header,
							ColorSchemeAssociationKind.HIGHLIGHT,
							backgroundState);
			SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities
					.getColorScheme(this.header,
							ColorSchemeAssociationKind.HIGHLIGHT_BORDER,
							backgroundState);

			HighlightPainterUtils.paintHighlight(g2d, null, this.header,
					new Rectangle(0, 0, this.getWidth(), this.getHeight()),
					0.0f, null, fillScheme, borderScheme);

			g2d.setColor(getGridColor(this.header));
			float strokeWidth = SubstanceSizeUtils
					.getBorderStrokeWidth(SubstanceSizeUtils
							.getComponentFontSize(header));
			g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND,
					BasicStroke.JOIN_BEVEL));
			g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
			g2d.setComposite(LafWidgetUtilities.getAlphaComposite(this.header,
					0.7f, g));

			int x = ltr ? (int) strokeWidth / 2 : getWidth() - 1
					- (int) strokeWidth / 2;
			g2d.drawLine(x, 0, x, getHeight());

			g2d.dispose();
		}
	}

	/**
	 * Processes the events on model changes on the table column model.
	 * 
	 * @param oldModel
	 *            Old column model.
	 * @param newModel
	 *            New column model.
	 */
	public void processColumnModelChangeEvent(TableColumnModel oldModel,
			TableColumnModel newModel) {
		if (oldModel != null) {
			oldModel.getSelectionModel().removeListSelectionListener(
					substanceFadeSelectionListener);
		}
		if (newModel != null) {
			newModel.getSelectionModel().addListSelectionListener(
					substanceFadeSelectionListener);
		}
		selectedIndices.clear();
		stateTransitionMultiTracker.clear();
	}

	/**
	 * Repaints a single column header during the fade animation cycle.
	 * 
	 * @author Kirill Grouchnikov
	 */
	protected class ColumnHeaderRepaintCallback extends
			UIThreadTimelineCallbackAdapter {
		/**
		 * Associated table header.
		 */
		protected JTableHeader header;

		/**
		 * Associated (animated) column index.
		 */
		protected int columnIndex;

		/**
		 * Creates a new animation repaint callback.
		 * 
		 * @param header
		 *            Associated table header.
		 * @param columnIndex
		 *            Associated (animated) column index.
		 */
		public ColumnHeaderRepaintCallback(JTableHeader header, int columnIndex) {
			this.header = header;
			this.columnIndex = columnIndex;
		}

		@Override
		public void onTimelinePulse(float durationFraction,
				float timelinePosition) {
			repaintColumnHeader();
		}

		@Override
		public void onTimelineStateChanged(TimelineState oldState,
				TimelineState newState, float durationFraction,
				float timelinePosition) {
			repaintColumnHeader();
		}

		/**
		 * Repaints the associated cell.
		 */
		private void repaintColumnHeader() {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
                public void run() {
					if (header == null) {
						// may happen if the LAF was switched in the meantime
						return;
					}
					try {
						// maybeUpdateLayoutState();
						int cellCount = header.getColumnModel()
								.getColumnCount();
						if ((cellCount > 0) && (columnIndex < cellCount)) {
							// need to retrieve the cell rectangle since the
							// cells can be moved while animating
							Rectangle rect = header.getHeaderRect(columnIndex);
							Rectangle damaged = new Rectangle(rect.x - 5,
									rect.y, rect.width + 10, rect.height);
							header.repaint(damaged);
						}
					} catch (RuntimeException re) {
						return;
					}
				}
			});
		}
	}

	public StateTransitionTracker getTracker(final int columnIndex,
			boolean initialRollover, boolean initialSelected) {
		StateTransitionTracker tracker = stateTransitionMultiTracker
				.getTracker(columnIndex);
		if (tracker == null) {
			ButtonModel model = new DefaultButtonModel();
			model.setSelected(initialSelected);
			model.setRollover(initialRollover);
			tracker = new StateTransitionTracker(header, model);
			tracker.registerModelListeners();
			tracker.setRepaintCallback(new RepaintCallback() {
				@Override
				public TimelineCallback getRepaintCallback() {
					return new ColumnHeaderRepaintCallback(header, columnIndex);
				}
			});
			stateTransitionMultiTracker.addTracker(columnIndex, tracker);
		}
		return tracker;
	}

}
