package jp.sf.amateras.mirage;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import jp.sf.amateras.mirage.annotation.ResultSet;
import jp.sf.amateras.mirage.bean.BeanDesc;
import jp.sf.amateras.mirage.bean.BeanDescFactory;
import jp.sf.amateras.mirage.bean.PropertyDesc;
import jp.sf.amateras.mirage.dialect.Dialect;
import jp.sf.amateras.mirage.dialect.StandardDialect;
import jp.sf.amateras.mirage.naming.DefaultNameConverter;
import jp.sf.amateras.mirage.naming.NameConverter;
import jp.sf.amateras.mirage.parser.Node;
import jp.sf.amateras.mirage.parser.SqlContext;
import jp.sf.amateras.mirage.parser.SqlContextImpl;
import jp.sf.amateras.mirage.parser.SqlParserImpl;
import jp.sf.amateras.mirage.provider.ConnectionProvider;
import jp.sf.amateras.mirage.type.DefaultValueType;
import jp.sf.amateras.mirage.type.ValueType;
import jp.sf.amateras.mirage.util.MirageUtil;

public class SqlManagerImpl implements SqlManager {

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

	private NameConverter nameConverter = new DefaultNameConverter();

	private SqlExecutor executor = new SqlExecutor();

	private CallExecutor callExecutor = new CallExecutor();

	private Dialect dialect = new StandardDialect();

	public SqlManagerImpl(){
		addValueType(new DefaultValueType());
		setNameConverter(new DefaultNameConverter());
		setDialect(dialect);
	}

//	@Override
	public void setNameConverter(NameConverter nameConverter) {
		this.nameConverter = nameConverter;
		this.executor.setNameConverter(nameConverter);
		this.callExecutor.setNameConverter(nameConverter);
	}

//	@Override
	public void setConnectionProvider(ConnectionProvider connectionProvider) {
		this.executor.setConnectionProvider(connectionProvider);
		this.callExecutor.setConnectionProvider(connectionProvider);
	}

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

	protected Node prepareNode(String sqlPath) {
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		InputStream in = cl.getResourceAsStream(sqlPath);
		if (in == null) {
			throw new RuntimeException(String.format(
					"resource: %s is not found.", sqlPath));
		}

		String sql = null;
		try {
			byte[] buf = new byte[1024 * 8];
			int length = 0;
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			while((length = in.read(buf)) != -1){
				out.write(buf, 0, length);
			}

			sql = new String(out.toByteArray(), "UTF-8");

		} catch(Exception ex){
			throw new RuntimeException(String.format("Failed to load SQL from: %s", sqlPath));
		}

		Node node = new SqlParserImpl(sql).parse();
		return node;
	}

	protected SqlContext prepareSqlContext(Object param){
		SqlContext context = new SqlContextImpl();

		if (param != null) {
			BeanDesc beanDesc = BeanDescFactory.getBeanDesc(param);
			for (int i = 0; i < beanDesc.getPropertyDescSize(); i++) {
				PropertyDesc pd = beanDesc.getPropertyDesc(i);
				context.addArg(pd.getPropertyName(), pd.getValue(param), pd
						.getPropertyType());
			}
		}

		return context;
	}

//	@Override
	public int executeUpdate(String sqlPath) {
		return executeUpdate(sqlPath, null);
	}

//	@Override
	public int executeUpdate(String sqlPath, Object param) {
		Node node = prepareNode(sqlPath);
		SqlContext context = prepareSqlContext(param);
		node.accept(context);

		return executor.executeUpdateSql(context.getSql(), context.getBindVariables());

	}

//	@Override
	public <T> List<T> getResultList(Class<T> clazz, String sqlPath) {
		return getResultList(clazz, sqlPath, null);
	}

//	@Override
	public <T> List<T> getResultList(Class<T> clazz, String sqlPath, Object param) {
		Node node = prepareNode(sqlPath);
		SqlContext context = prepareSqlContext(param);
		node.accept(context);

		return executor.getResultList(clazz, context.getSql(), context.getBindVariables());
	}

//	@Override
	public <T> T getSingleResult(Class<T> clazz, String sqlPath) {
		return getSingleResult(clazz, sqlPath, null);
	}

//	@Override
	public <T> T getSingleResult(Class<T> clazz, String sqlPath, Object param) {
		Node node = prepareNode(sqlPath);
		SqlContext context = prepareSqlContext(param);
		node.accept(context);

		return executor.getSingleResult(clazz, context.getSql(), context.getBindVariables());
	}

//	@Override
	public int deleteEntity(Object entity) {
		List<Object> params = new ArrayList<Object>();
		String executeSql = MirageUtil.buildDeleteSql(entity, nameConverter, params);

		return executor.executeUpdateSql(executeSql, params.toArray());
	}

	public <T> int deleteBatch(T... entities) {
		if(entities.length == 0){
			return 0;
		}

		List<Object[]> paramsList = new ArrayList<Object[]>();
		String executeSql = null;

		for(Object entity: entities){
			List<Object> params = new ArrayList<Object>();
			String sql = MirageUtil.buildDeleteSql(entity, nameConverter, params);

			if(executeSql == null){
				executeSql = sql;

			} else if(!sql.equals(executeSql)){
				throw new IllegalArgumentException("Different entity is contained in the entity list.");
			}

			paramsList.add(params.toArray());
		}

		return executor.executeBatchUpdateSql(executeSql, paramsList);
	}

	public <T> int deleteBatch(List<T> entities) {
		return deleteBatch(entities.toArray());
	}

	//	@Override
	public int insertEntity(Object entity) {
		List<Object> params = new ArrayList<Object>();
		String sql = MirageUtil.buildInsertSql(entity, nameConverter, params);

		return executor.executeUpdateSql(sql, params.toArray());
	}

	public <T> int insertBatch(T... entities){
		if(entities.length == 0){
			return 0;
		}

		List<Object[]> paramsList = new ArrayList<Object[]>();
		String executeSql = null;

		for(Object entity: entities){
			List<Object> params = new ArrayList<Object>();
			String sql = MirageUtil.buildInsertSql(entity, nameConverter, params);

			if(executeSql == null){
				executeSql = sql;

			} else if(!sql.equals(executeSql)){
				throw new IllegalArgumentException("Different entity is contained in the entity list.");
			}

			paramsList.add(params.toArray());
		}

		return executor.executeBatchUpdateSql(executeSql, paramsList);
	}

	public <T> int insertBatch(List<T> entities){
		return insertBatch(entities.toArray());
	}

//	@Override
	public int updateEntity(Object entity) {
		List<Object> params = new ArrayList<Object>();
		String executeSql = MirageUtil.buildUpdateSql(entity, nameConverter, params);

		return executor.executeUpdateSql(executeSql, params.toArray());
	}

	public <T> int updateBatch(T... entities) {
		if(entities.length == 0){
			return 0;
		}

		List<Object[]> paramsList = new ArrayList<Object[]>();
		String executeSql = null;

		for(Object entity: entities){
			List<Object> params = new ArrayList<Object>();
			String sql = MirageUtil.buildUpdateSql(entity, nameConverter, params);

			if(executeSql == null){
				executeSql = sql;

			} else if(!sql.equals(executeSql)){
				throw new IllegalArgumentException("Different entity is contained in the entity list.");
			}

			paramsList.add(params.toArray());
		}

		return executor.executeBatchUpdateSql(executeSql, paramsList);
	}

	public <T> int updateBatch(List<T> entities) {
		return updateBatch(entities.toArray());
	}

	//	@Override
	public <T> T findEntity(Class<T> clazz, Object... id) {
		String executeSql = MirageUtil.buildSelectSQL(clazz, nameConverter);
		return executor.getSingleResult(clazz, executeSql, id);
	}

//	@Override
	public void addValueType(ValueType valueType) {
		this.executor.addValueType(valueType);
		this.callExecutor.addValueType(valueType);
	}

//	@Override
	public <T> int getCount(String sqlPath) {
		return getCount(sqlPath, null);
	}

//	@Override
	public <T> int getCount(String sqlPath, Object param) {
		Node node = prepareNode(sqlPath);
		SqlContext context = prepareSqlContext(param);
		node.accept(context);
		String sql = "SELECT COUNT(*) FROM (" + context.getSql() + ")";

		return executor.getSingleResult(Integer.class, sql, context.getBindVariables());
	}

//	@Override
	public <T, R> R iterate(Class<T> clazz, IterationCallback<T, R> callback, String sqlPath) {
		return this.<T, R> iterate(clazz, callback, sqlPath, null);
	}

//	@Override
	public <T, R> R iterate(Class<T> clazz, IterationCallback<T, R> callback, String sqlPath, Object param) {

		Node node = prepareNode(sqlPath);
		SqlContext context = prepareSqlContext(param);
		node.accept(context);

		return executor.<T, R> iterate(clazz, callback, context.getSql(), context.getBindVariables());
	}

	public void call(String procedureName){
		String sql = toCallString(procedureName, false);
		callExecutor.call(sql);
	}

	public void call(String procedureName, Object parameter){
		String sql = toCallString(procedureName, parameter, false);
		callExecutor.call(sql, parameter);
	}

	public <T> T call(Class<T> resultClass, String functionName){
		String sql = toCallString(functionName, true);
		return callExecutor.call(resultClass, sql);
	}

	public <T> T call(Class<T> resultClass, String functionName, Object param){
		String sql = toCallString(functionName, param, true);
		return callExecutor.call(resultClass, sql, param);
	}

	public <T> List<T> callForList(Class<T> resultClass, String functionName){
		String sql = toCallString(functionName, true);
		return callExecutor.callForList(resultClass, sql);
	}

	public <T> List<T> callForList(Class<T> resultClass, String functionName, Object param){
		String sql = toCallString(functionName, param, true);
		return callExecutor.callForList(resultClass, sql, param);
	}

	protected String toCallString(String moduleName, boolean function){
		return toCallString(moduleName, null, function);
	}

	protected String toCallString(String moduleName, Object param, boolean function){
		StringBuilder sb = new StringBuilder();

		if(function){
			sb.append("{? = call ");
		} else {
			sb.append("{call ");
		}

		sb.append(moduleName);
		sb.append("(");
		if (param != null){
			StringBuilder p = new StringBuilder();
			BeanDesc beanDesc = BeanDescFactory.getBeanDesc(param);
			int parameterCount = 0;
			for (int i = 0; i < beanDesc.getPropertyDescSize(); i++) {
				PropertyDesc pd = beanDesc.getPropertyDesc(i);
				if (needsParameter(pd)){
					if (parameterCount > 0) {
						p.append(", ");
					}
					if (parameterCount >= 0) {
						p.append("?");
					}
					parameterCount++;
				}
			}
			sb.append(p.toString());
		}
		sb.append(")");
		sb.append("}");

		return sb.toString();
	}

	protected boolean needsParameter(PropertyDesc pd){
		ResultSet resultSet = pd.getAnnotation(ResultSet.class);
		if (resultSet != null){
			if (dialect.needsParameterForResultSet()){
				return true;
			} else {
				return false;
			}
		} else {
			return true;
		}
	}

	public <T> List<T> getResultListBySql(Class<T> clazz, String sql) {
		return getResultListBySql(clazz, sql, new Object[0]);
	}

	public <T> List<T> getResultListBySql(Class<T> clazz, String sql, Object... params) {
		return executor.getResultList(clazz, sql, params);
	}

	public <T> T getSingleResultBySql(Class<T> clazz, String sql) {
		return getSingleResultBySql(clazz, sql, new Object[0]);
	}

	public <T> T getSingleResultBySql(Class<T> clazz, String sql, Object... params) {
		return executor.getSingleResult(clazz, sql, params);
	}

	public <T, R> R iterateBySql(Class<T> clazz, IterationCallback<T, R> callback, String sql) {
		return this.<T, R> iterateBySql(clazz, callback, sql, new Object[0]);
	}

	public <T, R> R iterateBySql(Class<T> clazz, IterationCallback<T, R> callback, String sql, Object... params) {
		return executor.<T, R> iterate(clazz, callback, sql, params);
	}

	public int executeUpdateBySql(String sql) {
		return executeUpdateBySql(sql, new Object[0]);
	}

	public int executeUpdateBySql(String sql, Object... params) {
		return executor.executeUpdateSql(sql, params);
	}

}
