package nxm.sys.libg;

import java.awt.Adjustable;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.BitSet;
import java.util.Collections;
import java.util.EventObject;
import java.util.Vector;

import javax.swing.DefaultBoundedRangeModel;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JComponent;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

import nxm.sys.lib.Parser;

/** An extension of {@link JTable} that is designed to handle large files. This class separates the
    "generic" functionality necessary to handle large files from the functionality to handle Midas
    files (which is contained in {@link MJList}.

    <h2>Indexing</h2>
    This class manages four ways of indexing a cell:
    <pre>
      VIEW   - The index of a cell as viewed on the display
               where row 0 is always the top row visible.
               (Used by the graphics draw methods only.)

      MODEL  - Real-world-index as viewed by JTable.
               (Do not use since it can not handle large files.)

      REAL   - The real-world-index index into the data file.
               (This is always zero-based.)

      FILE   - Another name for REAL.

      MACRO  - The index used by a NeXtMidas macro (handled
               by MJList) used to allow macros to use one-based
               index values.
    </pre>
    The following methods can be used to convert between the different index systems.
    <pre>
      VIEW ----&gt; MODEL ----&gt; FILE ----&gt; MACRO
      [    32-bit    ]       [    64-bit    ]

        VIEW  --&gt; MODEL  getModelRowIndex(view) or getModelColIndex(view)
        MODEL --&gt; FILE   getFileRowIndex(model) or getFileColIndex(model)
        FILE  --&gt; MACRO  getMacroRowIndex(file) or getMacroColIndex(file)

      MACRO ----&gt; FILE ----&gt; MODEL ----&gt; VIEW
      [    32-bit    ]       [    64-bit    ]

        MACRO --&gt; FILE  getFileRowIndex(macro) or getFileColIndex(macro)
        FILE  --&gt; MODEL getModelRowIndex(file) or getModelColIndex(file)
        MODEL --&gt; VIEW  getViewRowIndex(model) or getViewColIndex(model)
    </pre>

    @author  J. Czechowski IV
    @since   NeXtMidas 2.1.0
 */
@SuppressWarnings("serial")
public abstract class LargeJTable extends JTable {
  /** Turns on/off scroll bar debugging */
  private static final boolean DEBUG_SCROLL_BAR = false;

  /** Cell click action: SELECT CELL   */ protected static final int CELL_ACTION_SELECT = 1;
  /** Cell click action: SELECT ROW    */ protected static final int CELL_ACTION_ROW    = 2;
  /** Cell click action: SELECT COLUMN */ protected static final int CELL_ACTION_COL    = 3;
  /** Cell click action: DO NOTHING    */ protected static final int CELL_ACTION_NONE   = 4;

  /** Row label click action: SELECT ROW */ protected static final int ROW_ACTION_SELECT  = 1;
  /** Row label click action: DO NOTHING */ protected static final int ROW_ACTION_NONE    = 2;

  /** Col label click action: SELECT COL */ protected static final int COL_ACTION_SELECT  = 1;
  /** Col label click action: SORT COL   */ protected static final int COL_ACTION_SORT    = 2;
  /** Col label click action: DO NOTHING */ protected static final int COL_ACTION_NONE    = 3;

  protected static final String cellClickActionList = "Select,Row,Col,None";
  protected static final String rowClickActionList  = "Select,None";
  protected static final String colClickActionList  = "Select,Sort,None";

  //JPH --- TBD -- Compute one set of defaults from the other...
  protected static final String CELL_ACTION_DEFAULT_STR   = "SELECT";
  protected static final String ROW_ACTION_DEFAULT_STR    = "SELECT";
  protected static final String COL_ACTION_DEFAULT_STR    = "SORT";

  /** Action when cell is clicked      */ protected int cellAction = CELL_ACTION_SELECT;
  /** Action when row label is clicked */ protected int rowAction  =  ROW_ACTION_SELECT;
  /** Action when col label is clicked */ protected int colAction  =    COL_ACTION_SORT;

  /** mimics LIST2 autoSelect */
  private boolean autoSelect = true;

  /** The row header. */
  private RowHeader rowHeader = new RowHeader(this);

  /** The table selection model. */
  private LargeTableSelectionModel tableSelModel = new LargeTableSelectionModel(this);

  /** The real index of the top row being displayed. */
  private long topRow = 0L;

  /** The vertical scroll bar. */
  private VertScrollBar vertScrollBar = new VertScrollBar(this);

  /** Creates a new instance. */
  public LargeJTable() {
    this((TableModel)null);
  }

  /** Creates a new instance with the given underlying model.
      @param tblModel  The table model.
   */
  public LargeJTable(TableModel tblModel) {
    this(tblModel, null);
  }

  /** Creates a new instance with the given underlying models.
      @param tblModel  The table model.
      @param colModel  The column model.
   */
  public LargeJTable(TableModel tblModel, TableColumnModel colModel) {
    this(tblModel,colModel,null);
  }

  /** Creates a new instance with the given underlying models.
      @param tblModel  The table model.
      @param colModel  The column model.
      @param selModel  The selection model.
   */
  public LargeJTable(TableModel tblModel, TableColumnModel colModel,
                     LargeListSelectionModel selModel) {
    super(tblModel,colModel,null);

    if (selModel==null) {
      setSelectionModel(tableSelModel.getRowSelModel());
    }
  }

  /** Gets autoSelect (cell/row/col highlighting)
   *  @return true if auto select */
  public boolean getAutoSelect () { return autoSelect; }

  /** Sets autoSelect (cell/row/col highlighting)
   *  @param autoselect true if auto select */
  public void setAutoSelect (boolean autoselect) { autoSelect = autoselect; }

  /** Sets the cell click action to CELL, ROW, COL, or NONE.
   *  @param str cell click action in String form */
  public void setCellClickAction(String str) {
    if (str == null) { str = CELL_ACTION_DEFAULT_STR; }
    else if (str.equalsIgnoreCase("COLUMN")) { str = "COL"; }
    cellAction = Parser.find(cellClickActionList,str,cellAction,0,Parser.E_WARNING); // warning added for Bug 760
  }

  /** Sets the row label click action to SELECT, or NONE.
   *  @param str row click action in String form */
  public void setRowClickAction(String str) {
    if (str == null) {str = ROW_ACTION_DEFAULT_STR;}
    rowAction = Parser.find(rowClickActionList,str,rowAction,0,Parser.E_WARNING); // warning added for Bug 760
  }

  /** Sets the column label click action to SELECT, SORT, or NONE.
   *  @param str column click action in String form */
  public void setColClickAction(String str) {
    if (str == null) {str = COL_ACTION_DEFAULT_STR;}
    colAction = Parser.find(colClickActionList,str,colAction,0,Parser.E_WARNING); // warning added for Bug 760
  }

  /** Gets the cell click action.
   *  @return the cell lick action in String form
   */
  public String getCellClickAction() {
    return Parser.get(cellClickActionList,cellAction);
  }

  /** Is the Cell LM click to treated like a regular selection,
      @return true if the cell LMB click is treated like a regular selection
      @since NeXtMidas 2.1.2
   */
  public final boolean isCellClickActionSelect() {
    return getCellSelectionAllowed();
  }

  /** Is the Cell LM click to treated like a ROW selection?
      @return true if the cell LMB click is treated like a ROW selection
      @since NeXtMidas 2.1.2
   */
  public boolean isCellClickActionRow() {
    return (cellAction==CELL_ACTION_ROW);
  }

  /** Is the Cell LM click to treated like a COLUMN selection?
      @return true if the cell LMB click is treated like a COLUMN selection
      @since NeXtMidas 2.1.2
   */
  public boolean isCellClickActionCol() {
    return (cellAction==CELL_ACTION_COL);
  }

  /** Gets the row label click action.
   *  @return the row label click action */
  public String getRowClickAction() {
    return Parser.get(rowClickActionList,rowAction);
  }

  /** Gets the column label click action.
   *  @return the column label click action */
  public String getColClickAction() {
    return Parser.get(colClickActionList,colAction);
  }

  /** Sets the new state for whether or not cell selection is allowed
      @param allowed new state for cell selection
   */
  public void setCellSelectionAllowed(boolean allowed) {
    if (allowed) { setCellClickAction("SELECT"); }
    else         { setCellClickAction("NONE"); }
    setAutoSelect(allowed);
  }

  /** Returns whether or not cell selection is allowed
      @return whether or not cell selection is allowed
   */
  public boolean getCellSelectionAllowed() {
    return (cellAction==CELL_ACTION_SELECT);
  }

  @Override
  public void setRowSelectionAllowed(boolean allowed) {
    if (allowed) { setRowClickAction("SELECT"); }
    else         { setRowClickAction("NONE");   }
  }

  @Override
  public boolean getRowSelectionAllowed() {
    return (rowAction==ROW_ACTION_SELECT);
  }

  @Override
  public void setColumnSelectionAllowed(boolean allowed) {
    if (allowed) { setColClickAction("SELECT"); }
    else         { setColClickAction("NONE");   }
  }

  @Override
  public boolean getColumnSelectionAllowed() {
    return (colAction==COL_ACTION_SELECT);
  }

  @Override
  protected ListSelectionModel createDefaultSelectionModel() {
    if (tableSelModel == null) {
      tableSelModel = new LargeTableSelectionModel(this);
    }

    return tableSelModel.getRowSelModel();
  }

  /** Sets the table selection model.
      @param selModel The selection model.
   */
  public void setTableSelectionModel(LargeTableSelectionModel selModel) {
    tableSelModel = selModel;
    setSelectionModel(selModel.getRowSelModel());
  }

  /** Gets the current selection model.
   *  @return the current selection model */
  public LargeTableSelectionModel getTableSelectionModel() {
    return tableSelModel;
  }

  @Override
  public void resizeAndRepaint() {
    super.resizeAndRepaint();
    if (rowHeader   != null) { rowHeader.repaint();   }
    if (tableHeader != null) { tableHeader.repaint(); }
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // New Methods
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** See "Indexing" at top of {@link LargeJTable}
   *  @param viewIndex The index of a cell as viewed on the display
   *                   where row 0 is always the top row visible.
   *  @return just returns input parameter
   */
  public int  getModelRowIndex(int viewIndex)   { return viewIndex;                               }
  /** See "Indexing" at top of {@link LargeJTable}
   *  @param modelIndex Real-world-index as viewed by JTable
   *  @return the Real row index */
  public long getRealRowIndex(int modelIndex)   { return modelIndex + getTopRowIndex();           }

  /** See "Indexing" at top of {@link LargeJTable}
   * @param realIndex The real-world-index index into the data file
   * @return the Model row index
   */
  public int  getModelRowIndex(long realIndex)  { return (int)(realIndex - getTopRowIndex());     }
  /** See "Indexing" at top of {@link LargeJTable}
   *  @param modelIndex Real-world-index as viewed by JTable
   *  @return the View row index
   */
  public int  getViewRowIndex(int modelIndex)   { return modelIndex;                              }

  /** See "Indexing" at top of {@link LargeJTable}
   *  @param viewIndex The index of a cell as viewed on the display
   *                   where row 0 is always the top row visible.
   *  @return the Model column index
   */
  public int  getModelColIndex(int viewIndex)   { return convertColumnIndexToModel(viewIndex);    }
  /** See "Indexing" at top of {@link LargeJTable}
   *  @param modelIndex Real-world-index as viewed by JTable
   *  @return the real column index
   */
  public long getRealColIndex(int modelIndex)   { return (long)modelIndex;                        }

  /** See "Indexing" at top of {@link LargeJTable}
   *  @param realIndex The real-world-index index into the data file.
   *  @return the Model column index
   */
  public int  getModelColIndex(long realIndex)  { return (int)realIndex;                          }
  /** See "Indexing" at top of {@link LargeJTable}
   *  @param viewIndex The index of a cell as viewed on the display
   *                   where row 0 is always the top row visible.
   * @return the View column index
   */
  public int  getViewColIndex(int viewIndex)    { return convertColumnIndexToView(viewIndex);     }

  /** Gets the index of the first row on the page.
   *  @return the index of the first row on the page */
  public long getTopRowIndex() {
    return topRow;
  }

  /** Sets the index of the first row on the page.
      @param index The index of the first row on the page if the index is outside the valid range it
                   will be adjusted automatically.
   */
  public void setTopRowIndex(long index) {
    final int GAP = 0; // Row gap at bottom of table

    long newTopRow = Math.max(0L, Math.min(getNumberOfRows()-getNumberOfVisibleRows()+GAP, index));

    if (topRow != newTopRow) {
      topRow = newTopRow;
      this.revalidate();
      this.repaint();
      rowHeader.repaint();
      vertScrollBar.revalidate();
    }
  }

  /** Gets the number of rows in the file.
   *  @return number of rows in the file */
  protected abstract long getNumberOfRows();

  /** Gets the number of rows visible on the screen.
   *  @return the number of rows visible on the screen. */
  protected int getNumberOfVisibleRows() {
    Dimension viewSize = null;

    if ((getScrollPane() != null) && (getScrollPane().getViewport() != null)) {
      viewSize = getScrollPane().getViewport().getExtentSize();
    } else {
      viewSize = getSize();
    }

    return (viewSize.height / getRowHeight());
  }

  /** Makes a given cell visible.
      @param row    The cell row.
      @param column The cell column.
   */
  public void showCell(long row, long column) {
    int col = getViewColIndex(getModelColIndex(column)); // VIEW index for the column

    setTopRowIndex(row);
    scrollRectToVisible(getCellRect(0, col, true));
  }

  /** Gets the ID of the given row. Use <code>getRowID(-1L)</code> to get a template for the row ID
      that can be used for sizing the rows, this will frequently just be a bunch of spaces or the
      text of the longest row ID.
      @param row The row number.
      @return The ID of the given row.
   */
  protected String getRowID(long row) {
    if (row < 0) { row = getNumberOfRows(); }

    return String.valueOf(row);
  }

  /** Gets the ID of the given column. Use <code>getColID(-1L)</code> to get a template for the
      column ID that can be used for sizing the columns, this will frequently just be a bunch of
      spaces or the text of the largest column ID.
      @param col The column number.
      @return The ID of the given column.
   */
  protected String getColID(long col) {
    String str = null;

    if (col < 0) {
      for (int i = 0; i < getColumnCount(); i++) {
        String name = getColumnName(i);

        if (name.length() > str.length()) {
          str = name;
        }
      }

    } else {
      str = getColumnName(getModelColIndex(col));
    }

    return str;
  }

  /** Determines if the given cell is selected.
      @param row The row index.
      @param col The column index.
      @return <code>true</code> if the cell is selected, <code>false</code> if not.
   */
  public boolean isCellSelected(long row, long col) {
    return tableSelModel.isCellSelected(row, col);
  }

  /** Calls {@link LargeListSelectionModel#setSelectionInterval(long, long)} to change the selection
      within a row
      @param start start of the interval
      @param end   end of the interval
   */
  public void setRowSelectionInterval(long start, long end) {
    tableSelModel.getRowSelModel().setSelectionInterval(start, end);
  }

  /** Calls {@link LargeListSelectionModel#addSelectionInterval(long, long)} to add to the current selection
      a selection within a row
      @param start start of the interval
      @param end   end of the interval
   */
  public void addRowSelectionInterval(long start, long end) {
    tableSelModel.getRowSelModel().addSelectionInterval(start, end);
  }
  /** Calls {@link LargeTableSelectionModel#changeSelection(long, long, boolean, boolean)} to change the selection
      @param start  row of newly selected cell
      @param end    column of newly selected cell
      @param toggle indicates if toggling cell selection
      @param extend indicates if extending cell selection
   */
  public void changeSelection(long start, long end, boolean toggle, boolean extend) {
        tableSelModel.changeSelection(start, end, toggle, extend);
  }

  // DEPRECATED: The next two lines are useless, except that they are a workaround for
  //             a bug in the Java 1.4.1 compiler which would error out otherwise.
  public boolean editCellAt(int row, int col)                { return super.editCellAt(row,col  ); }
  public boolean editCellAt(int row, int col, EventObject e) { return super.editCellAt(row,col,e); }

  /** Calls {@link JTable#editCellAt(int, int)} to edit a given cell if the cell is editable
      @param row    row of the cell to be edited
      @param column column of the cell to be edited
      @return whether or not the cell was able to be edited
   */
  public boolean editCellAt(long row, long column) {
    showCell(row, column);
    return super.editCellAt(getModelRowIndex(row), getModelColIndex(column));
  }

  /** Calls {@link #editCellAt(long, long)} to edit a given cell if the cell is editable
      @param row    row of the cell to be edited
      @param column column of the cell to be edited
      @param e      Event Object for editing the cell
      @return whether or not the cell was able to be edited
   */
  public boolean editCellAt(long row, long column, EventObject e) {
    showCell(row, column);

    return editCellAt(getModelRowIndex(row), getModelColIndex(column), e);
  }

  protected LargeListSelectionModel getLargeSelectionModel() {
    ListSelectionModel model = super.getSelectionModel();

    if (!(model instanceof LargeListSelectionModel)) { // <-- This should never happen.
      throw new RuntimeException(getClass().getName() + ": invalid selection model found.");
    }

    return (LargeListSelectionModel)model;
  }

  /** Updates the visibility of the row labels (on the left of the table). This method does nothing
      if the JTable isn't in a scroll pane.
      @param visible Indicates the desired visibility.
   */
  public void setRowLabelsVisible(final boolean visible) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        final JScrollPane  scrollPane = getScrollPane();

        if (scrollPane != null) {
          if (visible) {
            scrollPane.setRowHeaderView(getRowHeader());
          } else {
            scrollPane.setRowHeaderView(null);
          }
        }
      }
    });
  }

  /** Updates the visibility of the column labels (on the top of the table). This method does
      nothing if the JTable isn't in a scroll pane.
      @param visible Indicates the desired visibility.
   */
  public void setColLabelsVisible(final boolean visible) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        JScrollPane  scrollPane = getScrollPane();

        if (scrollPane != null) {
          if (visible) {
            scrollPane.setColumnHeaderView(getTableHeader());
          } else {
            scrollPane.setColumnHeaderView(null);
          }
        }
      }
    });
  }

  /** Gets the scroll pane being used.
      @return The scroll pane (or <code>null</code> if none).
   */
  protected JScrollPane getScrollPane() {
    Component parent = getParent();

    if ((parent != null) && (parent instanceof JViewport)) {
      parent = parent.getParent();
    }

    return (parent instanceof JScrollPane)? (JScrollPane)parent: null;
  }

  public void setRowHeight(int rowHeight) {
    super.setRowHeight(rowHeight);
    if (rowHeader != null) { // rowHeader is null during initialization
      rowHeader.setRowHeight(rowHeight);
    }
  }

  /** Gets the row header being used.
      @return The row header (or <code>null</code> if none).
   */
  protected RowHeader getRowHeader() {
    return rowHeader;
  }

  /** Creates the vertical scroll bar to use. This method makes it easier to debug the scroll bar.*/
  protected VertScrollBar getVertScrollBar() {
    return vertScrollBar;
  }

  /** Gets the default text width for a character in the table. */
  protected int getTextWidth() {
    return getFontMetrics(getFont()).charWidth('0');
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Overridden Methods
  //////////////////////////////////////////////////////////////////////////////////////////////////
  public final void setRowSelectionInterval(int start, int end) {
    setRowSelectionInterval(getRealRowIndex(start), getRealColIndex(end));
  }

  public final void addRowSelectionInterval(int start, int end) {
    addRowSelectionInterval(getRealRowIndex(start), getRealColIndex(end));
  }

  public final void changeSelection(int start, int end, boolean toggle, boolean extend) {
    changeSelection(getRealRowIndex(start), getRealColIndex(end), toggle, extend);
  }

  public boolean isCellSelected(int row, int col) {
    return tableSelModel.isCellSelected(getRealRowIndex(row), getRealColIndex(col));
  }

  public final ListSelectionModel getSelectionModel() {
    return getLargeSelectionModel();
  }

  public void clearSelection() {
    tableSelModel.clearSelection(false);
  }


  /** {@inheritDoc}
      @param newModel {@inheritDoc}
      @throws IllegalArgumentException If the model is not a {@link LargeListSelectionModel}.
   */
  public void setSelectionModel(ListSelectionModel newModel) {
    if (newModel instanceof LargeListSelectionModel) {
      super.setSelectionModel(newModel);

    } else {
      throw new IllegalArgumentException(getClass().getName() + ": invalid selection model found.");
    }
  }

  /** Gets the large table model being used.
   *  @return the large table model being used */
  public LargeTableModel getLargeTableModel() {
    TableModel model = super.getModel();

    return (model instanceof LargeTableModel)? (LargeTableModel)model: null;
  }



  /** Gets the intersection of the span of rows from top to btm with those that are visible.
      @param top The top row (-1 for first row).
      @param btm The btm row (-1 for last row).
      @return The intersection of the rows {top,btm} or null if they don't intersect.
   */
  protected long[] getRowsVisible(long top, long btm) {
    if (top < 0) { top = 0L; }
    if (btm < 0) { btm = getNumberOfRows(); }

    long min = getTopRowIndex();
    long max = min + getNumberOfVisibleRows();

    long[] range = null;

    // JC4: The next two lines are useful for debugging
    //System.out.println("(top <= max) = ("+top+" <= "+max+") = "+(top <= max));
    //System.out.println("(btm >= min) = ("+btm+" >= "+min+") = "+(btm >= min));
    if ((top <= max) && (btm >= min)) {
      range = new long[] { Math.max(top, min), Math.min(btm, max) };
    }

    return range;
  }

  /** Same as getRowsVisible, but converts output to model indicies. */
  protected int[] getModelRowsVisible(long top, long btm) {
    long[] range = getRowsVisible(top, btm);
    int[]  rows  = null;

    if (range != null) {
      rows = new int[] { getModelRowIndex(range[0]), getModelRowIndex(range[1]) };
    }

    return rows;
  }

  /** Gets the index of the named column. This checks for the column index in the following order,
      looking for the first match:
      <pre>
        - Exact name match (case-insensitive).
        - Numeric conversion ("1.0" = 1).
        - Index specifier ("_4_" = 4).
      </pre>
      @param name The column name.
      @return The index of the column or -1 if not found.
   */
  abstract long getColumnIndex(String name);

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Nested Classes
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Extends {@link TableModel} to support large tables. */
  public static interface LargeTableModel extends TableModel {

    /** Returns whether or not the given cell can be edited
        @param row row of the cell being analyzed
        @param col column of the cell being analyzed
        @return whether or not the given cell can be edited
     */
    public boolean isCellEditable(long row, long col);

    /** Gets the value of a given cell in the table
        @param row row of the cell to be read from
        @param col column of the cell to be read from
        @return the value in the cell
     */
    public Object getValueAt(long row, long col);

    /** Sets the value of a given cell in the table
        @param val value to be set
        @param row row of the destination cell
        @param col column of the destination cell
     */
    public void setValueAt(Object val, long row, long col);

    /** Fires a redraw iff any of the affected rows would appear on the screen.
        @param top The first modified row (-1 = start at beginning of file).
        @param btm The last  modified row (-1 = go to end of file).
     */
    void fireNeedsRedraw(long top, long btm);

    /** Fires a redraw iff any of the affected rows would appear on the screen.
        @param top The first inserted row (-1 = start at beginning of file).
        @param btm The last  inserted row (-1 = go to end of file).
     */
    void fireRowsInserted(long top, long btm);

    /** Fires a redraw iff any of the affected rows would appear on the screen.
        @param top The first removed row (-1 = start at beginning of file).
        @param btm The last  removed row (-1 = go to end of file).
     */
    void fireRowsDeleted(long top, long btm);

    /** Fires a redraw for the whole structure. */
    void fireStructureChanged();
  }

  /** A version of {@link ListSelectionModel} that handles large row values. */
  public static interface LargeListSelectionModel extends ListSelectionModel {
    /** Change the selection to be between index0 and index1 inclusive.
        @param index0 Start of the interval.
        @param index1 End of the interval
     */
    public void setSelectionInterval(long index0, long index1);

    /** Change the selection to be the union of the current selection and the indices between
        index0 and index1 inclusive.
        @param index0 Start of the interval.
        @param index1 End of the interval
     */
    public void addSelectionInterval(long index0, long index1);

    /** Change the selection to be the difference of the current selection and the indices between
        index0 and index1 inclusive.
        @param index0 Start of the interval.
        @param index1 End of the interval
     */
    public void removeSelectionInterval(long index0, long index1);

    /** Gets the first selected index of <code>-1L</code> if the selection is empty.
     *  @return first selected index */
    public long getFirstSelectionIndex();

    /** Gets the last selected index of <code>-1L</code> if the selection is empty.
     *  @return last selected index */
    public long getLastSelectionIndex();

    /** Indicates if the given index is selected.
     *  @param index index to check
     *  @return true if given index is selected
     */
    public boolean isSelectedIndex(long index);

    /** Gets the "anchor" index.
     *  @return the "anchor" index */
    public long getSelectionAnchor();

    /** Sets the "anchor" index.
     *  @param index the "anchor" index */
    public void setSelectionAnchor(long index);

    /** Gets the "lead" index.
     *  @return the "lead" index */
    public long getSelectionLead();

    /** Sets the "lead" index.
     *  @param index the "lead" index */
    public void setSelectionLead(long index);

    /** Insert length indices beginning before/after index.
        @param index  The index.
        @param length The number of items.
        @param before Indicates if before index.
     */
    public void insertIndexInterval(long index, long length, boolean before);

    /** Remove the indices in the interval index0,index1 (inclusive) from the selection model.
        @param index0 The start index.
        @param index1 The end index.
     */
    public void removeIndexInterval(long index0, long index1);
  }

  /** A version of {@link ListSelectionEvent} that handles large row values. */
  public static class LargeListSelectionEvent extends ListSelectionEvent {
    /** The first row */ private final long firstRow;
    /** The last row  */ private final long lastRow;

    /** Creates a new instance.
        @param source      The event source.
        @param firstRow    The first row.
        @param lastRow     The last row.
        @param isAdjusting Indicates if the value is adjusting.
     */
    public LargeListSelectionEvent(Object source, long firstRow, long lastRow, boolean isAdjusting){
      super(source, (int)Math.min((long)Integer.MAX_VALUE, firstRow),
                    (int)Math.min((long)Integer.MAX_VALUE, lastRow ), isAdjusting);

      this.firstRow = firstRow;
      this.lastRow  = lastRow;
    }

    /** Gets the index of the first row.
     *  @return the index of the first row  */
    public long getFirstRow() {
      return firstRow;
    }

    /** Gets the index of the last row.
     *  @return the index of the last row */
    public long getLastRow() {
      return lastRow;
    }

    /** Gets the first row or {@link Integer#MAX_VALUE} if the first row is greater than it. */
    @Override
    public int getFirstIndex() {
      return super.getFirstIndex();
    }

    /** Gets the last row or {@link Integer#MAX_VALUE} if the first row is greater than it. */
    @Override
    public int getLastIndex() {
      return super.getLastIndex();
    }
  }

  /** A version of {@link TableCellEditor} for use with large tables. */
  public static interface LargeTableCellEditor extends TableCellEditor {
    /** Gets the component used to draw the cell.
        @param table       The table.
        @param value       The current value for the cell.
        @param isSelected  Indicates if the cell is selected.
        @param row         The row index.
        @param column      The column index.
        @return The component used for editing.
     */
    public Component getTableCellEditorComponent(LargeJTable table, Object value,
                                                 boolean isSelected, long row, long column);
  }

  /** A version of {@link TableCellRenderer} for use with large tables. */
  public static interface LargeTableCellRenderer extends TableCellRenderer {
    /** Gets the component used to draw the cell.
        @param table       The table.
        @param value       The current value for the cell.
        @param isSelected  Indicates if the cell is selected.
        @param hasFocus    Indicates if the cell has focus.
        @param row         The row index.
        @param column      The column index.
        @return The component used for rendering.
     */
    public Component getTableCellRendererComponent(LargeJTable table, Object value,
                                                   boolean isSelected, boolean hasFocus,
                                                   long row, long column);
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // THE ROW HEADER  - Displays the row number on the left side of the table.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Displays the row number on the left side of the table. */
  public static class RowHeader extends JTable {
    /** The underlying table                     */ private final LargeJTable table;
    /** The number of rows, -1 = Needs Refresh  */ private       long        lastRow   = -1L;
    /** The width of a cell, -1 = Needs Refresh */ private       int         cellWidth =  -1;

    /** Creates a new instance.
        @param table The JTable this is providing the headers for.
     */
    private RowHeader(final LargeJTable table) {
      super();
      this.table = table;

      RowHeaderModel model = new RowHeaderModel(table,this);

      setModel(model);
      setSelectionModel(table.getSelectionModel());
      setDefaultRenderer(String.class, model);
    }

    public boolean isRowSelected(int row) { return table.isRowSelected(row); }
    public int     getRowHeight(int row)  { return table.getRowHeight(row);  }

    public Dimension getPreferredScrollableViewportSize() {
      return new Dimension(getCellWidth(true), table.getHeight());
    }

    public void changeSelection(int row, int column, boolean toggle, boolean extend)  {
      table.changeSelection(row, -1, toggle, extend);
    }

    public void processMouseEvent (MouseEvent me) {
      // This override method added in 3.1.0
      if (me.getButton()==3) table.clearSelection();  // right-clicks to clear selection per Bug 1133
      super.processMouseEvent(me);
    }

    public void processKeyEvent (KeyEvent ke) {
      // This override method added in 3.1.0
      table.processKeyEvent(ke);// forward to table itself to handle
    }

    /** Gets the width of a cell. */
    private int getCellWidth(boolean update) {
      if (update || (cellWidth < 0)) {
        final int PAD = 2;  // Number of pad characters

        int charWidth = table.getTextWidth();
        int strLength = table.getRowID(table.getNumberOfRows()-1).length() + PAD;
        int newWidth  = strLength * charWidth;

        if (newWidth != cellWidth) {
          table.getLargeTableModel().fireStructureChanged();
          cellWidth = newWidth;
        }
      }
      return cellWidth;
    }

    /** Get the model for the row header
        @return model for the row header
     */
    public RowHeaderModel getRowHeaderModel() {
      return (RowHeaderModel)getModel();
    }

    /** The model for the row header. */
    protected static class RowHeaderModel extends AbstractTableModel implements TableCellRenderer,
                                                                                TableModelListener {
      /** The row header  */ private final RowHeader   rowHeader;
      /** The large table */ private final LargeJTable table;

      /** Creates a new instance.
          @param table  The table.
          @param header The row header.
       */
      public RowHeaderModel(LargeJTable table, RowHeader header) {
        this.table = table;
        rowHeader  = header;
      }

      public void fireTableRowsUpdated(int top, int btm)  { super.fireTableRowsUpdated(top, btm); }
      public void fireTableRowsInserted(int top, int btm) { super.fireTableRowsInserted(top, btm); }
      public void fireTableRowsDeleted(int top, int btm)  { super.fireTableRowsDeleted(top, btm); }
      public void fireTableStructureChanged()             { super.fireTableStructureChanged(); }

      //// TableModel Methods //////////////////////////////////////////////////////////////////////
      public int    getColumnCount()             { return 1;                           }
      public int    getRowCount()                { return this.table.getRowCount();    }
      public Class<?> getColumnClass(int col)    { return String.class;                }
      public Object getValueAt(int row, int col) {
        return this.table.getRowID(this.table.getRealRowIndex(row));
      }

      //// TableModelListener Methods //////////////////////////////////////////////////////////////
      public void tableChanged(TableModelEvent evt) { fireTableChanged(evt); }

      //// TableCellRenderer Methods ///////////////////////////////////////////////////////////////
      public Component getTableCellRendererComponent(JTable table, Object val, boolean selected,
                                                     boolean focus, int row, int col) {
        // Use the default rendered of the header so that the colors match.
        TableCellRenderer renderer = table.getTableHeader().getDefaultRenderer();
        int height = this.table.getRowHeight(row);
        int width  = rowHeader.getCellWidth(false);


        // Use the default rendered of the header so that the colors match.
        Component comp = renderer.getTableCellRendererComponent(table,val,selected,focus,row,col);

        ((JComponent)comp).setPreferredSize(new Dimension(width,height));

        return comp;
      }
    }
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // THE SELECTION MODEL
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** The selection model that handles row/column/cell selections for a large JTable. <br>
      <br>
      Implementation notes:
      <pre>
        (1) Any methods that start with "do" such as "doAddCellSelection(long row, long col)"
            are called via "changeCellSelection(long row, long col, int type)". This provides
            a centeral location ("changeCellSelection(long row, long col, int type)") through
            which all cell selections must pass. (Similar is true for columns and rows.)

        (2) Any method that takes in a (long row, long col) is taking in the row and column
            of a given cell.

        (3) Any method that takes in a (long rowStart, long rowEnd, long colStart, long colEnd)
            is taking in a range of rows and columns (values are inclusive from start to end).

        (4) Any method that takes in a (long start, long end) is taking in a range of rows or
            columns.
      </pre>
   */
  public static class LargeTableSelectionModel {
    /** The anchor row for selections    (-1 = not set) */ private long rowAnchor = -1L;
    /** The lead row for selections      (-1 = not set) */ private long rowLead   = -1L;
    /** The anchor column for selections (-1 = not set) */ private long colAnchor = -1L;
    /** The lead column for selections   (-1 = not set) */ private long colLead   = -1L;

    /** The row selection set */ private final Vector<RowSelection> rowSet  = new Vector<>();
    /** The selected columns  */ private final BitSet               colSet  =   new BitSet();

    /** The row selection model    */ private final TableRowSelectionModel rowSelModel;
    /** The column selection model */ private final TableColSelectionModel colSelModel;
    /** The underlying table       */ private final LargeJTable            table;

    /** Selection type: Update Anchor         */ protected static final int ANCHOR      =  1;
    /** Selection type: Normal Click          */ protected static final int CLICK       =  2;
    /** Selection type: Ctrl+Click            */ protected static final int CTRL_CLICK  =  3;
    /** Selection type: Shift+Click           */ protected static final int SHIFT_CLICK =  4;
    /** Selection type: Deselect              */ protected static final int DESELECT    =  5;
    /** Selection type: Set selection         */ protected static final int SET         = -1;
    /** Selection type: Add to Selection      */ protected static final int ADD         = -2;
    /** Selection type: Remove from Selection */ protected static final int REMOVE      = -3;

    /** The list of selection types (does not include SET, ADD, REMOVE). */
    public static final String selectionList = "Anchor,Click,ShiftClick,CtrlClick,Deselect";

    /** Creates a new instance.
        @param table The table this is used for.
     */
    protected LargeTableSelectionModel(LargeJTable table) {
      this.table  = table;
      rowSelModel = new TableRowSelectionModel(this,table);
      colSelModel = new TableColSelectionModel(this,table);
    }

    /** Gets the row selection model to use.
     *  @return the row selection model to use */
    public LargeListSelectionModel getRowSelModel() { return rowSelModel; }
    /** Gets the column selection model to use.
     *  @return the column selection model to use */
    public LargeListSelectionModel getColSelModel() { return colSelModel; }
    /** Gets the table.
     *  @return the table */
    protected LargeJTable getTable() { return table; }
    /** Gets the table model.
     *  @return the table model */
    protected LargeTableModel getLargeTableModel() { return getTable().getLargeTableModel(); }

    /** Creates a string version of this class. This method is for debugging purposes ONLY and may
        change between versions.
        @return A string describing this class.
     */
    public String toString() {
      StringBuilder str = new StringBuilder(128);

      str.append(getClass().getName());
      str.append(": SelectColumns(");
      str.append(colSet);
      str.append("),");
      for (RowSelection rowsel : rowSet) {
        str.append(" ");
        str.append(rowsel);
      }

      return str.toString();
    }

    /** Gets the current selection. */
    protected RowSelection[] getCurrentSelection() {
      return rowSet.toArray(new RowSelection[0]);
    }

    /** Determines if the given row is selected.
        @param row The row index.
        @return <code>true</code> if the row is selected, <code>false</code> if not.
     */
    public final boolean isRowSelected(long row) {
      return isCellSelected(row, -1L);
    }

    /** Determines if the given column is selected.
        @param col The column index.
        @return <code>true</code> if the column is selected, <code>false</code> if not.
     */
    public final boolean isColSelected(long col) {
      return isCellSelected(-1L, col);
    }

    /** Determines if the given cell is selected.
        @param row The row index.
        @param col The column index.
        @return <code>true</code> if the cell is selected, <code>false</code> if not.
     */
    public boolean isCellSelected(long row, long col) {
      boolean selected = false;

      if (row < 0L) {
        selected = colSet.get((int)col);                            // Check for column selection

      } else {
        RowSelection rowSelection = getRowSel(row);

        if (rowSelection!=null) {
          selected = (rowSelection.getColSet()==null);             // Check for row selection


          if (!selected && (col >= 0L)) {
            selected = colSet.get((int)col)                        // Check for column selection
                    || rowSelection.getColSet().get((int)col);     // Check for cell selection
          }
        }
      }
      return selected;
    }

    /** Notifies listeners that the cell selection has changed.
        @param row The row for the cell.
        @param col The column for the cell.
     */
    protected void fireCellSelectionChange(long row, long col) {
      fireRowSelectionChange(row,row,col);
    }

    /** Notifies listeners that a row selection has changed.
        @param row The row that has been selected/deselected.
     */
    protected final void fireRowSelectionChange(long row) {
      fireRowSelectionChange(row,row,-1);
    }

    /** Notifies listeners that a row selection has changed.
        @param start The first row that has been selected/deselected.
        @param end   The last row that has been selected/deselected.
     */
    protected final void fireRowSelectionChange(long start, long end) {
      fireRowSelectionChange(start,end,-1);
    }

    /** Notifies listeners that a row selection has changed.
        @param start The first row that has been selected/deselected.
        @param end   The last row that has been selected/deselected.
        @param col   The column of the change.
     */
    protected void fireRowSelectionChange(long start, long end, long col) {
      // XXX This could be faster if only firing for the required columns.
      rowSelModel.fireSelectionChange(start, end, false);
    }

    /** Notifies listeners that a row selection has changed.
        @param start The first row that has been selected/deselected.
        @param end   The last row that has been selected/deselected.
        @param set   The set of columns that have changed.
     */
    protected final void fireRowSelectionChange(long start, long end, BitSet set) {
      if (set == null) { // All Columns
        fireRowSelectionChange(start,end);

      } else {
        for (int col = set.nextSetBit(0); col >= 0; col = set.nextSetBit(col+1)) {
          fireRowSelectionChange(start, end, (long)col);
        }
      }
    }

    /** Notifies listeners that a column selection has changed.
        @param col The column that has been selected/deselected.
     */
    protected final void fireColSelectionChange(long col) {
      colSelModel.fireSelectionChange(col, col, false);
    }

    /** Notifies listeners that a column selection has changed.
        @param start The first column that has been selected/deselected.
        @param end   The last column that has been selected/deselected.
     */
    protected void fireColSelectionChange(long start, long end) {
      colSelModel.fireSelectionChange(start, end, false);
    }

    /** Clears the entire selection.
     *  @param msg true if message (mouse click)
     */
    public final void clearSelection(boolean msg) {
      changeCellSelection(-1, -1, DESELECT, msg);
    }

    /** Clears the entire selection.
     *  @param msg unused */
    protected void doClearSelection(boolean msg) {
      doClearRowSelection(msg);
      doClearColSelection(msg);
    }

    /** Clears the row selection.
     *  @param msg unused */
    protected void doClearRowSelection(boolean msg) {
      if (!rowSet.isEmpty()) {
        long firstRow = getFirstRowSelected();
        long lastRow  = getLastRowSelected();

        rowSet.clear();

        fireRowSelectionChange(firstRow, lastRow);
      }
    }

    /** Clears the entire selection.
     *  @param msg unused */
    protected void doClearColSelection (boolean msg) {
      if (!colSet.isEmpty()) {
        long firstCol = getFirstColSelected();
        long lastCol  = getLastColSelected();

        colSet.clear();

        fireColSelectionChange(firstCol, lastCol);
      }
    }

    /** Changes the selection.
        @param row    The row.
        @param col    The column.
        @param toggle Indicates if toggling cell selection.
        @param extend Indicates if extending cell selection.
     */
    public final void changeSelection (long row, long col, boolean toggle, boolean extend) {
      int mode = rowSelModel.getSelectionMode();
      int type = 0;

      if (mode == ListSelectionModel.SINGLE_SELECTION) {
        type = CLICK;
      }
      else if (mode == ListSelectionModel.SINGLE_INTERVAL_SELECTION) {
        type = (extend)? SHIFT_CLICK : CLICK;
      }
      else { // MULTIPLE_INTERVAL_SELECTION
        if (toggle) {
          if (extend) type = ANCHOR;         // (Anchor Update) --> toggle=true  extend=true
          else        type = CTRL_CLICK;     // CTRL+CLICK      --> toggle=true  extend=false
        } else {
          if (extend) type = SHIFT_CLICK;    // SHIFT+CLICK     --> toggle=false extend=true
          else        type = CLICK;          // CLICK           --> toggle=false extend=false
        }
      }
      changeCellSelection(row, col, type, true);
    }

    /** Changes the selection. This is a convenience method since the boolean toggle/extend flags
        tend to be confusing.
        @param row  The row.
        @param col  The column.
        @param type The selection type ({@link #ANCHOR}, {@link #CLICK}, {@link #SHIFT_CLICK},
                    {@link #CTRL_CLICK} or {@link #DESELECT}).
        @param msg  true if message (mouse click)
     */
    protected void changeCellSelection (long row, long col, int type, boolean msg) {

      if ((table.autoSelect) || !msg) {  // Check for autoSelect and mouse click
        switch (type) {
          case ANCHOR:                                                                      break;
          case CLICK:       doClearSelection(msg); doAddCellSelection(row,row,col,col,msg); break;
          case SHIFT_CLICK: doExtendCellSelection(row,col,msg);                             break;
          case CTRL_CLICK:  doToggleCellSelection(row,col,msg);                             break;
        }
      }

      if (type == DESELECT) doClearSelection(msg);

      if ((type != SHIFT_CLICK) && (type != DESELECT)) {
        setRowAnchor(row);  setRowLead(row);
        setColAnchor(col);  setColLead(col);
      }
    }

    /** Changes the selection. This is a convenience method since the boolean toggle/extend flags
        tend to be confusing.
        @param start The start row.
        @param end   The end row.
        @param type  The selection type ({@link #SET}, {@link #ADD}, {@link #REMOVE},
                     or {@link #DESELECT}).
        @param msg   unused
     */
    @SuppressWarnings("fallthrough")
    protected void changeRowSelection (long start, long end, int type, boolean msg) {
      switch (type) {
        case SET:      doClearSelection(msg); // fallthrough
        case ADD:      doAddRowSelection(start,end,null,msg);    break;
        case REMOVE:   doRemoveRowSelection(start,end,null,msg); break;
        case DESELECT: doClearSelection(msg);                    break;
      }
    }

    /** Changes the selection. This is a convenience method since the boolean toggle/extend flags
        tend to be confusing.
        @param start The start column.
        @param end   The end column.
        @param type  The selection type ({@link #SET}, {@link #ADD}, {@link #REMOVE},
                     or {@link #DESELECT}).
        @param msg   unused
     */
    @SuppressWarnings("fallthrough")
    protected void changeColSelection (long start, long end, int type, boolean msg) {
      switch (type) {
        case SET:      doClearSelection(msg); // fallthrough
        case ADD:      doAddColSelection(start,end,msg);    break;
        case REMOVE:   doRemoveColSelection(start,end,msg); break;
        case DESELECT: doClearSelection(msg);               break;
      }
    }

    /** Extends a cell selection (shift-select extending from last anchor).
     *  @param row to index (from anchor)
     *  @param col to column (from anchor)
     *  @param msg unused
     */
    protected void doExtendCellSelection(long row, long col, boolean msg) {
      if (getRowAnchor() < 0) { setRowAnchor(row); }
      if (getColAnchor() < 0) { setColAnchor(col); }

      long rowStart = Math.min(getRowAnchor(), row);
      long rowEnd   = Math.max(getRowAnchor(), row);
      long colStart = Math.min(getColAnchor(), col);
      long colEnd   = Math.max(getColAnchor(), col);

      doClearSelection(msg);
      doAddCellSelection(rowStart,rowEnd,colStart,colEnd,msg);
    }

    /** Toggles a cell selection.
     *  @param row cell row index
     *  @param col cell column index
     *  @param msg unused
     */
    protected void doToggleCellSelection(long row, long col, boolean msg) {
      if (isCellSelected(row, col)) {
        doRemoveCellSelection(row,row,col,col,msg);
      } else {
        doAddCellSelection(row,row,col,col,msg);
      }
    }

    /** Adds a row selection.
     *  @param rowStart the start row index
     *  @param rowEnd   the end row index
     *  @param colStart the start column index
     *  @param colEnd   the end column index
     *  @param msg      unused
     */
    public void doAddCellSelection(long rowStart, long rowEnd, long colStart, long colEnd, boolean msg) {
      boolean ignore = false; // Indicates if skipping this action.

      if (rowStart < 0) {        // COLUMN CLICK
        ignore = (table.colAction != LargeJTable.COL_ACTION_SELECT);

      } else if (colStart < 0) { // ROW CLICK
        ignore = (table.rowAction != LargeJTable.ROW_ACTION_SELECT);

      } else {                   // CELL CLICK
             if (table.cellAction==LargeJTable.CELL_ACTION_ROW   ) { colStart = -1; colEnd = -1; }
        else if (table.cellAction==LargeJTable.CELL_ACTION_COL   ) { rowStart = -1; rowEnd = -1; }
        else if (table.cellAction==LargeJTable.CELL_ACTION_SELECT) { /* All Ok */                }
        else                                                       { ignore = true;              }
      }

      if (!ignore) {
        BitSet set = null;

        if ((colStart >= 0) && (colEnd >= colStart)) {
          set = new BitSet((int)colEnd);
          set.set((int)colStart, (int)colEnd+1);
        }
        if (rowStart == -1 && rowEnd == -1 && table!=null) {  // entire column selected
          rowStart = 0;
          rowEnd   = table.getNumberOfRows()-1;
        }

        doAddRowSelection(rowStart,rowEnd,set,msg);
      }
    }

    /** Adds a row selection.
     *  @param start the start row index (inclusive)
     *  @param end   the end row index (inclusive)
     *  @param set   the column set
     *  @param msg   unused
     */
    protected void doAddRowSelection(long start, long end, BitSet set, boolean msg) {
      for (long row = start; row <= end; row++) {
        RowSelection rowSel = getRowSel(row);

        if (rowSel == null) {
          /* Add newly selected row. */
          rowSet.add(getRowSelIndex(row), new RowSelection(row,set));

        } else if ((set == null) || (rowSel.getColSet() == null)) {
          /* Entire row is selected. */
          rowSel.setColSet(null);

        } else {
          /* Set row selection to the union of the two sets. */
          rowSel.getColSet().or(set);
        }
      }
      fireRowSelectionChange(start, end, set);
    }

    /** Adds selection of the specified columns.
     *  @param start the start index
     *  @param end   the end index
     *  @param msg   unused
     */
    public void doAddColSelection(long start, long end, boolean msg) {
      colSet.set((int)start, (int)end+1);
      fireColSelectionChange(start,end);
    }

    /** Removes selection of cell group.
     *  @param rowStart the start row (inclusive)
     *  @param rowEnd   the last row (inclusive)
     *  @param colStart the start column (inclusive)
     *  @param colEnd   the last column (inclusive)
     *  @param msg      unused
     */
    public void doRemoveCellSelection(long rowStart, long rowEnd, long colStart, long colEnd, boolean msg) {
      if (colStart < 0) {
        doRemoveRowSelection(rowStart,rowEnd,null,msg);

      } else {
        BitSet set = new BitSet();

        set.set((int)colStart, (int)colEnd+1);

        doRemoveRowSelection(rowStart,rowEnd,set,msg);
      }
    }

    /** Removes row selection (either entire rows or just specified columns)
        @param start The start row (inclusive).
        @param end   The end row (inclusive).
        @param set   The column selection.
        @param msg   unused
     */
    public void doRemoveRowSelection(long start, long end, BitSet set, boolean msg) {
      for (long row = start; row <= end; row++) {
        RowSelection rowSel = getRowSel(row);

        if (rowSel == null) {
          // Nothing to deselect.

        } else if (set == null) {
          rowSet.remove(getRowSelIndex(row)); // Entire row is deselected.

        } else if (rowSel.getColSet() == null) {
          // Can't deselect a set of cells if the entire row is selected.

        } else if (set.equals(rowSel.getColSet())) {
          rowSet.remove(getRowSelIndex(row)); // Remove all in row.

        } else {
          rowSel.getColSet().andNot(set); // Remove part of row
        }
      }
      fireRowSelectionChange(start, end, set);
    }

    /** Removes a col selection
     *  @param start from index (inclusive)
     *  @param end   to index (exclusive)
     *  @param msg   unused
     */
    public void doRemoveColSelection(long start, long end, boolean msg) {
      colSet.clear((int)start, (int)end);
      fireColSelectionChange(start,end);
    }

    /** Gets the index of the first selected row.
     *  @return the index of the first selected row. */
    public long getFirstRowSelected() {
      return (rowSet.isEmpty())? -1L : rowSet.get(0).getIndex();
    }

    /** Gets the index of the first selected column.
     *  @return the index of the last selected row */
    public long getFirstColSelected() {
      return (long)colSet.nextSetBit(0);
    }

    /** Gets the index of the last selected row.
     *  @return the index of the last selected row */
    public long getLastRowSelected() {
      return (rowSet.isEmpty())? -1L : rowSet.get(rowSet.size()-1).getIndex();
    }

    /** Gets the index of the last selected column.
     *  @return the index of the last selected column */
    public long getLastColSelected() {
      int last = -1;

      if (!colSet.isEmpty()) {
        for (int i = colSet.nextSetBit(0); i >= 0; i = colSet.nextSetBit(i+1)) {
          last = i;
        }
      }
      return (long)last;
    }

    /** Indicates if the row selection is empty.
     *  @return true if the row selection is empty */
    public boolean isRowSelectionEmpty() {
      return rowSet.isEmpty();
    }

    /** Indicates if the column selection is empty.
     *  @return true if the column selection is empty */
    public boolean isColSelectionEmpty() {
      return colSet.isEmpty();
    }

    /** Gets the row selection that corresponds to the given row index.
        @param row The row index.
        @return The appropriate selection object containing the row or null if none exists.
     */
    private RowSelection getRowSel(long row) {
      int          index  = getRowSelIndex(row);
      RowSelection rowSel = null; // The given row (null if none).

      if (index < rowSet.size()) {
        rowSel = rowSet.get(index);

        if (rowSel.getIndex() != row) {
          rowSel = null; // Not valid -- throw it out.
        }
      }

      return rowSel;
    }

    /** Gets the row selection that corresponds to the given row index.
        @param row The row index.
        @return The index of the appropriate selection.
     */
    private int getRowSelIndex(long row) {
      int index = 0;

      if ((rowSet.size() > 0) && (row >= 0)) {
        index = Collections.binarySearch(rowSet, Long.valueOf(row));

        if (index < 0) { index = -index - 1; } // Convert to one less than insert point.
      }

      return index;
    }

    /** Sets the anchor point.
        @param row The row.
        @param col The column.
     */
    private final void setAnchor(long row, long col) {
      setRowAnchor(row);
      setColAnchor(col);
    }

    /** Sets the lead point.
        @param row The row.
        @param col The column.
     */
    private final void setLead(long row, long col) {
      setRowLead(row);
      setColLead(col);
    }

    /** Gets the anchor row for selections
        @return the anchor row
     */
    public long getRowAnchor() { return rowAnchor; }
    /** Gets the lead row for selections
        @return the lead row
     */
    public long getRowLead()   { return rowLead;   }
    /** Gets the anchor column for selections
        @return the anchor column
     */
    public long getColAnchor() { return colAnchor; }
    /** Gets the lead column for selections
        @return the lead column
     */
    public long getColLead()   { return colLead;   }

    /** Sets the anchor row for selections
        @param index index for the new anchor row
     */
    public void setRowAnchor(long index) { rowAnchor = index; }
    /** Sets the lead row for selections
        @param index index for the new lead row
     */
    public void setRowLead(long index)   { rowLead   = index; }
    /** Sets the anchor column for selections
        @param index index for the new column row
     */
    public void setColAnchor(long index) { colAnchor = index; }
    /** Sets the lead column for selections
        @param index index for the new lead column
     */
    public void setColLead(long index)   { colLead   = index; }
  }

  /** A version of {@link ListSelectionModel} for a {@link LargeTableSelectionModel}. */
  private static class TableRowSelectionModel extends DefaultListSelectionModel implements LargeListSelectionModel {
    /** The table selection model */ protected final LargeTableSelectionModel tsm;
    /** The table                 */ protected final LargeJTable              table;

    /** Creates a new instance.
        @param tsm   The table selection model.
        @param table The table.
     */
    public TableRowSelectionModel(LargeTableSelectionModel tsm, LargeJTable table) {
      this.tsm   = tsm;
      this.table = table;
      setSelectionMode(MULTIPLE_INTERVAL_SELECTION);
    }

    @Override
    public void setSelectionInterval(long start, long end) {
      tsm.changeRowSelection(start, end, LargeTableSelectionModel.SET,true);
    }

    @Override
    public void addSelectionInterval(long start, long end) {
      tsm.changeRowSelection(start, end, LargeTableSelectionModel.ADD,true);
    }

    @Override
    public void removeSelectionInterval(long start, long end) {
      tsm.changeRowSelection(start, end, LargeTableSelectionModel.REMOVE,true);
    }

    @Override
    public void clearSelection() {
      tsm.changeRowSelection(-1, -1, LargeTableSelectionModel.DESELECT,true);
    }

    @Override
    public boolean isSelectionEmpty()          { return tsm.isRowSelectionEmpty();  }
    @Override
    public boolean isSelectedIndex(long index) { return tsm.isRowSelected(index);   }
    @Override
    public long    getFirstSelectionIndex()    { return tsm.getFirstRowSelected();  }
    @Override
    public long    getLastSelectionIndex()     { return tsm.getLastRowSelected();   }
    @Override
    public long    getSelectionAnchor()        { return tsm.getRowAnchor();         }
    @Override
    public long    getSelectionLead()          { return tsm.getRowLead();           }

    @Override
    public void setSelectionAnchor(long index) { tsm.setRowAnchor(index); }
    @Override
    public void setSelectionLead(long index)   { tsm.setRowLead(index);   }

    public void insertIndexInterval(long index, long length, boolean before) {
// XXX Fix This TODO
    }
    public void removeIndexInterval(long index0, long index1) {
// XXX Fix This TODO
    }

    /** Gets the offset to adjust the index values.
     *  @return the offset to adjust the index values
     */
    protected long getOffset() {
      return table.getTopRowIndex();
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // OVERRIDES
    ////////////////////////////////////////////////////////////////////////////////////////////////
    @Override
    public final boolean isSelectedIndex(int i)    {return isSelectedIndex(getOffset() + i);       }
    @Override
    public final int     getAnchorSelectionIndex() {return (int)(getSelectionAnchor()-getOffset());}
    @Override
    public final int     getLeadSelectionIndex()   {return (int)(getSelectionLead()  -getOffset());}

    @Override
    public final void    setLeadSelectionIndex(int i)   { setSelectionLead(getOffset() + i);   }
    @Override
    public final void    setAnchorSelectionIndex(int i) { setSelectionAnchor(getOffset() + i); }

    @Override
    public final void insertIndexInterval(int index, int length, boolean before) {
      insertIndexInterval(getOffset() + index, (long)length, before);
    }

    @Override
    public final void setSelectionInterval(int index0, int index1) {
      setSelectionInterval(getOffset() + index0, getOffset() + index1);
    }

    @Override
    public final void addSelectionInterval(int index0, int index1) {
      addSelectionInterval(index0 + getOffset(), index1 + getOffset());
    }

    @Override
    public final void removeIndexInterval(int index0, int index1) {
      removeIndexInterval(index0 + getOffset(), index1 + getOffset());
    }

    @Override
    public final void removeSelectionInterval(int index0, int index1) {
      removeSelectionInterval(index0 + getOffset(), index1 + getOffset());
    }

    @Override
    public final int getMaxSelectionIndex() {
      return (int)Math.min((long)Integer.MAX_VALUE, getLastSelectionIndex());
    }

    @Override
    public final int getMinSelectionIndex() {
      return (int)Math.min(0L, getFirstSelectionIndex());
    }

    /** Fires a change.
        @param first  First value updated.
        @param last   Last value updated.
        @param isAdj  Indicates if it is an adjustment.
     */
    public void fireSelectionChange(long first, long last, boolean isAdj) {
      int start = (int)(first - getOffset());
      int end   = (int)(last  - getOffset());

      fireValueChanged(start, end, isAdj);
    }
  }

  /** A version of {@link ListSelectionModel} for a {@link LargeTableSelectionModel}. */
  private static class TableColSelectionModel extends TableRowSelectionModel {
    /** Creates a new instance.
        @param tsm   The table selection model.
        @param table The table.
     */
    public TableColSelectionModel(LargeTableSelectionModel tsm, LargeJTable table) {
      super(tsm,table);
    }

    @Override
    public void setSelectionInterval(long start, long end) {
      tsm.changeColSelection(start, end, LargeTableSelectionModel.SET,true);
    }
    @Override
    public void addSelectionInterval(long start, long end) {
      tsm.changeColSelection(start, end, LargeTableSelectionModel.ADD,true);
    }
    @Override
    public void removeSelectionInterval(long start, long end) {
      tsm.changeColSelection(start, end, LargeTableSelectionModel.REMOVE,true);
    }

    @Override
    public void clearSelection() {
      tsm.changeColSelection(-1, -1, LargeTableSelectionModel.DESELECT,true);
    }

    @Override
    public boolean isSelectionEmpty()          { return tsm.isColSelectionEmpty();  }
    @Override
    public boolean isSelectedIndex(long index) { return tsm.isColSelected(index);   }
    @Override
    public long    getFirstSelectionIndex()    { return tsm.getFirstColSelected();  }
    @Override
    public long    getLastSelectionIndex()     { return tsm.getLastColSelected();   }
    @Override
    public long    getSelectionAnchor()        { return tsm.getColAnchor();         }
    @Override
    public long    getSelectionLead()          { return tsm.getColLead();           }

    @Override
    public void setSelectionAnchor(long index) { tsm.setColAnchor(index); }

    @Override
    public void setSelectionLead(long index)   { tsm.setColLead(index);   }

    @Override
    public void insertIndexInterval(long index, long length, boolean before) {
//System.out.println("insertIndexInterval("+index+", "+length+", "+before+")");
// XXX Fix This
    }
    @Override
    public void removeIndexInterval(long index0, long index1) {
//System.out.println("removeIndexInterval("+index0+", "+index1+")");
// XXX Fix This
    }

    @Override
    protected long getOffset() {
      return 0L;
    }
  }

  /** Manages a single row selection. */
  protected static class RowSelection implements Comparable<Object> {
    /** The start row (inclusive). */
    private long index;

    /** The selected columns. */
    private BitSet set;  // null = entire row selected

    /** Creates a new instance. This is the same as <code>this(index,null)</code>.
        @param index The row index.
     */
    public RowSelection(long index) {
      this(index,null);
    }

    /** Creates a new instance.
        @param index The row index.
        @param set   The selected columns.
     */
    public RowSelection(long index, BitSet set) {
      this.index = index;
      this.set   = set;
    }

    /** Gets the row index.
     *  @return the row index */
    public long getIndex() { return index; }

    /** Gets the column set.
     *  @return the column set */
    public BitSet getColSet() { return set; }

    /** Sets the row index.
     *  @param row the row index */
    public void setIndex(long row) { index = row; }

    /** Sets the column set.
     *  @param cs the column set */
    public void setColSet(BitSet cs) { set = cs; }

    /** @since 3.5.0 */
    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + (int) (index ^ (index >>> 32));
      result = prime * result + ((set == null) ? 0 : set.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (getClass() != obj.getClass()) {
        return false;
      }
      RowSelection other = (RowSelection) obj;
      if (index != other.index) {
        return false;
      }
      if (set == null) {
        if (other.set != null) {
          return false;
        }
      }
      else if (!set.equals(other.set)) {
        return false;
      }
      return true;
    }

    @Override
    public int compareTo(Object obj) {
      long diff = 0;

      if (obj instanceof Number) { diff = getIndex() - ((Number)obj).longValue();      }
      else                       { diff = getIndex() - ((RowSelection)obj).getIndex(); }

      return (int)Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, diff));
    }

    /** Creates a string version of this class. This method is for debugging purposes ONLY and may
        change between versions.
        @return A string describing this class.
     */
    @Override
    public String toString() {
      String str = null;

      if (getColSet() == null) {
        str = "SelectRow(row=" + getIndex() + ", cells=ALL)";
      } else {
        str = "SelectRow(row=" + getIndex() + ", cells=" + getColSet()+")";
      }

      return str;
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // THE VERTICAL SCROLL BAR
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** The special vertical scroll bar to use. This overrides several of the methods so that they
      can directly interact with the table to allow large files to be used. The scroll bar will
      work as follows:
      <pre>
        CLICK ON THE ARROW  - Move up/down one row.
        CLICK ON THE TRACK  - Move up/down one visible page (some overlap will be included).
        DRAG THE TAB/THUMB  - Move to relative position in file (in theory maximum resolution
                              is 0.01%, in practice it's closer to 0.5%).
      </pre>
      Note that because of the way this class is implemented many of the normal listeners will not
      work correctly when attached to this class.
   */
  protected static class VertScrollBar extends JScrollBar {
    /** Creates a new instance.
        @param table The table the scroll bar is attached to.
     */
    public VertScrollBar(LargeJTable table) {
      super(Adjustable.VERTICAL);

      setModel(new VertScrollBarModel(this, table));
      setUnitIncrement(0);  // Real value inserted below
      setBlockIncrement(0); // Real value inserted below
    }

    ////////
    // JC4: Some look and feels have issues if we don't override these methods as such.
    ////////
    @Override
    public int  getUnitIncrement()         { return VertScrollBarModel.UNIT_INC;           }
    @Override
    public int  getBlockIncrement()        { return VertScrollBarModel.BLOCK_INC;          }
    @Override
    public void setUnitIncrement(int inc)  { super.setUnitIncrement(getUnitIncrement());   }
    @Override
    public void setBlockIncrement(int inc) { super.setBlockIncrement(getBlockIncrement()); }

    ////////
    // JC4: Workaround for a refresh issue.
    ////////
    @Override
    public void setValue(int value) {
      super.setValue(value);
//      if (value != 0) {
        revalidate();
        repaint();
//      }
    }
  }

  /** Model to use with the {@link VertScrollBar}. Note that this is a separate class because
      some of the look and feels have issues if {@link VertScrollBar} acts as its own model.
   */
  private static class VertScrollBarModel extends DefaultBoundedRangeModel {
    /** JC4: Swing components will report all of the scroll bar changes as TRACK events (due to the
             way the UI is implemented) which prevent us from differentiating between BLOCK and
             UNIT increments made by the user. To get around this we set the BLOCK and UNIT
             increment amounts to extremely large values. This way when a setValue(..) comes in
             (i.e. this would be a TRACK event) we can key off of the extra-large (or in the case
             of decrement, extra-small) values and interpret them as the BLOCK and UNIT increments
             that they represent.

             Note that several of the look and feels (including Metal and Motif) call setValue(0)
             many times while the user is tracking (plus a few other times). To prevent this from
             being an issue (any more) the minimum value is intentionally > 0 and the value 0 is
             reserved due to this situation.
     */

    /** The minimum value of the scroll bar  */ private static final int    MIN_VALUE     =                   1;
    /** The maximum value of the scroll bar  */ private static final int    MAX_VALUE     =               10001;
    /** The unit incrament flag              */ private static final int    UNIT_INC      =            10000000;
    /** The block incrament flag             */ private static final int    BLOCK_INC     =           100000000;
    /** Percent overlap in a block increment */ private static final double BLOCK_OVERLAP =                0.80;
    /** The minimum extent                   */ private static final int    MIN_EXTENT    =                   0;
    /** The maximum extent                   */ private static final int    MAX_EXTENT    = MAX_VALUE-MIN_VALUE;

    /** The scroll bar                          */ private final VertScrollBar scrollBar;
    /** The table the scroll bar is attached to */ private final LargeJTable   table;

    /** Creates a new instance.
        @param scrollBar The scroll bar
        @param table     The table the scroll bar is attached to.
     */
    public VertScrollBarModel(VertScrollBar scrollBar, LargeJTable table) {
      super(MIN_VALUE, 0, MIN_VALUE, MAX_VALUE);
      this.scrollBar = scrollBar;
      this.table     = table;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // JC4: The next two methods compute the value and extent based on the table's parameters.
    @Override
    public int getExtent() {
      int pos = getRelativePos(table.getNumberOfVisibleRows(), MIN_EXTENT);
      if (DEBUG_SCROLL_BAR) { System.out.println("VerticalScrollBar.getExtent() = "+pos); }
      return pos;
    }

    @Override
    public int getValue() {
      int pos = getRelativePos(table.getTopRowIndex(), MIN_VALUE);
      if (DEBUG_SCROLL_BAR) { System.out.println("VerticalScrollBar.getValue() = "+pos); }
      return pos;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    // JC4: Some look and feels have issues if we don't override these methods.
    @Override
    public void setMinimum(int min) { }
    @Override
    public void setMaximum(int max) { }
    @Override
    public void setExtent(int vis)  { }

    @Override
    public void setValue(int val)   {
      setRangeProperties(val, getExtent(), MIN_VALUE, MAX_VALUE, getValueIsAdjusting());
    }

    @Override
    public void setRangeProperties(int val, int ext, int min, int max, boolean adj) {
      updateValue(val);
      super.setRangeProperties(getValue(), getExtent(), MIN_VALUE, MAX_VALUE, adj);
      scrollBar.revalidate();
      scrollBar.repaint();
    }

    /** This method basically voids the repeat call to {@link #updateValue(int)} in the
        {@link #setRangeProperties(int, int, int, int, boolean)} above,
        which causes some rounding issue with very large numbers (Bug 2591)
        @param flag  true if a series of changes are in progress
        @since NeXtMidas 3.1.2
     */
    public void setValueIsAdjusting (boolean flag) {
      super.setRangeProperties(getValue(), getExtent(), MIN_VALUE, MAX_VALUE, flag);
      scrollBar.revalidate();
      scrollBar.repaint();
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////

    /** Updates the scroll bar value. */
    private void updateValue(int value) {
      double  totalRows  = (double)table.getNumberOfRows();
      double  topRow     = (double)table.getTopRowIndex();
      double  size       = (double)table.getNumberOfVisibleRows() * BLOCK_OVERLAP;
      // line below changed to line below it in 3.1.2 because it prevents tracking to the bottom row for large files (Bug 2591)
      //double  pos      = ((double)value-MIN_VALUE) / MAX_VALUE * totalRows;
      double  pos        = ((double)value-MIN_VALUE) / (MAX_VALUE-1) * totalRows;  // -1 taken from max value top allow for 100% (Bug 2591)
      //double  visibleAmt = size / totalRows * MAX_VALUE; // <--- variable is NOT used
      double  row        = 0.0;
      boolean ignore     = false; // Should the change be ignored (usually false).
      String  type       = null;  // Type name

           if (value >= BLOCK_INC) { type = "BLOCK_INC";  row = topRow + size;                }
      else if (value >= UNIT_INC ) { type = "UNIT_INC";   row = topRow + 1;                   }
      else if (value >= MIN_VALUE) { type = "TRACK";      row = pos;                          }
      else if (value == 0        ) { type = "IGNORE";     row = topRow;        ignore = true; }
      else if (value >= -UNIT_INC) { type = "UNIT_DEC";   row = topRow - 1;                   }
      else                         { type = "BLOCK_DEC";  row = topRow - size;                }
      row = Math.round(row); // JC4: if we don't round we get stuck if N+1=N.9999 (rounding errors)

      if (DEBUG_SCROLL_BAR) {
        String str = "VerticalScrollBar.updateValue("+value+"): "+type+" ("+topRow+"-->"+row+")";
        if (ignore) { str = str + " **IGNORE**"; }
        System.out.println(str);
      }
      if (!ignore) {
        table.setTopRowIndex((long)row);
      }
    }

    /** Gets the relative position of the given row.
        @param row The row.
        @param def The default value to use when totalRows=0.
        @return The relative position or default value if totalRows=0.
     */
    private int getRelativePos(long row, int def) {
      double totalRows = (double)table.getNumberOfRows();
      int pos = def;

      if (totalRows > 0.0) {
        pos = (int)((((double)row) / totalRows) * MAX_VALUE);
      }

      return pos;
    }
  } // end class VertScrollBarModel

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // The selection Listener
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** SelectionListener */
  public static class SelectionListener {
    /** default constructor */
    public SelectionListener () {}
  }

}