package com.j256.ormlite.stmt.mapped;

import java.sql.SQLException;

import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.field.FieldType;
import com.j256.ormlite.field.SqlType;
import com.j256.ormlite.logger.Level;
import com.j256.ormlite.misc.IOUtils;
import com.j256.ormlite.stmt.ArgumentHolder;
import com.j256.ormlite.stmt.PreparedDelete;
import com.j256.ormlite.stmt.PreparedQuery;
import com.j256.ormlite.stmt.PreparedUpdate;
import com.j256.ormlite.stmt.StatementBuilder;
import com.j256.ormlite.stmt.StatementBuilder.StatementType;
import com.j256.ormlite.support.CompiledStatement;
import com.j256.ormlite.support.DatabaseConnection;
import com.j256.ormlite.table.TableInfo;

/**
 * Mapped statement used by the {@link StatementBuilder#prepareStatement(Long, boolean)} method.
 * 
 * @author graywatson
 */
public class MappedPreparedStmt<T, ID> extends BaseMappedQuery<T, ID>
		implements PreparedQuery<T>, PreparedDelete<T>, PreparedUpdate<T> {

	private final ArgumentHolder[] argHolders;
	private final Long limit;
	private final StatementType type;
	private final boolean cacheStore;

	public MappedPreparedStmt(Dao<T, ID> dao, TableInfo<T, ID> tableInfo, String statement, FieldType[] argFieldTypes,
			FieldType[] resultFieldTypes, ArgumentHolder[] argHolders, Long limit, StatementType type,
			boolean cacheStore) {
		super(dao, tableInfo, statement, argFieldTypes, resultFieldTypes);
		this.argHolders = argHolders;
		// this is an Integer because it may be null
		this.limit = limit;
		this.type = type;
		this.cacheStore = cacheStore;
	}

	@Override
	public CompiledStatement compile(DatabaseConnection databaseConnection, StatementType type) throws SQLException {
		return compile(databaseConnection, type, DatabaseConnection.DEFAULT_RESULT_FLAGS);
	}

	@Override
	public CompiledStatement compile(DatabaseConnection databaseConnection, StatementType type, int resultFlags)
			throws SQLException {
		if (this.type != type) {
			throw new SQLException("Could not compile this " + this.type + " statement since the caller is expecting a "
					+ type + " statement.  Check your QueryBuilder methods.");
		}
		CompiledStatement stmt =
				databaseConnection.compileStatement(statement, type, argFieldTypes, resultFlags, cacheStore);
		// this may return null if the stmt had to be closed
		return assignStatementArguments(stmt);
	}

	@Override
	public String getStatement() {
		return statement;
	}

	@Override
	public StatementType getType() {
		return type;
	}

	@Override
	public void setArgumentHolderValue(int index, Object value) throws SQLException {
		if (index < 0) {
			throw new SQLException("argument holder index " + index + " must be >= 0");
		}
		if (argHolders.length <= index) {
			throw new SQLException("argument holder index " + index + " is not valid, only " + argHolders.length
					+ " arguments in statement (index starts at 0)");
		}
		argHolders[index].setValue(value);
	}

	@Override
	public void setArgumentHolderValue(String columnName, Object value) throws SQLException {
		for (int i = 0; i < argHolders.length; i++) {
			if (argHolders[i].getColumnName().equals(columnName)) {
				argHolders[i].setValue(value);
				return;
			}
		}
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < argHolders.length; i++) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			sb.append(argHolders[i].getColumnName());
		}
		throw new SQLException("argument holder column-name `" + columnName + "' is unknown, valid names are: " + sb);
	}

	@Override
	public int getNumArgs() {
		if (argHolders == null) {
			return 0;
		} else {
			return argHolders.length;
		}
	}

	@Override
	public int getColumnNameIndex(String columnName) {
		for (int i = 0; i < argHolders.length; i++) {
			if (argHolders[i].getColumnName().equals(columnName)) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * Assign arguments to the statement.
	 * 
	 * @return The statement passed in or null if it had to be closed on error.
	 */
	private CompiledStatement assignStatementArguments(CompiledStatement stmt) throws SQLException {
		boolean ok = false;
		try {
			if (limit != null) {
				// we use this if SQL statement LIMITs are not supported by this database type
				stmt.setMaxRows(limit.intValue());
			}
			// set any arguments if we are logging our object
			Object[] argValues = null;
			if (logger.isLevelEnabled(Level.TRACE) && argHolders.length > 0) {
				argValues = new Object[argHolders.length];
			}
			for (int i = 0; i < argHolders.length; i++) {
				Object argValue = argHolders[i].getSqlArgValue();
				FieldType fieldType = argFieldTypes[i];
				SqlType sqlType;
				if (fieldType == null) {
					sqlType = argHolders[i].getSqlType();
				} else {
					sqlType = fieldType.getSqlType();
				}
				stmt.setObject(i, argValue, sqlType);
				if (argValues != null) {
					argValues[i] = argValue;
				}
			}
			logger.debug("prepared statement '{}' with {} args", statement, argHolders.length);
			if (argValues != null) {
				// need to do the (Object) cast to force args to be a single object
				logger.trace("prepared statement arguments: {}", (Object) argValues);
			}
			ok = true;
			return stmt;
		} finally {
			if (!ok) {
				IOUtils.closeThrowSqlException(stmt, "statement");
			}
		}
	}
}
