/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program 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 program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * ResultSetHelper.java
 * Copyright (C) 2005 University of Waikato, Hamilton, New Zealand
 *
 */


package weka.gui.sql;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Types;
import java.util.Vector;

/**
 * Represents an extended JTable, containing a table model based on a ResultSet
 * and the corresponding query.
 *
 * @author      FracPete (fracpete at waikato dot ac dot nz)
 * @version     $Revision: 5324 $
 */
public class ResultSetHelper {
  
  /** the resultset to work on. */
  protected ResultSet m_ResultSet;

  /** whether we initialized. */
  protected boolean m_Initialized = false;

  /** the maximum number of rows to retrieve. */
  protected int m_MaxRows = 0;

  /** the number of columns. */
  protected int m_ColumnCount = 0;

  /** the number of rows. */
  protected int m_RowCount = 0;

  /** the column names. */
  protected String[] m_ColumnNames = null;

  /** whether a column is numeric. */
  protected boolean[] m_NumericColumns = null;

  /** the class for each column. */
  protected Class[] m_ColumnClasses = null;

  /**
   * initializes the helper, with unlimited number of rows.
   * 
   * @param rs        the resultset to work on
   */
  public ResultSetHelper(ResultSet rs) {
    this(rs, 0);
  }

  /**
   * initializes the helper, with the given maximum number of rows (less than
   * 1 means unlimited).
   * 
   * @param rs        the resultset to work on
   * @param max       the maximum number of rows to retrieve
   */
  public ResultSetHelper(ResultSet rs, int max) {
    super();

    m_ResultSet = rs;
    m_MaxRows   = max;
  }

  /**
   * initializes, i.e. reads the data, etc.
   */
  protected void initialize() {
    ResultSetMetaData     meta;
    int                   i;
    
    if (m_Initialized)
      return;
    
    try {
      meta = m_ResultSet.getMetaData();

      // columns names
      m_ColumnNames = new String[meta.getColumnCount()];
      for (i = 1; i <= meta.getColumnCount(); i++)
        m_ColumnNames[i - 1] = meta.getColumnName(i);
      
      // numeric columns
      m_NumericColumns = new boolean[meta.getColumnCount()];
      for (i = 1; i <= meta.getColumnCount(); i++)
        m_NumericColumns[i - 1] = typeIsNumeric(meta.getColumnType(i));

      // column classes
      m_ColumnClasses = new Class[meta.getColumnCount()];
      for (i = 1; i <= meta.getColumnCount(); i++) {
	try {
	  m_ColumnClasses[i - 1] = typeToClass(meta.getColumnType(i));
	}
	catch (Exception ex) {
	  m_ColumnClasses[i - 1] = String.class;
	}
      }

      // dimensions
      m_ColumnCount = meta.getColumnCount();

      // if the JDBC driver doesn't support scrolling we can't determine
      // the row count here
      if (m_ResultSet.getType() == ResultSet.TYPE_FORWARD_ONLY) {
	m_RowCount = -1;
      }
      else {
	m_RowCount = 0;
	m_ResultSet.first();
	if (m_MaxRows > 0) {
	  try {
	    m_ResultSet.absolute(m_MaxRows);
	    m_RowCount = m_ResultSet.getRow();
	  }
	  catch (Exception ex) {
	    // ignore it
	  }
	}
	else {
	  m_ResultSet.last();
	  m_RowCount = m_ResultSet.getRow();
	}

	// sometimes, e.g. with a "desc <table>", we can't use absolute(int)
	// and getRow()???
	try {
	  if ( (m_RowCount == 0) && (m_ResultSet.first()) ) {
	    m_RowCount = 1;
	    while (m_ResultSet.next()) {
	      m_RowCount++;
	      if (m_ResultSet.getRow() == m_MaxRows)
		break;
	    };
	  }
	}
	catch (Exception e) {
	  // ignore it
	}
      }

      m_Initialized = true;
    }
    catch (Exception ex) {
      // ignore it
    }
  }

  /**
   * the underlying resultset.
   * 
   * @return		the resultset
   */
  public ResultSet getResultSet() {
    return m_ResultSet;
  }

  /**
   * returns the number of columns in the resultset.
   * 
   * @return		the number of columns
   */
  public int getColumnCount() {
    initialize();

    return m_ColumnCount;
  }

  /**
   * returns the number of rows in the resultset. If -1 then the number of
   * rows couldn't be determined, i.e., the cursors aren't scrollable.
   * 
   * @return		the number of rows, -1 if it wasn't possible to 
   * 			determine
   */
  public int getRowCount() {
    initialize();

    return m_RowCount;
  }

  /**
   * returns an array with the names of the columns in the resultset.
   * 
   * @return		the column names
   */
  public String[] getColumnNames() {
    initialize();

    return m_ColumnNames;
  }

  /**
   * returns an array that indicates whether a column is numeric or nor.
   * 
   * @return		the numeric columns
   */
  public boolean[] getNumericColumns() {
    initialize();

    return m_NumericColumns;
  }

  /**
   * returns the classes for the columns.
   * 
   * @return		the column classes
   */
  public Class[] getColumnClasses() {
    initialize();

    return m_ColumnClasses;
  }

  /**
   * whether a limit on the rows to retrieve was set.
   * 
   * @return		true if there's a limit
   */
  public boolean hasMaxRows() {
    return (m_MaxRows > 0);
  }

  /**
   * the maximum number of rows to retrieve, less than 1 means unlimited.
   * 
   * @return		the maximum number of rows
   */
  public int getMaxRows() {
    return m_MaxRows;
  }

  /**
   * returns an 2-dimensional array with the content of the resultset, the first
   * dimension is the row, the second the column (i.e., getCells()[y][x]).
   * Note: the data is not cached! It is always retrieved anew.
   * 
   * @return		the data
   */
  public Object[][] getCells() {
    int			i;
    int			n;
    Vector<Object[]>	result;
    Object[]		row;
    int			rowCount;
    boolean		proceed;
    
    initialize();

    result = new Vector<Object[]>();

    try {
      
      // do know the number of rows?
      rowCount = getRowCount();
      if (rowCount == -1) {
	rowCount = getMaxRows();
	proceed  = m_ResultSet.next();
      }
      else {
	proceed = m_ResultSet.first();
      }
      
      if (proceed) {
	i = 0;
	while (true) {
	  row = new Object[getColumnCount()];
	  result.add(row);

	  for (n = 0; n < getColumnCount(); n++) {
	    try {
	      // to get around byte arrays when using getObject(int)
	      if (getColumnClasses()[n] == String.class)
		row[n] = m_ResultSet.getString(n + 1);
	      else
		row[n] = m_ResultSet.getObject(n + 1);
	    }
	    catch (Exception e) {
	      row[n] = null;
	    }
	  }

	  // get next row, if possible
	  if (i == rowCount - 1) {
	    break;
	  }
	  else {
	    // no more rows -> exit
	    if (!m_ResultSet.next())
	      break;
	  }

	  i++;
	}
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }

    return result.toArray(new Object[result.size()][getColumnCount()]);
  }

  /**
   * Returns the class associated with a SQL type.
   *
   * @param type the SQL type
   * @return the Java class corresponding with the type
   */
  public static Class typeToClass(int type) {
    Class     result;
    
    switch (type) {
      case Types.BIGINT :
        result = Long.class;
        break;
      case Types.BINARY:
        result = String.class;
      case Types.BIT:
        result = Boolean.class;
        break;
      case Types.CHAR:
        result = Character.class;
        break;
      case Types.DATE:
        result = java.sql.Date.class;
        break;
      case Types.DECIMAL:
        result = Double.class;
        break;
      case Types.DOUBLE:
        result = Double.class;
        break;
      case Types.FLOAT:
        result = Float.class;
        break;
      case Types.INTEGER:
        result = Integer.class;
        break;
      case Types.LONGVARBINARY:
        result = String.class;
        break;
      case Types.LONGVARCHAR:
        result = String.class;
        break;
      case Types.NULL:
        result = String.class;
        break;
      case Types.NUMERIC:
        result = Double.class;
        break;
      case Types.OTHER:
        result = String.class;
        break;
      case Types.REAL:
        result = Double.class;
        break;
      case Types.SMALLINT:
        result = Short.class;
        break;
      case Types.TIME:
        result = java.sql.Time.class;
        break;
      case Types.TIMESTAMP:
        result = java.sql.Timestamp.class;
        break;
      case Types.TINYINT:
        result = Short.class;
        break;
      case Types.VARBINARY:
        result = String.class;
        break;
      case Types.VARCHAR:
        result = String.class;
        break;
      default:
        result = null;
    }

    return result;
  }

  /**
   * returns whether the SQL type is numeric (and therefore the justification
   * should be right).
   * 
   * @param type      the SQL type
   * @return          whether the given type is numeric
   */
  public static boolean typeIsNumeric(int type) {
    boolean     result;
    
    switch (type) {
      case Types.BIGINT :
        result = true;
        break;
      case Types.BINARY:
        result = false;
      case Types.BIT:
        result = false;
        break;
      case Types.CHAR:
        result = false;
        break;
      case Types.DATE:
        result = false;
        break;
      case Types.DECIMAL:
        result = true;
        break;
      case Types.DOUBLE:
        result = true;
        break;
      case Types.FLOAT:
        result = true;
        break;
      case Types.INTEGER:
        result = true;
        break;
      case Types.LONGVARBINARY:
        result = false;
        break;
      case Types.LONGVARCHAR:
        result = false;
        break;
      case Types.NULL:
        result = false;
        break;
      case Types.NUMERIC:
        result = true;
        break;
      case Types.OTHER:
        result = false;
        break;
      case Types.REAL:
        result = true;
        break;
      case Types.SMALLINT:
        result = true;
        break;
      case Types.TIME:
        result = false;
        break;
      case Types.TIMESTAMP:
        result = true;
        break;
      case Types.TINYINT:
        result = true;
        break;
      case Types.VARBINARY:
        result = false;
        break;
      case Types.VARCHAR:
        result = false;
        break;
      default:
        result = false;
    }

    return result;
  }
}
