/*
 * 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.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import org.opensourcephysics.controls.OSPLog;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControlElement;
import org.opensourcephysics.tools.Resource;
import org.opensourcephysics.tools.ResourceLoader;

public class DataFile extends DataAdapter {
  java.util.List<Data> dataList = null;
  protected static String[] delimiters = new String[] {" ", "\t", ",", ";"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$

  /**
   * Creates a DataFile using data in the given file.
   *
   * @param fileName
   */
  public DataFile(String fileName) {
    super(null);
    if(fileName!=null) {
      open(fileName);
    }
  }

  /**
   * Some objects (eg, a Group) do not contain data, but a list of Data
   * objects that do. This method is used by Data displaying tools to create
   * as many pages as needed.
   *
   * @return a list of Data objects, or null if this object contains data
   */
  public java.util.List<Data> getDataList() {
    return dataList;
  }

  /**
   * Opens an xml or data file specified by name.
   *
   * @param fileName   the file name
   * @return the file name, if successfully opened
   */
  public String open(String fileName) {
    dataList = null;
    data = null;
    OSPLog.fine("opening "+fileName); //$NON-NLS-1$
    Resource res = ResourceLoader.getResource(fileName);
    if(res!=null) {
      Reader in = res.openReader();
      String firstLine = readFirstLine(in);
      // if xml, read the file into an XML control and add tab
      if(firstLine.startsWith("<?xml")) { //$NON-NLS-1$
        XMLControlElement control = new XMLControlElement(fileName);
        dataList = control.getObjects(Data.class);
        return fileName;
      }
      // if not xml, attempt to import data and add tab
      else if(res.getString()!=null) {
        data = parseData(res.getString(), fileName);
        if(data!=null) {
          return fileName;
        }
      }
    }
    OSPLog.finest("no data found"); //$NON-NLS-1$
    return null;
  }

  /**
   * Parses character-delimited data from a string. This attempts to extract
   * the following information from the string:
   *
   * 1. A title to be used for the tab name 2. One or more columns of double
   * data values 3. Column names for the data columns
   *
   * @param dataString  the data string
   * @param fileName   name of file containing the data string (may be null)
   * @return DatasetManager with parsed data, or null if none found
   */
  public double[][] parseData(String dataString, String fileName) {
    BufferedReader input = new BufferedReader(new StringReader(dataString));
    String gnuPlotComment = "#"; //$NON-NLS-1$
    try {
      String textLine = input.readLine();
      for(int i = 0; i<DataFile.delimiters.length; i++) {
        ArrayList<double[]> rows = new ArrayList<double[]>();
        int columns = Integer.MAX_VALUE;
        String[] columnNames = null;
        String title = null;
        int lineCount = 0;
        while(textLine!=null) {                                      // process each line of text
          // look for gnuPlot-commented name and/or columnNames
          if(textLine.contains(gnuPlotComment)) {
            textLine = textLine.trim();
          }
          if(textLine.startsWith(gnuPlotComment)) {
            int k = textLine.indexOf("name:");                       //$NON-NLS-1$
            if(k>-1) {
              title = textLine.substring(k+5).trim();
            }
            k = textLine.indexOf("columnNames:");                    //$NON-NLS-1$
            if(k>-1) {
              textLine = textLine.substring(k+12).trim();
            } else {
              textLine = input.readLine();
              continue;
            }
          }
          // skip Vernier Format 2 header lines
          if((textLine.indexOf("Vernier Format")>-1                  //$NON-NLS-1$
            )||(textLine.indexOf(".cmbl")>-1)) {                     //$NON-NLS-1$
            textLine = input.readLine();
            continue;
          }
          String[] strings = DataFile.parseStrings(textLine, DataFile.delimiters[i]);
          double[] rowData = DataFile.parseDoubles(strings);
          // set null title if String[] length > 0, all entries
          // are NaN and only one entry is not ""
          if(rows.isEmpty()&&(strings.length>0)&&(title==null)) {
            String s = "";                                           //$NON-NLS-1$
            for(int k = 0; k<strings.length; k++) {
              if(Double.isNaN(rowData[k])&&!strings[k].equals("")) { //$NON-NLS-1$
                if(s.equals("")) {                                   //$NON-NLS-1$
                  s = strings[k];
                } else {
                  s = "";                                            //$NON-NLS-1$
                  break;
                }
              }
            }
            if(!s.equals("")) {                                      //$NON-NLS-1$
              title = s;
              textLine = input.readLine();
              continue;
            }
          }
          // set null column names if String[] length > 0,
          // all entries are NaN and none is ""
          if(rows.isEmpty()&&(strings.length>0)&&(columnNames==null)) {
            boolean valid = true;
            for(int k = 0; k<strings.length; k++) {
              if(!Double.isNaN(rowData[k])||strings[k].equals("")) { //$NON-NLS-1$
                valid = false;
                break;
              }
            }
            if(valid) {
              columnNames = strings;
              textLine = input.readLine();
              continue;
            }
          }
          // add double[] of length 1 or longer to rows
          if(strings.length>0) {
            lineCount++;
            boolean validData = true;
            boolean emptyData = true;
            for(int k = 0; k<strings.length; k++) {
              // invalid if any NaN entries other than ""
              if(Double.isNaN(rowData[k])&&!strings[k].equals("")) { //$NON-NLS-1$
                validData = false;
              }
              // look for empty row--every entry is ""
              if(!strings[k].equals("")) {                           //$NON-NLS-1$
                emptyData = false;
              }
            }
            if(rows.isEmpty()&&emptyData) {
              validData = false;
            }
            // add valid data, but ignore blank lines
            // that may precede real data
            if(validData) {
              rows.add(rowData);
              columns = Math.min(rowData.length, columns);
            }
          }
          // abort processing if no data found in first several lines
          if(rows.isEmpty()&&(lineCount>10)) {
            break;
          }
          textLine = input.readLine();
        }
        // create array if data found
        if(!rows.isEmpty()&&(columns>0)) {
          input.close();
          // first reassemble data from rows into columns
          double[][] dataArray = new double[columns][rows.size()];
          for(int row = 0; row<rows.size(); row++) {
            double[] next = rows.get(row);
            for(int j = 0; j<columns; j++) {
              dataArray[j][row] = next[j];
            }
          }
          setName((title==null) ? XML.getName(fileName) : title);
          this.setColumnNames(columnNames);
          OSPLog.finest("data found using delimiter \""              //$NON-NLS-1$
                        +DataFile.delimiters[i]+"\"");               //$NON-NLS-1$
          return dataArray;
        }
        // close the reader and open a new one
        input.close();
        input = new BufferedReader(new StringReader(dataString));
        textLine = input.readLine();
      }
    } catch(IOException e) {
      e.printStackTrace();
    }
    try {
      input.close();
    } catch(IOException ex) {
      ex.printStackTrace();
    }
    return null;
  }

  // ______________________________ protected methods ________________________
  protected String readFirstLine(Reader in) {
    BufferedReader input = null;
    if(in instanceof BufferedReader) {
      input = (BufferedReader) in;
    } else {
      input = new BufferedReader(in);
    }
    String openingLine;
    try {
      openingLine = input.readLine();
      while((openingLine==null)||openingLine.equals("")) { //$NON-NLS-1$
        openingLine = input.readLine();
      }
    } catch(IOException e) {
      e.printStackTrace();
      return null;
    }
    try {
      input.close();
    } catch(IOException ex) {
      ex.printStackTrace();
    }
    return openingLine;
  }

  /**
   * Parses a String into tokens separated by a specified delimiter. A token
   * may be "".
   *
   * @param text the text to parse
   * @param delimiter the delimiter
   * @return an array of String tokens
   */
  protected static String[] parseStrings(String text, String delimiter) {
    Collection<String> tokens = new ArrayList<String>();
    if(text!=null) {
      // get the first token
      String next = text;
      int i = text.indexOf(delimiter);
      if(i==-1) {   // no delimiter
        tokens.add(stripQuotes(next));
        text = null;
      } else {
        next = text.substring(0, i);
        text = text.substring(i+1);
      }
      // iterate thru the tokens and add to token list
      while(text!=null) {
        tokens.add(stripQuotes(next));
        i = text.indexOf(delimiter);
        if(i==-1) { // no delimiter
          next = text;
          tokens.add(stripQuotes(next));
          text = null;
        } else {
          next = text.substring(0, i).trim();
          text = text.substring(i+1);
        }
      }
    }
    return tokens.toArray(new String[0]);
  }

  /**
   * Parses a String into tokens separated by specified row and column
   * delimiters.
   *
   * @param text the text to parse
   * @param rowDelimiter the column delimiter
   * @param colDelimiter  the column delimiter
   * @return a 2D array of String tokens
   */
  protected static String[][] parseStrings(String text, String rowDelimiter, String colDelimiter) {
    String[] rows = parseStrings(text, rowDelimiter);
    String[][] tokens = new String[rows.length][0];
    for(int i = 0; i<rows.length; i++) {
      tokens[i] = parseStrings(rows[i], colDelimiter);
    }
    return tokens;
  }

  /**
   * Parses a String array into doubles. Unparsable strings are set to
   * Double.NaN.
   *
   * @param strings   the String array to parse
   * @return an array of doubles
   */
  protected static double[] parseDoubles(String[] strings) {
    double[] doubles = new double[strings.length];
    for(int i = 0; i<strings.length; i++) {
      if(strings[i].indexOf("\t")>-1) { //$NON-NLS-1$
        doubles[i] = Double.NaN;
      } else {
        try {
          doubles[i] = Double.parseDouble(strings[i]);
        } catch(NumberFormatException e) {
          doubles[i] = Double.NaN;
        }
      }
    }
    return doubles;
  }

  /**
   * Parses a String into doubles separated by specified row and column
   * delimiters.
   *
   * @param text  the text to parse
   * @param rowDelimiter the column delimiter
   * @param colDelimiter the column delimiter
   * @return a 2D array of doubles
   */
  protected static double[][] parseDoubles(String text, String rowDelimiter, String colDelimiter) {
    String[][] strings = parseStrings(text, rowDelimiter, colDelimiter);
    double[][] doubles = new double[strings.length][0];
    for(int i = 0; i<strings.length; i++) {
      double[] row = new double[strings[i].length];
      for(int j = 0; j<row.length; j++) {
        try {
          row[j] = Double.parseDouble(strings[i][j]);
        } catch(NumberFormatException e) {
          row[j] = Double.NaN;
        }
      }
      doubles[i] = row;
    }
    return doubles;
  }

  /**
   * Strips quotation marks around a string.
   *
   * @param text the text to strip
   * @return the stripped string
   */
  private static String stripQuotes(String text) {
    if(text.startsWith("\"")) {       //$NON-NLS-1$
      String stripped = text.substring(1);
      int n = stripped.indexOf("\""); //$NON-NLS-1$
      if(n==stripped.length()-1) {
        return stripped.substring(0, n);
      }
    }
    return text;
  }

  /**
   * Returns an array of row numbers.
   *
   * @param rowCount  length of the array
   * @return the array
   */
  protected static double[] getRowArray(int rowCount) {
    double[] rows = new double[rowCount];
    for(int i = 0; i<rowCount; i++) {
      rows[i] = i;
    }
    return rows;
  }

}

/*
 * 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
 */
