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

/* $Id: Expression.java,v 1.21 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.
 */

/**
 * Abstract class with static methods for parsing SQL expressions.
 *
 * @author chris.studholme@utoronto.ca
 */
abstract class Expression {

  /** Arbitrarily scaled priority number (for multiplication). */
  public static final int PRIORITY_MULTIPLY = 100;
  /** Arbitrarily scaled priority number (for addition). */
  public static final int PRIORITY_ADD      = 90;
  /** Arbitrarily scaled priority number (for comparisions). */
  public static final int PRIORITY_COMPARE  = 80;
  /** Arbitrarily scaled priority number (for NOT). */
  public static final int PRIORITY_NOT      = 70;
  /** Arbitrarily scaled priority number (for AND). */
  public static final int PRIORITY_AND      = 60;
  /** Arbitrarily scaled priority number (for OR). */
  public static final int PRIORITY_OR       = 50;


  /** List of SQL keywords.  This list is no where near complete. 
   */
  protected static final String[] sqlkeywords = {
    "and","as",
    "by",
    "cross",
    "from","full",
    "group",
    "having",
    "inner",
    "join",
    "left",
    "natural",
    "not",
    "on","or","order","outer",
    "right",
    "select",
    "union","using",
    "where"
  };

  /**
   * Static support method. Determine if the parameter represents a SQL 
   * keyword.  The parameter must be all lowercase.
   *
   * @param word word to check
   * @return true if word is a SQL keyword, false otherwise
   */
  public static boolean isKeyword(String word) {
    for (int i=0; i<sqlkeywords.length; ++i)
      if (sqlkeywords[i].equals(word))
	return true;
    return false;
  }


  /**
   * Get a new object representing a built-in SQL function.
   *
   * @param name name of function to lookup (all lowercase)
   * @return function object (or null if function not known)
   * @throws SQLException if an error occurs
   */
  public static Function getBuiltinFunction(String name) 
    throws SQLException {
    String[] functionclasses = DriverConfig.getFunctionClasses();
    for (int i=0; i<functionclasses.length; ++i) {
      try {
        // find class
        Class c = Class.forName(functionclasses[i]);

        // find forName() method
        Class params[] = new Class[1];
        params[0] = Class.forName("java.lang.String");
        Method method = c.getDeclaredMethod("forName",params);

        // invoke method
        Object args[] = new Object[1];
        args[0] = name;
        Object result = method.invoke(null,args);
	if (result!=null)
	  return (Function)result;
      }
      catch (ClassNotFoundException e) {
	throw new SQLException("missing java class '"+functionclasses[i]+"'");
      }
      catch (NoSuchMethodException e) {
	throw new SQLException("function class '"+functionclasses[i]+
			       "' missing forName() method");
      }
      catch (InvocationTargetException e) {
	throw new SQLException("function class '"+functionclasses[i]+
			       "' has invalid forName() method");
      }
      catch (IllegalAccessException e) {
	throw new SQLException("function class '"+functionclasses[i]+
			       "' has invalid forName() method");
      }
    }
    return null;
  }


  /****************  parsing methods  ****************/

  /**
   * <p>Parse an arbitrary SQL expression and return the root Function
   * of the graph of Function's that results.
   *
   * <p>The first thing parseExpression() does is call parseValue().  The
   * expression may consist of nothing more than a value parsable by
   * parseValue().
   *
   * @param tokenizer StreamTokenizer that SQL tokens should be read from
   * @param manager DatabaseManager currently in use (used for subqueries)
   * @return the root Function representing the expression
   * @throws SQLException if a database-access error occurs
   * @throws IOException if there is a problem with the StreamTokenizer
   */
  public static Function parseExpression(StreamTokenizer tokenizer,
					 DatabaseManager manager)
    throws SQLException, IOException {
    return parseExpression(tokenizer,manager,0);
  }
 
  /**
   * Generalized version of above method.  When parsing, if an operator with
   * a priority less than or equal to priority_min if found, parsing will
   * cease at that point.  
   *
   * @param tokenizer StreamTokenizer that SQL tokens should be read from
   * @param manager DatabaseManager currently in use (used for subqueries)
   * @param priority_min stop if an operator with a low priority is found
   * @return the root Function representing the expression
   * @throws SQLException if a database-access error occurs
   * @throws IOException if there is a problem with the StreamTokenizer
   */
  public static Function parseExpression(StreamTokenizer tokenizer,
					 DatabaseManager manager,
					 int priority_min)
    throws SQLException, IOException {

    Function root = null;
    Operator prev_op = null;
    int prev_priority = 0;
    boolean negate_next = false;
    boolean next_is_table = false;

    while (true) {
      
      // first parse next value (or expression with higher priority operators)
      if (root==null)
	root = parseValue(tokenizer,manager);
      else if (next_is_table)
	((Operator_In)prev_op).
	  setTable(parseTableExpression(tokenizer,manager));
      else {
	Function value = 
	  parseExpression(tokenizer,manager,prev_priority);
	if (negate_next)
	  value = new Operator_Negate(value);
	prev_op.addParameter(value);
      }
      negate_next = false;
      next_is_table = false;
      
      // check for NOT applied to comparison operator
      boolean not=false;
      if (tokenizer.ttype=='!' ||
	  (tokenizer.ttype==tokenizer.TT_WORD &&
	   tokenizer.sval.equals("not"))) {
	if (PRIORITY_COMPARE<=priority_min)
	  return root;
	not=true;
	tokenizer.nextToken();
      }
      
      // then check for an operator
      Operator op=null;
      int comparison_op=-1;
      Function between_value=null;
      switch (tokenizer.ttype) {
      case '*':
	if (PRIORITY_MULTIPLY<=priority_min) break;
	prev_priority = PRIORITY_MULTIPLY;
	op = new Operator_Multiply();
	break;
	
      case '/':
	if (PRIORITY_MULTIPLY<=priority_min) break;
	prev_priority = PRIORITY_MULTIPLY;
	op = new Operator_Divide();
	break;
	
      case '|':
	if (PRIORITY_ADD<=priority_min) break;
	prev_priority = PRIORITY_ADD;
	if (tokenizer.nextToken()!='|')
	  throw new SQLException("'|' expected at or before "+tokenizer);
	op = new Operator_Concat();
	break;
	
      case '-':
	negate_next=true;
      case '+':
	if (PRIORITY_ADD<=priority_min) break;
	prev_priority = PRIORITY_ADD;
	op = new Operator_Add();
	break;
	  
      case '=':
	if (PRIORITY_COMPARE<=priority_min) break;
	prev_priority = PRIORITY_COMPARE;
	comparison_op = not ? Operator_Compare.NE : Operator_Compare.EQU;
	not = false;
	break;
	  
      case '<':
	if (PRIORITY_COMPARE<=priority_min) break;
	prev_priority = PRIORITY_COMPARE;
	switch (tokenizer.nextToken()) {
	case '>':
	  comparison_op = Operator_Compare.NE;
	  break;
	case '=':
	  comparison_op = Operator_Compare.LTE;
	  break;
	default:
	  comparison_op = Operator_Compare.LT;
	  tokenizer.pushBack();
	}
	break;
	
      case '>':
	if (PRIORITY_COMPARE<=priority_min) break;
	prev_priority = PRIORITY_COMPARE;
	switch (tokenizer.nextToken()) {
	case '=':
	  comparison_op = Operator_Compare.GTE;
	  break;
	default:
	  comparison_op = Operator_Compare.GT;
	  tokenizer.pushBack();
	}
	break;
	
      case StreamTokenizer.TT_WORD:
	if (tokenizer.sval.equals("between")) {
	  if (PRIORITY_COMPARE<=priority_min) break;
	  prev_priority = PRIORITY_COMPARE;
	  tokenizer.nextToken();
	  between_value = 
	    parseExpression(tokenizer,manager,PRIORITY_COMPARE);
	  if (tokenizer.ttype!=StreamTokenizer.TT_WORD ||
	      !tokenizer.sval.equals("and"))
	    throw new SQLException("'AND' expected at or before "+tokenizer);
	  op = new Operator_Compare(Operator_Compare.BETWEEN);
	}
	
	else if (tokenizer.sval.equals("in")) {
	  if (PRIORITY_COMPARE<=priority_min) break;
	  prev_priority = PRIORITY_COMPARE;
	  if (not)
	    op = new Operator_In(Operator_In.NE,true);
	  else
	    op = new Operator_In(Operator_In.EQU,false);
	  not = false;
	  next_is_table = true;
	}
	
	else if (tokenizer.sval.equals("like")) {
 	  if (PRIORITY_COMPARE<=priority_min) break;
	  prev_priority = PRIORITY_COMPARE;
	  op = new Operator_Like();
	}
	
	else if (tokenizer.sval.equals("is")) {
 	  if (PRIORITY_COMPARE<=priority_min) break;
	  prev_priority = PRIORITY_COMPARE;
	  op = new Operator_Is();
	}
	
	else if (tokenizer.sval.equals("and")) {
 	  if (PRIORITY_AND<=priority_min) break;
	  prev_priority = PRIORITY_AND;
	  op = new Operator_Logical(true);
	}
	
	else if (tokenizer.sval.equals("or")) { 
 	  if (PRIORITY_OR<=priority_min) break;
	  prev_priority = PRIORITY_OR;
	  op = new Operator_Logical(false);
	}
	break;
      }

      if (comparison_op>=0) {
	// check for table operator
	if (tokenizer.nextToken()==tokenizer.TT_WORD) {
	  if (tokenizer.sval.equals("some") || tokenizer.sval.equals("any")) {
	    op = new Operator_In(comparison_op,false);
	    next_is_table = true;
	  }
	  else if (tokenizer.sval.equals("all")) {
	    op = new Operator_In(comparison_op,true);
	    next_is_table = true;
	  }
	}
	if (op==null) {
	  tokenizer.pushBack();
	  op = new Operator_Compare(comparison_op);
	}
      }
      
      if (op==null) {
	// didn't find valid a next operator, we're done
	return root;
      }

      // we'll use the operator so advance to next token
      tokenizer.nextToken();
      
      // organize tree
      op.addParameter(root);
      
      // add middle parameter for BETWEEN operator
      if (between_value!=null)
	op.addParameter(between_value);
      
      // create NOT operator if necessary
      if (not)
	root = new Operator_Not(op);
      else
	root = op;
      prev_op = op;
    }    
  }
  

  /**
   * <p>Parse an arbitrary SQL value and return the root Function
   * of the graph of Function's that results.  This method is useful
   * for parsing literals '-5.43', single parameter operators 'NOT a=b', and
   * subexpressions including row-constructors '(SELECT ...)'.  Use 
   * parseExpression() for parsing general expressions.
   *
   * @param tokenizer StreamTokenizer that SQL tokens should be read from
   * @param manager DatabaseManager currently in use (used for subqueries)
   * @return the root Function's representing the value
   * @throws SQLException if a database-access error occurs
   * @throws IOException if there is a problem with the StreamTokenizer
   */
  public static Function parseValue(StreamTokenizer tokenizer,
				    DatabaseManager manager) 
    throws SQLException, IOException {

    // check for single-parameter operators
    if (tokenizer.ttype==tokenizer.TT_WORD) {
      if (tokenizer.sval.equals("not")) {
	tokenizer.nextToken();
	Function param = 
	  parseExpression(tokenizer,manager,PRIORITY_NOT);
	return new Operator_Not(param);
      }
      else if (tokenizer.sval.equals("exists")) {
	tokenizer.nextToken();
	Operator_Exists o = new Operator_Exists();
	o.setTable(parseTableExpression(tokenizer,manager));
	return o;
      }
      else if (tokenizer.sval.equals("unique")) {
	throw new SQLException("UNIQUE not supported");
      }
    }

    boolean negative=false;
    boolean number_expected=false;
    if (tokenizer.ttype=='-') {
      negative=true;
      number_expected=true;
      tokenizer.nextToken();
    }
    else if (tokenizer.ttype=='+') {
      number_expected=true;
      tokenizer.nextToken();
    }
    
    Function value=null;
    switch (tokenizer.ttype) {
    case '\'':
    case '"':
      // quoted string constant
      if (number_expected)
 	throw new SQLException("number expected at or before "+tokenizer);
      value = new Literal(tokenizer.sval);
      break;

    case '*':
      // used only in COUNT(*)
      if (number_expected)
 	throw new SQLException("number expected at or before "+tokenizer);
      value = new IndirectFunction("*");
      break;
      
    case '(': {
      // subexpression or row-constructor (possibly a subquery)
      LiteralRow row = null;
      do {
	Function item;
	if (tokenizer.nextToken()==tokenizer.TT_WORD &&
	    tokenizer.sval.equals("select"))
	  item = new Select(tokenizer,manager);
	else 
	  item = parseExpression(tokenizer,manager);

	if (value==null)
	  value=item;
	else {
	  if (row==null) {
	    row = new LiteralRow(value);
	    value = row;
	  }
	  row.addParameter(item);
	}
      } while (tokenizer.ttype==',');
      if (tokenizer.ttype!=')')
 	throw new SQLException("')' expected at or before "+tokenizer);
     break;
    }
      
    case StreamTokenizer.TT_WORD:
      // check for true, false, null
      if (tokenizer.sval.equals("true"))
	value = new Literal(true);
      else if (tokenizer.sval.equals("false"))
	value = new Literal(false);
      else if (tokenizer.sval.equals("null") ||
	       tokenizer.sval.equals("unknown"))
	value = new Literal();
      
      // check for numeric constant
      else if (tokenizer.sval.charAt(0)>='0' &&
	       tokenizer.sval.charAt(0)<='9') {
	String s = tokenizer.sval;
	if (s.endsWith("e") || s.endsWith("E")) {
	  if (tokenizer.nextToken()!='-' || 
	      tokenizer.nextToken()!=tokenizer.TT_WORD)
	    throw new SQLException("invalid number '"+s+"'");
	  s += '-' + tokenizer.sval;
	}
	Number n;
	if (s.indexOf('e')>=0 || s.indexOf('E')>=0 || s.indexOf('.')>=0)
	  n = new Double(s);
	else if (s.length()>9)
	  n = new Long(s);
	else
	  n = new Integer(s);
	if (negative)
	  n = NumberMath.negate(n);
	negative=false;
	value = new Literal(n);
      }

      else { // function or column name
	value = manager.getFunction(tokenizer.sval);
	if (value==null) 
	  value = getBuiltinFunction(tokenizer.sval);
	if (value!=null) {
	  // function: parse parameters
	  if (tokenizer.nextToken()!='(')
	    throw new SQLException("'(' expected at or before "+tokenizer);
	  if (tokenizer.nextToken()==tokenizer.TT_WORD) {
	    // check for distinct/all
	    if (tokenizer.sval.equals("distinct")) {
	      if (!(value instanceof Aggregate))
		throw new SQLException("'DISTINCT' only allowed with aggregate");
	      ((Aggregate)value).setDistinct();
	      tokenizer.nextToken();
	    }
	    else if (tokenizer.sval.equals("all")) {
	      if (!(value instanceof Aggregate))
		throw new SQLException("'ALL' only allowed with aggregate");
	      tokenizer.nextToken();
	    }
	  }
	  if (tokenizer.ttype!=')') {
	    // function parameters
	    while (true) {
	      value.addParameter(parseExpression(tokenizer,manager));
	      if (tokenizer.ttype!=',')
		break;
	      tokenizer.nextToken();
	    }
	    if (tokenizer.ttype!=')')
	      throw new SQLException("')' expected at or before "+tokenizer);
	  }
	}

	else if (!isKeyword(tokenizer.sval)) {
          // column name
	  String name = tokenizer.sval;
	  if (name.endsWith(".")) {
	    if (tokenizer.nextToken()!='*')
	      throw new SQLException("'*' expected at or before "+tokenizer);
	    name = name.concat("*");
	  }
	  value = new IndirectFunction(name);
	}

	else
	  throw new SQLException("invalid column name '"+tokenizer.sval+"'");
      }
      break;
      
    default:
      throw new SQLException("value expected at or before "+tokenizer);
    }

    tokenizer.nextToken();
    return negative ? new Operator_Negate(value) : value;
  }


  /**
   * Parse an arbitrary table expression.  These are of the form:<ul>
   * <li><code>([value [,...]])</code> for a single column</li>
   * <li><code>([row-constructor [,...]])</code> for an explicit table</li>
   * <li><code>VALUES row-constructor [, ...]</code> for an explicit table</li>
   * <li><code>SELECT ...</code> for a query or subquery</li>
   * </ul>
   * where <code>row-constructor</code> is of the form 
   * <code>(column-value [, ...])</code> or <code>SELECT ...</code>.
   *
   * @param tokenizer StreamTokenizer that SQL tokens should be read from
   * @param manager DatabaseManager currently in use (used for subqueries)
   * @return the root Function's representing the value
   * @throws SQLException if a database-access error occurs
   * @throws IOException if there is a problem with the StreamTokenizer
   */
  public static Table parseTableExpression(StreamTokenizer tokenizer,
					   DatabaseManager manager) 
    throws SQLException, IOException {

    Table root = null;
    LiteralTable table = null;
    boolean close_bracket_expected = false;

    switch (tokenizer.ttype) {
    case '(':
      close_bracket_expected = true;
      break;
      
    case StreamTokenizer.TT_WORD:
      if (tokenizer.sval.equals("select"))
	return new Select(tokenizer,manager);
      else if (tokenizer.sval.equals("values")) {
	// must be a literal table
	table = new LiteralTable();
	root = table;
	break;
      }
      
    default:
      throw new SQLException("invalid row list at or before "+tokenizer);
    }

    do {
      // parse next row
      Function row;
      if (tokenizer.nextToken()==StreamTokenizer.TT_WORD &&
	  tokenizer.sval.equals("select"))
	row = new Select(tokenizer,manager);
      else
	row = parseValue(tokenizer,manager);

      if (table!=null) {
	// we already have a LiteralTable, add new row
	table.addRow(row);
      }
      else if (root!=null) {
	// we had what we thought was a table, but it is actually a 
        // row-constructor
	table = new LiteralTable();
	table.addRow((Function)root);
	table.addRow(row);
	root=table;
      }
      else {
	// first value, it may be a table itself
	if (row instanceof Table)
	  root = (Table)row;
	else {
	  table = new LiteralTable();
	  table.addRow(row);
	  root=table;
	}
      }
    } while (tokenizer.ttype==',');

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

    return root;
  }
};

