/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
//     arnaud nauwynck, tware - Bug 274975 - ensure custom sql calls are only translated once
package org.eclipse.persistence.queries;

import java.util.List;
import java.io.*;
import org.eclipse.persistence.internal.databaseaccess.*;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.expressions.ParameterExpression;
import org.eclipse.persistence.mappings.structures.ObjectRelationalDatabaseField;

/**
 * <b>Purpose</b>: Used as an abstraction of an SQL call.
 * A call is an SQL string with parameters.
 */
public class SQLCall extends DatabaseCall implements QueryStringCall {
    protected boolean hasCustomSQLArguments;
    transient protected boolean isTranslatedCustomQuery;

    /**
     * PUBLIC:
     * Create a new SQL call.
     */
    public SQLCall() {
        super();
        this.hasCustomSQLArguments = false;
        this.isTranslatedCustomQuery = false;
    }

    /**
     * PUBLIC:
     * Create a new SQL call.
     * Warning: Allowing an unverified SQL string to be passed into this
     * method makes your application vulnerable to SQL injection attacks.
     */
    public SQLCall(String sqlString) {
        this();
        setSQLString(sqlString);
    }

    /**
     * INTERNAL:
     * Set the data passed through setCustomSQLArgumentType and useCustomSQLCursorOutputAsResultSet methods.
     */
    protected void afterTranslateCustomQuery(List updatedParameters, List<Integer> updatedParameterTypes) {
        int size = getParameters().size();
        for (int i = 0; i < size; i++) {
            Integer parameterType = this.parameterTypes.get(i);
            Object parameter = this.parameters.get(i);
            if ((parameterType == MODIFY) || (parameterType == OUT) || (parameterType == OUT_CURSOR) || ((parameterType == IN) && parameter instanceof DatabaseField)) {
                DatabaseField field = afterTranslateCustomQueryUpdateParameter((DatabaseField)parameter, i, parameterType, updatedParameters, updatedParameterTypes);
                if (field!=null){
                    this.parameters.set(i, field);
                }
            } else if (parameterType == INOUT) {
                DatabaseField outField = afterTranslateCustomQueryUpdateParameter((DatabaseField)((Object[])parameter)[1], i, parameterType, updatedParameters, updatedParameterTypes);
                if (outField != null) {
                    if (((Object[])parameter)[0] instanceof DatabaseField){
                        if ( ((Object[])parameter)[0] != ((Object[])parameter)[1] ) {
                            DatabaseField inField = outField.clone();
                            inField.setName( ((DatabaseField)((Object[])parameter)[0]).getName());
                            ((Object[])parameter)[0] = inField;
                        } else {
                            ((Object[])parameter)[0] = outField;
                        }
                    }
                    ((Object[])parameter)[1] = outField;
                }
            } else if ((parameterType == IN) && (parameter instanceof DatabaseField)){
                DatabaseField field = afterTranslateCustomQueryUpdateParameter((DatabaseField)parameter, i, parameterType, updatedParameters, updatedParameterTypes);
                if (field != null) {
                    this.parameters.set(i, field);
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Set the data passed through setCustomSQLArgumentType and useCustomSQLCursorOutputAsResultSet methods.
     * This will return the null if the user did not add the field/type using the setCustomSQLArgumentType method
     */
    protected DatabaseField afterTranslateCustomQueryUpdateParameter(DatabaseField field, int index, Integer parameterType, List updatedParameters, List<Integer> updatedParameterTypes) {
        int size = updatedParameters.size();
        for (int j = 0; j < size; j++) {
            DatabaseField updateField = (DatabaseField)updatedParameters.get(j);
            if (field.equals(updateField)) {
                Integer updateParameterType = updatedParameterTypes.get(j);
                if (updateParameterType == null) {
                    return updateField;
                } else if (updateParameterType == OUT_CURSOR) {
                    if (parameterType == OUT) {
                        this.parameterTypes.set(index, OUT_CURSOR);
                        return updateField;
                    } else {
                        throw ValidationException.cannotSetCursorForParameterTypeOtherThanOut(field.getName(), toString());
                    }
                }
                break;
            }
        }
        return null;
    }

    /**
     * INTERNAL:
     * Used to avoid misinterpreting the # in custom SQL.
     */
    public boolean hasCustomSQLArguments() {
        return hasCustomSQLArguments;
    }

    @Override
    public boolean isSQLCall() {
        return true;
    }

    @Override
    public boolean isQueryStringCall() {
        return true;
    }

    /**
     * INTERNAL:
     * Called by prepare method only.
     */
    @Override
    protected void prepareInternal(AbstractSession session) {
        if (hasCustomSQLArguments()) {
            // hold results of setCustomSQLArgumentType and useCustomSQLCursorOutputAsResultSet methods
            List updatedParameters = null;
            List updatedParameterTypes = null;
            if (getParameters().size() > 0) {
                updatedParameters = getParameters();
                setParameters(org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance());
                updatedParameterTypes = getParameterTypes();
                setParameterTypes(org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance());
            }

            translateCustomQuery();

            if (updatedParameters != null) {
                afterTranslateCustomQuery(updatedParameters, updatedParameterTypes);
            }
        }

        super.prepareInternal(session);
    }

    /**
     * INTERNAL:
     * Used to avoid misinterpreting the # in custom SQL.
     */
    public void setHasCustomSQLArguments(boolean hasCustomSQLArguments) {
        this.hasCustomSQLArguments = hasCustomSQLArguments;
    }

    /**
     * PUBLIC:
     * This method should only be used with custom SQL:
     * it sets a type to IN, OUT or INOUT parameter (prefixed with #, ### or #### in custom SQL string).
     */
    public void setCustomSQLArgumentType(String customParameterName, Class type) {
        DatabaseField field = new DatabaseField(customParameterName);
        field.setType(type);
        getParameters().add(field);
        getParameterTypes().add(null);
    }

    /**
     * PUBLIC:
     * This method should only be used with custom SQL:
     * it sets a type to IN, OUT or INOUT parameter (prefixed with #, ### or #### in custom SQL string).
     * The argumentFieldName is the field or argument name used in the SQL.
     * The type is the JDBC type code for the parameter.
     */
    public void setCustomSQLArgumentType(String argumentFieldName, int type) {
        DatabaseField field = new DatabaseField(argumentFieldName);
        field.setSqlType(type);
        getParameters().add(field);
        getParameterTypes().add(null);
    }

    /**
     * PUBLIC:
     * This method should only be used with custom SQL:
     * it sets a type to IN, OUT or INOUT parameter (prefixed with #, ### or #### in custom SQL string).
     * The argumentFieldName is the field or argument name used in the SQL.
     * The type is the JDBC type code for the parameter.
     * The typeName is the JDBC type name, this may be required for ARRAY or STRUCT types.
     */
    public void setCustomSQLArgumentType(String argumentFieldName, int type, String typeName) {
        ObjectRelationalDatabaseField field = new ObjectRelationalDatabaseField(argumentFieldName);
        field.setSqlType(type);
        field.setSqlTypeName(typeName);
        getParameters().add(field);
        getParameterTypes().add(null);
    }

    /**
     * PUBLIC:
     * This method should only be used with custom SQL:
     * it sets a type to IN, OUT or INOUT parameter (prefixed with #, ### or #### in custom SQL string).
     * TThe argumentFieldName is the field or argument name used in the SQL.
     * The type is the JDBC type code for the parameter.
     * The typeName is the JDBC type name, this may be required for ARRAY or STRUCT types.
     * The javaType is the java class to return instead of the ARRAY and STRUCT types if a conversion is possible.
     */
    public void setCustomSQLArgumentType(String argumentFieldName, int type, String typeName, Class javaType) {
        ObjectRelationalDatabaseField field = new ObjectRelationalDatabaseField(argumentFieldName);
        field.setSqlType(type);
        field.setSqlTypeName(typeName);
        field.setType(javaType);
        getParameters().add(field);
        getParameterTypes().add(null);
    }

    /**
     * PUBLIC:
     * This method should only be used with custom SQL:
     * it sets a type to IN, OUT or INOUT parameter (prefixed with #, ### or #### in custom SQL string).
     * TThe argumentFieldName is the field or argument name used in the SQL.
     * The type is the JDBC type code for the parameter.
     * The typeName is the JDBC type name, this may be required for ARRAY or STRUCT types.
     * The nestedType is a DatabaseField with type information set to match the VARRAYs object types
     */
    public void setCustomSQLArgumentType(String argumentFieldName, int type, String typeName, DatabaseField nestedType) {
        ObjectRelationalDatabaseField field = new ObjectRelationalDatabaseField(argumentFieldName);
        field.setSqlType(type);
        field.setSqlTypeName(typeName);
        field.setNestedTypeField(nestedType);
        getParameters().add(field);
        getParameterTypes().add(null);
    }

    /**
     * PUBLIC:
     * This method should only be used with custom SQL:
     * it sets a type to IN, OUT or INOUT parameter (prefixed with #, ### or #### in custom SQL string).
     * TThe argumentFieldName is the field or argument name used in the SQL.
     * The type is the JDBC type code for the parameter.
     * The typeName is the JDBC type name, this may be required for ARRAY or STRUCT types.
     * The javaType is the java class to return instead of the ARRAY and STRUCT types if a conversion is possible.
     * The nestedType is a DatabaseField with type information set to match the VARRAYs object types
     */
    public void setCustomSQLArgumentType(String argumentFieldName, int type, String typeName, Class javaType, DatabaseField nestedType) {
        ObjectRelationalDatabaseField field = new ObjectRelationalDatabaseField(argumentFieldName);
        field.setSqlType(type);
        field.setSqlTypeName(typeName);
        field.setType(javaType);
        field.setNestedTypeField(nestedType);
        getParameters().add(field);
        getParameterTypes().add(null);
    }

    /**
     * Set the SQL string.
     * Warning: Allowing an unverified SQL string to be passed into this
     * method makes your application vulnerable to SQL injection attacks.
     */
    public void setSQLString(String sqlString) {
        setSQLStringInternal(sqlString);
    }

    /**
     * INTERNAL:
     * Keep track of the fact that this call has been translated.  This information is used to
     * ensure the translation code for a custom SQLCall is only run once
     * In the case of inheritance we will try to call the translation code once to get the
     * list of types and again for each subclass
     */
    @Override
    public void translateCustomQuery() {
        super.translateCustomQuery();
        isTranslatedCustomQuery = true;
    }

    /**
     * INTERNAL:
     * Only translate the call if it was not previously translated
     *
     * This code ensures the translation code for a custom SQLCall is only run once
     * In the case of inheritance we will try to call the translation code once to get the
     * list of types and again for each subclass
     */
    @Override
    public void translatePureSQLCustomQuery() {
        if (isTranslatedCustomQuery) {
            return;
        }
        super.translatePureSQLCustomQuery();
        isTranslatedCustomQuery = true;
    }

    /**
     * INTERNAL:
     * All values are printed as ? to allow for parameter binding or translation during the execute of the call.
     */
    public void appendTranslationParameter(Writer writer, ParameterExpression expression, DatabasePlatform platform, AbstractRecord record) throws IOException {
        try {
            platform.writeParameterMarker(writer, expression, record, this);
        } catch (IOException exception) {
            throw ValidationException.fileError(exception);
        }
        getParameters().add(expression);
        getParameterTypes().add(TRANSLATION);
    }

    /**
     * PUBLIC:
     * This method should only be used with custom SQL:
     * Used for Oracle result sets through procedures.
     * It defines OUT parameter (prefixed with ### in custom SQL string)
     * as a cursor output.
     */
    public void useCustomSQLCursorOutputAsResultSet(String customParameterName) {
        DatabaseField field = new DatabaseField(customParameterName);
        getParameters().add(field);
        getParameterTypes().add(OUT_CURSOR);
        setIsCursorOutputProcedure(true);
    }
}
