package nxm.sys.libg;

import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.Vector;

import javax.swing.Icon;
import javax.swing.JLabel;
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.TableCellRenderer;
import javax.swing.table.TableColumnModel;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.Shell;
import nxm.sys.libg.LargeJTable.LargeTableModel;

/** TableSorter is a decorator for TableModels; adding sorting functionality to a supplied
    TableModel. TableSorter does not store or copy the data in its TableModel; instead it
    maintains a map from the row indexes of the view to the row indexes of the model. As
    requests are made of the sorter (like getValueAt(row, col)) they are passed to the
    underlying model after the row numbers have been translated via the internal mapping
    array. This way, the TableSorter appears to hold another copy of the table with the rows
    in a different order. <br>
    <br>
    Clicking the mouse clears any previous sorting state and advances the sorting status of
    that column through three values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to
    NOT_SORTED again). <br>
    <br>
    <i>The original version of this class was provided in the "How to Use Tables" section of "The
    Java Tutorial": <a href="http://java.sun.com/docs/books/tutorial/uiswing/components/table.html">
    http://java.sun.com/docs/books/tutorial/uiswing/components/table.html</a></i> However it has
    been heavily revised since then.

    NOTE: TableSorter does NOT currently support Java Serialization, even though it extends classes that support it.

    @author  P. Milne, B. McLean, D. van Enckevort, P. Sekhon (Java Tutorial Version 2.0, 02/27/04)
    @author  J. Czechowski IV (revised version)
 */
public class TableSorter extends AbstractTableModel implements LargeTableModel {
  /** JC4:  Implementation note:
            For performance reasons the realIndex and viewIndex maps are never generated for
            DESCENDING order, rather they are always generated for ASCENDING order. This is
            to provide for quick switching between ASCENDING and DESCENDING. When viewed in
            DESCENDING order the ASCENDING order maps are simply read in reverse (i.e. use
            size-1-index as the index into the map).

            Additionally, the maps are kept around even after switching back to NOT_SORTED
            mode so that they will be immediately available when returning to a sorted mode.
            The maps are only cleared in two situations (1) the "real" model changes and
            (2) when clearSortingState() is called.

          Suggested Future Enhancements:
            (1) Modification of a single row in a table should not result in resorting of
                the entire table.
            (2) Modification of a cell that is not in the sort-by column should have no
                affect on the sorting.
            (3) If the table remains in the NOT_SORTED state for a long time (2+ min) the
                clearSortingState() could be called to release the memory used by the maps.
   */
  /** The table is sorted in descending order. */ public static final int DESCENDING = -1;
  /** The table is not sorted.                 */ public static final int NOT_SORTED =  0;
  /** The table is sorted in ascending order.  */ public static final int ASCENDING  =  1;

  /** NOTE: this should be changed if the structure/fields of this class changes. */
  private static final long serialVersionUID = 2012021020311L; // for Serializable interface

  /** Message given when user tries to serialize an TableSorter object */
  @InternalUseOnly
  public static final String UNSERIALIZABLE_TABLESORTER_OBJECT_MESSAGE =
      "nxm.sys.libg.TableSorter does NOT currently support Java Serialization";

  /** The table                             */ private           LargeJTable        table         =           null;
  /** The "real" table model                */ private transient LargeTableModel    tableModel    =           null;
  /** Renders the header with sort arrows   */ private           JTableHeader       tableHeader   =           null;
  /** Listens for mouse events to set sort  */ private transient MouseListener      mouseListener =           null;
  /** Listens to events in the "real" model */ private transient TableModelListener modelListener =           null;
  /** Sorts the table in ASCENDING order    */ private transient Comparator<Number> sorter        =           null;
  /** UP arrow symbol                       */ private transient Symbol             upTriangle    =           null;
  /** DOWN arrow symbol                     */ private transient Symbol             dnTriangle    =           null;
  /** The maximum sortable table size       */ private           int                maxSize       =              0;
  /** Warn the user when above maxSize      */ private           boolean            warnMaxSize   =           true;
  /** The index of the sort column          */ private           long               column        =             0L;
  /** The sort direction                    */ private           int                direction     =     NOT_SORTED;
  /** Indicates if the maps are up-to-date  */ private           boolean            upToDate      =          false;
  /** Maps view - real                      */ private           Vector<Number>     realIndex     = new Vector<>();
  /** Maps real - view                      */ private           Vector<Number>     viewIndex     = new Vector<>();
  /** Indicates if sorting is allowed       */ private           boolean            allowSorting  =           true;

  /** Creates a new instance.
      @param table       The actual table.
      @param tableModel  The "real" table model that accesses the file.
      @param tableHeader Renders the header with sort arrows.
      @param maxSize     The maximum table size that is sortable.
   */
  public TableSorter(LargeJTable table, LargeTableModel tableModel,
                     JTableHeader tableHeader, int maxSize) {
    this.table         = table;
    this.mouseListener = new MouseHandler();
    this.modelListener = new TableModelHandler();
    this.sorter        = new TableValueComparator();
    this.maxSize       = maxSize;
    this.warnMaxSize   = (maxSize > 0);
    this.upTriangle    = new Symbol.UTriangle(); upTriangle.setFilled(true);
    this.dnTriangle    = new Symbol.DTriangle(); dnTriangle.setFilled(true);
    setTableHeader(tableHeader);
    setTableModel(tableModel);
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // LargeTableModel Methods
  //////////////////////////////////////////////////////////////////////////////////////////////////
  @Override
  public void setValueAt(Object val, long row, long col) {
    tableModel.setValueAt(val, toReal(row), col);
  }

  @Override
  public boolean isCellEditable(long row, long col) {
    return tableModel.isCellEditable(toReal(row), col);
  }

  @Override
  public Object getValueAt(long row, long col) {
    return tableModel.getValueAt(toReal(row), col);
  }

  @Override
  public int getRowCount() {
    return tableModel.getRowCount();
  }

  @Override
  public int getColumnCount() {
    return tableModel.getColumnCount();
  }

  @Override
  public String getColumnName(int column) {
    return tableModel.getColumnName(column);
  }

  @Override
  public Class<?> getColumnClass(int column) {
    return tableModel.getColumnClass(column);
  }

  @Override
  public boolean isCellEditable(int rowIndex, int columnIndex) {
    return isCellEditable(table.getRealRowIndex(rowIndex), table.getRealColIndex(columnIndex));
  }

  @Override
  public Object getValueAt(int rowIndex, int columnIndex) {
    return getValueAt(table.getRealRowIndex(rowIndex), table.getRealColIndex(columnIndex));
  }

  @Override
  public void setValueAt(Object val, int rowIndex, int columnIndex) {
    setValueAt(val, table.getRealRowIndex(rowIndex), table.getRealColIndex(columnIndex));
  }

  @Override
  public void fireNeedsRedraw(long top, long btm) {
    if (isSorting()) {
      getTableModel().fireNeedsRedraw(-1L, -1L);
    } else {
      getTableModel().fireNeedsRedraw(top, btm);
    }
  }
  @Override
  public void fireRowsInserted(long top, long btm) {
    if (isSorting()) {
      /* It's faster to refresh all the rows than to figure out which
         rows need to be refreshed based on their current ordering.
      */
      getTableModel().fireRowsInserted(-1L, -1L);
    } else {
      getTableModel().fireRowsInserted(top, btm);
    }
  }

  @Override
  public void fireRowsDeleted(long top, long btm) {
    if (isSorting()) {
      /* It's faster to refresh all the rows than to figure out which
         rows need to be refreshed based on their current ordering.
      */
      getTableModel().fireRowsDeleted(-1L, -1L);
    } else {
      getTableModel().fireRowsDeleted(top, btm);
    }
  }

  @Override
  public void fireStructureChanged() {
    getTableModel().fireStructureChanged();
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Public Methods
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Gets the "normal" table model.
   *  @return the "normal" table model */
  public LargeTableModel getTableModel() {
    return tableModel;
  }

  /** Gets the table header.
   *  @return the table header */
  public JTableHeader getTableHeader() {
    return tableHeader;
  }

  /** Sets the table header.
   *  @param tableHeader the table header */
  public void setTableHeader(JTableHeader tableHeader) {
    if (this.tableHeader != null) {
      this.tableHeader.removeMouseListener(mouseListener);
      TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer();
      if (defaultRenderer instanceof SortableHeaderRenderer) {
        this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).renderer);
      }
    }
    this.tableHeader = tableHeader;
    if (this.tableHeader != null) {
      this.tableHeader.addMouseListener(mouseListener);
      this.tableHeader.setDefaultRenderer(
              new SortableHeaderRenderer(this.tableHeader.getDefaultRenderer()));
    }
  }

  /** Indicates if sorting is being done at this time.
   *  @return true if sorting is being done */
  public boolean isSorting() {
    return (direction != NOT_SORTED);
  }

  /** Gets the sorting status for a given column.
   *  @param column column
   *  @return sorting status for given column */
  public int getSortingStatus(int column) {
    return (column == this.column)? direction: NOT_SORTED;
  }

  /** Sets the sorting status.
      @param column    The column to sort by.
      @param direction One of {@link #NOT_SORTED}, {@link #ASCENDING}, {@link #DESCENDING}.
   */
  public void setSortingStatus(int column, int direction) {
    if ((column < 0) || !allowSorting) {
      direction = TableSorter.NOT_SORTED;
      column    = 0;
    }

    if ((this.direction == NOT_SORTED) && (direction == NOT_SORTED)) {
      if (this.column != column) {
        clearSortingState();
        this.column = column;
      }

    } else if (table.getNumberOfRows() > maxSize) {
      if (warnMaxSize) {
        Shell.warning("WARNING: Table is too large to sort."); // XXX Change this

        warnMaxSize = false; // Only issue the warning once.
      }

    } else if (this.column == column) {
      if (this.direction != direction) {
        this.direction = direction;
        sortingStatusChanged();
      }
    } else {
      clearSortingState();
      this.column    = column;
      this.direction = direction;
      sortingStatusChanged();
    }
  }

  /** Sets the sorting status.
      @param column  The name of the column to sort by prefixed by a '+' for ASCENDING or a
                     '-' for DESCENDING. Use a null string or "" to disable sorting.
   */
  public void setSortingStatus(String column) {
    int  dir = ASCENDING;  // Sort direction
    long col = -1;         // Sort column

    if ((column != null) && (column.length() > 1)) {
      if (column.startsWith("+")) {
        dir = ASCENDING;
        col = table.getColumnIndex(column.substring(1));
      } else if (column.startsWith("-")) {
        dir = DESCENDING;
        col = table.getColumnIndex(column.substring(1));
      } else {
        dir = ASCENDING;
        col = table.getColumnIndex(column);
      }
    } else if ((column != null) && (column.length() > 0)) {
      dir = ASCENDING;
      col = table.getColumnIndex(column);
    }

    setSortingStatus((int)col, dir);
  }

  /** Enables or disables sorting.
      @param enabled If this is false all sorting is disabled.
   */
  public void setSortingEnabled(boolean enabled) {
    if (enabled != allowSorting) {
      allowSorting = enabled;
      if (!allowSorting) { setSortingStatus(null); } // Clears previous sorting status.
    }
  }

  /** Indicates if sorting is enabled.
   *  @return true if sorting is enabled */
  public boolean getSortingEnabled() {
    return allowSorting;
  }

  /** Gets the sorting status.
      @return The name of the column to sort by prefixed by a '+' for ASCENDING or a
              '-' for DESCENDING. This will return "" if NOT_SORTED.
   */
  public String getSortingStatus() {
    String status = "";

         if (direction == ASCENDING ) { status = "+" + getColumnName((int)column); }
    else if (direction == DESCENDING) { status = "-" + getColumnName((int)column); }
    else                              { status = "";                               }

    return status;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Private Methods
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Get sort index. This is only used by toReal(long) and toView(long). */
  private long getSortIndex(Vector<Number> mapping, long row) {
    if (direction != NOT_SORTED) {
      int index = (direction == ASCENDING)? (int)row : (int)(table.getNumberOfRows() - 1 - row);

      if (!isUpToDate()) { resort(); }

      row = mapping.get(index).longValue();
    }
    return row;
  }


  /** Converts sorted index to real index. */
  long toReal(long row) {
//System.out.println("  toReal("+row+") = "+getSortIndex(realIndex, row));
    return getSortIndex(realIndex, row);
  }

  /** Converts real index to sorted index. */
  long toView(long row) {
    return getSortIndex(viewIndex, row);
  }

  /** Gets the Arrow icon used when rendering a given column.
      @param column The column number.
      @param size   The size of the icon in pixels (note the icon will be square).
   */
  protected Icon getHeaderRendererIcon(int column, int size) {
    int direction = getSortingStatus(column);
    Symbol icon = null;

         if (direction == ASCENDING ) { icon = upTriangle; }
    else if (direction == DESCENDING) { icon = dnTriangle; }

    if (icon != null) { icon.setSize(size/2); }

    return icon;
  }

  /** Resorts the column. */
  private synchronized void resort() {
    if (!isUpToDate()) {
      realIndex.setSize((int)table.getNumberOfRows());
      viewIndex.setSize((int)table.getNumberOfRows());

      /* Sort the realIndex. */
      for(int i = 0; i < realIndex.size(); i++) {
        realIndex.set(i, Integer.valueOf(i));
      }
      Collections.sort(realIndex, sorter);

      /* Build the viewIndex from the realIndex. */
      for(int i = 0; i < viewIndex.size(); i++) {
        int index = realIndex.get(i).intValue();

        viewIndex.set(index, Integer.valueOf(i));
      }

      upToDate = true;
    }
  }

  /** Gets the up-to-date status. */
  private synchronized boolean isUpToDate() {
    return upToDate && (table.getNumberOfRows() == realIndex.size());
  }

  /** Clears the sorting state but does not refresh the display. */
  private synchronized void clearSortingState() {
    direction = NOT_SORTED;
    upToDate  = false;
    column    = 0L;
    realIndex.clear();
    viewIndex.clear();
  }

  /** Sets the sort direction to NOT_SORTED, discards the current sort information and refreshes
      the display.
   */
  private void clearAll() {
    clearSortingState();
    sortingStatusChanged();
  }

  /** Sets the sort direction to NOT_SORTED and refreshes the display but does not discard the
      current sort information.
   */
  private void cancelSorting() {
    if (direction != NOT_SORTED) {
      direction = NOT_SORTED;
      sortingStatusChanged();
    }
  }

  /** Clears the sorting state and refreshes the display. */
  private void invalidateSorting() {
    if (upToDate) {
      upToDate = false;
      sortingStatusChanged();
    }
  }

  /** Makes appropriate updates to reflect changes in the sorting state. */
  private void sortingStatusChanged() {
    fireTableDataChanged();
    if (tableHeader != null) {
      tableHeader.repaint();
      table.getRowHeader().repaint();
    }
  }

  /** Sets the "normal" table model.
      @param tableModel The table model.
   */
  public void setTableModel(LargeTableModel tableModel) {
    if (this.tableModel != null) {
      this.tableModel.removeTableModelListener(modelListener);
    }

    this.tableModel = tableModel;
    if (this.tableModel != null) {
      this.tableModel.addTableModelListener(modelListener);
    }

    clearSortingState();
    sortingStatusChanged();
  }

  /** Compares two rows. */
  @SuppressWarnings({ "unchecked", "rawtypes" })
  private int compareRows(long row1, long row2) {
    int returnValue = 0;

    Object val1 = tableModel.getValueAt(row1,column);
    Object val2 = tableModel.getValueAt(row2,column);

    if (val1 instanceof String) {  // Fix to Bug 1973 ClassCastException for when val1 is String vs val2 is Data or a non-String
      returnValue = ((String) val1).compareTo(val2.toString());
    }
    else if (val1 instanceof Comparable) {
      returnValue = ((Comparable) val1).compareTo(val2);
    }
    else {
      returnValue = val1.toString().compareTo(val2.toString());
    }

    return returnValue;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Nested Classes
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** An implementation of the {@link Comparator} interface that takes in two numbers (as row
      numbers in the table) and compares the corresponding values in the table.
   */
  protected class TableValueComparator implements Comparator<Number> {
    @Override
    public int compare(Number o1, Number o2) {
      long row1 = o1.longValue();
      long row2 = o2.longValue();

      return compareRows(row1, row2);
    }
  }

  /** Handles mouse events for the one-click-sorting. */
  private class MouseHandler extends MouseAdapter {
    @Override
    public void mouseClicked(MouseEvent e) {
      JTableHeader h = (JTableHeader) e.getSource();
      TableColumnModel columnModel = h.getColumnModel();
      int viewColumn = columnModel.getColumnIndexAtX(e.getX());
      if (viewColumn == -1) return; // x is out of bounds, Bug 843

      int column = columnModel.getColumn(viewColumn).getModelIndex();
      if (column != -1) {
        int status = getSortingStatus(column);
        if (!e.isControlDown()) {
          cancelSorting();
        }
        // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or
        // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed.
        status = status + (e.isShiftDown() ? -1 : 1);
        status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, 1}
        setSortingStatus(column, status);
      }
    }
  }

  /** Renders the table header with the appropriate direction icon. */
  private class SortableHeaderRenderer implements TableCellRenderer, Serializable {

    /** NOTE: this should be changed if the structure/fields of this class changes. */
    private static final long serialVersionUID = 2012021020311L; // for Serializable interface

    /** The default renderer */ private transient TableCellRenderer renderer;

    /** Creates a new instance.
        @param renderer The default renderer (without a direction icon).
     */
    public SortableHeaderRenderer(TableCellRenderer renderer) {
      this.renderer = renderer;
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSel,
                                                   boolean hasFocus, int row, int col) {
      Component comp = renderer.getTableCellRendererComponent(table,value,isSel,hasFocus,row,col);
      if (comp instanceof JLabel) {
        int modelColumn = table.convertColumnIndexToModel(col);

        JLabel jlabel = ((JLabel)comp);
        jlabel.setHorizontalTextPosition(JLabel.LEFT);
        jlabel.setIcon(getHeaderRendererIcon(modelColumn, jlabel.getFont().getSize()));
      }
      return comp;
    }
  }

  /** The listener used to listen for changes in the "real table model". */
  private class TableModelHandler implements TableModelListener {
    @Override
    public void tableChanged(TableModelEvent e) {
      if (!isSorting()) {
        /* If we're not sorting by anything, just pass the event along. */
        fireTableChanged(e);

      } else if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
        /* If the table structure has changed, clear everything. */
        // JC4: This was causing issues when scrolling, see Bug 427 (24APR06)
        //clearAll();

      } else {
        /* Something has happened to the data that may have invalidated the sort order. */
        boolean invalidate = true;

        if (e.getFirstRow() == e.getLastRow()) {
          long numRows = table.getNumberOfRows();

          if (numRows == realIndex.size()) {
            long rowNum = toReal(e.getFirstRow());

            if (numRows <= 1) {
              invalidate = false;
            } else if (rowNum == 0L) {
              invalidate = (compareRows(rowNum, rowNum+1) > 0);
            } else if (rowNum == numRows-1) {
              invalidate = (compareRows(rowNum-1, rowNum) > 0);
            } else {
              invalidate = (compareRows(rowNum-1, rowNum) > 0)
                        || (compareRows(rowNum, rowNum+1) > 0);
            }
          }
        }
        if (invalidate) { invalidateSorting(); }
      }
    }
  }

  /** Overwrite default serialization for writeObject
   *  to throw an UnsupportedOperationException since Java Serialization currently not supported
   *
   *  @param oos Object Output Stream
   *  @throws IOException Exception thrown by writeObject
   */
  private void writeObject (ObjectOutputStream oos) throws IOException {
    throw new UnsupportedOperationException(UNSERIALIZABLE_TABLESORTER_OBJECT_MESSAGE);
  }

  /** Overwrite default deserialization for readObject
   *  to throw an UnsupportedOperationException since Java Serialization currently not supported
   *
   *  @param ois Object Input Stream
   *  @throws ClassNotFoundException Exception thrown by readObject
   *  @throws IOException Exception thrown by readObject
   */
  private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException {
    throw new UnsupportedOperationException(UNSERIALIZABLE_TABLESORTER_OBJECT_MESSAGE);
  }
}
