package ModSQL;
import java.sql.*;
import java.util.Arrays;
 
/* $Id: Operator_Compare.java,v 1.10 2003/07/09 06:59: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. 
 *
 * @author chris.studholme@utoronto.ca
 */
final class Operator_Compare extends Operator {

  /** 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;
  /** Comparison operator (between). */
  public static final int BETWEEN = 7;

  /** Which operator?  One of the above constants. */
  private int op;

  /** Per column comparison type. */
  private int[] compare_type; 
  /** One row per parameter (only if we have row-constructors). */
  private RowConstructor[] rows;
  /** Value of the parameters. */
  private Object[][] row_value;
  /** Number of columns in row-constructor. */
  private int ncolumns;


  /**
   * Constructor.
   *
   * @param op operator type
   */
  public Operator_Compare(int op) {
    this.op = op;
  }


  /**
   * <p>Prepare the function for use.  This method checks the parameters and
   * figures out how the comparison will take place (what type to use).
   *
   * @throws SQLException if the parameters are invalid
   */
  public void optimize() throws SQLException {
    super.optimize();

    // check operator type and number of parameters
    switch (op) {
    case EQU: 
    case GT:  
    case GTE: 
    case LT:  
    case LTE: 
    case NE:  
      if (parameters.length!=2) 
	throw new SQLException("comparison operator requires exactly two parameters");
      break;

    case BETWEEN:
      if (parameters.length!=3) 
	throw new SQLException("BETWEEN operator requires exactly three parameters");
      break;

    default:
      throw new SQLException("invalid comparison operator");
    }

    // check for row constructors
    ncolumns = parameters[0] instanceof RowConstructor ?
      ((RowConstructor)parameters[0]).getColumnCount() : 1;
    
    // check parameter types
    compare_type = new int[ncolumns];
    Arrays.fill(compare_type,Types.NULL);
    rows = null;
    if (ncolumns>1) {
      rows = new RowConstructor[parameters.length];
      row_value = new Object[parameters.length][];
      for (int i=0; i<parameters.length; ++i) {
	rows[i] = (RowConstructor)parameters[i];
	if (rows[i].getColumnCount()!=ncolumns)
	  throw new SQLException("column count mismatch");
	for (int j=0; j<ncolumns; ++j)
	  compare_type[j] = 
	    getCompatableType(compare_type[j],rows[i].getSQLType(j));
      }
    }
    else {
      // single column only (scalar values)
      for (int i=0; i<parameters.length; ++i)
	compare_type[0] = 
	  getCompatableType(compare_type[0],parameters[i].getSQLType());
      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");
      }
  }
  
  /**
   * Returns the name of this function for use by toString() method.
   *
   * @return name of function 
   */
  public String functionName() {
    switch (op) {
    case EQU: return "#EQU";
    case GT:  return "#GT";
    case GTE: return "#GTE";
    case LT:  return "#LT";
    case LTE: return "#LTE";
    case NE:  return "#NE";
    case BETWEEN: return "#BETWEEN";
    }
    return "#COMPARE";
  }


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

  /**
   * 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 String objects.
   *
   * @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 {
    if (ncolumns<=1) {
      if (op==BETWEEN) {
	// some nulls can yield "false" with BETWEEN
	evaluateParameters(aggregate);
	if (parameter_value[0]==null)
	  return null;
	if (parameter_value[1]==null) {
	  if (parameter_value[2]==null) return null;
	  return 
	    compare(parameter_value[0],parameter_value[2],compare_type[0])<=0 ?
	    null : new Boolean(false);
	}
	if (parameter_value[2]==null)
	  return 
	    compare(parameter_value[0],parameter_value[1],compare_type[0])>=0 ?
	    null : new Boolean(false);
	return new Boolean(test(parameter_value[0],parameter_value[1],
				parameter_value[2]));
      }
      // not BETWEEN
      if (!evaluateParameters(aggregate,true))
	return null;
      return new Boolean(test(parameter_value[0],parameter_value[1]));
    }

    // evaluate rows
    for (int i=0; i<evaluate_order.length; ++i)
      row_value[evaluate_order[i]] = 
	rows[evaluate_order[i]].evaluateRow(aggregate);

    if (op!=BETWEEN)
      return testRow(row_value[0],row_value[1],op,compare_type);

    // op==BETWEEN
    Boolean t1 = (Boolean)testRow(row_value[1],row_value[0],LTE,compare_type);
    if (t1!=null && t1.booleanValue()==false)
      return t1;
    Boolean t2 = (Boolean)testRow(row_value[0],row_value[2],LTE,compare_type);
    if (t2!=null && t2.booleanValue()==false)
      return t2;
    return t1==null || t2==null ? null : t1;
  }

  /**
   * 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 {
    if (ncolumns>1)
      return evaluate(false);

    if (op!=BETWEEN && !constant_non_null)
      return null;

    Boolean match = (Boolean)match_value;
    
    // evaluate first parameter
    if (!parameter_constant[evaluate_order[0]]) {
      if (op==BETWEEN && evaluate_order[0]!=0)
	parameter_value[evaluate_order[0]] =
	  parameters[evaluate_order[0]].evaluate(false);
      else if (match_value==null) {
	if (match_op==MATCH_NE)
	  parameter_value[evaluate_order[0]] =
	    parameters[evaluate_order[0]].evaluate(MATCH_NE,null);
	else
	  parameter_value[evaluate_order[0]] =
	    parameters[evaluate_order[0]].evaluate(false);
      }
      else if (match_op==MATCH_EQU) // looking for non-null value
	parameter_value[evaluate_order[0]] =
	  parameters[evaluate_order[0]].evaluate(MATCH_NE,null);
      if (parameter_value[evaluate_order[0]]==null) {
	if (op!=BETWEEN || evaluate_order[0]==0)
	  return null;
	else if (match_op==MATCH_EQU && 
		 match!=null && match.booleanValue()==true)
	  return null;
      }
    }
    
    // evaluate second parameter
    if (!parameter_constant[evaluate_order[1]]) {
      if (op==BETWEEN) {
	// we can do better here if evaluate_order[1]==0
	parameter_value[evaluate_order[1]] =
	  parameters[evaluate_order[1]].evaluate(false);
      }
      else {
	// not BETWEEN, first parameter is non-null
	if (match_value==null)
	  parameter_value[evaluate_order[1]] =
	    parameters[evaluate_order[1]].evaluate(match_op,match_value);
	else if (match_op==MATCH_NE) 
	  parameter_value[evaluate_order[1]] =
	    parameters[evaluate_order[1]].evaluate(false);
	else {
	  // searching for definite true or false
	  int fn;
	  switch (op) {
	  case EQU: 
	    fn = match.booleanValue() ? MATCH_EQU : MATCH_NE;
	    break;
	  case NE:  
	    fn = match.booleanValue() ? MATCH_NE : MATCH_EQU;
	    break;
	  case GT:  
	    if (match.booleanValue()==true)
	      fn = evaluate_order[0]==1 ? MATCH_GT : MATCH_LT;
	    else
	      fn = evaluate_order[0]==1 ? MATCH_LTE : MATCH_GTE;
	    break;
	  case GTE: 
	    if (match.booleanValue()==true)
	      fn = evaluate_order[0]==1 ? MATCH_GTE : MATCH_LTE;
	    else
	      fn = evaluate_order[0]==1 ? MATCH_LT : MATCH_GT;
	    break;
	  case LT:  
	    if (match.booleanValue()==true)
	      fn = evaluate_order[0]==1 ? MATCH_LT : MATCH_GT;
	    else
	      fn = evaluate_order[0]==1 ? MATCH_GTE : MATCH_LTE;
	    break;
	  case LTE: 
	    if (match.booleanValue()==true)
	      fn = evaluate_order[0]==1 ? MATCH_LTE : MATCH_GTE;
	    else
	      fn = evaluate_order[0]==1 ? MATCH_GT : MATCH_LT;
	    break;
	  default:
	    throw new SQLException("invalid comparison operator");
	  }
	  parameter_value[evaluate_order[1]] = 
	    parameters[evaluate_order[1]].
	    evaluate(fn,parameter_value[evaluate_order[0]]);
	}
	if (parameter_value[evaluate_order[1]]==null)
	  return null;
      }
    }

    // do comparison
    if (op!=BETWEEN)
      return new Boolean(test(parameter_value[0],parameter_value[1]));
    
    // op==BETWEEN after this point
    // evaluate third parameter
    if (!parameter_constant[evaluate_order[2]]) {
      // we can do better here
      parameter_value[evaluate_order[2]] =
	parameters[evaluate_order[2]].evaluate(false);
    }
    // some nulls can yield "false" with BETWEEN
    if (parameter_value[0]==null)
      return null;
    if (parameter_value[1]==null) {
      if (parameter_value[2]==null) return null;
      return 
	compare(parameter_value[0],parameter_value[2],compare_type[0])<=0 ?
	null : new Boolean(false);
    }
    if (parameter_value[2]==null)
      return 
	  compare(parameter_value[0],parameter_value[1],compare_type[0])>=0 ?
	null : new Boolean(false);
    // all parameters are non-null, do comparison
    return new Boolean(test(parameter_value[0],parameter_value[1],
			    parameter_value[2]));
  }

  /**
   * Compare two rows according to op and using compare_type array.
   *
   * @param r1 left row
   * @param r2 right row
   * @param op comparison operation
   * @param compare_type type to convert to for comparison
   * @return result of comparison
   * @throws SQLException if op is invalid or on type mismatch
   */
  public static Boolean testRow(Object[] r1, Object[] r2, 
				int op, int[] compare_type)
    throws SQLException {
    if (op!=EQU && op!=NE)
      return testRowInequality(r1,r2,op,compare_type);
    boolean null_found=false;
    for (int i=0; i<compare_type.length; ++i) {
      if (r1[i]==null || r2[i]==null)
        null_found=true;
      else if (equal(r1[i],r2[i],compare_type[i])==(op!=EQU))
	return new Boolean(false);
    }
    return null_found ? null : new Boolean(true);
  }

  /**
   * Compare two rows according to op (inequalities only) and using
   * compare_type array.
   *
   * @param r1 left row
   * @param r2 right row
   * @param op comparison operation
   * @param compare_type type to convert to for comparison
   * @return result of comparison
   * @throws SQLException if op is invalid or on type mismatch
   */
  public static Boolean testRowInequality(Object[] r1, Object[] r2, 
					  int op, int[] compare_type)
    throws SQLException {
    for (int i=0; i<compare_type.length; ++i) {
      if (r1[i]==null || r2[i]==null)
	return null;
      int c = compare(r1[i],r2[i],compare_type[i]);
      if (c!=0) {
	switch (op) {
	case GT:
	case GTE:
	  return new Boolean(c>0);
	case LT:
	case LTE:
	  return new Boolean(c<0);
	default:
	  throw new SQLException("invalid comparison operator");
	}
      }
    }
    return new Boolean(op==GTE || op==LTE);
  }

  /**
   * Compare two objects according to op.
   *
   * @param v1 left value
   * @param v2 right value
   * @param op comparison operation
   * @param compare_type type to convert to for comparison
   * @return result of comparison
   * @throws SQLException if op is invalid or on type mismatch
   */
  public static boolean test(Object v1, Object v2, int op, int compare_type)
    throws SQLException {
    switch (op) {
    case EQU: return equal(v1,v2,compare_type);
    case NE:  return !equal(v1,v2,compare_type);
    case GT:  return compare(v1,v2,compare_type)>0;
    case GTE: return compare(v1,v2,compare_type)>=0;
    case LT:  return compare(v1,v2,compare_type)<0;
    case LTE: return compare(v1,v2,compare_type)<=0;
    }
    throw new SQLException("invalid comparison operator");
  }
  public boolean test(Object v1, Object v2) throws SQLException {
    return test(v1,v2,op,compare_type[0]);
  }
  // for op==BETWEEN
  public static boolean test(Object v1, Object v2, Object v3, int compare_type)
    throws SQLException {
    return compare(v1,v2,compare_type)>=0 && compare(v1,v3,compare_type)<=0;
  }
  public boolean test(Object v1, Object v2, Object v3)
    throws SQLException {
    return test(v1,v2,v3,compare_type[0]);
  }


  /**
   * Check if two objects are equal by converting to a common type.
   *
   * @param v1 left value
   * @param v2 right value
   * @param compare_type type to convert to for comparison
   * @return true if the objects are equal
   */
  public static boolean equal(Object v1, Object v2, int common_type) {
    switch (common_type) {
    case Types.TINYINT:
    case Types.SMALLINT:
    case Types.INTEGER:
      return ((Number)v1).intValue()==((Number)v2).intValue();
      
    case Types.BIGINT:
      return ((Number)v1).longValue()==((Number)v2).longValue();

    case Types.REAL:
      return ((Number)v1).floatValue()==((Number)v2).floatValue();

    case Types.FLOAT:
    case Types.DOUBLE:
      return ((Number)v1).doubleValue()==((Number)v2).doubleValue();
    }
    return v1.equals(v2);
  }

  /**
   * Compare two objects by converting to a common type.
   *
   * @param v1 left value
   * @param v2 right value
   * @param compare_type type to convert to for comparison
   * @return negative if v1&lt;v2, positive if v1&gt;v2, 0 if equal
   * @throws SQLException if type mismatch
   */
  public static int compare(Object v1, Object v2, int common_type) 
    throws SQLException {
    switch (common_type) {
    case Types.CHAR:
    case Types.VARCHAR:
    case Types.LONGVARCHAR:
      return ((String)v1).compareTo(v2);
      
    case Types.TINYINT:
    case Types.SMALLINT:
      return ((Number)v1).intValue() - ((Number)v2).intValue();
      
    case Types.INTEGER:
      long i1 = ((Number)v1).intValue();
      long i2 = ((Number)v2).intValue();
      return i1<i2 ? -1 : i1==i2 ? 0 : 1;

    case Types.BIGINT:
      long l1 = ((Number)v1).longValue();
      long l2 = ((Number)v2).longValue();
      return l1<l2 ? -1 : l1==l2 ? 0 : 1;

    case Types.REAL:
      double f1 = ((Number)v1).floatValue();
      double f2 = ((Number)v2).floatValue();
      return f1<f2 ? -1 : f1==f2 ? 0 : 1;

    case Types.FLOAT:
    case Types.DOUBLE:
      double d1 = ((Number)v1).doubleValue();
      double d2 = ((Number)v2).doubleValue();
      return d1<d2 ? -1 : d1==d2 ? 0 : 1;
    }

    throw new SQLException("type mismatch during comparison");
  }
};
