/* Copyright (c) 2001-2024, 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.ResultSet;

import org.hsqldb.HsqlNameManager.HsqlName;
import org.hsqldb.HsqlNameManager.SimpleName;
import org.hsqldb.ParserBase.Recorder;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.jdbc.JDBCResultSet;
import org.hsqldb.lib.HsqlArrayList;
import org.hsqldb.lib.OrderedHashMap;
import org.hsqldb.lib.OrderedHashSet;
import org.hsqldb.map.BitMap;
import org.hsqldb.map.ValuePool;
import org.hsqldb.persist.HsqlDatabaseProperties;
import org.hsqldb.result.Result;
import org.hsqldb.rights.Grantee;
import org.hsqldb.types.RowType;
import org.hsqldb.types.Type;
import org.hsqldb.types.Types;

/**
 * Implementation of specific routine
 *
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 *
 * @version 2.7.3
 * @since 1.9.0
 */
public class Routine implements SchemaObject, RangeGroup, Cloneable {

    public static final int NO_SQL       = 1;
    public static final int CONTAINS_SQL = 2;
    public static final int READS_SQL    = 3;
    public static final int MODIFIES_SQL = 4;

    //
    public static final int LANGUAGE_JAVA = 1;
    public static final int LANGUAGE_SQL  = 2;

    //
    public static final int PARAM_STYLE_JAVA = 1;
    public static final int PARAM_STYLE_SQL  = 2;

    //
    static final Routine[] emptyArray = new Routine[]{};

    //
    RoutineSchema    routineSchema;
    private HsqlName name;
    private HsqlName specificName;
    Type[]           parameterTypes;
    int              typeGroups;
    Type             returnType;
    Table            returnTable;
    final int        routineType;
    int              language   = LANGUAGE_SQL;
    int              dataImpact = CONTAINS_SQL;
    int              parameterStyle;
    boolean          isDeterministic;
    boolean          isNullInputOutput;
    boolean          isNewSavepointLevel = true;
    int              maxDynamicResults   = 0;
    boolean          isRecursive;
    boolean          returnsTable;
    Statement        statement;

    //
    boolean isAggregate;
    boolean isIndex;
    boolean isSearch;

    //
    private String  methodName;
    Method          javaMethod;
    boolean         javaMethodWithConnection;
    private boolean isLibraryRoutine;

    //
    OrderedHashMap<String, ColumnSchema> parameterList = new OrderedHashMap<>();
    RangeVariable[]                      ranges = RangeVariable.emptyArray;

    //
    int variableCount;
    int cursorCount;

    //
    OrderedHashSet<HsqlName> references;

    //
    Table triggerTable;
    int   triggerType;
    int   triggerOperation;

    public Routine(int type) {

        routineType = type;
        returnType  = Type.SQL_ALL_TYPES;
        ranges = new RangeVariable[]{ new RangeVariable(
            parameterList,
            null,
            false,
            RangeVariable.PARAMETER_RANGE) };
    }

    public Routine(
            Table table,
            RangeVariable[] ranges,
            int impact,
            int triggerType,
            int operationType) {

        routineType           = SchemaObject.TRIGGER;
        returnType            = Type.SQL_ALL_TYPES;
        dataImpact            = impact;
        this.ranges           = ranges;
        this.triggerTable     = table;
        this.triggerType      = triggerType;
        this.triggerOperation = operationType;
    }

    public int getType() {
        return routineType;
    }

    public HsqlName getName() {
        return name;
    }

    public HsqlName getSchemaName() {

        if (routineType == SchemaObject.TRIGGER) {
            return triggerTable.getSchemaName();
        }

        return name.schema;
    }

    public HsqlName getCatalogName() {
        return name.schema.schema;
    }

    public Grantee getOwner() {
        return name.schema.owner;
    }

    public OrderedHashSet<HsqlName> getReferences() {
        return references;
    }

    public void compile(Session session, SchemaObject parentObject) {

        ParserRoutine p = new ParserRoutine(
            session,
            new Scanner(session, statement.getSQL()));

        session.sessionContext.pushRoutineTables();

        try {
            p.read();

            Recorder recorder = p.startRecording();
            Statement newStatement = p.compileSQLProcedureStatementOrNull(
                this,
                null);
            String   sql      = recorder.getSQL();

            newStatement.setSQL(sql);
            setProcedure(newStatement);
            resolve(session);
        } finally {
            session.sessionContext.popRoutineTables();
        }
    }

    public String getSQL() {
        return getDefinitionSQL(true);
    }

    public String getSQLAlter() {

        StringBuilder sb = new StringBuilder(64);

        sb.append(Tokens.T_ALTER)
          .append(' ')
          .append(Tokens.T_SPECIFIC)
          .append(' ')
          .append(Tokens.T_ROUTINE)
          .append(' ')
          .append(specificName.getSchemaQualifiedStatementName())
          .append(' ')
          .append(Tokens.T_BODY)
          .append(' ')
          .append(statement.getSQL());

        return sb.toString();
    }

    public String getSQLDeclaration() {
        return getDefinitionSQL(false);
    }

    private String getDefinitionSQL(boolean withBody) {

        StringBuilder sb = new StringBuilder();

        sb.append(Tokens.T_CREATE).append(' ');

        if (isAggregate) {
            sb.append(Tokens.T_AGGREGATE).append(' ');
        }

        if (routineType == SchemaObject.PROCEDURE) {
            sb.append(Tokens.T_PROCEDURE);
        } else {
            sb.append(Tokens.T_FUNCTION);
        }

        sb.append(' ')
          .append(name.getSchemaQualifiedStatementName())
          .append('(');

        for (int i = 0; i < parameterList.size(); i++) {
            if (i > 0) {
                sb.append(',');
            }

            ColumnSchema param = parameterList.get(i);

            // in - out
            sb.append(param.getSQL());
        }

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

        if (routineType == SchemaObject.FUNCTION) {
            sb.append(Tokens.T_RETURNS).append(' ');

            if (returnsTable) {
                sb.append(Tokens.T_TABLE)
                  .append(returnTable.getColumnListWithTypeSQL());
            } else {
                sb.append(returnType.getTypeDefinition());
            }

            sb.append(' ');
        }

        // SPECIFIC
        if (specificName != null) {
            sb.append(Tokens.T_SPECIFIC)
              .append(' ')
              .append(specificName.getStatementName())
              .append(' ');
        }

        //
        sb.append(Tokens.T_LANGUAGE).append(' ');

        if (language == LANGUAGE_JAVA) {
            sb.append(Tokens.T_JAVA);
        } else {
            sb.append(Tokens.T_SQL);
        }

        sb.append(' ');

        //
        if (!isDeterministic) {
            sb.append(Tokens.T_NOT).append(' ');
        }

        sb.append(Tokens.T_DETERMINISTIC).append(' ');

        //
        sb.append(getDataImpactString()).append(' ');

        //
        if (routineType == SchemaObject.FUNCTION) {
            if (isNullInputOutput) {
                sb.append(Tokens.T_RETURNS).append(' ').append(Tokens.T_NULL);
            } else {
                sb.append(Tokens.T_CALLED);
            }

            sb.append(' ')
              .append(Tokens.T_ON)
              .append(' ')
              .append(Tokens.T_NULL)
              .append(' ')
              .append(Tokens.T_INPUT)
              .append(' ');
        } else {
            if (isNewSavepointLevel) {
                sb.append(Tokens.T_NEW);
            } else {
                sb.append(Tokens.T_OLD);
            }

            sb.append(' ')
              .append(Tokens.T_SAVEPOINT)
              .append(' ')
              .append(Tokens.T_LEVEL)
              .append(' ');

            if (maxDynamicResults != 0) {
                sb.append(' ')
                  .append(Tokens.T_DYNAMIC)
                  .append(' ')
                  .append(Tokens.T_RESULT)
                  .append(' ')
                  .append(Tokens.T_SETS)
                  .append(' ')
                  .append(maxDynamicResults)
                  .append(' ');
            }
        }

        if (language == LANGUAGE_JAVA) {
            sb.append(Tokens.T_EXTERNAL)
              .append(' ')
              .append(Tokens.T_NAME)
              .append(' ')
              .append('\'')
              .append(methodName)
              .append('\'');
        } else {
            if (withBody) {
                sb.append(statement.getSQL());
            } else {
                sb.append(Tokens.T_SIGNAL)
                  .append(' ')
                  .append(Tokens.T_SQLSTATE)
                  .append(' ')
                  .append('\'')
                  .append("45000")
                  .append('\'');
            }
        }

        return sb.toString();
    }

    public String getSQLBodyDefinition() {

        StringBuilder sb = new StringBuilder();

        if (language == LANGUAGE_JAVA) {
            sb.append(Tokens.T_EXTERNAL)
              .append(' ')
              .append(Tokens.T_NAME)
              .append(' ')
              .append('\'')
              .append(methodName)
              .append('\'');
        } else {
            sb.append(statement.getSQL());
        }

        return sb.toString();
    }

    public String getExternalName() {

        if (language == LANGUAGE_JAVA) {
            return methodName;
        } else {
            return null;
        }
    }

    public long getChangeTimestamp() {
        return 0;
    }

    public void addParameter(ColumnSchema param) {

        HsqlName name      = param.getName();
        String   paramName = name == null
                             ? HsqlNameManager.getAutoNoNameColumnString(
                                 parameterList.size())
                             : name.name;
        boolean  result    = parameterList.add(paramName, param);

        if (!result) {
            throw Error.error(ErrorCode.X_42614);
        }
    }

    public void setLanguage(int lang) {
        language = lang;
    }

    public int getLanguage() {
        return language;
    }

    boolean isPSM() {
        return language == LANGUAGE_SQL;
    }

    public void setDataImpact(int impact) {
        dataImpact = impact;
    }

    public int getDataImpact() {
        return dataImpact;
    }

    public String getDataImpactString() {

        StringBuilder sb = new StringBuilder();

        switch (dataImpact) {

            case NO_SQL :
                sb.append(Tokens.T_NO).append(' ').append(Tokens.T_SQL);
                break;

            case CONTAINS_SQL :
                sb.append(Tokens.T_CONTAINS).append(' ').append(Tokens.T_SQL);
                break;

            case READS_SQL :
                sb.append(Tokens.T_READS)
                  .append(' ')
                  .append(Tokens.T_SQL)
                  .append(' ')
                  .append(Tokens.T_DATA);
                break;

            case MODIFIES_SQL :
                sb.append(Tokens.T_MODIFIES)
                  .append(' ')
                  .append(Tokens.T_SQL)
                  .append(' ')
                  .append(Tokens.T_DATA);
                break;
        }

        return sb.toString();
    }

    public void setReturnType(Type type) {
        returnType = type;
    }

    public Type getReturnType() {
        return returnType;
    }

    public Table getTable() {
        return returnTable;
    }

    public void setProcedure(Statement statement) {
        this.statement = statement;
    }

    public Statement getProcedure() {
        return statement;
    }

    public void setSpecificName(HsqlName name) {
        specificName = name;
    }

    public int getMaxDynamicResults() {
        return maxDynamicResults;
    }

    public void setName(HsqlName name) {
        this.name = name;
    }

    public HsqlName getSpecificName() {
        return specificName;
    }

    public void setDeterministic(boolean value) {
        isDeterministic = value;
    }

    public boolean isDeterministic() {
        return isDeterministic;
    }

    public void setNullInputOutput(boolean value) {
        isNullInputOutput = value;
    }

    public boolean isNullInputOutput() {
        return isNullInputOutput;
    }

    public void setNewSavepointLevel(boolean value) {
        isNewSavepointLevel = value;
    }

    public void setMaxDynamicResults(int value) {
        maxDynamicResults = value;
    }

    public void setParameterStyle(int style) {
        parameterStyle = style;
    }

    public void setMethodURL(String url) {
        this.methodName = url;
    }

    public Method getMethod() {
        return javaMethod;
    }

    public void setMethod(Method method) {
        this.javaMethod = method;
    }

    public void setReturnTable(TableDerived table) {

        this.returnTable  = table;
        this.returnsTable = true;

        SimpleName[] names = new SimpleName[table.getColumnCount()];
        Type[]       types = table.getColumnTypes();

        returnType = new RowType(types);
    }

    public boolean returnsTable() {
        return returnsTable;
    }

    public void setAggregate(boolean isAggregate) {
        this.isAggregate = isAggregate;
    }

    public boolean isAggregate() {
        return isAggregate;
    }

    public void resolve(Session session) {

        setLanguage(language);

        if (language == Routine.LANGUAGE_SQL) {
            if (dataImpact == NO_SQL) {
                throw Error.error(ErrorCode.X_42604, "CONTAINS SQL");
            }

            if (parameterStyle == PARAM_STYLE_JAVA) {
                throw Error.error(ErrorCode.X_42604, "PARAMETER STYLE");
            }
        }

        if (language == Routine.LANGUAGE_SQL) {
            if (parameterStyle != 0 && parameterStyle != PARAM_STYLE_SQL) {
                throw Error.error(ErrorCode.X_42604, "PARAMETER STYLE");
            }
        }

        parameterTypes = new Type[parameterList.size()];
        typeGroups     = 0;

        for (int i = 0; i < parameterTypes.length; i++) {
            ColumnSchema param = parameterList.get(i);

            parameterTypes[i] = param.dataType;

            if (i < 4) {
                typeGroups = BitMap.setByte(
                    typeGroups,
                    (byte) param.dataType.typeComparisonGroup,
                    i * 8);
            }
        }

        if (isAggregate) {
            if (parameterTypes.length != 4) {
                throw Error.error(ErrorCode.X_42610);
            }

            boolean check = parameterTypes[1].typeCode == Types.BOOLEAN;

            //
            ColumnSchema param = parameterList.get(0);

            check &= param.getParameterMode()
                     == SchemaObject.ParameterModes.PARAM_IN;
            param = parameterList.get(1);
            check &= param.getParameterMode()
                     == SchemaObject.ParameterModes.PARAM_IN;
            param = parameterList.get(2);
            check &= param.getParameterMode()
                     == SchemaObject.ParameterModes.PARAM_INOUT;
            param = parameterList.get(3);
            check &= param.getParameterMode()
                     == SchemaObject.ParameterModes.PARAM_INOUT;

            if (!check) {
                throw Error.error(ErrorCode.X_42610);
            }
        }

        resolveReferences(session);
    }

    void resolveReferences(Session session) {

        if (statement != null) {
            statement.resolve(session);
            checkSQLData(session);
        }

        if (methodName != null && javaMethod == null) {
            boolean[] hasConnection = new boolean[1];

            javaMethod = getMethod(
                methodName,
                this,
                hasConnection,
                returnsTable);

            if (javaMethod == null) {
                throw Error.error(ErrorCode.X_46103);
            }

            javaMethodWithConnection = hasConnection[0];

            String className = javaMethod.getDeclaringClass().getName();

            if (className.equals("java.lang.Math")) {
                isLibraryRoutine = true;
            }
        }

        setReferences();
    }

    private void setReferences() {

        OrderedHashSet<HsqlName> set = new OrderedHashSet<>();

        for (int i = 0; i < parameterTypes.length; i++) {
            ColumnSchema             param = parameterList.get(i);
            OrderedHashSet<HsqlName> refs  = param.getReferences();

            if (refs != null) {
                set.addAll(refs);
            }
        }

        if (statement != null) {
            set.addAll(statement.getReferences());
        }

        isRecursive = false;

        if (set.contains(getSpecificName())) {
            set.remove(getSpecificName());

            isRecursive = true;
        }

        references = set;
    }

    void checkSQLData(Session session) {

        OrderedHashSet<HsqlName> set = statement.getReferences();

        for (int i = 0; i < set.size(); i++) {
            HsqlName name = set.get(i);

            if (name.type == SchemaObject.SPECIFIC_ROUTINE) {
                Routine routine =
                    (Routine) session.database.schemaManager.findSchemaObject(
                        name);

                if (routine.dataImpact == Routine.READS_SQL) {
                    if (dataImpact == Routine.CONTAINS_SQL) {
                        throw Error.error(
                            ErrorCode.X_42608,
                            Tokens.T_READS + ' ' + Tokens.T_SQL + ' '
                            + Tokens.T_DATA);
                    }
                } else if (routine.dataImpact == Routine.MODIFIES_SQL) {
                    if (dataImpact == Routine.CONTAINS_SQL
                            || dataImpact == Routine.READS_SQL) {
                        throw Error.error(
                            ErrorCode.X_42608,
                            Tokens.T_MODIFIES + ' ' + Tokens.T_SQL + ' '
                            + Tokens.T_DATA);
                    }
                }
            }
        }

        if (dataImpact == Routine.CONTAINS_SQL
                || dataImpact == Routine.READS_SQL) {
            HsqlName[] names = statement.getTableNamesForWrite();

            for (int i = 0; i < names.length; i++) {
                if (names[i].schema != SqlInvariants.SYSTEM_SCHEMA_HSQLNAME) {
                    throw Error.error(
                        ErrorCode.X_42608,
                        Tokens.T_MODIFIES + ' ' + Tokens.T_SQL + ' '
                        + Tokens.T_DATA);
                }
            }
        }

        if (dataImpact == Routine.CONTAINS_SQL) {
            HsqlName[] names = statement.getTableNamesForRead();

            for (int i = 0; i < names.length; i++) {
                if (names[i].schema != SqlInvariants.SYSTEM_SCHEMA_HSQLNAME) {
                    throw Error.error(
                        ErrorCode.X_42608,
                        Tokens.T_READS + ' ' + Tokens.T_SQL + ' '
                        + Tokens.T_DATA);
                }
            }
        }
    }

    public boolean isTrigger() {
        return routineType == SchemaObject.TRIGGER;
    }

    public boolean isProcedure() {
        return routineType == SchemaObject.PROCEDURE;
    }

    public boolean isFunction() {
        return routineType == SchemaObject.FUNCTION;
    }

    public ColumnSchema getParameter(int i) {
        return parameterList.get(i);
    }

    Type[] getParameterTypes() {
        return parameterTypes;
    }

    int getParameterSignature() {
        return typeGroups;
    }

    public int getParameterCount() {
        return parameterTypes.length;
    }

    public int getParameterCount(int type) {

        int count = 0;

        for (int i = 0; i < parameterList.size(); i++) {
            ColumnSchema col = parameterList.get(i);

            if (col.getParameterMode() == type) {
                count++;
            }
        }

        return count;
    }

    public int getParameterIndex(String name) {
        return parameterList.getIndex(name);
    }

    public RangeVariable[] getRangeVariables() {
        return ranges;
    }

    public void setCorrelated() {

        //
    }

    public boolean isVariable() {
        return true;
    }

    public int getVariableCount() {
        return variableCount;
    }

    public int getCursorCount() {
        return cursorCount;
    }

    public boolean isLibraryRoutine() {
        return isLibraryRoutine;
    }

    public HsqlName[] getTableNamesForRead() {

        if (statement == null) {
            return HsqlName.emptyArray;
        }

        return statement.getTableNamesForRead();
    }

    public HsqlName[] getTableNamesForWrite() {

        if (statement == null) {
            return HsqlName.emptyArray;
        }

        return statement.getTableNamesForWrite();
    }

    public void resetAlteredRoutineSettings() {

        if (isPSM()) {
            methodName               = null;
            javaMethod               = null;
            javaMethodWithConnection = false;
            parameterStyle           = PARAM_STYLE_SQL;

            if (dataImpact == NO_SQL) {
                dataImpact = CONTAINS_SQL;
            }
        } else {
            statement     = null;
            references    = null;
            variableCount = 0;
            cursorCount   = 0;
            ranges        = RangeVariable.emptyArray;
        }
    }

    public void setAsAlteredRoutine(Routine routine) {

        language          = routine.language;
        dataImpact        = routine.dataImpact;
        parameterStyle    = routine.parameterStyle;
        isDeterministic   = routine.isDeterministic;
        isNullInputOutput = routine.isNullInputOutput;
        maxDynamicResults = routine.maxDynamicResults;
        isRecursive       = routine.isRecursive;
        javaMethod        = routine.javaMethod;

        //
        isRecursive              = routine.isRecursive;
        javaMethodWithConnection = routine.javaMethodWithConnection;
        methodName               = routine.methodName;
        statement                = routine.statement;
        references               = routine.references;
        variableCount            = routine.variableCount;
        cursorCount              = routine.cursorCount;
        ranges                   = routine.ranges;
    }

    Object[] convertArgsToJava(Session session, Object[] callArguments) {

        int      extraArg = javaMethodWithConnection
                            ? 1
                            : 0;
        Object[] data     = new Object[javaMethod.getParameterTypes().length];
        Type[]   types    = getParameterTypes();
        int      i        = 0;

        for (; i < types.length; i++) {
            Object       value = callArguments[i];
            ColumnSchema param = getParameter(i);

            if (param.parameterMode == SchemaObject.ParameterModes.PARAM_IN) {
                data[i + extraArg] = types[i].convertSQLToJava(session, value);
            } else {
                Object   jdbcValue = types[i].convertSQLToJava(session, value);
                Class<?> cl        = types[i].getJDBCClass();
                Object   array     = java.lang.reflect.Array.newInstance(cl, 1);

                java.lang.reflect.Array.set(array, 0, jdbcValue);

                data[i + extraArg] = array;
            }
        }

        for (; i + extraArg < data.length; i++) {
            data[i + extraArg] = new java.sql.ResultSet[1];
        }

        return data;
    }

    void convertArgsToSQL(
            Session session,
            Object[] callArguments,
            Object[] data) {

        int    extraArg = javaMethodWithConnection
                          ? 1
                          : 0;
        Type[] types    = getParameterTypes();
        int    i        = 0;

        for (; i < types.length; i++) {
            Object       value = data[i + extraArg];
            ColumnSchema param = getParameter(i);

            if (param.parameterMode != SchemaObject.ParameterModes.PARAM_IN) {
                value = java.lang.reflect.Array.get(value, 0);
            }

            callArguments[i] = types[i].convertJavaToSQL(session, value);
        }

        Result head = null;

        for (; i + extraArg < data.length; i++) {
            ResultSet rs = ((ResultSet[]) data[i + extraArg])[0];

            if (rs != null) {
                if (rs instanceof JDBCResultSet) {
                    Result r = ((JDBCResultSet) rs).result;

                    if (head == null) {
                        callArguments[i] = r;
                        head             = r;
                    } else {
                        head.addChainedResult(r);
                    }
                } else {
                    throw Error.error(
                        ErrorCode.X_46000,
                        "ResultSet not native");
                }
            }
        }
    }

    public Result invokeJavaMethodDirect(Object[] data) {

        Result result;

        try {
            Object returnValue = javaMethod.invoke(null, data);

            returnValue = returnType.convertJavaToSQL(null, returnValue);
            result      = Result.newPSMResult(returnValue);
        } catch (Throwable t) {
            result = Result.newErrorResult(
                Error.error(t, ErrorCode.X_46000, getName().name));
        }

        return result;
    }

    Result invokeJavaMethod(Session session, Object[] data) {

        Result   result;
        HsqlName oldSessionSchema = session.getCurrentSchemaHsqlName();

        try {
            if (dataImpact == Routine.NO_SQL) {
                session.sessionContext.isReadOnly = Boolean.TRUE;

                session.setNoSQL();
            } else if (dataImpact == Routine.CONTAINS_SQL) {
                session.sessionContext.isReadOnly = Boolean.TRUE;
            } else if (dataImpact == Routine.READS_SQL) {
                session.sessionContext.isReadOnly = Boolean.TRUE;
            }

            session.setCurrentSchemaHsqlName(getSchemaName());

            Object returnValue = javaMethod.invoke(null, data);

            if (returnsTable()) {
                if (returnValue instanceof JDBCResultSet) {
                    result = ((JDBCResultSet) returnValue).result;
                } else {

                    // convert ResultSet to table
                    throw Error.runtimeError(
                        ErrorCode.U_S0500,
                        "FunctionSQLInvoked");
                }
            } else {
                returnValue = returnType.convertJavaToSQL(session, returnValue);
                result      = Result.newPSMResult(returnValue);
            }
        } catch (InvocationTargetException e) {
            result = Result.newErrorResult(
                Error.error(e, ErrorCode.X_46000, getName().name));
        } catch (IllegalAccessException e) {
            result = Result.newErrorResult(
                Error.error(e, ErrorCode.X_46000, getName().name));
        } catch (Throwable e) {
            result = Result.newErrorResult(
                Error.error(e, ErrorCode.X_46000, getName().name));
        }

        session.setCurrentSchemaHsqlName(oldSessionSchema);

        return result;
    }

    public Result invoke(
            Session session,
            Object[] data,
            Object[] aggregateData,
            boolean push) {

        Result result;

        if (push) {
            session.sessionContext.pushRoutineInvocation();
        }

        if (isPSM()) {
            try {
                session.sessionContext.routineArguments = data;
                session.sessionContext.routineVariables =
                    ValuePool.emptyObjectArray;

                if (variableCount > 0) {
                    session.sessionContext.routineVariables =
                        new Object[variableCount];
                }

                session.sessionContext.routineCursors = Result.emptyArray;

                if (cursorCount > 0) {
                    session.sessionContext.routineCursors =
                        new Result[cursorCount];
                }

                result = statement.execute(session);

                if (aggregateData != null) {
                    System.arraycopy(
                        data,
                        1,
                        aggregateData,
                        0,
                        aggregateData.length);
                }
            } catch (Throwable e) {
                result = Result.newErrorResult(e);
            }
        } else {
            if (isAggregate) {
                data = convertArgsToJava(session, data);
            }

            result = invokeJavaMethod(session, data);

            if (isAggregate) {
                Object[] callResult = new Object[data.length];

                convertArgsToSQL(session, callResult, data);
                System.arraycopy(
                    callResult,
                    1,
                    aggregateData,
                    0,
                    aggregateData.length);
            }
        }

        if (push) {
            session.sessionContext.popRoutineInvocation();
        }

        return result;
    }

    public Routine duplicate() {

        try {
            return (Routine) super.clone();
        } catch (CloneNotSupportedException e) {
            throw Error.runtimeError(ErrorCode.U_S0500, "Type");
        }
    }

    static Method getMethod(
            String name,
            Routine routine,
            boolean[] hasConnection,
            boolean returnsTable) {

        int i = name.indexOf(':');

        if (i != -1) {
            if (!name.substring(0, i).equals(SqlInvariants.CLASSPATH_NAME)) {
                throw Error.error(ErrorCode.X_46102, name);
            }

            name = name.substring(i + 1);
        }

        Method[] methods       = getMethods(name);
        int      firstMismatch = -1;

        for (i = 0; i < methods.length; i++) {
            int offset = 0;

            hasConnection[0] = false;

            Method     method = methods[i];
            Class<?>[] params = method.getParameterTypes();
            int        matchedParamCount;

            if (params.length > 0
                    && params[0].equals(java.sql.Connection.class)) {
                offset           = 1;
                hasConnection[0] = true;
            }

            matchedParamCount = params.length - offset;

            if (routine.isProcedure()) {
                for (int j = offset; j < params.length; j++) {
                    if (params[j].isArray()
                            && java.sql.ResultSet.class.isAssignableFrom(
                                params[j].getComponentType())) {
                        matchedParamCount = j - offset;
                        break;
                    }
                }
            }

            if (matchedParamCount != routine.parameterTypes.length) {
                continue;
            }

            if (returnsTable) {
                if (!java.sql.ResultSet.class.isAssignableFrom(
                        method.getReturnType())) {
                    continue;
                }
            } else {
                Type methodReturnType = Types.getParameterSQLType(
                    method.getReturnType());

                if (methodReturnType == null) {
                    continue;
                }

                if (!routine.returnType.canBeAssignedFrom(methodReturnType)) {
                    continue;
                }

                if (!methodReturnType.isLobType()
                        && (methodReturnType.isBinaryType()
                            || methodReturnType.isCharacterType())) {

                    //
                } else {
                    int routineRetType = routine.returnType.typeCode;

                    if (routineRetType == Types.SQL_NUMERIC) {
                        routineRetType = Types.SQL_DECIMAL;
                    }

                    if (methodReturnType.typeCode != routineRetType) {
                        continue;
                    }
                }
            }

            for (int j = 0; j < routine.parameterTypes.length; j++) {
                boolean  isInOut = false;
                Class<?> param   = params[j + offset];

                if (param.isArray()) {
                    if (!byte[].class.equals(param)) {
                        param = param.getComponentType();

                        if (param.isPrimitive()) {
                            method = null;
                            break;
                        }

                        isInOut = true;
                    }
                }

                Type methodParamType = Types.getParameterSQLType(param);

                if (methodParamType == null) {
                    method = null;
                    break;
                }

                boolean result = routine.parameterTypes[j].typeComparisonGroup
                                 == methodParamType.typeComparisonGroup;

                // exact type for number
                if (result && routine.parameterTypes[j].isNumberType()) {
                    int routineParamType = routine.parameterTypes[j].typeCode;

                    if (routineParamType == Types.SQL_NUMERIC) {
                        routineParamType = Types.SQL_DECIMAL;
                    }

                    result = routineParamType == methodParamType.typeCode;
                }

                if (isInOut
                        && routine.getParameter(j).parameterMode
                           == SchemaObject.ParameterModes.PARAM_IN) {
                    result = false;
                }

                if (!result) {
                    method = null;

                    if (j + offset > firstMismatch) {
                        firstMismatch = j + offset;
                    }

                    break;
                }
            }

            if (method != null) {
                for (int j = 0; j < routine.parameterTypes.length; j++) {
                    routine.getParameter(j)
                           .setNullable(!params[j + offset].isPrimitive());
                }

                return method;
            }
        }

        if (firstMismatch >= 0) {
            ColumnSchema param = routine.getParameter(firstMismatch);

            throw Error.error(ErrorCode.X_46511, param.getNameString());
        }

        return null;
    }

    static Method[] getMethods(String name) {

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

        if (i == -1) {
            throw Error.error(ErrorCode.X_42501, name);
        }

        String   className  = name.substring(0, i);
        String   methodname = name.substring(i + 1);
        Class<?> cl;
        Method[] methods = null;

        if (!HsqlDatabaseProperties.supportsJavaMethod(name)) {
            throw Error.error(ErrorCode.X_42501, className);
        }

        try {
            cl = Class.forName(
                className,
                true,
                Thread.currentThread().getContextClassLoader());
        } catch (Throwable t1) {
            try {
                cl = Class.forName(className);
            } catch (Throwable t) {
                throw Error.error(
                    t,
                    ErrorCode.X_42501,
                    ErrorCode.M_Message_Pair,
                    new String[]{ t.toString(), className });
            }
        }

        try {
            methods = cl.getMethods();
        } catch (Throwable t) {
            throw Error.error(
                t,
                ErrorCode.X_42501,
                ErrorCode.M_Message_Pair,
                new String[]{ t.toString(), className });
        }

        HsqlArrayList<Method> list = new HsqlArrayList<>();

        for (i = 0; i < methods.length; i++) {
            int    offset    = 0;
            int    endIndex  = Integer.MAX_VALUE;
            Method method    = methods[i];
            int    modifiers = method.getModifiers();

            if (!method.getName().equals(methodname)
                    || !Modifier.isStatic(modifiers)
                    || !Modifier.isPublic(modifiers)) {
                continue;
            }

            Class<?>[] params = methods[i].getParameterTypes();

            if (params.length > 0
                    && params[0].equals(java.sql.Connection.class)) {
                offset = 1;
            }

            for (int j = offset; j < params.length; j++) {
                Class<?> param = params[j];

                if (param.isArray()) {
                    if (!byte[].class.equals(param)) {
                        param = param.getComponentType();

                        if (param.isPrimitive()) {
                            method = null;
                            break;
                        }

                        if (java.sql.ResultSet.class.isAssignableFrom(param)) {
                            if (endIndex > j) {
                                endIndex = j;
                            }
                        }
                    }

                    if (j >= endIndex) {
                        if (java.sql.ResultSet.class.isAssignableFrom(param)) {
                            continue;
                        } else {
                            method = null;
                            break;
                        }
                    }
                } else {
                    if (j > endIndex) {
                        method = null;
                        break;
                    }
                }

                Type methodParamType = Types.getParameterSQLType(param);

                if (methodParamType == null) {
                    method = null;
                    break;
                }
            }

            if (method == null) {
                continue;
            }

            if (java.sql.ResultSet.class.isAssignableFrom(
                    method.getReturnType())) {
                list.add(methods[i]);
            } else {
                Type methodReturnType = Types.getParameterSQLType(
                    method.getReturnType());

                if (methodReturnType != null) {
                    list.add(methods[i]);
                }
            }
        }

        methods = new Method[list.size()];

        list.toArray(methods);

        return methods;
    }

    public static Routine[] newRoutines(Session session, Method[] methods) {

        Routine[] routines = new Routine[methods.length];

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

            routines[i] = newRoutine(session, method);
        }

        return routines;
    }

    /**
     * Returns a new function Routine object based solely on a Java Method object.
     */
    public static Routine newRoutine(Session session, Method method) {

        Routine    routine   = new Routine(SchemaObject.FUNCTION);
        Class<?>[] params    = method.getParameterTypes();
        String     className = method.getDeclaringClass().getName();
        String     name;
        int        offset;

        if (params.length > 0 && params[0].equals(java.sql.Connection.class)) {
            offset = 1;
        } else {
            offset = 0;
        }

        StringBuilder sb = new StringBuilder(64);

        sb.append("CLASSPATH:")
          .append(method.getDeclaringClass().getName())
          .append('.')
          .append(method.getName());

        name = sb.toString();

        if (className.equals("java.lang.Math")) {
            routine.isLibraryRoutine = true;
        }

        for (int j = offset; j < params.length; j++) {
            Type methodParamType = Types.getParameterSQLType(params[j]);
            HsqlName colName = session.database.nameManager.newHsqlName(
                "C" + (j - offset + 1),
                false,
                SchemaObject.PARAMETER);
            ColumnSchema param = new ColumnSchema(
                colName,
                methodParamType,
                !params[j].isPrimitive(),
                false,
                null);

            routine.addParameter(param);
        }

        routine.setLanguage(Routine.LANGUAGE_JAVA);
        routine.setMethod(method);
        routine.setMethodURL(name);
        routine.setDataImpact(Routine.NO_SQL);

        Type methodReturnType = Types.getParameterSQLType(
            method.getReturnType());

        routine.javaMethodWithConnection = offset == 1;

        routine.setReturnType(methodReturnType);
        routine.resolve(session);

        return routine;
    }

    /**
     * simply named function only - requires return type
     */
    public static void createRoutines(
            Session session,
            HsqlName schema,
            String name) {

        Method[]  methods  = Routine.getMethods(name);
        Routine[] routines = Routine.newRoutines(session, methods);
        HsqlName routineName = session.database.nameManager.newHsqlName(
            schema,
            name,
            true,
            SchemaObject.FUNCTION);

        for (int i = 0; i < routines.length; i++) {
            if (routines[i].getReturnType() != Type.SQL_ALL_TYPES) {
                routines[i].setName(routineName);
                session.database.schemaManager.addSchemaObject(routines[i]);
            }
        }
    }
}
