/*
 * 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
//     07/13/2012-2.5 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
//     08/24/2012-2.5 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
//     09/03/2015 - Will Dazey
//       - 456067 : Added support for defining query timeout units
package org.eclipse.persistence.internal.queries;

import java.util.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.databaseaccess.*;
import org.eclipse.persistence.internal.expressions.*;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.queries.*;

/**
 * <p><b>Purpose</b>:
 * Mechanism used for custom SQL and stored procedure queries.
 * <p>
 * <p><b>Responsibilities</b>:
 * Executes the appropriate call.
 *
 * @author James Sutherland
 * @since TOPLink/Java 2.0
 */
public class CallQueryMechanism extends DatasourceCallQueryMechanism {

    public CallQueryMechanism() {
    }

    /**
     * Initialize the state of the query
     * @param query - owner of mechanism
     */
    public CallQueryMechanism(DatabaseQuery query) {
        super(query);
    }

    /**
     * Initialize the state of the query
     * @param query - owner of mechanism
     * @param call - sql call
     */
    public CallQueryMechanism(DatabaseQuery query, DatabaseCall call) {
        super(query, call);
        call.setIsFieldMatchingRequired(true);
    }

    /**
     * Return the call.
     */
    public DatabaseCall getDatabaseCall() {
        return (DatabaseCall)call;
    }

    /**
     * Unprepare the call if required.
     * Clone and unprepare stored procedure calls, so they can be reprepared with possible different optional arguments.
     */
    public void unprepare() {
        DatabaseQuery query = this.query;
        if (hasMultipleCalls()) {
            this.calls = ((Vector)this.calls.clone());
            int size = this.calls.size();
            for (int index = 0; index < size; index++) {
                DatabaseCall call = (DatabaseCall)this.calls.get(index);
                if (call.isPrepared() && call.isStoredProcedureCall()
                        && ((StoredProcedureCall)call).hasOptionalArguments()) {
                    call = (DatabaseCall)call.clone();
                    call.setIsPrepared(false);
                    call.setQuery(query);
                    this.calls.set(index, call);
                }
            }
        } else if (this.call != null) {
            if (this.call.isPrepared() && this.call.isStoredProcedureCall()
                    && ((StoredProcedureCall)this.call).hasOptionalArguments()) {
                this.call = (DatabaseCall)this.call.clone();
                this.call.setIsPrepared(false);
                this.call.setQuery(query);
            }
        }
    }

    /**
     * INTERNAL:
     * This is different from 'prepareForExecution' in that this is called on the original query,
     * and the other is called on the copy of the query.
     * This query is copied for concurrency so this prepare can only setup things that
     * will apply to any future execution of this query.
     */
    public void prepareCall() throws QueryException {
        DatabaseQuery query = this.query;
        AbstractSession executionSession = query.getExecutionSession();
        if (hasMultipleCalls()) {
            if (query.shouldCloneCall()) {
                this.calls = ((Vector)this.calls.clone());
            }
            int size = this.calls.size();
            for (int index = 0; index < size; index++) {
                DatasourceCall call = (DatasourceCall)this.calls.get(index);
                if (query.shouldCloneCall()) {
                    // Need to clone the call if setting query specific properties on it as the call may be shared.
                    call = (DatabaseCall)call.clone();
                    call.setQuery(query);
                    this.calls.set(index, call);
                }
                if (call instanceof DatabaseCall) {
                    configureDatabaseCall((DatabaseCall)call);
                }
                call.prepare(executionSession);
            }
        } else if (this.call != null) {
            if (query.shouldCloneCall()) {
                // Need to clone the call if setting query specific properties on it as the call may be shared.
                this.call = (DatasourceCall)this.call.clone();
                this.call.setQuery(query);
            }
            DatasourceCall call = this.call;
            if (call instanceof DatabaseCall) {
                configureDatabaseCall((DatabaseCall)call);
            }
            this.call.prepare(executionSession);
        }
    }

    /**
     * Set the call level query options into the call.
     */
    protected void configureDatabaseCall(DatabaseCall call) {
        if (!this.query.shouldIgnoreBindAllParameters()) {
            call.setUsesBinding(this.query.shouldBindAllParameters());
        }
        if (!this.query.shouldIgnoreCacheStatement()) {
            call.setShouldCacheStatement(this.query.shouldCacheStatement());
        }
        call.setQueryTimeout(this.query.getQueryTimeout());
        call.setQueryTimeoutUnit(this.query.getQueryTimeoutUnit());
        if (this.query.isNativeConnectionRequired()) {
            call.setIsNativeConnectionRequired(true);
        }
        if (this.query.isReadQuery()) {
            ReadQuery readQuery = (ReadQuery)this.query;
            // Some DB don't support FirstRow in SELECT statements in spite of supporting MaxResults(Symfoware).
            // We should check FirstRow and MaxResults separately.
            if (!call.shouldIgnoreFirstRowSetting()){
                if (readQuery.getFirstResult() != 0) {
                    call.setFirstResult(readQuery.getFirstResult());
                    call.setIsResultSetScrollable(true);
                    call.setResultSetType(java.sql.ResultSet.TYPE_SCROLL_INSENSITIVE);
                    call.setResultSetConcurrency(java.sql.ResultSet.CONCUR_READ_ONLY);
                }
            }
            if (!call.shouldIgnoreMaxResultsSetting()){
                call.setMaxRows(readQuery.getMaxRows());
            }
            call.setResultSetFetchSize(readQuery.getFetchSize());
        }
    }

    /**
     * Pre-build configure the SQL call.
     */
    public void prepareCursorSelectAllRows() throws QueryException {
        getCall().returnCursor();

        ContainerPolicy cp;
        DatabaseQuery query = getQuery();
        if (query.isReadAllQuery()) {
            cp = ((ReadAllQuery)query).getContainerPolicy();
        } else {
            cp = ((DataReadQuery)query).getContainerPolicy();
        }
        if (cp.isScrollableCursorPolicy()) {
            ScrollableCursorPolicy scp = (ScrollableCursorPolicy)cp;
            DatabaseCall call = getDatabaseCall();
            call.setIsResultSetScrollable(true);
            call.setResultSetType(scp.getResultSetType());
            call.setResultSetConcurrency(scp.getResultSetConcurrency());
            // Only set the fetch size to be the page size, if the fetch size was not set on the query.
            if (((ReadQuery)getQuery()).getFetchSize() == 0) {
                call.setResultSetFetchSize(scp.getPageSize());
            }
        }
        if (getQuery().isReportQuery()){
            prepareReportQueryItems();
        }
        prepareCall();
    }

    /**
     * Pre-build configure the SQL call.
     */
    public void prepareDeleteAll() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatabaseCall call = (DatabaseCall)callsEnum.nextElement();
                call.returnNothing();
            }
        } else {
            getCall().returnNothing();
        }

        prepareCall();
    }

    /**
     * Pre-build configure the SQL call.
     */
    public void prepareDeleteObject() {
        boolean usesOptimisticLocking = ((DeleteObjectQuery)getQuery()).usesOptimisticLocking();
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatabaseCall call = (DatabaseCall)callsEnum.nextElement();
                call.returnNothing();
                if (usesOptimisticLocking) {
                    call.setHasOptimisticLock(true);
                }
            }
        } else {
            getCall().returnNothing();
            if (usesOptimisticLocking) {
                getDatabaseCall().setHasOptimisticLock(true);
            }
        }
        prepareCall();
    }

    /**
     * Pre-build configure the SQL call.
     */
    public void prepareDoesExist(DatabaseField field) {
        getCall().returnOneRow();
        Vector fields = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(1);
        fields.addElement(field);
        getDatabaseCall().setFields(fields);
        prepareCall();
    }

    /**
     * Pre-build configure the SQL call for an execute call.
     */
    public void prepareExecute() {
        DatabaseCall call = getDatabaseCall();
        call.setExecuteUpdate();
        call.setIsFieldMatchingRequired(isCallQueryMechanism());
        prepareCall();
    }

    /**
     * Pre-build configure the SQL call.
     */
    public void prepareExecuteSelect() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatabaseCall databseCall = (DatabaseCall)callsEnum.nextElement();
                databseCall.returnManyRows();
                databseCall.setIsFieldMatchingRequired(isCallQueryMechanism());
            }
        } else {
            DatabaseCall call = getDatabaseCall();
            call.returnManyRows();
            call.setIsFieldMatchingRequired(isCallQueryMechanism());
        }
        prepareCall();
    }

    /**
     * Pre-build configure the SQL call.
     */
    public void prepareSelectAllRows() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatabaseCall call = (DatabaseCall)callsEnum.nextElement();
                call.returnManyRows();
                if (isCallQueryMechanism()) {
                    call.setIsFieldMatchingRequired(true);
                    // Set the fields including joined and partial fields and compute joined indexes,
                    // this requires and assume that the custom SQL returns the fields in the correct order.
                    call.setFields(((ObjectLevelReadQuery)getQuery()).getSelectionFields());
                    prepareJoining((ObjectLevelReadQuery)getQuery());
                }
            }
        } else {
            getCall().returnManyRows();
            if (isCallQueryMechanism()) {
                DatabaseCall call = getDatabaseCall();
                call.setIsFieldMatchingRequired(true);
                // Set the fields including joined and partial fields and compute joined indexes,
                // this requires and assume that the custom SQL returns the fields in the correct order.
                call.setFields(((ObjectLevelReadQuery)getQuery()).getSelectionFields());
                prepareJoining((ObjectLevelReadQuery)getQuery());
            }
        }
        prepareCall();
    }

    /**
     * Prepare the joining indexes if joining.
     */
    protected void prepareJoining(ObjectLevelReadQuery query) {
        if (query.hasJoining()) {
            query.getJoinedAttributeManager().computeJoiningMappingIndexes(true, getSession(), 0);
        }
    }

    /**
     * Pre-build configure the SQL call.
     */
    public void prepareSelectOneRow() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatabaseCall call = (DatabaseCall)callsEnum.nextElement();
                call.returnOneRow();
                if (isCallQueryMechanism()) {
                    call.setIsFieldMatchingRequired(true);
                    // Set the fields including joined and partial fields and compute joined indexes,
                    // this requires and assume that the custom SQL returns the fields in the correct order.
                    call.setFields(((ObjectLevelReadQuery)getQuery()).getSelectionFields());
                    prepareJoining((ObjectLevelReadQuery)getQuery());
                }
            }
        } else {
            getCall().returnOneRow();
            if (isCallQueryMechanism()) {
                DatabaseCall call = getDatabaseCall();
                call.setIsFieldMatchingRequired(true);
                // Set the fields including joined and partial fields and compute joined indexes,
                // this requires and assume that the custom SQL returns the fields in the correct order.
                call.setFields(((ObjectLevelReadQuery)getQuery()).getSelectionFields());
                prepareJoining((ObjectLevelReadQuery)getQuery());
            }
        }
        prepareCall();
    }

    /**
     * Pre-build configure the SQL call.
     */
    public void prepareUpdateObject() {
        if (hasMultipleCalls()) {
            int size = this.calls.size();
            for (int index = 0; index < size; index++) {
                DatabaseCall call = (DatabaseCall)this.calls.get(index);
                if (!call.isReturnSet()) {
                    call.returnNothing();
                }
                if (this.query.getDescriptor().usesOptimisticLocking()) {
                    call.setHasOptimisticLock(true);
                }
            }
        } else if (this.call != null) {
            if (!call.isReturnSet()) {
                this.call.returnNothing();
            }
            if (this.query.getDescriptor().usesOptimisticLocking()) {
                ((DatabaseCall)this.call).setHasOptimisticLock(true);
            }
        }
        prepareCall();
    }

    /**
     * INTERNAL:
     * Configure the call to be a dynamic custom SQL call, so that it ignores the # token.
     */
    public void setCallHasCustomSQLArguments() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatabaseCall databaseCall = (DatabaseCall)callsEnum.nextElement();
                if (databaseCall.isSQLCall()) {
                    ((SQLCall)databaseCall).setHasCustomSQLArguments(true);
                }
            }
        } else if (getCall().isSQLCall()) {
            ((SQLCall)getCall()).setHasCustomSQLArguments(true);
        }
    }

    /**
     * Update the foreign key fields when resolving a bi-directional reference in a UOW.
     * This must always be dynamic as it is called within an insert query and is really part of the insert
     * and does not fire update events or worry about locking.
     */
    protected void updateForeignKeyFieldAfterInsert(WriteObjectQuery writeQuery) {
        ClassDescriptor descriptor = getDescriptor();
        for (DatabaseTable table : descriptor.getTables()) {
            AbstractRecord row = descriptor.getObjectBuilder().buildRowForUpdateAfterShallowInsert(writeQuery.getObject(), writeQuery.getSession(), table);
            if (!row.isEmpty()) {
                SQLUpdateStatement updateStatement = new SQLUpdateStatement();
                updateStatement.setModifyRow(row);
                updateStatement.setTranslationRow(getTranslationRow());
                updateStatement.setTable(table);
                updateStatement.setWhereClause(descriptor.getObjectBuilder().buildPrimaryKeyExpression(table));// Must not check version, ok as just inserted it.
                // Bug 2996585
                StatementQueryMechanism updateMechanism = new StatementQueryMechanism(writeQuery, updateStatement);
                writeQuery.setModifyRow(row);
                updateMechanism.updateObject();
            }
        }
    }

    /**
     * Update the foreign key fields to null when resolving a deletion cycle.
     * This must always be dynamic as it is called within an delete query and is really part of the delete
     * and does not fire update events or worry about locking.
     */
    @Override
    public void updateForeignKeyFieldBeforeDelete() {
        ClassDescriptor descriptor = getDescriptor();
        DeleteObjectQuery deleteQuery = (DeleteObjectQuery)getQuery();
        for (DatabaseTable table : descriptor.getTables()) {
            // need nullify the same fields that would be updated after shallow insert
            AbstractRecord row = descriptor.getObjectBuilder().buildRowForUpdateBeforeShallowDelete(deleteQuery.getObject(), deleteQuery.getSession(), table);
            if (!row.isEmpty()) {
                SQLUpdateStatement updateStatement = new SQLUpdateStatement();
                updateStatement.setModifyRow(row);
                updateStatement.setTranslationRow(getTranslationRow());
                updateStatement.setTable(table);
                updateStatement.setWhereClause(descriptor.getObjectBuilder().buildPrimaryKeyExpression(table));// Must not check version, ok as delete will.
                StatementQueryMechanism updateMechanism = new StatementQueryMechanism(deleteQuery, updateStatement);
                deleteQuery.setModifyRow(row);
                updateMechanism.updateObject();
            }
        }
    }
}
