package ModSQL;
import java.sql.*;
import java.util.*;
 
/* $Id: Operator_In.java,v 1.8 2003/07/09 06:54:49 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>SQL comparison operators with a table on the RHS. 
 *
 * @author chris.studholme@utoronto.ca
 */
final class Operator_In extends Operator {

  /* NOTE: the following list of constants must be kept in sync with the
   * list in Operator_Compare.
   */

  /** Comparison operator (equal). */
  public static final int EQU = 1;
  /** Comparison operator (greater than). */
  public static final int GT  = 2;
  /** Comparison operator (greater or equal). */
  public static final int GTE = 3;
  /** Comparison operator (less than). */
  public static final int LT  = 4;
  /** Comparison operator (less or equal). */
  public static final int LTE = 5;
  /** Comparison operator (not equal). */
  public static final int NE  = 6;

  /** Which operator?  One of the above constants. */
  private int op;
  /** True if all rows must match. */
  private boolean match_all;
  /** Table on right-hand-side (RHS). */
  private Table table;

  /** Per column comparison type. */
  private int[] compare_type;
  /** Row-constructor on LHS if needed. */
  private RowConstructor row;
  /** Number of columns in row-constructor. */
  private int ncolumns;

  /** Hash table to speedup tests for equality. */
  private HashSet hashset;
  /** Does the hash table contain a null value. */
  private boolean hashset_hasnull;


  /**
   * Constructor.
   *
   * @param op comparison operator
   * @param match_all true to match all rows
   */
  public Operator_In(int op, boolean match_all) {
    this.op = op;
    this.match_all = match_all;
    table = null;
  }

  /**
   * Specify the table to be scanned.
   *
   * @param table table on RHS of comparison
   */
  public void setTable(Table table) {
    this.table = table;
  }

  /**
   * Since table is not a normal parameter, we need to ensure objects to
   * register with get passed to it.  This method also ensures that the
   * LHS gets these objects too.
   *
   * @param o object to register with
   * @throws SQLException if a database error occurs
   */
  public void registerWith(Object o) throws SQLException {
    super.registerWith(o);
    if (table!=null)
      table.registerWith(o);
  }

  /**
   * <p>Prepare the function for use.  This method checks the parameters and
   * figures out how the comparison will take place (what type to use).
   *
   * <p>If the RHS consists of a single column, the operator is EQU or NE,
   * and the table is constant, a hash table of the rows is created to
   * greatly speed up comparisons.
   *
   * @throws SQLException if the parameters are invalid
   */
  public void optimize() throws SQLException {
    super.optimize();

    // make sure we have a table
    if (table==null)
      throw new SQLException("table required");
    table.optimize();

    // check number of parameters
    if (parameters.length!=1) 
      throw new SQLException("exactly one non-table parameter required");

    // check for row constructors
    ncolumns = parameters[0] instanceof RowConstructor ?
      ((RowConstructor)parameters[0]).getColumnCount() : 1;

    // check parameter types
    compare_type = new int[ncolumns];
    row = null;
    if (ncolumns>1) {
      row = (RowConstructor)parameters[0];
      if (table.getColumnCount()!=ncolumns)
	throw new SQLException("column count mismatch");
      for (int j=0; j<ncolumns; ++j)
	compare_type[j] =
	  getCompatableType(row.getSQLType(j),table.getSQLType(j));
    }
    else {
      // single column only (scalar values)
      compare_type[0] =
	getCompatableType(parameters[0].getSQLType(),table.getSQLType(0));
      //evaluateConstantParameters();
    }
    // make sure column types are appropriate
    for (int j=0; j<ncolumns; ++j)
      switch (compare_type[j]) {
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
      case Types.BIGINT:
      case Types.REAL:
      case Types.FLOAT:
      case Types.DOUBLE:
      case Types.CHAR:
      case Types.VARCHAR:
      case Types.LONGVARCHAR:
        break;
      default:
        if (op!=EQU && op!=NE)
          throw new SQLException("unsupported type for comparison operator");
      }

    // create a HashSet if it will help (note: multi-column is very trickly)
    hashset = null;
    if ((op==EQU || op==NE) && table.isConstant() && ncolumns==1) {
      try {
	table.beforeFirst();
	int nrows = (int)table.getRowCount();
	if (nrows<0) nrows = DriverConfig.getHashTableSize();
	else if (nrows==0) nrows=1;
	hashset = new HashSet(nrows);
	hashset_hasnull = false;
	if (DriverConfig.debugLevel>0)
	  System.err.println("ModSQL.Operator_In: creating hash table (initial size "+nrows+")");
	while (table.next()) {
	  // need to copy array because table might reuse its arrays
	  Object[] orig_values = table.getRow();
	  if (orig_values[0]==null)
	    hashset_hasnull = true;
	  else {
	    Object[] values = new Object[orig_values.length];
	    for (int i=0; i<ncolumns; ++i)
	      values[i] = convertToSQLType(orig_values[i],compare_type[i]);
	    hashset.add(Arrays.asList(values));
	  }
	}
      }
      catch (EndOfTable e) {
	throw new SQLExceptionWithCause("SOFTWARE BUG",e);
      }
      if (DriverConfig.debugLevel>0)
	System.err.println("ModSQL.Operator_In: hash table created ("+
			   hashset.size()+" entries)");
    }
  }
  
  /**
   * Returns the name of this function for use by toString() method.
   *
   * @return name of function 
   */
  public String functionName() {
    StringBuffer result = new StringBuffer();
    switch (op) {
    case EQU: result.append("#EQU"); break;
    case GT:  result.append("#GT");  break;
    case GTE: result.append("#GTE"); break;
    case LT:  result.append("#LT");  break;
    case LTE: result.append("#LTE"); break;
    case NE:  result.append("#NE");  break;
    default:  
      result.append("#COMPARE");
    }
    result.append(match_all?"_ALL":"_SOME");
    return result.toString();
  }

  /**
   * This method returns ",RHS", provided a table has been specified for
   * the RHS.
   *
   * @return table parameter as a string
   */
  public String postfixParameters() {
    return table!=null ? ','+table.toString() : "";
  }

  /****************  result meta-data  ****************/
  
  /**
   * Determine if this function returns a constant value.  This method returns
   * true if both the LHS and RHS are constant.
   *
   * @return true if the value returned is constant
   * @throws SQLException if a database-access error occurs
   */
  public boolean isConstant() throws SQLException {
    return parameters[0].isConstant() && table.isConstant();
  }

  /**
   * <p>Determine if this function will return a value that is an aggregate of
   * many database rows.  The function is an aggregate if the LHS is an
   * aggregate and the RHS is constant.  If the LHS is an aggregate, but
   * the RHS is not constant, an exception is thrown.
   *
   * @return true if the function aggregates several rows of data
   * @throws SQLException if a database-access error occurs
   */
  public boolean isAggregate() throws SQLException {
    if (parameters[0].isAggregate()) {
      if (table.isConstant())
	return true;
      throw new SQLException("aggregate and non-aggregate functions");
    }
    return false;
  }

  /**
   * Returns Types.BIT.
   *
   * @return SQL type of data to be returned
   */
  public int getSQLType() {
    return Types.BIT;
  }


  /**
   * Returns -1 as this operator will never return a String object.
   *
   * @return maximum size of String returned or -1 if unknown
   */
  public int getMaxResultSize() {
    return -1;
  }


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


  /**
   * Evaluate parameters and compare.
   *
   * @param aggregate passed to parameters
   * @return result object
   * @throws SQLException if a database-access error occurs
   * @throws EndOfTable if thrown by a parameter
   */
  public Object evaluate(boolean aggregate) throws SQLException, EndOfTable {

    // evaluate target row
    Object[] r1;
    if (row!=null)
      r1 = row.evaluateRow(aggregate);
    else {
      parameter_value[0] = parameters[0].evaluate(aggregate);
      r1 = parameter_value;
    }

    if (hashset!=null) {
      // check for empty set
      if (hashset.size()==0 && !hashset_hasnull)
	return new Boolean(match_all);

      // convert r1 to compatible type
      Object[] values = new Object[r1.length];
      for (int i=0; i<ncolumns; ++i)
	values[i] = convertToSQLType(r1[i],compare_type[i]);
      if (values[0]==null) 
	return null;

      // lookup row in hashset
      boolean result = hashset.contains(Arrays.asList(values));
      if (DriverConfig.debugLevel>=3)
	System.err.println("ModSQL.Operator_In: hash lookup '"+values[0]+"'"+
			   " result="+result);

      /* Oh, I really wish what follows could be commented in such a way that
       * it makes some sense.
       */
      if (result==true) {
	if (match_all==(op!=EQU))
	  return new Boolean(!match_all);
	else {
	  if (hashset.size()>1)
	    return new Boolean(!match_all);
	  else if (hashset_hasnull)
	    return null;
	  else
	    return new Boolean(match_all);
	}
      }
      else { // result==false
	if (match_all==(op!=EQU))
	  return hashset_hasnull ? null : new Boolean(match_all);
	else
	  return hashset.size()>0 ? new Boolean(!match_all) : null;
      }
    }

    // test against each row of table
    boolean null_found=false;
    table.beforeFirst();
    while (table.next()) {
      Object[] r2 = table.getRow();
      Boolean result = testRow(r1,r2);
      if (result==null)
	null_found=true;
      else if (result.booleanValue()!=match_all)
	return result;
    }
    return null_found ? null : new Boolean(match_all);
  }

  /**
   * Evaluate parameters and compare.
   *
   * @param match_op how the value should be matched
   * @param match_value desired value
   * @return result object
   * @throws SQLException if a database-access error occurs
   * @throws EndOfTable if thrown by a parameter
   */
  public Object evaluate(int match_op, Object match_value)
    throws SQLException, EndOfTable {
    // can we do better here?
    return evaluate(false);
  }

  /**
   * Compare two rows according to op and using compare_type array.
   *
   * @param r1 left row
   * @param r2 right row
   * @return result of comparison
   * @throws SQLException if op is invalid or on type mismatch
   */
  public Boolean testRow(Object[] r1, Object[] r2) throws SQLException {
    return Operator_Compare.testRow(r1,r2,op,compare_type);
  }
};
