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

/* $Id: Create.java,v 1.11 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>Create will parse a CREATE SQL statement.  Currently, only tables
 * and indices can be created.
 *
 * @author chris.studholme@utoronto.ca
 */
final class Create implements Query {

  /** Name of index to create. */
  String indexname=null;
  /** Type of index to create. */
  String indextype=null;
  /** Name of table to index. */
  String indexed_table=null;
  /** Name of column to index. */
  String indexed_column=null;

  /** Name of table to create. */
  String tablename=null;
  /** Names of columns to include in new table. */
  String[] column_names=null;
  /** SQL types of columns to create. */
  int[][] column_types=null;

  /** Table manager used to open tables and create new tables. */
  protected transient DatabaseManager tablemanager;

  /**
   * 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
   */
  protected Create(StreamTokenizer tokenizer, DatabaseManager manager) 
    throws SQLException, IOException {
    tablemanager = manager;
    ParseCreate(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("CREATE");
    if (tablename!=null) {
      result.append(" TABLE ");
      result.append(tablename);
      result.append(" (");
      if (column_names!=null && column_types!=null) {
	result.append(column_names[0]+" ");
	result.append(columnType(column_types[0]));
	for (int i=1; i<column_names.length; ++i) {
	  result.append(", "+column_names[i]+" ");
	  result.append(columnType(column_types[i]));
	}
      }
      result.append(")");
    }
    else if ((indexed_table!=null)&&(indexed_column!=null)) {
      result.append(" INDEX");
      if (indexname!=null)
	result.append(" "+indexname);
      result.append(" ON ");
      result.append(indexed_table);
      if (indextype!=null)
	result.append(" USING "+indextype.toUpperCase());
      result.append(" ("+indexed_column+")");
    }
    else 
      result.append(" [UNKNOWN]");
    if (with_brackets) result.append(')');
    return result.toString();
  }

  /**
   * Parse create query of the form:<br>
   *   <blockquote><code>
   *   CREATE INDEX [indexname] ON tablename [USING indextype] (column);<br>
   *   CREATE TABLE tablename (column list);
   *   </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 ParseCreate(StreamTokenizer tokenizer)
    throws SQLException, IOException {
    
    if ((tokenizer.ttype!=StreamTokenizer.TT_WORD)||
	(!tokenizer.sval.equals("create")))
      throw new SQLException("'CREATE' expected");
    tokenizer.nextToken();

    // subfunctions
    if (tokenizer.ttype!=StreamTokenizer.TT_WORD) 
      throw new SQLException("invalid CREATE expression at or before "+
			     tokenizer);

    if (tokenizer.sval.equals("index")) {
      tokenizer.nextToken();
      ParseCreateIndex(tokenizer);
    }
    else if (tokenizer.sval.equals("table")) {
      tokenizer.nextToken();
      ParseCreateTable(tokenizer);
    }
    else {
      throw new SQLException("CREATE expression not supported at or before "+
			     tokenizer);
    }
  }

   
  /**
   * Parse create index query of the form:<br>
   *   <blockquote><code>
   *   CREATE INDEX [indexname] ON tablename [USING indextype] (column);<br>
   *   </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 ParseCreateIndex(StreamTokenizer tokenizer) 
    throws SQLException, IOException {

    // optional index name
    indexname=null;
    if ((tokenizer.ttype==StreamTokenizer.TT_WORD)&&
	(!tokenizer.sval.equals("on"))) {
      indexname=tokenizer.sval;
      tokenizer.nextToken();
    }

    // ON keyword
    if ((tokenizer.ttype==StreamTokenizer.TT_WORD)&&
	(tokenizer.sval.equals("on")))
      tokenizer.nextToken();
    else
      throw new SQLException("'ON' expected at or before "+tokenizer);

    // table name
    indexed_table=null;
    if (tokenizer.ttype==StreamTokenizer.TT_WORD) {
      indexed_table=tokenizer.sval;
      tokenizer.nextToken();
    }
    else
      throw new SQLException("table name expected at or before "+tokenizer);

    // optional using
    indextype=null;
    if ((tokenizer.ttype==StreamTokenizer.TT_WORD)&&
	(tokenizer.sval.equals("using"))) {
      tokenizer.nextToken();
      if (tokenizer.ttype!=StreamTokenizer.TT_WORD) 
	throw new SQLException("index type expected at or before "+tokenizer);
      indextype=tokenizer.sval;
      tokenizer.nextToken();
    }

    // column name
    if (tokenizer.ttype!='(') 
      throw new SQLException("'(' expected at or before "+tokenizer);
    tokenizer.nextToken();

    indexed_column=null;
    if (tokenizer.ttype==StreamTokenizer.TT_WORD) {
      indexed_column=tokenizer.sval;
      tokenizer.nextToken();
    }
    else
      throw new SQLException("column name expected at or before "+tokenizer);
    
    if (tokenizer.ttype==',') 
      throw new SQLException("multi-column index not supported");
    if (tokenizer.ttype!=')') 
      throw new SQLException("')' expected at or before "+tokenizer);
    tokenizer.nextToken();
  }

  /**
   * Parses an SQL type and returns a two element array describing the
   * type.  The first element of the array is the SQL type code.  The
   * second element is the maximum length for variable length fields.
   *
   * @param tokenizer StreamTokenizer that SQL tokens should be read from
   * @return two element array describing parsed type
   * @throws SQLException if an error occurs
   * @throws IOException if there is a problem reading the query
   */
  private static int[] parseColumnType(StreamTokenizer tokenizer)
    throws SQLException, IOException {
    if (tokenizer.ttype!=StreamTokenizer.TT_WORD)
      throw new SQLException("column type expected at or before "+tokenizer);

    int[] result = new int[2];
    String ts=tokenizer.sval;
    if (ts.equals("boolean") || ts.equals("bool"))
      result[0]=Types.BIT;
    else if (ts.equals("tinyint") || ts.equals("byte"))
      result[0]=Types.TINYINT;
    else if (ts.equals("smallint") || ts.equals("short"))
      result[0]=Types.SMALLINT;
    else if (ts.equals("integer") || ts.equals("int"))
      result[0]=Types.INTEGER;
    else if (ts.equals("bigint") || ts.equals("long"))
      result[0]=Types.BIGINT;
    else if (ts.equals("real") || ts.equals("single"))
      result[0]=Types.REAL;
    else if (ts.equals("double") || ts.equals("float"))
      result[0]=Types.DOUBLE;
    else if (ts.equals("char") || ts.equals("character")) {
      result[0]=Types.VARCHAR;
      result[1]=1;
    }
    else if (ts.equals("varchar") || ts.equals("text"))
      result[0]=Types.VARCHAR;
    else
      throw new SQLException("column type expected at or before "+tokenizer);
    tokenizer.nextToken();

    // field length
    if (tokenizer.ttype=='(') {
      tokenizer.nextToken();
      if (tokenizer.ttype!=StreamTokenizer.TT_WORD)
	throw new SQLException("number expected at or before "+tokenizer);
      result[1]=Integer.parseInt(tokenizer.sval);
      if (result[1]<=0)
	throw new SQLException("invalid length at or before "+tokenizer);
      tokenizer.nextToken();
      if (tokenizer.ttype!=')') 
	throw new SQLException("')' expected at or before "+tokenizer);
      tokenizer.nextToken();
    }

    return result;
  }

  /**
   * Parse create table query of the form:<br>
   *   <blockquote><code>
   *   CREATE TABLE tablename (column list);
   *   </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 ParseCreateTable(StreamTokenizer tokenizer) 
    throws SQLException, IOException {

    // table name
    tablename=null;
    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!='(') 
      throw new SQLException("'(' expected at or before "+tokenizer);

    ArrayList names = new ArrayList();
    ArrayList types = new ArrayList();
    do {
      // column name
      tokenizer.nextToken();
      if (tokenizer.ttype!=StreamTokenizer.TT_WORD)
	throw new SQLException("column name expected at or before "+tokenizer);
      names.add(tokenizer.sval);
      tokenizer.nextToken();
      // column type
      types.add(parseColumnType(tokenizer));
    } while (tokenizer.ttype==',');

    column_names = new String[names.size()]; 
    column_names = (String[])names.toArray(column_names);
    column_types = new int[types.size()][];
    column_types = (int[][])types.toArray(column_types);

    if (tokenizer.ttype!=')') 
      throw new SQLException("')' expected at or before "+tokenizer);
    tokenizer.nextToken();
  }

  /**
   * Optimize query.
   */
  public void optimize() {
  }

  /**
   * Execute the query.
   *
   * @return 1 if successful
   * @throws SQLException if an error occurs
   */
  public int execute() throws SQLException {
    if (indexed_table!=null && indexed_column!=null) {
      // attempt to have underlying database create the index
      if (tablemanager.createIndex(indexed_table,indexed_column))
	return 1;
      // create the index ourselves
      if (indextype==null || indextype.equals("sort"))
	IndexTable_Sort.createIndex(tablemanager,indexed_table,indexed_column);
      else if (indextype.equals("hash"))
	IndexTable_Hash.createIndex(tablemanager,indexed_table,indexed_column);
      else
	throw new SQLException("invalid index type '"+indextype+"'");
      return 1;
    }

    if (tablename!=null) {
      // create table
      DatabaseTable table = tablemanager.createTable(tablename,false);
      if (table==null)
	throw new SQLException("failed to create table '"+tablename+"'");
      for (int i=0; i<column_names.length; ++i)
	table.addColumn(column_names[i],column_types[i][0],column_types[i][1]);
      return 1;
    }

    throw new SQLException("cannot execute CREATE statement");
  }


  /**
   * Returns a String representation of an SQL type.
   *
   * @param type first index is type, second is maximum length where needed
   * @return String representing type
   */
  private static String columnType(int[] type) {
    switch(type[0]) {
    case Types.BIT:
      //case Types.BOOLEAN:
      return "BIT";

    case Types.TINYINT:
      return "TINYINT";
    case Types.SMALLINT:
      return "SMALLINT";
    case Types.INTEGER:
      return "INTEGER";
    case Types.BIGINT:
      return "BIGINT";

    case Types.REAL:
      return "REAL";

    case Types.FLOAT:
    case Types.DOUBLE:
      return "DOUBLE";

    case Types.CHAR:
    case Types.VARCHAR:
    case Types.LONGVARCHAR:
      if (type[1]>1)
	return "VARCHAR("+type[1]+")";
      else if (type[1]==1)
	return "CHARACTER";
      else // type[1]==0
	return "TEXT";
      
    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:
      return "UNKNOWN";
    }
  }

};
