package ModSQL;
import java.sql.*;
 
/* $Id: Function_Math.java,v 1.7 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.
 */

/**
 * This class implements standard SQL math functions:  ABS, CEIL, FLOOR,
 * GREATEST, LEAST, MOD, POWER, ROUND, SIGN, and SQRT.
 *
 * @author chris.studholme@utoronto.ca
 */
final class Function_Math extends AbstractFunction {

  /** Function is ABS(). */
  public static final int ABS      = 0;
  /** Function is CEIL(). */
  public static final int CEIL     = 1;
  /** Function is FLOOR(). */
  public static final int FLOOR    = 2;
  /** Function is GREATEST(). */
  public static final int GREATEST = 3;
  /** Function is LEAST(). */
  public static final int LEAST    = 4;
  /** Function is MOD(). */
  public static final int MOD      = 5;
  /** Function is POWER(). */
  public static final int POWER    = 6;
  /** Function is ROUND(). */
  public static final int ROUND    = 7;
  /** Function is SIGN(). */
  public static final int SIGN     = 8;
  /** Function is SQRT(). */
  public static final int SQRT     = 9;

  /** Which function are we?  One of the above constants. */
  private int op;
  /** SQL return type for this function. */
  private int return_type;

  /**
   * Returns a new instance of this class if name is recognized.  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("abs"))   return new Function_Math(ABS);
    if (name.equals("ceil"))  return new Function_Math(CEIL);
    if (name.equals("floor")) return new Function_Math(FLOOR);
    if (name.equals("greatest")) return new Function_Math(GREATEST);
    if (name.equals("least")) return new Function_Math(LEAST);
    if (name.equals("mod"))   return new Function_Math(MOD);
    if (name.equals("power")) return new Function_Math(POWER);
    if (name.equals("round")) return new Function_Math(ROUND);
    if (name.equals("sign"))  return new Function_Math(SIGN);
    if (name.equals("sqrt"))  return new Function_Math(SQRT);
    return null;
  }

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


  /****************  setup methods  ****************/

  /**
   * Prepare the function for use.  This method figures out the return type
   * of the function.
   *
   * @throws SQLException if the parameters are invalid
   */
  public void optimize() throws SQLException {
    super.optimize();
    
    // make sure we have at least one parameter
    if (parameters.length<1)
      throw new SQLException(functionName()+
                             " requires at least one parameter");
    
    switch (op) {
    case ABS:
    case CEIL:
    case FLOOR:
    case SIGN:
    case SQRT:
      if (parameters.length>1)
	throw new SQLException(functionName()+" accepts only one parameter");
      break;

    case MOD:
    case POWER:
      if (parameters.length!=2)
	throw new SQLException(functionName()+" requires two parameters");
      break;

    case ROUND:
      if (parameters.length>2)
	throw new SQLException(functionName()+
			       " accepts at most two parameters");
    case GREATEST:
    case LEAST:
      break;

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

    return_type = Types.NULL;
    boolean null_found=false;
    for (int i=0; i<parameters.length; ++i) {
      int type = parameters[i].getSQLType();
      return_type = getCompatableType(return_type,type);
      if (type==Types.NULL) null_found=true;
    }
    if (null_found) {
      return_type = Types.NULL;
      return;
    }

    switch (return_type) {
    case Types.NULL:
    case Types.TINYINT:
    case Types.SMALLINT:
    case Types.INTEGER:
    case Types.BIGINT:
    case Types.REAL:
    case Types.FLOAT:
    case Types.DOUBLE:
      break;
    default:
      throw new SQLException("numeric parameters expected in "+functionName());
    }

    // alter return_type if necessary
    switch (op) {
    case MOD: 
      return_type = parameters[1].getSQLType();
      break;

    case SIGN:
      return_type = Types.INTEGER;
      break;

    case SQRT:
    case ROUND:
      return_type = Types.DOUBLE;
      break;
    }

    if (!evaluateConstantParameters())
      return_type = Types.NULL;
  }


  /****************  information methods  ****************/

  /**
   * Returns the SQL type of this function.
   *
   * @return SQL type of data to be returned
   */
  public int getSQLType() {
    return return_type;
  }

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

  /**
   * Returns the name of this function for use by toString() method.
   *
   * @return name of function
   */
  public String functionName() {
    switch (op) {
    case ABS:   return "ABS";
    case CEIL:  return "CEIL";
    case FLOOR: return "FLOOR";
    case GREATEST: return "GREATEST";
    case LEAST: return "LEAST";
    case MOD:   return "MOD";
    case POWER: return "POWER";
    case ROUND: return "ROUND";
    case SIGN:  return "SIGN";
    case SQRT:  return "SQRT";
    }
    return "#UNKNOWN";
  }

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

  /**
   * <p>Evaluate parameters and compute function.
   *
   * @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 (return_type==Types.NULL)
      return null;

    // evaluate all parameters
    for (int i=0; i<evaluate_order.length; ++i) {
      if (!parameter_constant[evaluate_order[i]]) {
        parameter_value[evaluate_order[i]] = 
	  parameters[evaluate_order[i]].evaluate(aggregate);
	if (parameter_value[evaluate_order[i]]==null)
	  return null;
      }
    }

    // if we make it this far, all parameters are non-null

    switch (op) {
    case ABS:
      return NumberMath.abs((Number)parameter_value[0]);

    case CEIL:
      return NumberMath.ceil((Number)parameter_value[0]);

    case FLOOR:
      return NumberMath.floor((Number)parameter_value[0]);

    case SIGN:
      return new Integer(NumberMath.sign((Number)parameter_value[0]));

    case SQRT:
      return new Double(NumberMath.sqrt((Number)parameter_value[0]));

    case MOD:
      return NumberMath.mod((Number)parameter_value[0],
			    (Number)parameter_value[1]);

    case ROUND:
      return parameters.length>1 ? 
	round((Number)parameter_value[0],(Number)parameter_value[1]) : 
	round((Number)parameter_value[0]);
      
    case POWER:
      return power((Number)parameter_value[0],
		   (Number)parameter_value[1],return_type);

    case GREATEST:
    case LEAST:
      Object result = parameter_value[0];
      for (int i=1; i<parameter_value.length; ++i) {
	int c = Operator_Compare.compare(result,parameter_value[i],
					 return_type);
	if (op==LEAST) c = -c;
	if (c<0) result = parameter_value[i];
      }
      return convertToSQLType(result,return_type);
    }

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

  
  /****************  static methods  ****************/

  /**
   * Round number to nearest integer.
   *
   * @param n number to round
   * @return Double object with rounded value
   */
  public static Number round(Number n) {
    return new Double(Math.round(n.doubleValue()));
  }

  /**
   * Round number of specified number of decimal places.
   *
   * @param n number to round
   * @param d number of decimal places to round to (integer)
   * @return Double object with rounded value
   */
  public static Number round(Number n, Number d) {
    double p = Math.pow(10,d.intValue());
    return new Double(Math.round(n.doubleValue()*p)/p);
  }

  /**
   * Compute n to the power of e and return a number of the desired type.
   *
   * @param n base
   * @param e exponent
   * @param return_type SQL type of object to return
   * @return Number object with computed power
   * @throws SQLException if type mismatch
   */
  public static Number power(Number n, Number e, int return_type) 
    throws SQLException {
    double p = Math.pow(n.doubleValue(),e.doubleValue());
    switch (return_type) {
    case Types.TINYINT:
      return new Byte((byte)Math.round(p));

    case Types.SMALLINT:
      return new Short((short)Math.round(p));

    case Types.INTEGER:
      return new Integer((int)Math.round(p));

    case Types.BIGINT:
      return new Long(Math.round(p));

    case Types.REAL:
      return new Float(p);

    case Types.FLOAT:
    case Types.DOUBLE:
      return new Double(p);
    }
    throw new SQLException("type mismatch in power");
  }

};

