package ModSQL;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;

/* $Id: AbstractFunction.java,v 1.15 2003/05/29 02:31:07 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 implementation of Function to simplify the implementation
 * of real functions.
 *
 * @author chris.studholme@utoronto.ca
 */
public abstract class AbstractFunction implements Function {

  /** Array of parameters before optimize. */
  protected ArrayList parameters_array;
  /** Parameter ordering before optimize. */
  protected ArrayList evaluate_order_array;

  /** Array of parameters after optimize. */
  protected Function[] parameters;
  /** Array for storing parameter values (constant or otherwise). */
  protected Object[] parameter_value;

  /** Array to keep track of which parameters are constant. */
  protected boolean[] parameter_constant;
  /** Are all parameters constant? */
  protected boolean all_constant;
  /** Are all of the constant parameters non-null? */
  protected boolean constant_non_null;

  /** Order in which to evaluate parameters.  
   * Each entry is an index into the parameters array.  */
  protected int[] evaluate_order;
  /** Are the parameters out-of-order?  Used by toString(). */
  protected boolean out_of_order;


  /** Default constructor.  Initializes pre-optimize arrays.
   */
  public AbstractFunction() {
    parameters_array = new ArrayList();
    evaluate_order_array = new ArrayList();

    evaluate_order = null;
    parameters = null;
    parameter_constant = null;
    parameter_value = null;
    all_constant = false;
  }


  /**
   * Get the number of parameters currently supplied to this function.
   *
   * @return number of parameters
   */
  public int getParameterCount() {
    return parameters_array!=null ? 
      parameters_array.size() : parameters.length;
  }

  /**
   * Get a particular parameter.
   *
   * @param index index of parameter to get
   * @return parameter (function)
   */
  public Function getParameter(int index) {
    return parameters_array!=null ? 
      (Function)parameters_array.get(index) :
      parameters[index];
  }

  /**
   * Adds a parameter to the list of parameters maintained by this function.
   *
   * @param item function to add
   * @throws SQLException if optimize has already been called
   */
  public void addParameter(Function item) throws SQLException {
    if (parameters_array==null)
      throw new SQLException("cannot add parameters after optimize");
    parameters_array.add(item);
  }

  /**
   * Specify the order in which the parameters should be evaluated.  
   *
   * @param index index of parameter
   * @param order number indicating order in which parameter is evaluated
   * @throws SQLException if optimize has already been called
   */
  public void evaluateOrder(int index, int order) throws SQLException {
    if (evaluate_order_array==null)
      throw new SQLException("cannot set evaluate order after optimize");
    while (order>=evaluate_order_array.size())
      evaluate_order_array.add(null);
    evaluate_order_array.set(order,new Integer(index));
  }

  /**
   * This implementation simply calls registerWith() for all parameters.
   *
   * @param o object to register with
   * @throws SQLException if an error occurs
   */
  public void registerWith(Object o) throws SQLException {
    if (parameters_array!=null) {
      for (int i=0; i<parameters_array.size(); ++i)
	((Function)parameters_array.get(i)).registerWith(o);
    }
    else {
      for (int i=0; i<parameters.length; ++i)
	parameters[i].registerWith(o);
    }    
  }

  /**
   * <p>This method sets up static arrays for the parameters and their values,
   * calls optimize() for each parameter, and initializes the evalute_order
   * array to appropriate values (using information provided using 
   * evaluateOrder(), if any).
   *
   * <p>The all_constant variable is initialized to true if and only if the
   * number of parameters is 0.  The constant_non_null variable is 
   * initialized to true.  The parameter_constant array is not initialized.
   * Use evaluateConstantParameters() after optimize() to initialize these
   * variables to more useful values.
   *
   * @throws SQLException if an error occurs
   */
  public void optimize() throws SQLException {
    if (parameters_array==null)
      return;

    // set up static parameter array
    parameters = new Function[parameters_array.size()];
    parameters = (Function[])parameters_array.toArray(parameters);
    parameters_array=null;

    // optimize parameters
    for (int i=0; i<parameters.length; ++i)
      parameters[i].optimize();

    // figure out evaluate order
    evaluate_order = new int[parameters.length];
    boolean[] param_used = new boolean[parameters.length];
    Arrays.fill(param_used,false);
    int pos=0;
    out_of_order=false;
    for (int i=0; i<evaluate_order_array.size(); ++i) {
      Number index = (Number)evaluate_order_array.get(i);
      if (index!=null) {
	int j = index.intValue();
	if (param_used[j])
	  throw new SQLException("invalid parameter order");
	param_used[j] = true;
	if (j!=pos) out_of_order=true;
	evaluate_order[pos++] = j;
      }
    }
    evaluate_order_array=null;
    if (pos<parameters.length) 
      for (int i=0; i<param_used.length; ++i)
	if (param_used[i]==false)
	  evaluate_order[pos++] = i;
    if (parameters.length<2) out_of_order=false;

    all_constant = (parameters.length==0);
    constant_non_null = true;

    parameter_value = new Object[parameters.length];
  }

  /**
   * <p>Evaluate parameters that advertise themselves as constant.  This
   * method initializes the parameter_constant, all_constant, and
   * constant_non_null variables to their correct values.
   *
   * <p>This method may only be used after optimize() has been called.  
   * Typically, a class that overrides optimize() will first call
   * super.optimize() and then use this method later within their new
   * optimize() implementation.
   *
   * @return true if all constant parameters were non-null
   * @throws SQLException if an error occurs while evaluating a parameter
   */
  public boolean evaluateConstantParameters() throws SQLException {
    parameter_constant = new boolean[parameters.length];
    all_constant = true;
    constant_non_null = true;
    try {
      for (int i=0; i<parameters.length; ++i) {
	parameter_constant[i] = parameters[i].isConstant();
	if (parameter_constant[i]) {
	  parameter_value[i] = parameters[i].evaluate(false);
	  if (parameter_value[i]==null)
	    constant_non_null=false;
	}
	else 
	  all_constant = false;
      }
    }
    catch (EndOfTable e) {
      throw new SQLException("EndOfTable thrown by constant function");
    }
    return constant_non_null;
  }

  /**
   * Returns the name of this function for use by the toString() method.
   *
   * @return name of function 
   */
  public abstract String functionName();

  /**
   * Optional String to insert before parameter list in toString().  This
   * method is used by AbstractAggregate to insert DISTINCT or "*" as
   * needed.  The default implementation returns the empty string.
   */
  public String prefixParameters() {
    return "";
  }

  /**
   * Optional String to insert after parameter list in toString().  This
   * method is used by Operator_In to add the table expression as the second
   * parameter.  If a comma is needed to seperate the parameter list from
   * this postfix, this method is expected to include the initial comma.
   * The default implementation returns the empty string.
   */
  public String postfixParameters() {
    return "";
  }

  /**
   * Outputs "functionName(parameter,...)".
   *
   * @return user-readable function declaration
   */
  public String toString() {
    StringBuffer dest = new StringBuffer();
    dest.append(functionName());
    dest.append('(');
    dest.append(prefixParameters());
    if (parameters_array!=null) {
      if (parameters_array.size()>0) {
	dest.append(parameters_array.get(0));
	for (int i=1; i<parameters_array.size(); ++i) {
	  dest.append(',');
	  dest.append(parameters_array.get(i));
	}
      }
    }
    else {
      if (parameters.length>0) {
	if (out_of_order) {
	  int order[] = new int[evaluate_order.length];
	  for (int i=0; i<evaluate_order.length; ++i)
	    order[evaluate_order[i]] = i+1;
	  dest.append("{"+order[0]+"}");
	  dest.append(parameters[0]);
	  for (int i=1; i<parameters.length; ++i) {
	    dest.append(',');
	    dest.append("{"+order[i]+"}");
	    dest.append(parameters[i]);
	  }
	}
	else {
	  dest.append(parameters[0]);
	  for (int i=1; i<parameters.length; ++i) {
	    dest.append(',');
	    dest.append(parameters[i]);
	  }
	}
      }
    }
    dest.append(postfixParameters());
    dest.append(')');
    return dest.toString();
  }


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

  /**
   * Determine if this function returns a constant value.  If all of the
   * functions parameters are constant, then the function is considered
   * constant as well.
   *
   * @return true if the value returned is constant
   * @throws SQLException if an error occurs
   */
  public boolean isConstant() throws SQLException {
    if (parameters_array!=null) {
      for (int i=0; i<parameters_array.size(); ++i)
	if (((Function)parameters_array.get(i)).isConstant()==false)
	  return false;
    }
    else if (parameter_constant!=null) {
      return all_constant;
    }
    else {
      for (int i=0; i<parameters.length; ++i)
	if (parameters[i].isConstant()==false)
	  return false;
    }
    return true;
  }

  /**
   * <p>The implementation of this method is intended to be used by 
   * non-aggregate functions.  Aggregate functions should extend
   * AbstractAggregate instead of AbstractFunction.  This implementation
   * will return true if and only if all of the function's parameters are
   * either aggregate or constant.
   *
   * @return true if the function aggregates several rows of data
   * @throws SQLException if an error occurs
   */
  public boolean isAggregate() throws SQLException {
    if (parameters==null)
      throw new SQLException("cannot call isAggregate before optimize");
    boolean aggregate = false;
    boolean nonaggregate = false;
    for (int i=0; i<parameters.length; ++i) {
      if (parameters[i].isAggregate())
	aggregate=true;
      else if (!parameters[i].isConstant()) 
	nonaggregate=true;
    }
    if (aggregate && nonaggregate)
      throw new SQLException("aggregate and non-aggregate parameters");
    return aggregate;
  }


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

  /**
   * Evaluates all parameters and stores the results in the parameter_value
   * array.  If evaluateConstantParameters() was called earlier, constant
   * parameters will not be re-evaluated.
   *
   * @param aggregate evaluate aggregates
   * @param stop_at_null stop if a null if found
   * @return true if all parameters are non-null, false otherwise
   * @throws SQLException if an error occurs
   * @throws EndOfTable if thrown by a parameter
   */
  protected boolean evaluateParameters(boolean aggregate, boolean stop_at_null)
    throws SQLException, EndOfTable {
    if (all_constant || (stop_at_null && !constant_non_null)) 
      return constant_non_null;
    boolean non_null=constant_non_null;
    if (parameter_constant!=null) {
      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) {
	    if (stop_at_null) return false;
	    non_null=false;
	  }
	}
    }
    else {
      for (int i=0; i<evaluate_order.length; ++i) {
	parameter_value[evaluate_order[i]] = 
	  parameters[evaluate_order[i]].evaluate(aggregate);
	if (parameter_value[evaluate_order[i]]==null) {
	  if (stop_at_null) return false;
	  non_null=false;
	}
      }
    }
    return non_null;
  }
  /**
   * Same as above version, but with stop_at_null set to false.
   *
   * @param aggregate evaluate aggregates
   * @return true if all parameters are non-null, false otherwise
   * @throws SQLException if an error occurs
   * @throws EndOfTable if thrown by a parameter
   */
  protected boolean evaluateParameters(boolean aggregate) 
    throws SQLException,EndOfTable {
    return evaluateParameters(aggregate,false);
  }

  /**
   * Default implementation that simply calls evaluate(false).
   *
   * @param match_op how the value should be matched
   * @param match_value desired value
   * @return result object
   * @throws SQLException if an error occurs
   * @throws EndOfTable if thrown by a parameter
   */
  public Object evaluate(int match_op, Object match_value)
    throws SQLException, EndOfTable {
    return evaluate(false);
  }

  /**
   * This implementation simply calls reset() for all parameters and is
   * intended to be used by non-aggregate functions.  If any additional
   * Function objects need to be reset, this method must be overridden.
   *
   * @throws SQLException if an error occurs
   */
  public void reset() throws SQLException {
    if (parameters==null)
      throw new SQLException("cannot call reset before optimize");
    for (int i=0; i<parameters.length; ++i)
      parameters[i].reset();
  }


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

  /**
   * Convert an Object to a compatible Object corresponding to the provided
   * SQL type.
   *
   * @param o Object to convert
   * @param type type from java.sql.Types to convert to
   * @return Object of desired type (or null if o is null)
   * @throws SQLException if no compatible type is available
   */
  public static Object convertToSQLType(Object o, int type) 
    throws SQLException {
    if (o==null) return null;
    switch (type) {
    case Types.NULL:
      return null;

    case Types.CHAR:
    case Types.VARCHAR:
    case Types.LONGVARCHAR:
      return o.toString();

    case Types.BIT:
      //case Types.BOOLEAN:
      if (o instanceof Boolean)
	return o;
      break;

    case Types.DATE:
      if (o instanceof java.sql.Date)
	return o;
      break;

    case Types.TIME:
      if (o instanceof java.sql.Time)
	return o;
      break;

    case Types.TIMESTAMP:
      if (o instanceof java.sql.Timestamp)
	return o;
      break;

    case Types.TINYINT:
      if (o instanceof Byte)
	return o;
      if (o instanceof Number)
	return new Byte(((Number)o).byteValue());
      break;

    case Types.SMALLINT:
      if (o instanceof Short)
	return o;
      if (o instanceof Number)
	return new Short(((Number)o).shortValue());
      break;

    case Types.INTEGER:
      if (o instanceof Integer)
	return o;
      if (o instanceof Number)
	return new Integer(((Number)o).intValue());
      break;

    case Types.BIGINT:
      if (o instanceof Long)
	return o;
      if (o instanceof Number)
	return new Long(((Number)o).longValue());
      break;

    case Types.REAL:
      if (o instanceof Float)
	return o;
      if (o instanceof Number)
	return new Float(((Number)o).floatValue());
      break;

    case Types.FLOAT:
    case Types.DOUBLE:
      if (o instanceof Double)
	return o;
      if (o instanceof Number)
	return new Double(((Number)o).doubleValue());
      break;

    case Types.NUMERIC:
      if (o instanceof java.math.BigDecimal)
	return o;
      if (o instanceof Number)
	return new java.math.BigDecimal(((Number)o).doubleValue());
      break;

    case Types.JAVA_OBJECT:
      return o;
    }
    throw new SQLException("type mismatch");
  }

  /**
   * Determine the SQL type of a java Object.  If the reference is non-null,
   * but the object type cannot be determined, java.sql.Types.JAVA_OBJECT is
   * returned.
   *
   * @param o object to check
   * @return value from java.sql.Types
   */
  public static int getObjectSQLType(Object o) {
    if (o==null)
      return Types.NULL;
    if (o instanceof String)
      return Types.VARCHAR;
    if (o instanceof Boolean)
      return Types.BIT;
    if (o instanceof Byte)
      return Types.TINYINT;
    if (o instanceof Short)
      return Types.SMALLINT;
    if (o instanceof Integer)
      return Types.INTEGER;
    if (o instanceof Long)
      return Types.BIGINT;
    if (o instanceof Float)
      return Types.REAL;
    if (o instanceof Double)
      return Types.DOUBLE;
    if (o instanceof java.math.BigDecimal)
      return Types.NUMERIC;
    if (o instanceof java.sql.Date)
      return Types.DATE;
    if (o instanceof java.sql.Time)
      return Types.TIME;
    if (o instanceof java.sql.Timestamp)
      return Types.TIMESTAMP;
    return Types.JAVA_OBJECT;
  }

  /**
   * Determine a java.sql.Types type that is compatable with both of the
   * passed parameters.  If either parameter is NULL, the type of the
   * other parameter will be returned.
   *
   * @param t1 type of first value
   * @param t2 type of second value
   * @return compatible type
   * @throws SQLException if the types are incompatible
   */
  public static int getCompatableType(int t1, int t2) throws SQLException {
    if (t1==t2)
      return t1;
    if (t1==Types.NULL)
      return t2;
    if (t2==Types.NULL)
      return t1;

    switch (t1) {
    case Types.CHAR:
    case Types.VARCHAR:
      switch (t2) {
      case Types.CHAR:
      case Types.VARCHAR:
	return Types.VARCHAR;
      case Types.LONGVARCHAR:
	return Types.LONGVARCHAR;
      }
      break;

    case Types.LONGVARCHAR:
      switch (t2) {
      case Types.CHAR:
      case Types.VARCHAR:
	return Types.LONGVARCHAR;
      }
      break;

    case Types.TINYINT:
    case Types.SMALLINT:
      switch (t2) {
      case Types.TINYINT:
      case Types.SMALLINT:
	return Types.SMALLINT;
      case Types.INTEGER:
      case Types.BIGINT:
      case Types.REAL:
      case Types.FLOAT:
      case Types.DOUBLE:
	return t2;
      }
      break;

    case Types.INTEGER:
      switch (t2) {
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
	return Types.INTEGER;
      case Types.BIGINT:
      case Types.REAL:
      case Types.FLOAT:
      case Types.DOUBLE:
	return t2;
      }
      break;

    case Types.BIGINT:
      switch (t2) {
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
      case Types.BIGINT:
	return Types.BIGINT;
      case Types.REAL:
      case Types.FLOAT:
      case Types.DOUBLE:
	return Types.DOUBLE;
      }
      break;

    case Types.REAL:
      switch (t2) {
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
      case Types.REAL:
	return Types.REAL;
      case Types.BIGINT:
      case Types.FLOAT:
      case Types.DOUBLE:
	return Types.DOUBLE;
      }
      break;

    case Types.FLOAT:
    case Types.DOUBLE:
      switch (t2) {
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
      case Types.BIGINT:
      case Types.REAL:
      case Types.FLOAT:
	return t1;
      case Types.DOUBLE:
	return Types.DOUBLE;
      }
      break;
    }

    throw new SQLException("type mismatch");
  }

};
