package ModSQL;
import java.sql.*;
import java.util.ArrayList;

/* $Id: LiteralTable.java,v 1.7 2003/05/29 05:48:45 cvs Exp $
 *
 * Copyright (c) 2003 Chris Studholme <chris.studholme@utoronto.ca>
 *
 * May be copied or modified under the terms of the GNU General Public
 * License.  See COPYING for more information.
 */

/**
 * A literal table is a list of rows consisting of RowConstructor or Function
 * objects.
 *
 * @author chris.studholme@utoronto.ca
 */
class LiteralTable implements Table {

  /** Array of rows before optimize() has been called. */
  private ArrayList rows_array;

  /** Array of rows after optimize(). */
  private Function[] rows;
  /** Type of each column in table. */
  private int[] column_types;
  /** Number of columns in table. */
  private int ncolumns;
  /** Index of current row (-1 for before first). */
  private int current_row;
  /** Value of current row (null for before first or after last). */
  private Object[] current_value;

  /**
   * Constructor.
   */
  public LiteralTable() {
    rows_array = new ArrayList();
    rows = null;
    ncolumns=0;
    current_row = -1;
    current_value = null;
  }

  /**
   * <p>Add a row to the table.  All rows of the table must have the same
   * number of columns.  If row is not of type RowConstructor, it is
   * assumed to have exactly one column.
   *
   * <p>This method can only be called before optimize().
   *
   * @param row either a Function or RowConstructor
   * @throws SQLException if an error occurs
   */
  public void addRow(Function row) throws SQLException {
    if (rows_array==null)
      throw new SQLException("cannot add rows after optimize");
    if (rows_array.size()==0) {
      if (row instanceof RowConstructor) {
	ncolumns = ((RowConstructor)row).getColumnCount();
	if (ncolumns<=0)
	  throw new SQLException("table must have at least one column");
      }
      else
	ncolumns=1;
    }
    else {
      int c = row instanceof RowConstructor ? 
	((RowConstructor)row).getColumnCount() : 1;
      if (c!=ncolumns)
	throw new SQLException("all rows must have same number of columns");
    }
    rows_array.add(row);
  }

  /**
   * This method will pass the object on to all Function objects contained
   * within.
   *
   * @param o object to register with
   * @throws SQLException if an error occurs
   */
  public void registerWith(Object o) throws SQLException {
    if (rows_array!=null) {
      for (int i=0; i<rows_array.size(); ++i)
	((Function)rows_array.get(i)).registerWith(o);
    }
    else {
      for (int i=0; i<rows.length; ++i)
	rows[i].registerWith(o);
    }
  }
  
  /**
   * Prepare the table for use.  This method will call optimize() for
   * each row, determine the types of each column, verify that the rows
   * are compatible in type, and fix the rows array.
   *
   * @throws SQLException if an error occurs
   */
  public void optimize() throws SQLException {
    if (rows_array==null)
      return;
    if (rows_array.size()<=0)
      throw new SQLException("table must have at least one row");
    rows = new Function[rows_array.size()];
    rows = (Function[])rows_array.toArray(rows);
    rows_array=null;
    column_types = new int[ncolumns];
    for (int j=0; j<column_types.length; ++j)
      column_types[j] = Types.NULL;
    for (int i=0; i<rows.length; ++i) {
      rows[i].optimize();
      if (rows[i] instanceof RowConstructor)
	for (int j=0; j<column_types.length; ++j) {
	  int type = ((RowConstructor)rows[i]).getSQLType(j);
	  column_types[j] = 
	    AbstractFunction.getCompatableType(column_types[j],type);
	}
      else
	column_types[0] = 
	  AbstractFunction.getCompatableType(column_types[0],
					     rows[i].getSQLType());
    }
  }

  /**
   * Human readable description of table.  There are two styles here:
   * <ul>
   * <li>"(row1,row2,...)"
   * <li>"VALUES row1,row2,..."
   * </ul>
   *
   * @param with_brackets indicates whether the expression is bracketed
   * @return human readable description of table
   */
  public String toString(boolean with_brackets) {
    StringBuffer result = new StringBuffer();
    if (with_brackets)
      result.append('(');
    else
      result.append("VALUES ");
    if (rows_array!=null) {
      for (int i=0; i<rows_array.size(); ++i) {
	if (i>0) result.append(',');
	result.append(rows_array.get(i));
      }
    }
    else {
      result.append(rows[0]);
      for (int i=1; i<rows.length; ++i) {
	result.append(',');
	result.append(rows[i]);
      }
    }
    if (with_brackets)
      result.append(')');
    return result.toString();
  }
  /**
   * String description of table in the form "(row1,row2,...)".
   *
   * @return human readable description of table
   */
  public String toString() {
    return toString(true);
  }


  /****************  result meta-data  ****************/

  /**
   * <p>Determine if this table is constant.  
   *
   * <p>This method can only be called after optimize().
   *
   * @return true if all rows are constant
   * @throws SQLException if an error occurs
   */
  public boolean isConstant() throws SQLException {
    for (int i=0; i<rows.length; ++i)
      if (!rows[i].isConstant())
	return false;
    return true;
  }

  /**
   * <p>Returns the number of columns in the table.  This value will be greater
   * than zero.
   *
   * <p>This method can only be called after optimize().
   *
   * @return number of columns in table
   */
  public int getColumnCount() {
    return ncolumns;
  }

  /**
   * <p>Return number of rows.
   *
   * <p>This method can only be called after optimize().
   *
   * @return number of rows
   */
  public long getRowCount() {
    return rows_array!=null ? rows_array.size() : rows.length;
  }

  /**
   * Get the name of a column.  This method always returns null since
   * literal tables do not have named columns.
   * 
   * @param column column number (starting from zero)
   * @return name of column (or null if column has no name)
   */
  public String getColumnName(int column) {
    return null;
  }

  /**
   * Return the SQL type of the specified column.
   *
   * @param column column number (starting from zero)
   * @return SQL type of data to be returned
   */
  public int getSQLType(int column) {
    return column_types[column];
  }

  /**
   * Return the maximum number of characters String values in the specified
   * column will have.  If the column is not of String type or if the maximum
   * length is not known, -1 should be returned.
   *
   * @param column column number (starting from zero)
   * @return maximum size of String returned or -1 if unknown
   * @throws SQLException if an error occurs
   */
  public int getMaxResultSize(int column) throws SQLException {
    int result = 0;
    for (int i=0; i<rows.length; ++i) {
      int len = rows[i] instanceof RowConstructor ?
	((RowConstructor)rows[i]).getMaxResultSize(column) : 
	rows[i].getMaxResultSize();
      if (len<0)
	return -1;
      if (len>result)
	result=len;
    }
    return result;
  }


  /****************  evaluation methods  ****************/

  /**
   * Reset the table to before the first row.  After a call to this method,
   * next() will advance to the first row in the table.
   */
  public void beforeFirst() {
    current_row = -1;
    current_value = null;
  }

  /**
   * The table is initially positioned before its first row; the
   * first call to next makes the first row the current row; the
   * second call makes the second row the current row, etc.
   *
   * @return true if the new current row is valid; false if there are no more rows
   */
  public boolean next() {
    current_value = null;
    return ++current_row<rows.length;
  }

  /**
   * Get the current row as an array of Objects.  
   *
   * @return array of objects or null if current row is not valid
   * @throws SQLException if an error occurs
   * @throws EndOfTable if thrown when evaluating the row
   */
  public Object[] getRow() throws SQLException, EndOfTable {
    if (current_value==null && 0<=current_row && current_row<rows.length) {
      if (rows[current_row] instanceof RowConstructor)
	current_value = ((RowConstructor)rows[current_row]).evaluateRow(false);
      else {
	current_value = new Object[1];
	current_value[0] = rows[current_row].evaluate(false);
      }
      for (int j=0; j<current_value.length; ++j)
	current_value[j] = 
	  AbstractFunction.convertToSQLType(current_value[j],column_types[j]);
    }
    return current_value;
  }

  /**
   * Get the value of a column in the current row as a Java object.  This
   * method will throw an exception if the current row is not valid.
   *
   * @param column column number (starting from zero)
   * @return an Object holding the column value
   * @throws SQLException if an error occurs
   * @throws EndOfTable if thrown when evaluating the row
   */
  public Object getObject(int column) throws SQLException, EndOfTable {
    if (current_value==null)
      getRow();
    if (current_value==null)
      throw new SQLException("row is not valid");
    return current_value[column];
  }


};
