/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.jdbc.object;

import java.util.List;
import java.util.Map;
import javax.sql.DataSource;

import org.springframework.jdbc.core.CallableStatementCreator;
import org.springframework.jdbc.core.CallableStatementCreatorFactory;
import org.springframework.jdbc.core.ParameterMapper;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * RdbmsOperation using a JdbcTemplate and representing a SQL-based
 * call such as a stored procedure or a stored function.
 *
 * <p>Configures a CallableStatementCreatorFactory based on the declared
 * parameters.
 *
 * @author Rod Johnson
 * @author Thomas Risberg
 * @see CallableStatementCreatorFactory
 */
public abstract class SqlCall extends RdbmsOperation {

	/**
	 * Flag used to indicate that this call is for a function and to
	 * use the {? = call get_invoice_count(?)} syntax.
	 */
	private boolean function = false;

	/**
	 * Flag used to indicate that the sql for this call should be used exactly as
	 * it is defined. No need to add the escape syntax and parameter place holders.
	 */
	private boolean sqlReadyForUse = false;

	/**
	 * Call string as defined in java.sql.CallableStatement.
	 * String of form {call add_invoice(?, ?, ?)} or {? = call get_invoice_count(?)}
	 * if isFunction is set to true. Updated after each parameter is added.
	 */
	@Nullable
	private String callString;

	/**
	 * Object enabling us to create CallableStatementCreators
	 * efficiently, based on this class's declared parameters.
	 */
	@Nullable
	private CallableStatementCreatorFactory callableStatementFactory;


	/**
	 * Constructor to allow use as a JavaBean.
	 * A DataSource, SQL and any parameters must be supplied before
	 * invoking the {@code compile} method and using this object.
	 * @see #setDataSource
	 * @see #setSql
	 * @see #compile
	 */
	public SqlCall() {
	}

	/**
	 * Create a new SqlCall object with SQL, but without parameters.
	 * Must add parameters or settle with none.
	 * @param ds the DataSource to obtain connections from
	 * @param sql the SQL to execute
	 */
	public SqlCall(DataSource ds, String sql) {
		setDataSource(ds);
		setSql(sql);
	}


	/**
	 * Set whether this call is for a function.
	 */
	public void setFunction(boolean function) {
		this.function = function;
	}

	/**
	 * Return whether this call is for a function.
	 */
	public boolean isFunction() {
		return this.function;
	}

	/**
	 * Set whether the SQL can be used as is.
	 */
	public void setSqlReadyForUse(boolean sqlReadyForUse) {
		this.sqlReadyForUse = sqlReadyForUse;
	}

	/**
	 * Return whether the SQL can be used as is.
	 */
	public boolean isSqlReadyForUse() {
		return this.sqlReadyForUse;
	}


	/**
	 * Overridden method to configure the CallableStatementCreatorFactory
	 * based on our declared parameters.
	 * @see RdbmsOperation#compileInternal()
	 */
	@Override
	protected final void compileInternal() {
		if (isSqlReadyForUse()) {
			this.callString = resolveSql();
		}
		else {
			StringBuilder callString = new StringBuilder(32);
			List<SqlParameter> parameters = getDeclaredParameters();
			int parameterCount = 0;
			if (isFunction()) {
				callString.append("{? = call ").append(resolveSql()).append('(');
				parameterCount = -1;
			}
			else {
				callString.append("{call ").append(resolveSql()).append('(');
			}
			for (SqlParameter parameter : parameters) {
				if (!parameter.isResultsParameter()) {
					if (parameterCount > 0) {
						callString.append(", ");
					}
					if (parameterCount >= 0) {
						callString.append('?');
					}
					parameterCount++;
				}
			}
			callString.append(")}");
			this.callString = callString.toString();
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Compiled stored procedure. Call string is [" + this.callString + "]");
		}

		this.callableStatementFactory = new CallableStatementCreatorFactory(this.callString, getDeclaredParameters());
		this.callableStatementFactory.setResultSetType(getResultSetType());
		this.callableStatementFactory.setUpdatableResults(isUpdatableResults());

		onCompileInternal();
	}

	/**
	 * Hook method that subclasses may override to react to compilation.
	 * This implementation does nothing.
	 */
	protected void onCompileInternal() {
	}

	/**
	 * Get the call string.
	 */
	@Nullable
	public String getCallString() {
		return this.callString;
	}

	/**
	 * Return a CallableStatementCreator to perform an operation
	 * with this parameters.
	 * @param inParams parameters. May be {@code null}.
	 */
	protected CallableStatementCreator newCallableStatementCreator(@Nullable Map<String, ?> inParams) {
		Assert.state(this.callableStatementFactory != null, "No CallableStatementFactory available");
		return this.callableStatementFactory.newCallableStatementCreator(inParams);
	}

	/**
	 * Return a CallableStatementCreator to perform an operation
	 * with the parameters returned from this ParameterMapper.
	 * @param inParamMapper parametermapper. May not be {@code null}.
	 */
	protected CallableStatementCreator newCallableStatementCreator(ParameterMapper inParamMapper) {
		Assert.state(this.callableStatementFactory != null, "No CallableStatementFactory available");
		return this.callableStatementFactory.newCallableStatementCreator(inParamMapper);
	}

}
