package jp.sf.amateras.mirage;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import jp.sf.amateras.mirage.bean.BeanDesc;
import jp.sf.amateras.mirage.bean.BeanDescFactory;
import jp.sf.amateras.mirage.dialect.Dialect;
import jp.sf.amateras.mirage.exception.SQLRuntimeException;
import jp.sf.amateras.mirage.naming.NameConverter;
import jp.sf.amateras.mirage.provider.ConnectionProvider;
import jp.sf.amateras.mirage.type.ValueType;
import jp.sf.amateras.mirage.util.JdbcUtil;
import jp.sf.amateras.mirage.util.MirageUtil;

public class SqlExecutor {

	private static final Logger logger = Logger.getLogger(SqlExecutor.class.getName());

	private NameConverter nameConverter;
	private ConnectionProvider connectionProvider;
	private Dialect dialect;
	private List<ValueType> valueTypes = new ArrayList<ValueType>();

	public void setConnectionProvider(ConnectionProvider connectionProvider){
		this.connectionProvider = connectionProvider;
	}

	public void setNameConverter(NameConverter nameConverter){
		this.nameConverter = nameConverter;
	}

	public void addValueType(ValueType valueType){
		this.valueTypes.add(valueType);
	}

	public void setDialect(Dialect dialect){
		this.dialect = dialect;
	}

	private static void printParameters(Object[] params){
		if(params == null){
			return;
		}
		for(int i=0; i<params.length; i++){
			logger.info(String.format("params[%d]=%s", i, params[i]));
		}
	}

	public <T> List<T> getResultList(Class<T> clazz, String sql, Object[] params) {
		PreparedStatement stmt = null;
		ResultSet rs = null;
		try {
			stmt = connectionProvider.getConnection().prepareStatement(sql);
			setParameters(stmt, params);

			List<T> list = new ArrayList<T>();

			if(logger.isLoggable(Level.INFO)){
				logger.info(sql);
				printParameters(params);
			}

			rs = stmt.executeQuery();
			ResultSetMetaData meta = rs.getMetaData();
			int columnCount = meta.getColumnCount();

			BeanDesc beanDesc = BeanDescFactory.getBeanDesc(clazz);

			while(rs.next()){
				T entity = MirageUtil.createEntity(clazz, rs, meta, columnCount, beanDesc,
						dialect, valueTypes, nameConverter);
				list.add(entity);
			}

			return list;

		} catch (SQLException ex) {
			throw new SQLRuntimeException(ex);

		} catch(RuntimeException ex){
			throw (RuntimeException) ex;

		} catch(Exception ex){
			throw new RuntimeException(ex);

		} finally {
			JdbcUtil.close(rs);
			JdbcUtil.close(stmt);
		}
	}

	public <T, R> R iterate(Class<T> clazz, IterationCallback<T, R> callback, String sql, Object[] params) {
		PreparedStatement stmt = null;
		ResultSet rs = null;
		try {
			stmt = connectionProvider.getConnection().prepareStatement(sql);
			setParameters(stmt, params);

			if(logger.isLoggable(Level.INFO)){
				logger.info(sql);
				printParameters(params);
			}

			rs = stmt.executeQuery();
			ResultSetMetaData meta = rs.getMetaData();
			int columnCount = meta.getColumnCount();

			BeanDesc beanDesc = BeanDescFactory.getBeanDesc(clazz);
			R result = null;

			while(rs.next()){
				T entity = MirageUtil.createEntity(clazz, rs, meta, columnCount, beanDesc,
						dialect, valueTypes, nameConverter);
				result = callback.iterate(entity);
			}

			return result;

		} catch (SQLException ex) {
			throw new SQLRuntimeException(ex);

		} catch(RuntimeException ex){
			throw (RuntimeException) ex;

		} catch(Exception ex){
			throw new RuntimeException(ex);

		} finally {
			JdbcUtil.close(rs);
			JdbcUtil.close(stmt);
		}
	}

	public <T> T getSingleResult(Class<T> clazz, String sql, Object[] params) throws SQLRuntimeException {
		PreparedStatement stmt = null;
		ResultSet rs = null;
		try {
			stmt = connectionProvider.getConnection().prepareStatement(sql);
			setParameters(stmt, params);

			if(logger.isLoggable(Level.INFO)){
				logger.info(sql);
				printParameters(params);
			}

			rs = stmt.executeQuery();
			ResultSetMetaData meta = rs.getMetaData();
			int columnCount = meta.getColumnCount();

			BeanDesc beanDesc = BeanDescFactory.getBeanDesc(clazz);

			if(rs.next()){
				T entity = MirageUtil.createEntity(clazz, rs, meta, columnCount, beanDesc,
						dialect, valueTypes, nameConverter);
				return entity;
			}

			return null;

		} catch (SQLException ex) {
			throw new SQLRuntimeException(ex);

		} catch(RuntimeException ex){
			throw (RuntimeException) ex;

		} catch(Exception ex){
			throw new RuntimeException(ex);

		} finally {
			JdbcUtil.close(rs);
			JdbcUtil.close(stmt);
		}
	}

	public int executeUpdateSql(String sql, Object[] params) throws SQLRuntimeException {
		PreparedStatement stmt = null;
		try {
			Connection conn = connectionProvider.getConnection();

			if(logger.isLoggable(Level.INFO)){
				logger.info(sql);
				printParameters(params);
			}

			stmt = conn.prepareStatement(sql);
			setParameters(stmt, params);

			int result = stmt.executeUpdate();
			return result;

		} catch(SQLException ex){
			throw new SQLRuntimeException(ex);
		} finally {
			JdbcUtil.close(stmt);
		}
	}

	public int executeBatchUpdateSql(String sql, List<Object[]> paramsList) throws SQLRuntimeException {
		PreparedStatement stmt = null;
		try {
			Connection conn = connectionProvider.getConnection();

			if(logger.isLoggable(Level.INFO)){
				logger.info(sql);
				for(int i=0; i < paramsList.size(); i++){
					Object[] params = paramsList.get(i);
					logger.info("[" + i + "]");
					printParameters(params);
				}
			}

			stmt = conn.prepareStatement(sql);
			for(Object[] params: paramsList){
				setParameters(stmt, params);
				stmt.addBatch();
			}

			int[] results = stmt.executeBatch();

			int updateRows = 0;
			for(int result: results){
				updateRows += result;
			}

			return updateRows;

		} catch(SQLException ex){
			throw new SQLRuntimeException(ex);
		} finally {
			JdbcUtil.close(stmt);
		}
	}

	protected void setParameters(PreparedStatement stmt, Object[] vars) throws SQLException {
		for (int i = 0; i < vars.length; i++) {
			if(vars[i] == null){
				stmt.setObject(i + 1, null);
			} else {
				Class<?> varType = vars[i].getClass();
				if(dialect.getValueType() != null){
					ValueType valueType = dialect.getValueType();
					if(valueType.isSupport(varType)){
						valueType.set(varType, stmt, vars[i], i + 1);
						continue;
					}
				}
				for(ValueType valueType: valueTypes){
					if(valueType.isSupport(varType)){
						valueType.set(varType, stmt, vars[i], i + 1);
						continue;
					}
				}
			}
		}
	}

}
