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

/* $Id: Insert.java,v 1.14 2003/09/24 19:59:34 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.
 */

/**
 * <p>Insert parses and executes an SQL INSERT query.
 *
 * @author chris.studholme@utoronto.ca
 */
final class Insert implements Query {

  /** Name of table. */
  private String tablename=null;
  /** Array of column names to insert. */
  private String[] columns=null;
  /** Rows of data to insert. */
  private Table source=null;

  /** Database table to insert rows into (set by optimize). */
  private DatabaseTable table;
  /** Array of indices of columns to insert (set by optimize). */
  private int[] column_index;
  /** Array of column types (set by optimize). */
  private int[] column_type;

  /** Table manager used to open tables. */
  private transient DatabaseManager manager;

  /**
   * Contructor to parse query.
   *
   * @param tokenizer StreamTokenizer that SQL tokens should be read from
   * @param manager manager to use when looking up tables
   * @throws SQLException if an error occurs
   * @throws IOException if there is a problem reading the query
   */
  public Insert(StreamTokenizer tokenizer, DatabaseManager manager) 
    throws SQLException, IOException {
    this.manager = manager;
    ParseInsert(tokenizer);
  }

  /**
   * Close query and free resources in use.
   */
  public void close() {
  }

  /**
   * Returns human-readable string version of query (with surrounding 
   * brackets).
   *
   * @return String representation of query
   */
  public String toString() {
    return toString(true);
  }

  /**
   * Returns human-readable string version of query.
   *
   * @param with_brackets true to include surrounding brackets
   * @return String representation of query
   */
  public String toString(boolean with_brackets) {
    StringBuffer result = new StringBuffer();
    if (with_brackets) result.append('(');
    result.append("INSERT INTO ");
    result.append(tablename!=null ? tablename : "[UNKNOWN]");

    // column list
    if (columns!=null) {
      result.append(" (");
      result.append(columns[0]);
      for (int i=1; i<columns.length; ++i)
	result.append(", "+columns[i]);
      result.append(")");
    }

    // source
    if (source!=null) {
      result.append(' ');
      result.append(source.toString(false));
    }

    if (with_brackets) result.append(')');
    return result.toString();
  }


  /**
   * Parse update query of the form:<br>
   *   <blockquote><code>
   *   INSERT INTO tablename (columns,...) VALUES (values,...);<br>
   *   INSERT INTO tablename (columns,...) SELECT ...;
   *   </code></blockquote>
   *
   * @param tokenizer StreamTokenizer that SQL tokens should be read from
   * @throws SQLException if an error occurs
   * @throws IOException if there is a problem reading the query
   */   
  private void ParseInsert(StreamTokenizer tokenizer)
    throws SQLException, IOException {

    tablename=null;
    columns=null;
    source=null;
    
    if ((tokenizer.ttype!=StreamTokenizer.TT_WORD)||
	(!tokenizer.sval.equals("insert")))
      throw new SQLException("'INSERT' expected");
    tokenizer.nextToken();

    if ((tokenizer.ttype!=StreamTokenizer.TT_WORD)||
	(!tokenizer.sval.equals("into")))
      throw new SQLException("'INTO' expected");
    tokenizer.nextToken();

    // table name
    if (tokenizer.ttype!=StreamTokenizer.TT_WORD) 
      throw new SQLException("table name expected at or before "+tokenizer);
    tablename=tokenizer.sval;
    tokenizer.nextToken();

    // column list
    if (tokenizer.ttype=='(') {
      ArrayList list = new ArrayList();
      do {
	// column name
	tokenizer.nextToken();
	if (tokenizer.ttype!=StreamTokenizer.TT_WORD)
	  throw new SQLException("column name expected at or before "+
				 tokenizer);
	list.add(tokenizer.sval);
	tokenizer.nextToken();
      } while (tokenizer.ttype==',');
      if (tokenizer.ttype!=')')
	throw new SQLException("')' expected at or before "+tokenizer);
      tokenizer.nextToken();
      columns = new String[list.size()];
      columns = (String[])list.toArray(columns);
    }

    // VALUES or SELECT
    source = Expression.parseTableExpression(tokenizer,manager);
  }

  /**
   * Optimize query.
   *
   * @throws SQLException if an error occurs
   */
  public void optimize() throws SQLException {
    if (tablename==null)
      throw new SQLException("cannot execute INSERT statement");
    table = manager.openTable(tablename,false);

    // lookup columns
    if (columns!=null) {
      column_index = new int[columns.length];
      column_type = new int[columns.length];
      for (int i=0; i<columns.length; ++i) {
	column_index[i] = table.findColumn(columns[i]);
	if (column_index[i]<=0)
	  throw new SQLException("column '"+columns[i]+"' not found");
	column_type[i] = table.getColumnType(column_index[i]);
      }
    }
    else {
      column_index = new int[table.getColumnCount()];
      column_type = new int[column_index.length];
      for (int i=0; i<column_index.length; ++i) {
	column_index[i] = i+1;
	column_type[i] = table.getColumnType(column_index[i]);
      }
    }

    if (source==null)
      throw new SQLException("no values to insert");

    source.optimize();

    if (column_index.length!=source.getColumnCount()) 
      throw new SQLException("incorrect number of columns");
  }

  /**
   * Execute the query.
   *
   * @return number of rows inserted
   * @throws SQLException if an error occurs
   */
  public int execute() throws SQLException {
    
    int count=0;
    try {
      while (source.next()) {
	// insert row
	Object[] row = source.getRow();
	table.addRow();
	for (int i=0; i<column_index.length; ++i) 
	  table.updateObject(column_index[i],
			     getCompatibleType(row[i],column_type[i]));
	table.commitUpdates();
	++count;
      }
    }
    catch (EndOfTable e) {
      throw new SQLExceptionWithCause("SOFTWARE BUG",e);
    }
    return count;
  }

  /**
   * Convert the supplied object to an object compatible with the specified
   * SQL type.  If the supplied object already has the correct type, it
   * will be returned.  If null is passed in, null will be returned.
   *
   * @param x object to convert
   * @param type SQL type to convert to
   * @return object compatible with the specified type
   * @throws SQLException if a type mismatch occurs
   */
  public static Object getCompatibleType(Object x, int type) 
    throws SQLException {
    if (x==null) return null;
    switch (type) {
    case Types.BIT:
      //case Types.BOOLEAN:
      if (x instanceof Boolean) return x;
      if (x instanceof Number) 
	return new Boolean(((Number)x).doubleValue()!=0);
      break;
      
    case Types.TINYINT:
      if (x instanceof Byte) return x;
      break;
    case Types.SMALLINT:
      if (x instanceof Short) return x;
      if (x instanceof Byte)
	return new Short(((Number)x).shortValue());
      break;
    case Types.INTEGER:
      if (x instanceof Integer) return x;
      if (x instanceof Byte || x instanceof Short) 
	return new Integer(((Number)x).intValue());
      break;
    case Types.BIGINT:
      if (x instanceof Long) return x;
      if (x instanceof Byte || x instanceof Short || x instanceof Integer) 
	return new Long(((Number)x).longValue());
      break;

    case Types.REAL:
      if (x instanceof Float) return x;
      if (x instanceof Number)
	return new Float(((Number)x).floatValue());
      break;

    case Types.FLOAT:
    case Types.DOUBLE:
      if (x instanceof Double) return x;
      if (x instanceof Number) 
	return new Double(((Number)x).doubleValue());
      break;

    case Types.CHAR:
    case Types.VARCHAR:
    case Types.LONGVARCHAR:
      if (x instanceof String) return x;
      break;

    case Types.BINARY:
    case Types.BLOB:
    case Types.CLOB:
    case Types.DATE:
    case Types.DECIMAL:
    case Types.DISTINCT:
    case Types.JAVA_OBJECT:
    case Types.LONGVARBINARY:
    case Types.NULL:
    case Types.NUMERIC:
    case Types.OTHER:
    case Types.REF:
    case Types.STRUCT:
    case Types.TIME:
    case Types.TIMESTAMP:
    case Types.VARBINARY:
      //case Types.DATALINK:
    default:
    }
    throw new SQLException("type mismatch");
  }
};
