package ModSQL;
import java.sql.*;

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

/**
 * Implementation of SQL aggregate functions SUM(), AVG() and STDEV().
 * The return type of each function is function and parameter dependent.
 * AVG() and STDEV() will return type DOUBLE and SUM() will return either
 * type BIGINT or DOUBLE, depending on whether the parameter is an integer
 * or a floating point number, respectively.  If there is no parameter,
 * the return type is NULL for all functions.  If there are no non-null
 * values to aggregate, SUM() will return 0, but AVG() and STDEV() will 
 * return NULL. 
 *
 * @author chris.studholme@utoronto.ca
 */
final class Aggregate_Math extends AbstractAggregate {

  /** Function is SUM(). */
  public static final int SUM = 1;
  /** Function is AVG(). */
  public static final int AVG = 2;
  /** Function is STDEV(). */
  public static final int STDEV = 3;
  //public static final int PRODUCT = 4;
  
  /** Which function are we?  One of the constants above. */
  private int op;
  /** SQL return type for this function. */
  private int return_type = Types.NULL;

  /** Current count of non-null values. */
  private long count=0;
  /** Current sum as a BIGINT. */
  private long sum_long=0;
  /** Current sum as a DOUBLE. */
  private double sum_double=0;
  /** Current sum of squares. */
  private double sum_square=0;

  /**
   * Returns a new instance of this class if name is "sum", "avg", or
   * "stdev".  The instance is initialized as needed to represent the
   * desired function.
   *
   * @param name name of function to find
   * @return instance of this class or null if name does not match
   */
  public static Function forName(String name) {
    if (name.equals("sum"))
      return new Aggregate_Math(SUM);
    if (name.equals("avg"))
      return new Aggregate_Math(AVG);
    if (name.equals("stdev"))
      return new Aggregate_Math(STDEV);
    //if (name.equals("product"))
    //return new Aggregate_Math(PRODUCT);
    return null;
  }

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

  /**
   * <p>Prepare the function for use.  All functions accept at most one 
   * parameter and require a parameter if DISTINCT is specified.  This
   * method also decides on an appropriate return type.
   *
   * @throws SQLException if the parameters are invalid
   */
  public void optimize() throws SQLException {
    super.optimize();
    if (parameters.length>1)
      throw new SQLException("too many parameters in "+functionName());
    if (distinct && parameters.length==0)
      throw new SQLException("DISTINCT requires a parameter");
    // figure out return type
    if (parameters.length==1) {
      switch (op) {
      case SUM: 
	switch (parameters[0].getSQLType()) {
	case Types.REAL:
	case Types.FLOAT:
	case Types.DOUBLE:
	  return_type = Types.DOUBLE;
	  break;
	default:
	  return_type = Types.BIGINT;
	}
	break;

      case AVG: 
      case STDEV:
	return_type = Types.DOUBLE;
	break;

      default:
	throw new SQLException("SOFTWARE BUG: invalid operation");
      }
    }
  }

  /**
   * Returns the name of this function for use by toString() method.
   *
   * @return name of function
   */
  public String functionName() {
    switch (op) {
    case SUM: return "SUM";
    case AVG: return "AVG";
    case STDEV: return "STDEV";
    }
    return "#UNKNOWN";
  }

  /**
   * Return the SQL type of the value that this function expects to return.
   *
   * @return SQL type of data to be returned
   */
  public int getSQLType() throws SQLException {
    return return_type;
  }

  /**
   * Returns -1 as these functions never return String objects.
   *
   * @return maximum size of String returned or -1 if unknown
   */
  public int getMaxResultSize() {
    return -1;
  }

  /**
   * Resets the current count and sums to zero.
   *
   * @throws SQLException if a database error occurs
   */
  public void reset() throws SQLException {
    super.reset();
    count=0;
    sum_long=0;
    sum_double=0;
    sum_square=0;
  }

  /**
   * <p>Evaluate parameters and update count and sums.  If aggregate is false,
   * the parameter is evaluated and the count and sums are updated, but this
   * method always returns null.  If aggregate is true, the parameter is
   * not evaluated, and the current evaluation of the function is returned.
   *
   * @param aggregate true to return final aggregate value
   * @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 (parameters.length==0)
      return null;

    if (aggregate) {
      switch (op) {
      case SUM:
	return return_type==Types.BIGINT ? 
	  (Number)new Long(sum_long) : (Number)new Double(sum_double);
	
      case AVG: 
	return count!=0 ? new Double(sum_double/count) : null;

      case STDEV:
	return count!=0 ? 
	  new Double(Math.sqrt((sum_square-sum_double)/count)) : null;
	
      default:
	throw new SQLException("SOFTWARE BUG: invalid operation");
      }
    }

    // evaluate parameter
    Object value = parameters[0].evaluate(false);
    if (value!=null && (!distinct || addDistinctValue(value))) {
      if (!(value instanceof Number))
	throw new SQLException(functionName()+" requires numbers");

      ++count;
      switch (op) {
      case SUM:
	if (return_type==Types.BIGINT)
	  sum_long += ((Number)value).longValue();
	else
	  sum_double += ((Number)value).doubleValue();
	break;
	
      case AVG: 
	sum_double += ((Number)value).doubleValue();
	break;

      case STDEV:
	double d = ((Number)value).doubleValue();
	sum_double += d;
	sum_square += d*d;
	break;
	
      default:
	throw new SQLException("SOFTWARE BUG: invalid operation");
      }
    }
    return null;
  }
};

