/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.display;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.text.DecimalFormat;
import java.util.EventObject;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;

/**
 * A JTable to display int, double and String array values.
 * Version 1.1 uses a Timer to coalesce Refresh Table events
 *
 * @author Douglas Brown
 * @author Wolfgang Christian
 * @version 1.1
 */
public class ArrayTable extends JTable implements ActionListener {
  int refreshDelay = 300;                                                     // time in ms to delay refresh events
  javax.swing.Timer refreshTimer = new javax.swing.Timer(refreshDelay, this); // delay for refreshTable
  ArrayTableModel tableModel;
  ArrayIndexRenderer indexRenderer = new ArrayIndexRenderer();
  ArrayCellRenderer cellRenderer = new ArrayCellRenderer();
  java.util.Dictionary<Integer, DecimalFormat> formatDictionary = new java.util.Hashtable<Integer, DecimalFormat>();
  String formatPattern = "0.000";                                             //$NON-NLS-1$
  DecimalFormat defaultFormat = new DecimalFormat(formatPattern);
  Object prevValue;

  /**
   * Constructor for 1D int array.
   *
   * @param array the array
   */
  public ArrayTable(int[] array) {
    tableModel = new ArrayTableModel(array);
    init();
  }

  /**
   * Constructor for 2D int array.
   *
   * @param array the array
   */
  public ArrayTable(int[][] array) {
    tableModel = new ArrayTableModel(array);
    init();
  }

  /**
   * Constructor for 1D double array.
   *
   * @param array the array
   */
  public ArrayTable(double[] array) {
    tableModel = new ArrayTableModel(array);
    init();
  }

  /**
   * Constructor for 2D double array.
   *
   * @param array the array
   */
  public ArrayTable(double[][] array) {
    tableModel = new ArrayTableModel(array);
    init();
  }

  /**
   * Constructor for 1D String array.
   *
   * @param array the array
   */
  public ArrayTable(String[] array) {
    tableModel = new ArrayTableModel(array);
    init();
  }

  /**
   * Constructor for 2D String array.
   *
   * @param array the array
   */
  public ArrayTable(String[][] array) {
    tableModel = new ArrayTableModel(array);
    init();
  }

  /**
   * Constructor for 1D boolean array.
   *
   * @param array the array
   */
  public ArrayTable(boolean[] array) {
    tableModel = new ArrayTableModel(array);
    init();
  }

  /**
   * Constructor for 2D boolean array.
   *
   * @param array the array
   */
  public ArrayTable(boolean[][] array) {
    tableModel = new ArrayTableModel(array);
    init();
  }

  /**
   * Initializes the table.
   */
  protected void init() {
    refreshTimer.setRepeats(false);
    refreshTimer.setCoalesce(true);
    setModel(tableModel);
    tableModel.addTableModelListener(new TableModelListener() {
      public void tableChanged(TableModelEvent e) {
        int row = e.getFirstRow();
        int col = tableModel.showRowNumber ? e.getColumn()+1 : e.getColumn();
        Object value=getValueAt(row, col);
        if((value!=null)&&!value.equals(prevValue)) {
          // forward the table model event to property change listeners
          ArrayTable.this.firePropertyChange("cell", null, e); //$NON-NLS-1$
        }
      }

    });
    setDefaultRenderer(Object.class, cellRenderer);
    setColumnSelectionAllowed(true);
    getTableHeader().setReorderingAllowed(false);
    getTableHeader().setDefaultRenderer(indexRenderer);
    setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
    setGridColor(Color.BLACK);
    int width = 24;
    String name;
    TableColumn column;
    if(getColumnCount()>0) {
      // set width of column 0 (row index)
      name = getColumnName(0);
      column = getColumn(name);
      column.setMinWidth(width);
      column.setMaxWidth(2*width);
      column.setWidth(width);
    }
    ((java.util.Hashtable<Integer, DecimalFormat>) formatDictionary).clear(); // empty dictionary during initialization
    // set width of other columns
    width = 60;
    for(int i = 1, n = getColumnCount(); i<n; i++) {
      name = getColumnName(i);
      column = getColumn(name);
      column.setMinWidth(width);
      column.setMaxWidth(3*width);
      column.setWidth(width);
    }
    // Override the default tab behavior
    InputMap im = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    KeyStroke tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
    final Action prevTabAction = getActionMap().get(im.get(tab));
    Action tabAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        // tab to the next editable cell
        prevTabAction.actionPerformed(e);
        JTable table = (JTable) e.getSource();
        int rowCount = table.getRowCount();
        int row = table.getSelectedRow();
        int column = table.getSelectedColumn();
        while(!table.isCellEditable(row, column)) {
          if(column==0) {
            column = 1;
          } else {
            row += 1;
          }
          if(row==rowCount) {
            row = 0;
          }
          if((row==table.getSelectedRow())&&(column==table.getSelectedColumn())) {
            break;
          }
        }
        table.changeSelection(row, column, false, false);
      }

    };
    getActionMap().put(im.get(tab), tabAction);
    // enter key starts editing
    Action enterAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        // start editing
        JTable table = (JTable) e.getSource();
        int row = table.getSelectedRow();
        int column = table.getSelectedColumn();
        table.editCellAt(row, column, e);
        Component comp = table.getEditorComponent();
        if(comp instanceof JTextField) {
          JTextField field = (JTextField) comp;
          field.requestFocus();
          field.selectAll();
        }
      }

    };
    KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
    getActionMap().put(im.get(enter), enterAction);
  }

  /**
   * Starts editing the cell at <code>row</code> and <code>column</code>.
   * Overrides JTable method.
   *
   * @param  row the row to be edited
   * @param  column the column to be edited
   * @param  e ignored
   * @return false if the cell cannot be edited or the indices are invalid
   */
  public boolean editCellAt(int row, int column, EventObject e) {
    boolean editing = super.editCellAt(row, column, e);
    // save current value for comparison with value after editing
    if(editing) {
      prevValue = getValueAt(row, column);
    }
    return editing;
  }

  /*
  public void refreshTableOLD() {
    Runnable refresh = new Runnable() {
      public synchronized void run() {
        tableChanged(new TableModelEvent(tableModel, TableModelEvent.HEADER_ROW));
      }

    };
    SwingUtilities.invokeLater(refresh);
  }

*/

  /**
   *  Performs the action for the refresh timer by refreshing the data in the DataTable.
   *
   * @param  evt
   */
  public void actionPerformed(ActionEvent evt) {
    tableChanged(new TableModelEvent(tableModel, TableModelEvent.HEADER_ROW));
  }

  /**
   * Sets the <code>Timer</code>'s initial time delay (in milliseconds)
   * to wait after the timer is started
   * before firing the first event.
   * @param delay
   */
  public void setRefreshDelay(int delay) {
    refreshTimer.setInitialDelay(delay);
  }

  /**
   *  Refresh the data in the table.
   */
  public void refreshTable() {
    refreshTimer.start();
  }

  /**
   * Sets the default numeric display format for all columns
   * @param defaultFormat
   */
  public void setNumericFormat(String str) {
    if((str!=null)&&!str.equals(formatPattern)) { // format has changed
      formatPattern = str;
      defaultFormat = new DecimalFormat(str);
      refreshTable();
    }
  }

  /**
   * Sets the numeric display format for each column
   * @param defaultFormat
   */
  public void setNumericFormat(String[] str) {
    ((java.util.Hashtable<Integer, DecimalFormat>) formatDictionary).clear();
    for(int i = 0, n = str.length; i<n; i++) {
      formatDictionary.put(i, new DecimalFormat(str[i]));
    }
    refreshTable();
  }

  /**
   * Sets the first row's index.
   *
   * @param index
   */
  public void setFirstRowIndex(int index) {
    if(index==tableModel.firstRow) {
      return; // nothing changed
    }
    tableModel.setFirstRowIndex(index);
    refreshTable();
  }

  /**
   * Sets the first column's index.
   *
   * @param index
   */
  public void setFirstColIndex(int index) {
    if(index==tableModel.firstCol) {
      return; // nothing changed
    }
    tableModel.setFirstColIndex(index);
    refreshTable();
  }

  /**
   * Sets the display row number flag. Table displays row number.
   *
   * @param vis <code>true<\code> if table display row number
   */
  public void setRowNumberVisible(boolean vis) {
    if(vis==tableModel.showRowNumber) {
      return; // nothing changed
    }
    tableModel.setRowNumberVisible(vis);
    refreshTable();
  }

  /**
   * Sets the editable property.
   *
   * @param editable true allows editing of the cell values that are not locked.
   */
  public void setEditable(boolean editable) {
    if(editable==tableModel.editable) {
      return; // nothing changed
    }
    tableModel.setEditable(editable);
    refreshTable();
  }

  /**
   * Returns true of the table's row and column values are interchanged.
   * @return
   */
  public boolean isTransposed() {
    return tableModel.isTransposed();
  }

  /**
   * Sets the transposed property for the array.
   * A transposed array switches its row and column values in the display.
   *
   * @param transposed
   */
  public void setTransposed(boolean transposed) {
    if(transposed==tableModel.transposed) {
      return; // nothing changed
    }
    tableModel.transposed = transposed;
    refreshTable();
  }

  /**
   * Sets columns names.
   * @param names
   */
  public void setColumnNames(String[] names) {
    if(tableModel.setColumnNames(names)) {
      refreshTable(); // refresh if there has been a change
    }
  }

  /**
   * Sets the column's locked flag.
   *
   * @param column   int
   * @param locked   boolean
   */
  public void setColumnLock(int columnIndex, boolean locked) {
    if(tableModel.setColumnLock(columnIndex, locked)) {
      refreshTable(); // refresh if there has been a change
    }
  }

  /**
   * Sets the lock flag for multiple columns.  Previously set locks are cleared.
   *
   * @param locked   boolean array
   */
  public void setColumnLocks(boolean[] locked) {
    if(tableModel.setColumnLocks(locked)) {
      refreshTable(); // refresh if there has been a change
    }
  }

  /**
   * Gets the default font of this component.
   * @return this component's font
   */
  public Font getFont() {
    if(indexRenderer==null) {
      indexRenderer = new ArrayIndexRenderer();
    }
    return indexRenderer.getFont();
  }

  
  /**
   * Sets the font for this component.
   *
   * @param font the desired <code>Font</code> for this component
   * @see java.awt.Component#getFont
   */
  public void setFont(Font font){ // Added by Paco
    super.setFont(font);
    if(indexRenderer==null) {
      indexRenderer = new ArrayIndexRenderer();
    }
    if(cellRenderer==null) {
      cellRenderer = new ArrayCellRenderer();
    }
    indexRenderer.setFont(font);
    cellRenderer.setFont(font);
  }
  
  /**
   * Sets the foreground color of this component.  It is up to the
   * look and feel to honor this property, some may choose to ignore
   * it.
   *
   * @param color  the desired foreground <code>Color</code> 
   * @see java.awt.Component#getForeground
   */
  public void setForeground(Color color){
    super.setForeground(color);
    if(indexRenderer==null) {
      indexRenderer = new ArrayIndexRenderer();
    }
    indexRenderer.setForeground(color);
  }
  
  /**
   * Sets the foreground color of the cell rendering component.  It is up to the
   * look and feel to honor this property, some may choose to ignore
   * it.
   *
   * @param color  the desired foreground <code>Color</code> 
   */
  public void setDataForeground(Color color){
    if(cellRenderer==null) {
      cellRenderer = new ArrayCellRenderer();
    }
    cellRenderer.setForeground(color);
  }

  /**
   * Sets the background color of this component.  It is up to the
   * look and feel to honor this property, some may choose to ignore
   * it.
   *
   * @param color  the desired background <code>Color</code> 
   * @see java.awt.Component#getBackground
   */
  public void setBackground(Color color){
    super.setBackground(color);
    if(indexRenderer==null) {
      indexRenderer = new ArrayIndexRenderer();
    }
    indexRenderer.setBackground(color);
  }
  
  /**
   * Sets the background color of the data cell rendering component.  It is up to the
   * look and feel to honor this property, some may choose to ignore
   * it.
   *
   * @param color  the desired background <code>Color</code> 
   */
  public void setDataBackground(Color color){
    if(cellRenderer==null) {
      cellRenderer = new ArrayCellRenderer();
    }
    cellRenderer.setBackground(color);
  }

  /**
   * Returns the renderer for a cell specified by row and column.
   *
   * @param row the row number
   * @param column the column number
   * @return the cell renderer
   */
  public TableCellRenderer getCellRenderer(int row, int column) {
    int i = convertColumnIndexToModel(column);
    if((i==0)&&tableModel.showRowNumber) {
      return indexRenderer;
    }
    return getDefaultRenderer(getColumnClass(column));
  }

  /**
   * A cell renderer for array cells.
   */
  static class ArrayCellRenderer extends DefaultTableCellRenderer {
    /**
     * Constructor
     */
    public ArrayCellRenderer() {
      super();
      setForeground(Color.BLACK);
      setHorizontalAlignment(SwingConstants.RIGHT);
      setBackground(Color.WHITE);
    }

    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
      super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
      boolean editable = table.isCellEditable(row, column);
      setEnabled(editable);
      ArrayTable arrayTable = (ArrayTable) table;
      DecimalFormat cellFormat = arrayTable.formatDictionary.get(column); // does column have a special format?
      if(cellFormat==null) {
        cellFormat = arrayTable.defaultFormat; // use default format
      }
      if(value==null) {
        setText("");                           //$NON-NLS-1$
      } else if(cellFormat==null) {            // default format not set
        setText(value.toString());
      } else {
        try {
          setText(cellFormat.format(value));
        } catch(IllegalArgumentException ex) { // convert to string if value cannot be formatted
          setText(value.toString());
        }
      }
      setBorder(new CellBorder(new Color(224, 224, 224)));
      return this;
    }

  }

  /**
   * A cell renderer for array indices.
   */
  static class ArrayIndexRenderer extends JLabel implements TableCellRenderer {

    /**
     * Constructor
     */
    public ArrayIndexRenderer() {
      super();
      setBorder(BorderFactory.createEtchedBorder());
      setOpaque(true);                                       // make background visible
      setForeground(Color.BLACK);
      setBackground(UIManager.getColor("Panel.background")); //$NON-NLS-1$
    }

    /**
     * Returns a label for the specified cell.
     *
     * @param table ignored
     * @param value the row number to be displayed
     * @param isSelected ignored
     * @param hasFocus ignored
     * @param row ignored
     * @param column the column number
     * @return a label with the row number
     */
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
      if(column==0) {
        setHorizontalAlignment(SwingConstants.RIGHT);
      } else {
        setHorizontalAlignment(SwingConstants.CENTER);
      }
      if(value==null) {
        setText(""); //$NON-NLS-1$
      } else {
        setText(value.toString());
      }
      setPreferredSize(new Dimension(20, 18));
      return this;
    }

  }

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.

 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
