/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2007 Phex Development Group
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: TreeTableModelAdapter.java 3859 2007-07-01 20:15:19Z gregork $
 */
package phex.gui.common.treetable;

import java.util.Comparator;

import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.*;
import javax.swing.tree.TreePath;

import phex.gui.common.table.FWTableModel;
import phex.gui.models.ISortableModel;

/**
 * This is a wrapper class takes a TreeTableModel and implements 
 * the table model interface. The implementation is trivial, with 
 * all of the event dispatching support provided by the superclass: 
 * the AbstractTableModel. 
 */
public class TreeTableModelAdapter extends FWTableModel implements ISortableModel
{    
    private TreeTableModel model; // immutable
    private final JTree tree; // immutable
    private JTreeTable treeTable = null; // logically immutable
    
    
    private TreeModelListener treeModelListener;
    /**
     * Maintains a TreeTableModel and a JTree as purely implementation details.
     * Developers can plug in any type of custom TreeTableModel through a
     * JXTreeTable constructor or through setTreeTableModel().
     *
     * @param model Underlying data model for the JXTreeTable that will ultimately
     * be bound to this TreeTableModelAdapter
     * @param tree TreeTableCellRenderer instantiated with the same model as
     * specified by the model parameter of this constructor
     * @throws IllegalArgumentException if a null model argument is passed
     * @throws IllegalArgumentException if a null tree argument is passed
     */
    TreeTableModelAdapter(TreeTableModel model, JTree tree) {
        assert model != null;
        assert tree != null;

        this.tree = tree; // need tree to implement getRowCount()
        setTreeTableModel(model);

        tree.addTreeExpansionListener(new TreeExpansionListener() {
            // Don't use fireTableRowsInserted() here; the selection model
            // would get updated twice.
            public void treeExpanded(TreeExpansionEvent event) {
                fireTableDataChanged();
            }

            public void treeCollapsed(TreeExpansionEvent event) {
                fireTableDataChanged();
            }
        });
    }

    /**
     * 
     * @param model must not be null!
     */
    public void setTreeTableModel(TreeTableModel model) {
        TreeTableModel old = getTreeTableModel();
        if (old != null) {
            old.removeTreeModelListener(getTreeModelListener());
        }
        this.model = model;
        // Install a TreeModelListener that can update the table when
        // tree changes. 
        model.addTreeModelListener(getTreeModelListener());
        fireTableStructureChanged();
    }

    /**
     * @return
     */
    private TreeModelListener getTreeModelListener() {
        if (treeModelListener == null) {
            treeModelListener = new TreeModelListener() {
                // We use delayedFireTableDataChanged as we can
                // not be guaranteed the tree will have finished processing
                // the event before us.
                public void treeNodesChanged(TreeModelEvent e) {
                    delayedFireTableDataChanged(e, 0);
                }

                public void treeNodesInserted(TreeModelEvent e) {
                    delayedFireTableDataChanged(e, 1);
                }

                public void treeNodesRemoved(TreeModelEvent e) {
                    delayedFireTableDataChanged(e, 2);
                }

                public void treeStructureChanged(TreeModelEvent e) {
                    delayedFireTableDataChanged();
                }
            };
        }
        return treeModelListener;
    }

    /**
     * Returns the real TreeTableModel that is wrapped by this TreeTableModelAdapter.
     *
     * @return the real TreeTableModel that is wrapped by this TreeTableModelAdapter
     */
    public TreeTableModel getTreeTableModel() {
        return model;
    }

    /**
     * Returns the JXTreeTable instance to which this TreeTableModelAdapter is
     * permanently and exclusively bound. For use by
     * {@link org.jdesktop.swingx.JXTreeTable#setModel(javax.swing.table.TableModel)}.
     *
     * @return JXTreeTable to which this TreeTableModelAdapter is permanently bound
     */
    protected JTreeTable getTreeTable() {
        return treeTable;
    }

    /**
     * Immutably binds this TreeTableModelAdapter to the specified JXTreeTable.
     *
     * @param treeTable the JXTreeTable instance that this adapter is bound to.
     */
    protected final void bind(JTreeTable treeTable) {
        // Suppress potentially subversive invocation!
        // Prevent clearing out the deck for possible hijack attempt later!
        if (treeTable == null) {
            throw new IllegalArgumentException("null treeTable");
        }

        if (this.treeTable == null) {
            this.treeTable = treeTable;
        }
        else {
            throw new IllegalArgumentException("adapter already bound");
        }
    }

    // Wrappers, implementing TableModel interface.
    // TableModelListener management provided by AbstractTableModel superclass.

    public Class getColumnClass(int column) {
        return model.getColumnClass(column);
    }

    public int getColumnCount() {
        return model.getColumnCount();
    }

    public String getColumnName(int column) {
        return model.getColumnName(column);
    }

    public int getRowCount() {
        return tree.getRowCount();
    }

    public Object getValueAt(int row, int column) {
        return model.getValueAt(nodeForRow(row), column);
    }

    public boolean isCellEditable(int row, int column) {
        return model.isCellEditable(nodeForRow(row), column);
    }

    public void setValueAt(Object value, int row, int column) {
        model.setValueAt(value, nodeForRow(row), column);
    }

    protected Object nodeForRow(int row) {
        return tree.getPathForRow(row).getLastPathComponent();
    }

    /**
     * Invokes fireTableDataChanged after all the pending events have been
     * processed. SwingUtilities.invokeLater is used to handle this.
     */
    private void delayedFireTableDataChanged() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                fireTableDataChanged();
            }
        });
    }

    /**
     * Invokes fireTableDataChanged after all the pending events have been
     * processed. SwingUtilities.invokeLater is used to handle this.
     */
    private void delayedFireTableDataChanged(final TreeModelEvent tme, final int typeChange) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                int indices[] = tme.getChildIndices();
                TreePath path = tme.getTreePath();
                if (indices != null) { 
                    if (tree.isExpanded(path)) { // Dont bother to update if the parent 
                                                // node is collapsed
                        int startingRow = tree.getRowForPath(path)+1;
                        int min = Integer.MAX_VALUE;
                        int max = Integer.MIN_VALUE;
                        for (int i=0;i<indices.length;i++) {
                            if (indices[i] < min) {
                                min = indices[i];
                            }
                            if (indices[i] > max) {
                                max = indices[i];
                            }
                        }
                        switch (typeChange) {
                            case 0 :
                                fireTableRowsUpdated(startingRow + min, startingRow+max);
                            break;
                            case 1: 
                                fireTableRowsInserted(startingRow + min, startingRow+max);
                            break;
                            case 2:
                                fireTableRowsDeleted(startingRow + min, startingRow+max);
                            break;
                        }
                    } else { 
                    // not expanded - but change might effect appearance of parent
                    // Issue #82-swingx
                        int row = tree.getRowForPath(path);
                        // fix Issue #247-swingx: prevent accidental structureChanged
                        // for collapsed path 
                        // in this case row == -1, which == TableEvent.HEADER_ROW
                        if (row >= 0) fireTableRowsUpdated(row, row);
                    }
                }
                else {  // case where the event is fired to identify root.
                    fireTableDataChanged();
                }
            }
        });
    }
   

    // Wrappers, implementing TableModel interface. 

    /**
      * Returns the most comparator that is used for sorting of the cell values
      * in the column. This is used by the FWSortedTableModel to perform the
      * sorting. If not overwritten the method returns null causing the
      * FWSortedTableModel to use a NaturalComparator. It expects all Objects that
      * are returned from getComparableValueAt() to implement the Comparable interface.
      *
      */
    public Comparator getColumnComparator(int column)
    {
        return model.getColumnComparator(column);
    }

    /**
     * Returns an attribute value that is used for comparing on sorting
     * for the cell at row and column. If not overwritten the call is forwarded
     * to getValueAt().
     * The returned Object is compared via the Comparator returned from
     * getColumnComparator(). If no comparator is specified the returned Object
     * must implement the Comparable interface.
     */
    public Object getComparableValueAt(int row, int column)
    {
        return model.getComparableValueAt(nodeForRow(row), column);
    }

    /**
     * Indicates if a column is hideable.
     */
    @Override
    public boolean isColumnHideable( int columnIndex )
    {
        return model.isColumnHideable( columnIndex );
    }
    
    /**
     * Indicates if a column is visible by default.
     */
    @Override
    public boolean isColumnDefaultVisible( int columnIndex )
    {
        return model.isColumnDefaultVisible( columnIndex );
    }
    
    @Override
    public Object getColumnId( int columnIndex )
    {
        return model.getColumnId( columnIndex );
    }

    @Override
    public void fireTableDataChanged()
    {
        super.fireTableDataChanged();
    }

    /**
     * @see phex.gui.models.ISortableModel#getSortByColumn()
     */
    public int getSortByColumn()
    {
        if ( model instanceof ISortableModel )
        {
            return ((ISortableModel)model).getSortByColumn(); 
        }
        else
        {
            throw new UnsupportedOperationException( "TreeTableModel not an ISortableModel." );
        }
    }

    /* (non-Javadoc)
     * @see phex.gui.models.ISortableModel#isSortedAscending()
     */
    public boolean isSortedAscending()
    {
        if ( model instanceof ISortableModel )
        {
            return ((ISortableModel)model).isSortedAscending(); 
        }
        else
        {
            throw new UnsupportedOperationException( "TreeTableModel not an ISortableModel." );
        }
    }

    /* (non-Javadoc)
     * @see phex.gui.models.ISortableModel#sortByColumn(int, boolean)
     */
    public void sortByColumn(int column, boolean isSortedAscending)
    {
        if ( model instanceof ISortableModel )
        {
            ((ISortableModel)model).sortByColumn( column, isSortedAscending);
        }
        else
        {
            throw new UnsupportedOperationException( "TreeTableModel not an ISortableModel." );
        }
    }
}
