/*******************************************************************************
 * This file is part of GECAMed.
 * 
 * GECAMed is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License (L-GPL) as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * GECAMed 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 (L-GPL)
 * along with GECAMed.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * GECAMed is Copyrighted by the Centre de Recherche Public Henri Tudor (http://www.tudor.lu)
 * (c) CRP Henri Tudor, Luxembourg, 2008
 *******************************************************************************/
package lu.tudor.santec.gecamed.core.gui.widgets.searchtableheader;

import java.awt.Color;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Iterator;
import java.util.Vector;

import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

//***************************************************************************
//* Class Definition and Members                                            *
//***************************************************************************

/**
 * The SearchTableHeader implements a table header that can define a editable
 * component per table column allowing to specify search criteria for that
 * particual table column. The SearchTableHeader provides the basic functionality
 * to implement this kind of headers.
 * @author nico.mack@tudor.lu
 * @author johannes.hermen@tudor.lu
 */

public class SearchTableHeader extends MouseAdapter implements TableColumnModelListener, 
															   FocusListener,
															   KeyListener
	{
    protected JTable			  m_Table;
    protected JTableHeader        m_TableHeader;
    protected JComponent          m_EditingComponent;
    protected TableColumnModel    m_ColumnModel;
    
    private int              	  m_CurrentlyEditingColumn 	 = -1;
    private int              	  m_NewEditingColumn 		 = -1;
    
    private Vector <PropertyChangeListener>	m_PropertyListeners;
    private Vector <SearchColumnAttributes>	m_SearchColumns;
     
//---------------------------------------------------------------------------
//***************************************************************************
//* Class Constants                                                         *
//***************************************************************************
//---------------------------------------------------------------------------

    protected static final String	c_EmptyValue = "";
 
	public static final String c_ExclusiveCriterionChanged		= "SearchTableHeader.ExclusiveCriterionChanged";
	public static final String c_NonExclusiveCriterionChanged	= "SearchTableHeader.NonExclusiveCriterionChanged";

//---------------------------------------------------------------------------
//***************************************************************************
//* Constructor(s)                                                          *
//***************************************************************************
//---------------------------------------------------------------------------

public SearchTableHeader (JTable p_Table) 
	{
    int	l_Index;
	
	m_Table = p_Table;
    
    // Table Header need to detect double clicks
    
    m_TableHeader = m_Table.getTableHeader();
    m_TableHeader.addMouseListener(this);
           
    // Need to know if column events occur
           
    m_ColumnModel = m_Table.getColumnModel(); 
    m_ColumnModel.addColumnModelListener(this);      
    
    m_SearchColumns = new Vector <SearchColumnAttributes> ();
    
    for (l_Index = 0; l_Index < m_ColumnModel.getColumnCount(); l_Index++)
    	{
    	m_SearchColumns.add( new SearchColumnAttributes ());
    	}
        
    m_PropertyListeners = new Vector<PropertyChangeListener>();
	} 

//---------------------------------------------------------------------------
//***************************************************************************
//* Primitives                                       						*
//***************************************************************************
//---------------------------------------------------------------------------
/**
 * method takes care of the housekeeping when search criteria are being reset.
 * Housekeeping chores consist first of all in fireing property change events
 * for all columns that were searching that search has been canceled. Next
 * we have to remove currently editing search component, if any. Last but not
 * least we have to reset table headers so that all search related changes to
 * table header are no longer visible.
 */
//---------------------------------------------------------------------------

private void resetSearch ()
	{
	int 					l_Column;
	SearchColumnAttributes	l_SearchColumn;
	
	for (l_Column = 0; l_Column < m_SearchColumns.size(); l_Column++)
   		{
		l_SearchColumn = m_SearchColumns.elementAt(l_Column);
		if (l_SearchColumn.isSearching()) fireSearchCriterionChanged (l_Column, null);
   		l_SearchColumn.setSearching(false);
   		}
	
	if (m_EditingComponent != null) 
		{
		m_TableHeader.remove(m_EditingComponent);
		m_EditingComponent = null;
		}
	
	resetTableHeaders();
    m_CurrentlyEditingColumn = -1;
    m_NewEditingColumn		 = -1;
	}

//---------------------------------------------------------------------------
/**
 * Returns the search column attributes for the specified column.
 * @param p_Column specifies the column to get search attributes for.
 * @return the search column attributes for the specified column. Method
 * returns <code>null</code> if specified column is out of range.
 */
//---------------------------------------------------------------------------

private SearchColumnAttributes getSearchColumn (int p_Column)
	{
	SearchColumnAttributes l_SearchColumn = null;
	
	if ((p_Column >= 0) && (p_Column < m_SearchColumns.size()))
		{
		l_SearchColumn = m_SearchColumns.elementAt(p_Column);
		}

	return l_SearchColumn;
	}
	
//---------------------------------------------------------------------------
//***************************************************************************
//* Class Body                                      						*
//***************************************************************************
//---------------------------------------------------------------------------
/**
 * Method is part of the FocusListener interface
 */
//---------------------------------------------------------------------------

public void focusGained (FocusEvent p_Event) 
	{ 
	m_CurrentlyEditingColumn = m_NewEditingColumn;
	if (m_EditingComponent != null) m_EditingComponent.repaint();
	}
       
//---------------------------------------------------------------------------
/**
 * Method is part of the FocusListener interface
 */
//---------------------------------------------------------------------------
 
public void focusLost (FocusEvent p_Event) 
	{
	if ((m_CurrentlyEditingColumn >= 0) && !p_Event.isTemporary()) this.endEditing();
    }
       
//---------------------------------------------------------------------------
/**
 * Method is part of the TableColumnModelListener. When columnAdded method
 * is called, editing will be ended. Things get ugly if header of column 
 * being edited is moved. 
 */
//---------------------------------------------------------------------------
       
public void columnAdded (TableColumnModelEvent p_Event) 
	{
	this.endEditing();
    }

//---------------------------------------------------------------------------
/**
 * Method is part of the TableColumnModelListener. When columnMarginChanged method
 * is called, bounds of editing component will have to be adjusted so that it
 * is properly displayed.
 */
//---------------------------------------------------------------------------

public void columnMarginChanged (ChangeEvent p_Event) 
	{
	if ((m_CurrentlyEditingColumn >= 0) && (m_EditingComponent != null))
		m_EditingComponent.setBounds(m_TableHeader.getHeaderRect(m_CurrentlyEditingColumn));
	}

//---------------------------------------------------------------------------
/**
 * Method is part of the TableColumnModelListener. When columnMoved method
 * is called, editing will be ended. Things get ugly if header of column 
 * being edited is moved. 
 */
//---------------------------------------------------------------------------

public void columnMoved (TableColumnModelEvent p_Event) 
	{
	this.endEditing();
    }

//---------------------------------------------------------------------------
/**
 * Method is part of the TableColumnModelListener. When columnRemoved method
 * is called, editing will be ended. Things get ugly if header of column 
 * being edited is moved. 
 */
//---------------------------------------------------------------------------

public void columnRemoved (TableColumnModelEvent p_Event) 
	{
	this.endEditing();
    }

//---------------------------------------------------------------------------
/**
 * Method is part of the TableColumnModelListener. Method does nothing in the
 * scope of this class.
 */
//---------------------------------------------------------------------------

public void columnSelectionChanged(ListSelectionEvent p_Event) 
	{
	}

//---------------------------------------------------------------------------
/**
 * Method is part of the KeylListener interface. Method does nothing in the
 * scope of this class.
 */
//---------------------------------------------------------------------------

public void keyTyped (KeyEvent p_Event) 
	{
	//if (p_Event.getKeyCode() == KeyEvent.VK_ENTER) p_Event.consume();
	}

//---------------------------------------------------------------------------
/**
 * Method is part of the KeylListener interface. Pressing the Escape key should
 * reset search.
 */
//---------------------------------------------------------------------------

public void keyPressed (KeyEvent p_Event) 
	{
	//if (p_Event.getKeyCode() == KeyEvent.VK_ENTER) p_Event.consume();
	if (p_Event.getKeyCode() == KeyEvent.VK_ESCAPE)
		{
		this.resetSearch();
		}
	}

//---------------------------------------------------------------------------
/**
 * Method is part of the KeylListener interface. Method does nothing in the
 * scope of this class.
 */
//---------------------------------------------------------------------------

public void keyReleased (KeyEvent p_Event) 
	{
//	JComponent	l_Source;
//	
//	if (  (p_Event.getKeyCode() == KeyEvent.VK_ENTER)
//		|| (p_Event.getKeyCode() == KeyEvent.VK_TAB))
//		{
//		if (p_Event.getSource() instanceof JComponent)
//			{
//			l_Source = (JComponent) p_Event.getSource();
//			l_Source.transferFocus();
//			p_Event.consume();
//			}
//		}
	}
//---------------------------------------------------------------------------
/**
 * Method is part of the MouseListener interface. When the mouse is clicked,
 * a couple of things need to be done. If the click was not inside the currently
 * editing column header then we have to end current edit first. Next, if click
 * occured inside a column header which is searchable and enabled then we
 * start editing. If clicked search column is mutual exclusive, then we have to
 * reset table headers first to make sure only newly specified search criterion
 * is visible.
 * @param p_Event specifies the mouse clicked event to be processed.
 */
//---------------------------------------------------------------------------

public void mouseClicked( MouseEvent p_Event) 
	{
	int						l_Column;
	SearchColumnAttributes	l_SearchColumn;
	
    if (m_TableHeader == null) return;
    
    l_Column = m_TableHeader.columnAtPoint(p_Event.getPoint());
    
    if (l_Column != m_CurrentlyEditingColumn) this.endEditing();
    
    if ((l_Column >= 0) && (p_Event.getButton() == MouseEvent.BUTTON3)) 
    	{
    	l_SearchColumn = m_SearchColumns.elementAt(l_Column);
    	if (l_SearchColumn.isEnabled())
     		{
    	   	if (l_SearchColumn.isMutualExclusive()) resetTableHeaders();
       
    		m_NewEditingColumn = l_Column; 
      		
    		m_EditingComponent = this.getEditingComponent (m_NewEditingColumn);
    		m_EditingComponent.addFocusListener(this);
                   
    		m_TableHeader.add(m_EditingComponent);                   
    		m_EditingComponent.setBounds(m_TableHeader.getHeaderRect(m_NewEditingColumn));
      		m_TableHeader.resizeAndRepaint();
      		m_EditingComponent.requestFocus();
    		
       		l_SearchColumn.setSearching(true);
     		}
     	}
	} 

//---------------------------------------------------------------------------
/**
 * the endEditing method retrieves the value from the editable column header.
 * Depending on whether retrieved value is empty or not, the method either
 * resets column header to original appearance or, if a value was specified,
 * the method puts column header in bold and appends the specifies search
 * criterion to it. Last but not least, the methods fires a property change
 * event, notifying listeners about the fact that a specific search criterion 
 * changed.
 */
//---------------------------------------------------------------------------
      
public void endEditing() 
	{
    Object					l_Criterion;
	String					l_SearchText = c_EmptyValue;
	TableColumn				l_Column;
    int						l_CurrentlyEditingColumn;
	
    if (m_CurrentlyEditingColumn >= 0) 
    	{
        l_CurrentlyEditingColumn = m_CurrentlyEditingColumn;
        m_CurrentlyEditingColumn = -1;
        m_NewEditingColumn		 = -1;
        
        l_Criterion	 = this.getEditingComponentValue (m_EditingComponent, l_CurrentlyEditingColumn);
    	l_SearchText = this.getEditingComponentText  (m_EditingComponent, l_CurrentlyEditingColumn);            	
        l_Column = m_ColumnModel.getColumn(l_CurrentlyEditingColumn);
        
        if (l_SearchText.equals(c_EmptyValue))
        	{
        	l_Column.setHeaderValue( m_Table.getModel().getColumnName(l_CurrentlyEditingColumn));
        	}
        else 
        	{
        	l_Column.setHeaderValue("<html><b>" + m_Table.getModel().getColumnName(l_CurrentlyEditingColumn)+": " + l_SearchText +"</b></html>");
        	}
                  
        if (m_EditingComponent != null) 
			{
    		m_TableHeader.remove(m_EditingComponent);
    		m_EditingComponent = null;
			}
        
        m_TableHeader.repaint(m_TableHeader.getHeaderRect(l_CurrentlyEditingColumn));
       
        fireSearchCriterionChanged (l_CurrentlyEditingColumn, l_Criterion);
        }
	}

//---------------------------------------------------------------------------
/**
 * Resets table headers to their original appearance.
 */
//---------------------------------------------------------------------------

public void resetTableHeaders() 
	{
    int						l_NumberOfColumns;
	int						l_ColumnIndex;
	TableColumn				l_Column;
	SearchColumnAttributes	l_SearchColumn;
	
    l_NumberOfColumns = m_ColumnModel.getColumnCount();
    
    for (l_ColumnIndex = 0; l_ColumnIndex < l_NumberOfColumns; l_ColumnIndex++) 
    	{
    	if (this.isMutualExclusive(l_ColumnIndex))
    		{
    		l_Column = m_ColumnModel.getColumn(l_ColumnIndex);
    		l_Column.setHeaderValue(m_Table.getModel().getColumnName(l_ColumnIndex));
    	
    		l_SearchColumn = this.getSearchColumn(l_ColumnIndex);
    		if (l_SearchColumn != null) l_SearchColumn.setSearching(false);  	
    		}
    	}
 
    m_TableHeader.repaint();
    }

//---------------------------------------------------------------------------
/**
 * Enables or disables the specified search column.
 * @param p_Column specifies the column to either enable or disable.
 * @param p_EnableIt specifies whether to enable or disable the specified 
 * column.
 */
//---------------------------------------------------------------------------

public void setEnabled (int p_Column, boolean p_EnableIt)
	{
	SearchColumnAttributes l_SearchColumn;
	
	l_SearchColumn = this.getSearchColumn(p_Column);
	if (l_SearchColumn != null) l_SearchColumn.setEnabled(p_EnableIt);
	}

//---------------------------------------------------------------------------
/**
 * Checks whether specified column is enabled or not.
 * @return <code>true</code> if specified column is enabled, <code>false</code>
 * otherwise.
 */
//---------------------------------------------------------------------------

public boolean isEnabled (int p_Column)
	{
	SearchColumnAttributes l_SearchColumn;
	
	l_SearchColumn = this.getSearchColumn(p_Column);
	if (l_SearchColumn != null) return l_SearchColumn.isEnabled();
	else return false;
	}

//---------------------------------------------------------------------------
/**
 * Turns the specified search column either into a mutual exclusive or inclusive
 * column. If a search column is mutual exclusive, then if this column is
 * searching, all other columns will be reset.
 * @param p_Column specifies the column to set mutual exclusivness for.
 * @param p_EnableIt specifies whether to enable or disable mutual exclusivness
 * for specified column. Specify <code>true</code> to make specified column
 * a mutual exclusive column, <code>false</code> otherwise.
 */
//---------------------------------------------------------------------------

public void setMutualExclusive (int p_Column, boolean p_EnableIt)
	{
	SearchColumnAttributes l_SearchColumn;
	
	l_SearchColumn = this.getSearchColumn(p_Column);
	if (l_SearchColumn != null) l_SearchColumn.setMutualExclusive(p_EnableIt);
	}

//---------------------------------------------------------------------------
/**
 * Checks whether specified column is mutual exclusive or not.
 * @return <code>true</code> if specified column is mutual exclusive, <code>false</code>
 * otherwise.
 */
//---------------------------------------------------------------------------

public boolean isMutualExclusive (int p_Column)
	{
	SearchColumnAttributes l_SearchColumn;
	
	l_SearchColumn = this.getSearchColumn(p_Column);
	if (l_SearchColumn != null) return l_SearchColumn.isMutualExclusive();
	else return false;
	}

//---------------------------------------------------------------------------
/**
 * Checks whether specified column is currenlty searching or not.
 * @return <code>true</code> if specified column is currenlty searching, 
 * <code>false</code> otherwise.
 */
//---------------------------------------------------------------------------

public boolean isSearchColumn (int p_Column)
	{
	SearchColumnAttributes l_SearchColumn;
	
	l_SearchColumn = this.getSearchColumn(p_Column);
	if (l_SearchColumn != null) return l_SearchColumn.isSearching();
	else return false;
	}

//---------------------------------------------------------------------------
/**
 * Returns the editing component for the specified column.
 * @param p_Column specifies the column to get editing component for.
 * @return the component being used to edit specified columns search criterion.
 */
//---------------------------------------------------------------------------

protected JComponent getEditingComponent (int p_Column) 
	{
 	JTextField l_TextField = new JTextField();  
		           
 	l_TextField.setForeground(Color.BLUE);
 	l_TextField.addKeyListener(this);
  	
 	return l_TextField;
	}

//---------------------------------------------------------------------------
/**
 * Returns the value of the editing component.
 * @param p_EditingComponent specifies the editing component to get value of.
 * @param p_Column specifies the column to get value of editing component of.
 * @return the value of the specified specified editing component.
 */
//---------------------------------------------------------------------------

protected Object getEditingComponentValue (JComponent p_EditingComponent, int p_Column)
	{
	Object l_Value = null;
	
	if (p_EditingComponent == null) return null;
    
    if (p_EditingComponent instanceof JTextField) 
		{
		l_Value = ((JTextField)p_EditingComponent).getText();
		} 
	
	return l_Value;
	}

//---------------------------------------------------------------------------
/**
 * Returns a textual representation of the editing component.
 * @param p_EditingComponent specifies the editing component to get textual 
 * representation of.
 * @param p_Column specifies the column to get textual representation of editing 
 * component.
 * @return textual representation of the specified editing component. In case
 * value of editing component was <code>null</code>, the method returns
 * <code>c_EmptyValue</code> value.
 */
//---------------------------------------------------------------------------

protected String getEditingComponentText (JComponent p_EditingComponent, int p_Column)
	{
	Object l_Value;
	
	l_Value = this.getEditingComponentValue(p_EditingComponent, p_Column);
	if (l_Value == null) return c_EmptyValue;
					else return l_Value.toString();
	}


//---------------------------------------------------------------------------
/**
 * Adds the specified property change listener to the list of listeners
 * who would like to be notified about search criterion changes.
 * @param p_Listener specifies the property change listener to add.
 */
//---------------------------------------------------------------------------
      
public void addPropertyChangeListener (PropertyChangeListener p_Listener) 
	{
    m_PropertyListeners.add (p_Listener);
    }
       
//---------------------------------------------------------------------------
/**
 * Removes the specified property change listener from the list of listeners
 * who would like to be notified about search criterion changes.
 * @param p_Listener specifies the property change listener to remove.
 */
//---------------------------------------------------------------------------

public void removePropertyChangeListener (PropertyChangeListener p_Listener) 
	{
	m_PropertyListeners.remove (p_Listener);
    }
       
//---------------------------------------------------------------------------
/**
 * Notifies registered property change listeners about changes regarding
 * the search criterion for the specified column.
 * @param p_Column specifies column that would like to signalize a change
 * of its search criterion.
 * @param p_Criterion specifies the changed value of the search criterion.
 */
//---------------------------------------------------------------------------

private void fireSearchCriterionChanged (Integer p_Column, Object p_Criterion) 
	{
	SearchColumnAttributes			  l_SearchColumn;
	Iterator <PropertyChangeListener> l_ListenerIterator;
	PropertyChangeListener			  l_Listener;
	PropertyChangeEvent				  l_Event;
	
	l_SearchColumn = this.getSearchColumn(p_Column);
	if (l_SearchColumn == null) return;
	
	if (l_SearchColumn.isMutualExclusive())
		{	
		l_Event = new PropertyChangeEvent(this, c_ExclusiveCriterionChanged, 
										    	p_Column, 
										    	p_Criterion);
		}
	else
		{
		l_Event = new PropertyChangeEvent(this, c_NonExclusiveCriterionChanged, 
		    									p_Column, 
		    									p_Criterion);
		}
	
	if (p_Criterion == null)
		{
        l_SearchColumn = m_SearchColumns.elementAt(p_Column);
    	if (l_SearchColumn.isEnabled()) l_SearchColumn.setSearching(false);
		}
		
	l_ListenerIterator = m_PropertyListeners.iterator();
	
	while (l_ListenerIterator.hasNext())
		{
		l_Listener = l_ListenerIterator.next();
		l_Listener.propertyChange(l_Event);
		}
	}
     
//***************************************************************************
//* End Of Class                                                            *
//***************************************************************************
}

