package com.j256.ormlite.android;

import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;

import com.j256.ormlite.android.compat.ApiCompatibility;
import com.j256.ormlite.android.compat.ApiCompatibility.CancellationHook;
import com.j256.ormlite.android.compat.ApiCompatibilityUtils;
import com.j256.ormlite.dao.ObjectCache;
import com.j256.ormlite.field.SqlType;
import com.j256.ormlite.logger.Logger;
import com.j256.ormlite.logger.LoggerFactory;
import com.j256.ormlite.misc.SqlExceptionUtil;
import com.j256.ormlite.stmt.StatementBuilder.StatementType;
import com.j256.ormlite.support.CompiledStatement;
import com.j256.ormlite.support.DatabaseResults;

/**
 * Android implementation of the compiled statement.
 * 
 * @author kevingalligan, graywatson
 */
public class AndroidCompiledStatement implements CompiledStatement {

	private static Logger logger = LoggerFactory.getLogger(AndroidCompiledStatement.class);

	private static final String[] NO_STRING_ARGS = new String[0];
	private static final ApiCompatibility apiCompatibility = ApiCompatibilityUtils.getCompatibility();

	private final String sql;
	private final SQLiteDatabase db;
	private final StatementType type;
	private final boolean cancelQueriesEnabled;
	private final boolean cacheStore;

	private Cursor cursor;
	private List<Object> args;
	private Integer max;
	private CancellationHook cancellationHook;

	public AndroidCompiledStatement(String sql, SQLiteDatabase db, StatementType type, boolean cancelQueriesEnabled,
			boolean cacheStore) {
		this.sql = sql;
		this.db = db;
		this.type = type;
		this.cancelQueriesEnabled = cancelQueriesEnabled;
		this.cacheStore = cacheStore;
	}

	@Override
	public int getColumnCount() throws SQLException {
		return getCursor().getColumnCount();
	}

	@Override
	public String getColumnName(int column) throws SQLException {
		return getCursor().getColumnName(column);
	}

	@Override
	public DatabaseResults runQuery(ObjectCache objectCache) throws SQLException {
		// this could come from DELETE or UPDATE, just not a SELECT
		if (!type.isOkForQuery()) {
			throw new IllegalArgumentException("Cannot call query on a " + type + " statement");
		}
		return new AndroidDatabaseResults(getCursor(), objectCache, cacheStore);
	}

	@Override
	public int runUpdate() throws SQLException {
		if (!type.isOkForUpdate()) {
			throw new IllegalArgumentException("Cannot call update on a " + type + " statement");
		}
		String finalSql;
		if (max == null) {
			finalSql = sql;
		} else {
			finalSql = sql + " " + max;
		}
		return execSql(db, "runUpdate", finalSql, getArgArray());
	}

	@Override
	public int runExecute() throws SQLException {
		if (!type.isOkForExecute()) {
			throw new IllegalArgumentException("Cannot call execute on a " + type + " statement");
		}
		return execSql(db, "runExecute", sql, getArgArray());
	}

	@Override
	public void close() throws IOException {
		if (cursor != null && !cursor.isClosed()) {
			try {
				cursor.close();
			} catch (android.database.SQLException e) {
				throw new IOException("Problems closing Android cursor", e);
			}
		}
		cancellationHook = null;
	}

	@Override
	public void closeQuietly() {
		if (cursor != null) {
			cursor.close();
		}
	}

	@Override
	public void cancel() {
		if (cancellationHook != null) {
			cancellationHook.cancel();
		}
	}

	@Override
	public void setObject(int parameterIndex, Object obj, SqlType sqlType) throws SQLException {
		isInPrep();
		if (args == null) {
			args = new ArrayList<Object>();
		}
		if (obj == null) {
			args.add(parameterIndex, null);
			return;
		}

		switch (sqlType) {
			case STRING:
			case LONG_STRING:
			case DATE:
			case BOOLEAN:
			case CHAR:
			case BYTE:
			case SHORT:
			case INTEGER:
			case LONG:
			case FLOAT:
			case DOUBLE:
				args.add(parameterIndex, obj.toString());
				break;
			case BYTE_ARRAY:
			case SERIALIZABLE:
				args.add(parameterIndex, obj);
				break;
			case BLOB:
				// this is only for derby serializable
			case BIG_DECIMAL:
				// this should be handled as a STRING
				throw new SQLException("Invalid Android type: " + sqlType);
			case UNKNOWN:
			default:
				throw new SQLException("Unknown sql argument type: " + sqlType);
		}
	}

	@Override
	public void setMaxRows(int max) throws SQLException {
		isInPrep();
		this.max = max;
	}

	@Override
	public void setQueryTimeout(long millis) {
		// as far as I could tell this is not supported by Android API
	}

	/***
	 * This is mostly an internal class but is exposed for those people who need access to the Cursor itself.
	 * 
	 * <p>
	 * NOTE: This is not thread safe. Not sure if we need it, but keep that in mind.
	 * </p>
	 */
	public Cursor getCursor() throws SQLException {
		if (cursor == null) {
			String finalSql = null;
			try {
				if (max == null) {
					finalSql = sql;
				} else {
					finalSql = sql + " LIMIT " + max;
				}
				if (cancelQueriesEnabled) {
					cancellationHook = apiCompatibility.createCancellationHook();
				}
				cursor = apiCompatibility.rawQuery(db, finalSql, getStringArray(), cancellationHook);
				cursor.moveToFirst();
				logger.trace("{}: started rawQuery cursor for: {}", this, finalSql);
			} catch (android.database.SQLException e) {
				throw SqlExceptionUtil.create("Problems executing Android query: " + finalSql, e);
			}
		}

		return cursor;
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + "@" + Integer.toHexString(super.hashCode());
	}

	/**
	 * Execute some SQL on the database and return the number of rows changed.
	 */
	static int execSql(SQLiteDatabase db, String label, String finalSql, Object[] argArray) throws SQLException {
		try {
			db.execSQL(finalSql, argArray);
		} catch (android.database.SQLException e) {
			throw SqlExceptionUtil.create("Problems executing " + label + " Android statement: " + finalSql, e);
		}
		int result;
		SQLiteStatement stmt = null;
		try {
			// ask sqlite how many rows were just changed
			stmt = db.compileStatement("SELECT CHANGES()");
			result = (int) stmt.simpleQueryForLong();
		} catch (android.database.SQLException e) {
			// ignore the exception and just return 1 if it failed
			result = 1;
		} finally {
			if (stmt != null) {
				stmt.close();
			}
		}
		logger.trace("executing statement {} changed {} rows: {}", label, result, finalSql);
		return result;
	}

	private void isInPrep() throws SQLException {
		if (cursor != null) {
			throw new SQLException("Query already run. Cannot add argument values.");
		}
	}

	private Object[] getArgArray() {
		if (args == null) {
			// this will work for Object[] as well as String[]
			return NO_STRING_ARGS;
		} else {
			return args.toArray(new Object[args.size()]);
		}
	}

	private String[] getStringArray() {
		if (args == null) {
			return NO_STRING_ARGS;
		} else {
			// we assume we have Strings in args
			return args.toArray(new String[args.size()]);
		}
	}
}