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.PrimaryKey;
import jp.sf.amateras.mirage.annotation.PrimaryKey.GenerationType;
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());

	protected NameConverter nameConverter = new DefaultNameConverter();

	protected Dialect dialect = new StandardDialect();

	protected SqlExecutor sqlExecutor = new SqlExecutor();

	protected CallExecutor callExecutor = new CallExecutor();

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

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

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

	public void setDialect(Dialect dialect){
		this.dialect = dialect;
		this.sqlExecutor.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;
	}

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

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

		return sqlExecutor.executeUpdateSql(context.getSql(), context.getBindVariables(), null);

	}

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

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

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

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

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

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

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

		return sqlExecutor.executeUpdateSql(executeSql, params.toArray(), null);
	}

	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 sqlExecutor.executeBatchUpdateSql(executeSql, paramsList, null);
	}

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

	/**
	 * Sets GenerationType.SEQUENCE properties value.
	 */
	private void fillPrimaryKeysBySequence(Object entity){
		if(!dialect.supportsGenerationType(GenerationType.SEQUENCE)){
			return;
		}

		BeanDesc beanDesc = BeanDescFactory.getBeanDesc(entity.getClass());
		int size = beanDesc.getPropertyDescSize();

		for(int i=0; i < size; i++){
			PropertyDesc propertyDesc = beanDesc.getPropertyDesc(i);
			PrimaryKey primaryKey = propertyDesc.getAnnotation(PrimaryKey.class);

			if(primaryKey != null && primaryKey.generationType() == GenerationType.SEQUENCE){
				String sql = dialect.getSequenceSql(primaryKey.generator());
				Object value = sqlExecutor.getSingleResult(propertyDesc.getPropertyType(), sql, new Object[0]);
				propertyDesc.setValue(entity, value);
			}
		}
	}

	public int insertEntity(Object entity) {
		fillPrimaryKeysBySequence(entity);

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

		return sqlExecutor.executeUpdateSql(sql, params.toArray(), entity);
	}

	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){
			fillPrimaryKeysBySequence(entity);

			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());
		}
		// TODO ここ？
		return sqlExecutor.executeBatchUpdateSql(executeSql, paramsList, entities);
	}

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

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

		return sqlExecutor.executeUpdateSql(executeSql, params.toArray(), null);
	}

	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 sqlExecutor.executeBatchUpdateSql(executeSql, paramsList, null);
	}

	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 sqlExecutor.getSingleResult(clazz, executeSql, id);
	}

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

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

//	@Override
	public int getCount(String sqlPath, Object param) {
		Node node = prepareNode(sqlPath);
		SqlContext context = prepareSqlContext(param);
		node.accept(context);
		String sql = dialect.getCountSql(context.getSql());

		return sqlExecutor.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 sqlExecutor.<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 sqlExecutor.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 sqlExecutor.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 sqlExecutor.<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 sqlExecutor.executeUpdateSql(sql, params, null);
	}

//	public long getSequenceNextValue(String sequenceName) {
//		String sql = dialect.getSequenceSql(sequenceName);
//		if(sql == null){
//			throw new UnsupportedOperationException();
//		}
//		return getSingleResultBySql(Long.class, sql);
//	}

}
