/* Copyright (c) 1995-2000, The Hypersonic SQL Group.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Hypersonic SQL Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the Hypersonic SQL Group.
 *
 *
 * For work added by the HSQL Development Group:
 *
 * Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.Connection;

import org.hsqldb.lib.HashMap;
import org.hsqldb.lib.HsqlArrayList;
import org.hsqldb.lib.StringConverter;
import org.hsqldb.types.Binary;
import org.hsqldb.types.JavaObject;

// fredt@users 20020912 - patch 1.7.1 - shortcut treatment of identity() call
// fredt@users 20020912 - patch 1.7.1 - cache java.lang.reflect.Method objects
// fredt@users 20021013 - patch 1.7.1 - ignore non-static methods
// boucherb@users 20030201 - patch 1.7.2 - direct calls for org.hsqldb.Library
// fredt@users 20030621 - patch 1.7.2 - shortcut treatment of session calls
// boucherb@users 200404xx - doc 1.7.2 - updates toward 1.7.2 final

/**
 * Provides services to evaluate SQL function and stored procedure calls,
 * by invoking Java methods.
 *
 * Extended in successive versions of HSQLDB.
 *
 * @author Thomas Mueller (Hypersonic SQL Group)
 * @version 1.8.0
 * @since Hypersonic SQL
 */
class Function {

    private String         sFunction;
    private Method         mMethod;
    private String         returnClassName;
    private Class[]        aArgClasses;
    private int            iReturnType;
    private int            iArgCount;
    private int            iSqlArgCount;
    private int            iSqlArgStart;
    private int[]          iArgType;
    private boolean[]      bArgNullable;
    Expression[]           eArg;
    private boolean        bConnection;
    private static HashMap methodCache = new HashMap();
    private int            fID;
    String                 name;        // name used to call function
    boolean                isSimple;    //CURRENT_TIME, NOW etc.
    boolean                hasAggregate;

    /**
     * Constructs a new Function object with the given function call name
     * and using the specified Session context. <p>
     *
     * The call name is the fully qualified name of a static Java method, in
     * the form "package.class.method."  This implies that Java
     * methods with the same fully qualified name but different signatures
     * cannot be used properly as HSQLDB SQL functions or stored procedures.
     * For instance, it is impossible to call both System.getProperty(String)
     * and System.getProperty(String,String) under this arrangement, because
     * the HSQLDB Function object is unable to differentiate between the two;
     * it simply chooses the first method matching the FQN in the array of
     * methods obtained from calling getMethods() on an instance of the
     * Class indicated in the FQN, hiding all other methods with the same
     * FQN. <p>
     *
     * The function FQN must match at least one static Java method FQN in the
     * specified class or construction cannot procede and an HsqlException is
     * thrown. <p>
     *
     * The isSimple parameter is true when certain SQL standard functions
     * that are used without brackets are invokded.
     *
     * @param name this Function object's call name
     * @param fqn the fully qualified name of a Java method
     * @param isSimple if true, used to evalate CURRENT_TIME, NOW etc.
     *      evaluate
     * @throws HsqlException if the specified function FQN corresponds to no
     *      Java method
     */
    Function(String name, String fqn, boolean isSimple) throws HsqlException {

        this.name     = name;
        this.isSimple = isSimple;

//        cSession      = session;
        sFunction = fqn;
        fID       = Library.functionID(fqn);

        int i = fqn.lastIndexOf('.');

        Trace.check(i != -1, Trace.UNEXPECTED_TOKEN, fqn);

        String classname = fqn.substring(0, i);

        mMethod = (Method) methodCache.get(fqn);

        if (mMethod == null) {
            String methodname    = fqn.substring(i + 1);
            Class  classinstance = null;

            try {
                classinstance = Class.forName(classname);
            } catch (Exception e) {
                throw Trace.error(Trace.FUNCTION_NOT_FOUND,
                                  Trace.Message_Pair, new Object[] {
                    classname, e
                });
            }

            // public only, but includes those inherited from
            // superclasses and superinterfaces.  List is unordered.
            Method[] methods = classinstance.getMethods();

            for (i = 0; i < methods.length; i++) {
                Method m = methods[i];

                if (m.getName().equals(methodname)
                        && Modifier.isStatic(m.getModifiers())) {
                    mMethod = m;

                    break;
                }
            }

            Trace.check(mMethod != null, Trace.UNKNOWN_FUNCTION, methodname);
            methodCache.put(fqn, mMethod);
        }

        Class returnClass = mMethod.getReturnType();

        if (returnClass.equals(org.hsqldb.Result.class)) {

            // For now, we can write stored procedures whose
            // descriptor explicitly specifies the above return type.
            // Later, this will be modified or replaced to provide proper
            // support for jdbcCallableStatement OUT mode return parameter,
            // multiple results (Result.MULTI etc.)
            iReturnType = Types.OTHER;
        } else {

            // Now we support the following construction-time return type
            // Classes, as specified by the method descriptor:
            //
            // 1.) any primitive or primitive wrapper type, except Byte(.TYPE),
            //     Short(.TYPE) and Float(.TYPE) (TBD; narrow if no truncation)
            //
            // 2.) any primitive array type
            //
            // 3.) any non-primitive array whose base component implements
            // java.io.Serializable
            //
            // 4.) any class implementing java.io.Serializable, except those
            //     described in 1.) as currently unsupported
            //
            // 5.) java.lang.Object
            //
            // For java.lang.Object, checking is deferred from the construction
            // stage to the evaluation stage.  In general, for the evaluation
            // to succeed, the runtime class of the retrieved Object must be
            //
            // 1.) any primitive or primitive wrapper type, except Byte(.TYPE),
            //     Short(.TYPE) and Float(.TYPE) (TBD; narrow if no trunction)
            //
            // 2.) any primitive array type
            // 3.) any non-primitive array whose base component implements
            //     java.io.Serializable
            //
            // 4.) any class implementing java.io.Serializable, except those
            //     described in 1.) as currently unsupported
            //
            // Additionally, it is possible for the evaluation to succeed under
            // an SQL CALL if the runtime Class of the returned Object is not
            // from the list above but is from the list below:
            //
            // 1.) is org.hsqldb.Result
            // 2.) is org.hsqldb.jdbc.jdbcResultSet
            //
            // In these special cases, the statement executor notices the
            // types and presents the client with a view the underlying result
            // rather than with a view of the object as an opaque scalar value
            //
            iReturnType = Types.getParameterTypeNr(returnClass);
        }

        returnClassName =
            Types.getFunctionReturnClassName(returnClass.getName());
        aArgClasses  = mMethod.getParameterTypes();
        iArgCount    = aArgClasses.length;
        iArgType     = new int[iArgCount];
        bArgNullable = new boolean[iArgCount];

        for (i = 0; i < aArgClasses.length; i++) {
            Class  a    = aArgClasses[i];
            String type = a.getName();

            if ((i == 0) && a.equals(Connection.class)) {

                // TODO: provide jdbc:default:connection url functionality
                //
                // only the first parameter can be a Connection
                bConnection = true;
            } else {

                // see discussion above for iReturnType
                iArgType[i]     = Types.getParameterTypeNr(a);
                bArgNullable[i] = !a.isPrimitive();
            }
        }

        iSqlArgCount = iArgCount;

        if (bConnection) {
            iSqlArgCount--;

            iSqlArgStart = 1;
        } else {
            iSqlArgStart = 0;
        }

        eArg = new Expression[iArgCount];
    }

    /**
     * Evaluates and returns this Function in the context of the session.<p>
     */
    Object getValue(Session session) throws HsqlException {

        switch (fID) {

            case Library.curtime :
                return session.getCurrentTime();

            case Library.curdate :
                return session.getCurrentDate();

            case Library.database :
                return session.getDatabase().getPath();

            case Library.getAutoCommit :
                return session.isAutoCommit() ? Boolean.TRUE
                                              : Boolean.FALSE;

            case Library.identity :
                return session.getLastIdentity();

            case Library.isReadOnlyDatabase :
                return session.getDatabase().databaseReadOnly ? Boolean.TRUE
                                                              : Boolean.FALSE;

            case Library.isReadOnlyConnection :
                return session.isReadOnly() ? Boolean.TRUE
                                            : Boolean.FALSE;

            case Library.isReadOnlyDatabaseFiles :
                return session.getDatabase().isFilesReadOnly() ? Boolean.TRUE
                                                               : Boolean
                                                               .FALSE;

            case Library.now :
                return session.getCurrentTimestamp();

            case Library.user :
                return session.getUsername();
        }

        Object[] oArg = getArguments(session);

        if (oArg == null) {
            return null;
        }

        return getValue(session, oArg);
    }

    /**
     * Evaluates the Function with the given arguments in the session context.
     */
    Object getValue(Session session,
                    Object[] arguments) throws HsqlException {

        if (bConnection) {
            arguments[0] = session.getInternalConnection();
        }

        try {
            Object ret = (fID >= 0) ? Library.invoke(fID, arguments)
                                    : mMethod.invoke(null, arguments);

            return Column.convertObject(ret, iReturnType);
        } catch (InvocationTargetException e) {

            // thrown by user functions
            Throwable t = e.getTargetException();
            String    s = sFunction + " : " + t.toString();

            throw Trace.error(Trace.FUNCTION_CALL_ERROR, s);
        } catch (IllegalAccessException e) {

            // never thrown in this method
            throw Trace.error(Trace.FUNCTION_CALL_ERROR);
        }

        // Library function throw HsqlException
    }

    private Object[] getArguments(Session session) throws HsqlException {

        int      i    = bConnection ? 1
                                    : 0;
        Object[] oArg = new Object[iArgCount];

        for (; i < iArgCount; i++) {
            Expression e = eArg[i];
            Object     o = null;

            if (e != null) {

                // no argument: null
                o = e.getValue(session, iArgType[i]);
            }

            if ((o == null) &&!bArgNullable[i]) {

                // null argument for primitive datatype: don't call
                return null;
            }

            if (o instanceof JavaObject) {
                o = ((JavaObject) o).getObject();
            } else if (o instanceof Binary) {
                o = ((Binary) o).getBytes();
            }

            oArg[i] = o;
        }

        return oArg;
    }

    /**
     * returns null if any non-nullable element of values is null
     */
    private Object[] getNotNull(Object[] values) throws HsqlException {

        int i = bConnection ? 1
                            : 0;

        for (; i < iArgCount; i++) {
            Object o = values[i];

            if (o == null &&!bArgNullable[i]) {

                // null argument for primitive datatype: don't call
                return null;
            }
        }

        return values;
    }

    void collectInGroupByExpressions(HsqlArrayList colExps) {

        for (int i = 0; i < iArgCount; i++) {
            Expression e = eArg[i];

            if (e != null) {
                e.collectInGroupByExpressions(colExps);
            }
        }
    }

    Object getAggregatedValue(Session session,
                              Object currValue) throws HsqlException {

        Object[] valueArray = (Object[]) currValue;

        if (valueArray == null) {
            valueArray = new Object[iArgCount];
        }

        for (int i = 0; i < iArgCount; i++) {
            Expression e = eArg[i];

            if (eArg[i] != null) {
                if (eArg[i].isAggregate()) {
                    valueArray[i] = Column.convertObject(
                        e.getAggregatedValue(session, valueArray[i]),
                        iArgType[i]);
                } else {
                    valueArray[i] = e.getValue(session, iArgType[i]);
                }
            }
        }

        valueArray = getNotNull(valueArray);

        if (valueArray == null) {
            return null;
        }

        return getValue(session, valueArray);
    }

    Object updateAggregatingValue(Session session,
                                  Object currValue) throws HsqlException {

        Object[] valueArray = (Object[]) currValue;

        if (valueArray == null) {
            valueArray = new Object[iArgCount];
        }

        for (int i = 0; i < iArgCount; i++) {
            Expression e = eArg[i];

            if (eArg[i] != null) {
                valueArray[i] = e.updateAggregatingValue(session,
                        valueArray[i]);
            }
        }

        return valueArray;
    }

    /**
     * Returns the number of parameters that must be supplied to evaluate
     * this Function object from SQL.  <p>
     *
     * This value may be different than the number of parameters of the
     * underlying Java method.  This is because HSQLDB automatically detects
     * if the first parameter is of type java.sql.Connection, and supplies a
     * live Connection object constructed from the evaluating session context
     * if so.
     */
    int getArgCount() {
        return iSqlArgCount;
    }

    /**
     * Remnoves the Table filters from Expression parameters to this Function.
     *
     * @throws HsqlException if there is a problem resolving a parameter
     * against the specified TableFilter
     */
/*
    void removeFilters() throws HsqlException {

        Expression e;

        for (int i = iSqlArgStart; i < iArgCount; i++) {
            e = eArg[i];

            if (e != null) {
                e.removeFilters();
            }
        }
    }
*/
    void replaceAliases(Expression[] columns,
                        int length) throws HsqlException {

        Expression e;

        for (int i = iSqlArgStart; i < iArgCount; i++) {
            e = eArg[i];

            if (e != null) {
                if (e.exprType == Expression.COLUMN) {
                    eArg[i] = e.getExpressionForAlias(columns, length);
                } else {
                    e.replaceAliases(columns, length);
                }
            }
        }
    }

    /**
     * Checks the Expresion parameters to this Function object against the
     * set of TableFilter.
     */
    void checkTables(HsqlArrayList fa) throws HsqlException {

        Expression e;

        for (int i = iSqlArgStart; i < iArgCount; i++) {
            e = eArg[i];

            if (e != null) {
                e.checkTables(fa);
            }
        }
    }

    /**
     * Resolves the Expression parameters to this Function object against the
     * specified TableFilter.
     */
    void resolveTables(TableFilter f) throws HsqlException {

        Expression e;

        for (int i = iSqlArgStart; i < iArgCount; i++) {
            e = eArg[i];

            if (e != null) {
                e.resolveTables(f);
            }
        }
    }

    /**
     * Resolves the type of this expression and performs certain
     * transformations and optimisations of the expression tree.
     */
    void resolveType(Session session) throws HsqlException {

        Expression e;

        for (int i = iSqlArgStart; i < iArgCount; i++) {
            e = eArg[i];

            if (e != null) {
                if (e.isParam()) {
                    e.setDataType(iArgType[i]);

                    e.nullability    = getArgNullability(i);
                    e.valueClassName = getArgClass(i).getName();
                } else {
                    e.resolveTypes(session);
                }
            }
        }
    }

    /**
     * Checks each of this object's arguments for resolution, throwing an
     * HsqlException if any arguments have not yet been resolved. <p>
     * The check boolean argument is passed on to further check calls.<p>
     */
    boolean checkResolved(boolean check) throws HsqlException {

        boolean result = true;

        for (int i = iSqlArgStart; i < iArgCount; i++) {
            if (eArg[i] != null) {
                result = result && eArg[i].checkResolved(check);
            }
        }

        return result;
    }

    /**
     * Returns the type of the argument at the specified
     * offset in this Function object's paramter list. <p>
     */
    int getArgType(int i) {
        return iArgType[i];
    }

    /**
     * Returns the type of this Function
     * object's return type. <p>
     */
    int getReturnType() {
        return iReturnType;
    }

    /**
     * Binds the specified expression to the specified position in this
     * Function object's parameter list. <p>
     */
    void setArgument(int i, Expression e) {

        if (bConnection) {
            i++;
        }

        eArg[i]      = e;
        hasAggregate = hasAggregate || (e != null && e.isAggregate());
    }

    /**
     * Returns a DDL representation of this object. <p>
     */
    String getDLL() throws HsqlException {

        StringBuffer sb = new StringBuffer();

        // get the name as used by the CHECK statement
        String ddlName = name;

        if (isSimple) {
            return name;
        } else if (Token.T_TRIM.equals(name)) {

            // special case for TRIM
            sb.append(name).append('(');

            boolean leading  = eArg[2].testCondition(null);
            boolean trailing = eArg[3].testCondition(null);

            if (leading && trailing) {
                sb.append(Token.T_BOTH);
            } else {
                sb.append(leading ? Token.T_LEADING
                                  : Token.T_TRAILING);
            }

            // to do change to string
            sb.append(' ');

            String charval = (String) eArg[1].getValue(null);

            sb.append(Column.createSQLString(charval)).append(' ');
            sb.append(Token.T_FROM).append(' ');
            sb.append(eArg[0].getDDL()).append(')');

            return sb.toString();
        }

        if (sFunction.equals(name)) {
            ddlName = StringConverter.toQuotedString(name, '"', true);
        }

        sb.append(ddlName).append('(');

        for (int i = iSqlArgStart; i < eArg.length; i++) {
            sb.append(eArg[i].getDDL());

            if (i < eArg.length - 1) {
                sb.append(',');
            }
        }

        sb.append(')');

        return sb.toString();
    }

    /**
     * Returns a String representation of this object. <p>
     */
    public String describe(Session session) {

        StringBuffer sb = new StringBuffer();

        sb.append(super.toString()).append("=[\n");
        sb.append(sFunction).append("(");

        for (int i = iSqlArgStart; i < eArg.length; i++) {
            sb.append("[").append(eArg[i].describe(session)).append("]");
        }

        sb.append(") returns ").append(Types.getTypeString(getReturnType()));
        sb.append("]\n");

        return sb.toString();
    }

    /**
     * Returns the Java Class of the object returned by getValue(). <p>
     */
    String getReturnClassName() {
        return returnClassName;
    }

    /**
     * Returns the Java Class of the i'th argument. <p>
     */
    Class getArgClass(int i) {
        return aArgClasses[i];
    }

    /**
     * Returns the SQL nullability code of the i'th argument. <p>
     */
    int getArgNullability(int i) {
        return bArgNullable[i] ? Expression.NULLABLE
                               : Expression.NO_NULLS;
    }

    Method getMethod() {
        return mMethod;
    }
}
