/*
 *   The org.opensourcephysics.display package contains the drawing framework
 *   for the book Simulations in Physics.  This framework defines objects that
 *   implement the Drawable interface and a DrawingPanel for rendering these objects.
 *   Copyright (c) 2003  H. Gould, J. Tobochnik, and W. Christian.
 */

/* This class was modified by Max Perkins and Dr. J. Hasbun in February 2004.
 * It was modified to allow for data pasting, file reading, cell deletion, and
 * table clearing. 
 * Five functions were added and four were modified. The constructor was modified
 * to allow the applet mode of QERegressionApp to work (the commented lines are the
 * only modifications). The createMenuBar() function was modified to accompany the 
 * paste, read, deletion, and clear functionality. The saveAs() function was modified
 * because of an incorrect, but not faulty, line. The getSelectedData() was modified
 * due to null pointer errors. The paste(), read(), and delete(), isRelevant(), and 
 * clear() functions were added.
 * 
 * Max Perkins was sponsored by the NASA Space Grant Consortium through Dr. Ben de Mayo 
 * of the Physics Department at the State University of West Georgia.
 */

package org.opensourcephysics.jahasbun.QE.display;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import java.io.*;
import java.util.*;
import org.opensourcephysics.display.*;

/**
 *  TableFrame displays a DataTable with a scroll pane in a frame.
 *
 * @author     Joshua Gould
 * @author     Wolfgang Christian
 * @created    August 16, 2002
 * @version    1.0
 */

public class EDataTableFrame extends OSPFrame {

  protected JMenuBar  menuBar;
  protected JMenu     fileMenu;
  protected JMenu     editMenu;
  protected JMenuItem saveAsItem;
  protected JMenuItem pasteItem;  // added
  protected JMenuItem deleteItem; // added
  protected JMenuItem readItem;   // added
  protected JMenuItem clearAllItem;  // added
  protected EDataTable table;  

  /**
   *  TableFrame Constructor
   *
   * @param  _table  Description of the Parameter
   */
  public EDataTableFrame(EDataTable _table) {
    this("Data Table", _table);
  }

  /**
   *  TableFrame Constructor
   *
   * @param  title
   * @param  _table  Description of the Parameter
   */
  // modified by Max Perkins and Dr. J. Hasbun
  public EDataTableFrame(String title, EDataTable _table) {
    super(title);
    table = _table;
    JScrollPane scrollPane = new JScrollPane(table);
    Container   c          = getContentPane();
    c.add(scrollPane, BorderLayout.CENTER);
    pack();
    setVisible(true);    
    //if(!OSPFrame.appletMode) {
      createMenuBar();
    //}
  }

  private void createMenuBar() {
    menuBar = new JMenuBar();
    setJMenuBar(menuBar);
    fileMenu = new JMenu("File");
    editMenu = new JMenu("Edit");
    menuBar.add(fileMenu);
    menuBar.add(editMenu);
    JMenuItem readItem     = new JMenuItem("Read File");
    JMenuItem saveAsItem   = new JMenuItem("Save As...");
    JMenuItem copyItem     = new JMenuItem("Copy");    
    JMenuItem pasteItem    = new JMenuItem("Paste");
    JMenuItem deleteItem   = new JMenuItem("Delete");
    JMenuItem selectAlItem = new JMenuItem("Select All");
    JMenuItem clearAllItem = new JMenuItem("Clear All");
    fileMenu.add(readItem);
    fileMenu.add(saveAsItem);
    editMenu.add(copyItem);
    editMenu.add(pasteItem);
    editMenu.add(deleteItem);
    editMenu.add(clearAllItem);
    editMenu.add(selectAlItem);
    
    int MENU_SHORTCUT_KEY_MASK = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();    
    
    copyItem.setAccelerator(KeyStroke.getKeyStroke('C', MENU_SHORTCUT_KEY_MASK));
    copyItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
          try {
            copy();
          } catch(SecurityException s) {}
      }
      
    });
    selectAlItem.setAccelerator(KeyStroke.getKeyStroke('A', MENU_SHORTCUT_KEY_MASK));
    selectAlItem.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        table.selectAll();
      }
    });    
    saveAsItem.setAccelerator(KeyStroke.getKeyStroke('S', MENU_SHORTCUT_KEY_MASK));
    saveAsItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        saveAs();
      }
    });
    pasteItem.setAccelerator(KeyStroke.getKeyStroke('P', MENU_SHORTCUT_KEY_MASK));
    pasteItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
          try {
            paste();
          } catch(SecurityException s) {}
      }
    });
    deleteItem.setAccelerator(KeyStroke.getKeyStroke('D', MENU_SHORTCUT_KEY_MASK));
    deleteItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        delete();
      }
    });
    readItem.setAccelerator(KeyStroke.getKeyStroke('O', MENU_SHORTCUT_KEY_MASK));
    readItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        try {
            read();
        } catch(SecurityException s) {}          
      }
    });
    clearAllItem.setAccelerator(KeyStroke.getKeyStroke('D', MENU_SHORTCUT_KEY_MASK));
    clearAllItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
            clearAll();
      }
    });
    validate();
  }
  
  /** Clears the data table **/
  // added by Max Perkins and Dr. J. Hasbun
  public void clearAll() {
    Object blank_char = new Double(Double.NaN);
    
    int col = 0, row = 0;
    while(row < table.getRowCount()) {
        table.setValueAt(blank_char, row, col);
        if(col == 0)
            col = 1;
        else {
            col = 0;
            row++;
        }
    }
    table.refreshTable();
  }
  
  /** Opens a file and puts data into table **/
  /** Added by Max Perkins and Dr. J. Hasbun (February 2004) **/
  public void read() {
      File file = GUIUtils.showOpenDialog(this);
      String strData = null;
      if(file == null || file.canRead() == false) {
        return;
      }
      try {
        FileReader  fr = new FileReader(file);
        int theChar = fr.read();      
      
        // get the data from the file and store it in strData
        while(theChar != -1) {
            strData += String.valueOf((char)theChar);
            theChar = fr.read();
        }
        fr.close();
        
      } catch(IOException e) {
        JOptionPane.showMessageDialog(this, "An error occurred while opening your file. Please try again.", "Error",
                                    JOptionPane.ERROR_MESSAGE);
      }
      if(strData.length() < 2) return;
      String str = null;            
      char theChar;
      double coord;      
      int [] selectedColumns = table.getSelectedColumns();
      int [] selectedRows = table.getSelectedRows();
      int selectedRowCount = selectedRows.length;
      int selectedColumnCount = selectedColumns.length;
      int row = 0, col = 1;
      int selRowIndex = 0, selColIndex = 0;
      Object blank_char = new Double(Double.NaN);
      
      // If there is no newline character at the end, append it
      // Because of the nature of isRelevant(), he last number 
      // will not be read if it is not appended.
      // Note: paste() uses the same code from here on
      if(strData.charAt(strData.length() - 1) != '\n') {
        char [] charArray = new char[strData.length() + 1];
        strData.getChars(0, strData.length(), charArray, 0);        
        charArray[strData.length()] = '\n';        
        strData = String.valueOf(charArray);
      }

      /* Loop through all the characters in strData
       * If it is a number isRelevant(), store it. If not, 
       * then the end of the string of numbers has been
       * reached and you can convert it into a "double."
       * 
       * This is how the data is stored (example):
       * "1 2\n3 4\n5 6"
       *
       * Using isRelevant() saves only the numbers (including any
       * possible signs) and converts those.
       *
       * When the complete number is found, it is appended to the table
       * 
       */
      for(int i = 0; i < strData.length(); i++) {
          theChar = strData.charAt(i);
          if(isRelevant(theChar) == true) {              
              if(str == null) // if we're starting a new number
                str = String.valueOf(theChar);
              else           // else keep appending to the current one
                str += String.valueOf(theChar);
          }
          else if(isRelevant(theChar) == false && str != null){ // we've hit a irrelevant character and we're
                                                                // ready to append to the data table
              
            // if nothing in the table is highlighted, paste into the beginning
            if(selectedRowCount == 0 && selectedColumnCount == 0) { 
                table.setValueAt(str, row, col);                
                if(col == 1) {
                    col = 0;
                }
                else if(col == 0) {
                    col = 1;
                    row++;
                }                
            }
            else { // else paste into whatever is selected. the data are truncated if the selected
                   // cell number do not match the number of values
                
                // paste down one column if only one is selected
                if(selectedColumnCount == 1) {
                    if(selRowIndex < selectedRowCount)
                        table.setValueAt(str, selectedRows[selRowIndex++], selectedColumns[0]);
                }
                // paste down two selected columns
                else {
                    if(selRowIndex < selectedRowCount) {
                        table.setValueAt(str, selectedRows[selRowIndex], selectedColumns[selColIndex]);
                        // alternate x and y cells, going down after second column
                        if(selColIndex == 0)
                            selColIndex = 1;
                        else {
                            selColIndex = 0;
                            selRowIndex++;
                        }
                    }
                }
            }
            str = null; // reset string so we can start a new number
          }
      }
      // if nothing is selected, fill the cells after the pasted data with blanks
      // if the number of values you have is less than the number of cells selected
      // fill in the rest of the cells with blanks      
      if(selectedRowCount == 0 && selectedColumnCount == 0) {
          for(int j = row; j < table.getRowCount() && table.getValueAt(j, 1) != null; j++) {
                    table.setValueAt(blank_char, j, 1);
                    table.setValueAt(blank_char, j, 0);
          }
      }               
      else if(selectedColumnCount == 1 && selRowIndex != (selectedRowCount - 1)) {
        while(selRowIndex < selectedRowCount)
            table.setValueAt(blank_char, selectedRows[selRowIndex++], selectedColumns[0]);
      } 
      else if(selectedColumnCount == 2 && selRowIndex != (selectedRowCount - 1)) {
        while(selRowIndex < selectedRowCount) {
            table.setValueAt(blank_char, selectedRows[selRowIndex], selectedColumns[selColIndex]);
            // alternate x and y cells, going down after second column
            if(selColIndex == 0)
                selColIndex = 1;
            else {
                selColIndex = 0;
                selRowIndex++;
            }
        }
      }
      table.refreshTable();                            
  }
  
  /** Copies the data in the table to the system clipboard */
  public void copy() {
    Clipboard       clipboard       = Toolkit.getDefaultToolkit().getSystemClipboard();
    int[]           selectedRows    = table.getSelectedRows();
    int[]           selectedColumns = table.getSelectedColumns();
    StringBuffer    buf             = getSelectedData(selectedRows, selectedColumns);
    StringSelection stringSelection = new StringSelection(buf.toString());
    clipboard.setContents(stringSelection, stringSelection);
  }

  /** Pastes clipboard data into table **/
  // added by Max Perkins and Dr. J. Hasbun
  public void paste() {
      Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
      Transferable contents = clipboard.getContents(new Object());
      DataFlavor flavor = new DataFlavor();      
      Object data = null;
      String strData = null;
      Object blank_char = new Double(Double.NaN);
      
      try {
        data = contents.getTransferData(flavor.stringFlavor);        
        strData = data.toString();             
        
        if(strData.length() < 2) return;
      } catch(UnsupportedFlavorException e) {
      } catch(IOException e) {      
      }                  
      String str = null;            
      char theChar;
      double coord;      
      int [] selectedColumns = table.getSelectedColumns();
      int [] selectedRows = table.getSelectedRows();
      int selectedRowCount = selectedRows.length;
      int selectedColumnCount = selectedColumns.length;
      int row = 0, col = 1;
      int selRowIndex = 0, selColIndex = 0;
      
      // If there is no newline character at the end, append it
      // Because of the nature of isRelevant(), he last number 
      // will not be read if it is not appended.
      // Note: read() uses the same code from here on
      if(strData.charAt(strData.length() - 1) != '\n') {
        char [] charArray = new char[strData.length() + 1];
        strData.getChars(0, strData.length(), charArray, 0);        
        charArray[strData.length()] = '\n';        
        strData = String.valueOf(charArray);
      }

      /* Loop through all the characters in strData
       * If it is a number isRelevant(), store it. If not, 
       * then the end of the string of numbers has been
       * reached and you can convert it into a "double."
       * 
       * This is how the data is stored (example):
       * "1 2\n3 4\n5 6"
       *
       * Using isRelevant() saves only the numbers (including any
       * possible signs) and converts those.
       *
       * When the complete number is found, it is appended to the table
       * 
       */      
      for(int i = 0; i < strData.length(); i++) {
          theChar = strData.charAt(i);
          if(isRelevant(theChar) == true) {              
              if(str == null) // if we're starting a new number
                str = String.valueOf(theChar);
              else           // else keep appending to the current one
                str += String.valueOf(theChar);
          }
          else if(isRelevant(theChar) == false && str != null){ // we've hit a irrelevant character and we're
                                                                // ready to append to the data table
              
            // if nothing in the table is highlighted, paste into the beginning
            if(selectedRowCount == 0 && selectedColumnCount == 0) { 
                table.setValueAt(str, row, col);                
                if(col == 1) {
                    col = 0;
                }
                else if(col == 0) {
                    col = 1;
                    row++;
                }                
            }
            else { // else paste into whatever is selected. the data are truncated if the selected
                   // cell number do not match the number of values
                
                // paste down one column if only one is selected
                if(selectedColumnCount == 1) {
                    if(selRowIndex < selectedRowCount)
                        table.setValueAt(str, selectedRows[selRowIndex++], selectedColumns[0]);
                }
                // paste down two selected columns
                else {
                    if(selRowIndex < selectedRowCount) {
                        table.setValueAt(str, selectedRows[selRowIndex], selectedColumns[selColIndex]);
                        // alternate x and y cells, going down after second column
                        if(selColIndex == 0)
                            selColIndex = 1;
                        else {
                            selColIndex = 0;
                            selRowIndex++;
                        }
                    }
                }
            }
            str = null; // reset string so we can start a new number
          }
      }
      
      // if nothing is selected, fill the cells after the pasted data with blanks
      // if the number of values you have is less than the number of cells selected
      // fill in the rest of the cells with blanks      
      if(selectedRowCount == 0 && selectedColumnCount == 0) {
          for(int j = row; j < table.getRowCount() && table.getValueAt(j, 1) != null; j++) {
                    table.setValueAt(blank_char, j, 1);
                    table.setValueAt(blank_char, j, 0);
          }
      }               
      else if(selectedColumnCount == 1 && selRowIndex != (selectedRowCount - 1)) {
        while(selRowIndex < selectedRowCount)
            table.setValueAt(blank_char, selectedRows[selRowIndex++], selectedColumns[0]);
      } 
      else if(selectedColumnCount == 2 && selRowIndex != (selectedRowCount - 1)) {
        while(selRowIndex < selectedRowCount) {
            table.setValueAt(blank_char, selectedRows[selRowIndex], selectedColumns[selColIndex]);
            // alternate x and y cells, going down after second column
            if(selColIndex == 0)
                selColIndex = 1;
            else {
                selColIndex = 0;
                selRowIndex++;
            }
        }
      }
      table.refreshTable();      
  }
  
  /** Used in paste() and read(), this tests whether you have a number,
   *  sign, or decimal point
   *
   * added by Max Perkins and Dr. J. Hasbun, February 2004
   */
  private boolean isRelevant(char theChar) {        
    if(theChar == '1' || theChar == '2' || theChar == '3' ||
       theChar == '4' || theChar == '5' || theChar == '6' ||
       theChar == '7' || theChar == '8' || theChar == '9' ||
       theChar == '0' || theChar == '-' || theChar == '.')
        return true;
    return false;
  }
  
  /** Deletes selected cells **/
  // added by Max Perkins and Dr. Hasbun, February 2004
  public void delete() {    
    int[] selectedRows    = table.getSelectedRows();
    int[] selectedColumns = table.getSelectedColumns();
    int selRowIndex = 0, selColIndex = 0;
    Object blank_char = new Double(Double.NaN);
    
    // if you have only one selected column, clear down
    // that column
    if(selectedColumns.length == 1) {
        for(int i = 0; i < selectedRows.length; i++)
            table.setValueAt(blank_char, selectedRows[i], selectedColumns[0]);                
    }
    // else clear down both selected columns, alternating
    // cells and going down as needed
    else {
        while(selRowIndex < selectedRows.length) {
            table.setValueAt(blank_char, selectedRows[selRowIndex], selectedColumns[selColIndex]);
            if(selColIndex == 0)
                selColIndex = 1;
            else {
                selColIndex = 0;
                selRowIndex++;
            }
        }
    }
    table.refreshTable();
  }
  
  /**
   *  Gets the data selected by the user in the table.
   *
   * @param  selectedRows     Description of the Parameter
   * @param  selectedColumns  Description of the Parameter
   * @return                  the selected data.
   */
  // edited by Max Perkins and Dr. J. Hasbun, February 2004
  public StringBuffer getSelectedData(int[] selectedRows, int[] selectedColumns) {
    StringBuffer buf = new StringBuffer();
    Object value = null; // moved here
    start:
    for(int i = 0; i < selectedRows.length; i++) {
      for(int j = 0; j < selectedColumns.length; j++) {
        int row  = i;        
        int temp = table.convertColumnIndexToModel(selectedColumns[j]);
        if(table.isRowNumberVisible()) {
          if(temp == 0) {
            continue;
          }
        }
        
        /* Dr. Hasbun -
         *    What happened was that in the previous code, the Double.NaN values (or blanks, they = NULL !!)
         * were being compared first, instead of being checked to see if they were null first. That was causing
         *the null pointer exception. The exception breaks the loop, and that doesn't cause the function
         *to return the StringBuffer to print(). Because print() has a null argument, it prints nothing
         *to the file. The original code is commented out.
         *  
         *  *sigh* 
         *      -Max
         */
        //Object value = table.getValueAt(row, selectedColumns[j]);  // column converted to model
        value = table.getValueAt(row, selectedColumns[j]);  // column converted to model
        if(value != null) {          
            if(value.toString().compareTo("NaN") == 0 || value.toString().compareTo("") == 0)
                continue start;
            else {
                buf.append(value);        
            }
        } 
        // added the below condition so there will be no unnecessary tabs placed
        // the old function did this:
        //     x1<tab>y1<tab><newline>
        //     x2<tab>y2<tab><newline>
        // mine does this:
        //     x1<tab>y1<newline>
        //     x2<tab>y1<newline>
        if(selectedColumns[j] == 1)
            buf.append("\t");
        
        //if(value.toString().compareTo("NaN") == 0 || value.toString().compareTo("") == 0)
        //    continue start;
        //if(value != null) {          
        //  buf.append(value);
        //}
        //buf.append("\t");
        //        
      }
      if(value != null) // moved value to outside loop so I can do this
        buf.append("\n");
    }
    return buf;
  }

  /**
   *  Pops open a save file dialog to save the data in this table to a file.
   */
  // edited by Max Perkins and Dr. J. Hasbun, February 2004
  public void saveAs() {
    File file = GUIUtils.showSaveDialog(this);
    if(file == null) {
      return;
    }
    int firstRow    = 0;
    int lastRow     = table.getRowCount() - 1;
    int lastColumn  = table.getColumnCount() - 1;
    int firstColumn = 0;
    if(table.isRowNumberVisible()) {
      firstColumn++;
    }
    int[] selectedRows    = new int[lastRow + 1];
    int[] selectedColumns = new int[lastColumn + 1];
    for(int i = firstRow; i <= lastRow; i++) {
      selectedRows[i] = i;
    }
    for(int i = firstColumn; i <= lastColumn; i++) {
      selectedColumns[i] = i;
    }
    try {
      FileWriter  fw = new FileWriter(file);
      PrintWriter pw = new PrintWriter(fw);   // rather than the below line, Max Perkins & Dr. J. Hasbun
      //PrintWriter pw = pw = new PrintWriter(fw);   ????
      pw.print(getSelectedData(selectedRows, selectedColumns));
      pw.close();
    } catch(IOException e) {
      JOptionPane.showMessageDialog(this, "An error occurred while saving your file. Please try again.", "Error",
                                    JOptionPane.ERROR_MESSAGE);
    }
  }
}

