/*
 *    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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * SortedTableModel.java
 * Copyright (C) 2005-2010 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui;

import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;

import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

import weka.core.ClassDiscovery;

/**
 * Represents a TableModel with sorting functionality.
 *
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 7059 $
 */

public class SortedTableModel
  extends AbstractTableModel
  implements TableModelListener {

  /** for serialization */
  static final long serialVersionUID = 4030907921461127548L;

  /**
   * Helper class for sorting the columns.
   */
  public static class SortContainer
    implements Comparable<SortContainer> {

    /** the value to sort. */
    protected Comparable m_Value;

    /** the index of the value. */
    protected int m_Index;

    /**
     * Initializes the container.
     *
     * @param value	the value to sort on
     * @param index	the original index
     */
    public SortContainer(Comparable value, int index) {
      super();

      m_Value = value;
      m_Index = index;
    }

    /**
     * Returns the value to sort on.
     *
     * @return		the value
     */
    public Comparable getValue() {
      return m_Value;
    }

    /**
     * Returns the original index of the item.
     *
     * @return		the index
     */
    public int getIndex() {
      return m_Index;
    }

    /**
     * Compares this object with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object. Null is considered
     * smallest. If both values are null, then 0 is returned.
     *
     * @param o 	the object to be compared.
     * @return  	a negative integer, zero, or a positive integer as this object
     *			is less than, equal to, or greater than the specified object.
     * @throws ClassCastException 	if the specified object's type prevents it
     *         				from being compared to this object.
     */
    public int compareTo(SortContainer o) {
      if ((m_Value == null) || (o.getValue() == null)) {
	if (m_Value == o.getValue())
	  return 0;
	if (m_Value == null)
	  return -1;
	else
	  return +1;
      }
      else {
	return m_Value.compareTo(o.getValue());
      }
    }

    /**
     * Indicates whether some other object is "equal to" this one.
     *
     * @param obj	the reference object with which to compare.
     * @return		true if this object is the same as the obj argument;
     * 			false otherwise.
     * @throws ClassCastException 	if the specified object's type prevents it
     *         				from being compared to this object.
     */
    public boolean equals(Object obj) {
      return (compareTo((SortContainer) obj) == 0);
    }

    /**
     * Returns a string representation of the sort container.
     *
     * @return		the string representation (value + index)
     */
    public String toString() {
      return Messages.getInstance().getString("SortedTableModel_ToString_Text_First") + m_Value + Messages.getInstance().getString("SortedTableModel_ToString_Text_Second") + m_Index;
    }
  }

  /** the actual table model */
  protected TableModel mModel;

  /** the mapping between displayed and actual index */
  protected int[] mIndices;

  /** the sort column */
  protected int mSortColumn;

  /** whether sorting is ascending or descending */
  protected boolean mAscending;

  /**
   * initializes with no model
   */
  public SortedTableModel() {
    this(null);
  }

  /**
   * initializes with the given model
   *
   * @param model       the model to initialize the sorted model with
   */
  public SortedTableModel(TableModel model) {
    setModel(model);
  }

  /**
   * sets the model to use
   *
   * @param value       the model to use
   */
  public void setModel(TableModel value) {
    mModel = value;

    // initialize indices
    if (mModel == null) {
      mIndices = null;
    }
    else {
      initializeIndices();
      mSortColumn = -1;
      mAscending  = true;
      mModel.addTableModelListener(this);
    }
  }

  /**
   * (re-)initializes the indices
   */
  protected void initializeIndices() {
    int       i;

    mIndices = new int[mModel.getRowCount()];
    for (i = 0; i < mIndices.length; i++)
      mIndices[i] = i;
  }

  /**
   * returns the current model, can be null
   *
   * @return            the current model
   */
  public TableModel getModel() {
    return mModel;
  }

  /**
   * returns whether the table was sorted
   *
   * @return        true if the table was sorted
   */
  public boolean isSorted() {
    return (mSortColumn > -1);
  }

  /**
   * whether the model is initialized
   *
   * @return            true if the model is not null and the sort indices
   *                    match the number of rows
   */
  protected boolean isInitialized() {
    return (getModel() != null);
  }

  /**
   * Returns the actual underlying row the given visible one represents. Useful
   * for retrieving "non-visual" data that is also stored in a TableModel.
   *
   * @param visibleRow	the displayed row to retrieve the original row for
   * @return		the original row
   */
  public int getActualRow(int visibleRow) {
    if (!isInitialized())
      return -1;
    else
      return mIndices[visibleRow];
  }

  /**
   * Returns the most specific superclass for all the cell values in the
   * column.
   *
   * @param columnIndex     the index of the column
   * @return                the class of the specified column
   */
  public Class getColumnClass(int columnIndex) {
    if (!isInitialized())
      return null;
    else
      return getModel().getColumnClass(columnIndex);
  }

  /**
   * Returns the number of columns in the model
   *
   * @return          the number of columns in the model
   */
  public int getColumnCount() {
    if (!isInitialized())
      return 0;
    else
      return getModel().getColumnCount();
  }

  /**
   * Returns the name of the column at columnIndex
   *
   * @param columnIndex   the column to retrieve the name for
   * @return              the name of the specified column
   */
  public String getColumnName(int columnIndex) {
    if (!isInitialized())
      return null;
    else
      return getModel().getColumnName(columnIndex);
  }

  /**
   * Returns the number of rows in the model.
   *
   * @return              the number of rows in the model
   */
  public int getRowCount() {
    if (!isInitialized())
      return 0;
    else
      return getModel().getRowCount();
  }

  /**
   * Returns the value for the cell at columnIndex and rowIndex.
   *
   * @param rowIndex      the row
   * @param columnIndex   the column
   * @return              the value of the sepcified cell
   */
  public Object getValueAt(int rowIndex, int columnIndex) {
    if (!isInitialized())
      return null;
    else
      return getModel().getValueAt(mIndices[rowIndex], columnIndex);
  }

  /**
   * Returns true if the cell at rowIndex and columnIndex is editable.
   *
   * @param rowIndex      the row
   * @param columnIndex   the column
   * @return              true if the cell is editable
   */
  public boolean isCellEditable(int rowIndex, int columnIndex) {
    if (!isInitialized())
      return false;
    else
      return getModel().isCellEditable(mIndices[rowIndex], columnIndex);
  }

  /**
   * Sets the value in the cell at columnIndex and rowIndex to aValue.
   *
   * @param aValue        the new value of the cell
   * @param rowIndex      the row
   * @param columnIndex   the column
   */
  public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
    if (isInitialized())
      getModel().setValueAt(aValue, mIndices[rowIndex], columnIndex);
  }

  /**
   * sorts the table over the given column (ascending)
   *
   * @param columnIndex     the column to sort over
   */
  public void sort(int columnIndex) {
    sort(columnIndex, true);
  }

  /**
   * sorts the table over the given column, either ascending or descending
   *
   * @param columnIndex     the column to sort over
   * @param ascending       ascending if true, otherwise descending
   */
  public void sort(int columnIndex, boolean ascending) {
    int       			columnType;
    int       			i;
    ArrayList<SortContainer>	sorted;
    SortContainer		cont;
    Object			value;

    // can we sort?
    if (    (!isInitialized())
         || (getModel().getRowCount() != mIndices.length) ) {

      System.out.println(
          this.getClass().getName() + Messages.getInstance().getString("SortedTableModel_Sort_Text_Firt"));

      return;
    }

    // init
    mSortColumn = columnIndex;
    mAscending  = ascending;
    initializeIndices();

    // determine the column type: 0=string/other, 1=comparable
    if (ClassDiscovery.hasInterface(Comparable.class, getColumnClass(mSortColumn)))
      columnType = 1;
    else
      columnType = 0;

    // create list for sorting
    sorted = new ArrayList<SortContainer>();
    for (i = 0; i < getRowCount(); i++) {
      value = mModel.getValueAt(mIndices[i], mSortColumn);
      if (columnType == 0)
	cont = new SortContainer((value == null) ? null : value.toString(), mIndices[i]);
      else
	cont = new SortContainer((Comparable) value, mIndices[i]);
      sorted.add(cont);
    }
    Collections.sort(sorted);

    for (i = 0; i < sorted.size(); i++) {
      if (mAscending)
	mIndices[i] = sorted.get(i).getIndex();
      else
	mIndices[i] = sorted.get(sorted.size() - 1 - i).getIndex();
    }

    sorted.clear();
    sorted = null;
  }

  /**
   * This fine grain notification tells listeners the exact range of cells,
   * rows, or columns that changed.
   *
   * @param e       the event
   */
  public void tableChanged(TableModelEvent e) {
    initializeIndices();
    if (isSorted())
      sort(mSortColumn, mAscending);

    fireTableChanged(e);
  }

  /**
   * Adds a mouselistener to the header: left-click on the header sorts in
   * ascending manner, using shift-left-click in descending manner.
   *
   * @param table       the table to add the listener to
   */
  public void addMouseListenerToHeader(JTable table) {
    final SortedTableModel modelFinal = this;
    final JTable tableFinal = table;
    tableFinal.setColumnSelectionAllowed(false);
    JTableHeader header = tableFinal.getTableHeader();

    if (header != null) {
      MouseAdapter listMouseListener = new MouseAdapter() {
        public void mouseClicked(MouseEvent e) {
          TableColumnModel columnModel = tableFinal.getColumnModel();
          int viewColumn = columnModel.getColumnIndexAtX(e.getX());
          int column = tableFinal.convertColumnIndexToModel(viewColumn);
          if (    e.getButton() == MouseEvent.BUTTON1
               && e.getClickCount() == 1
               && !e.isAltDown()
               && column != -1 ) {
            int shiftPressed = e.getModifiers() & InputEvent.SHIFT_MASK;
            boolean ascending = (shiftPressed == 0);
            modelFinal.sort(column, ascending);
          }
        }
      };

      header.addMouseListener(listMouseListener);
    }
  }
}
