package jp.sf.amateras.mirage;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

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.exception.IORuntimeException;
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.BigDecimalValueType;
import jp.sf.amateras.mirage.type.BooleanPrimitiveValueType;
import jp.sf.amateras.mirage.type.BooleanValueType;
import jp.sf.amateras.mirage.type.ByteArrayValueType;
import jp.sf.amateras.mirage.type.DoublePrimitiveValueType;
import jp.sf.amateras.mirage.type.DoubleValueType;
import jp.sf.amateras.mirage.type.FloatPrimitiveValueType;
import jp.sf.amateras.mirage.type.FloatValueType;
import jp.sf.amateras.mirage.type.IntegerPrimitiveValueType;
import jp.sf.amateras.mirage.type.IntegerValueType;
import jp.sf.amateras.mirage.type.LongPrimitiveValueType;
import jp.sf.amateras.mirage.type.LongValueType;
import jp.sf.amateras.mirage.type.ShortPrimitiveValueType;
import jp.sf.amateras.mirage.type.ShortValueType;
import jp.sf.amateras.mirage.type.SqlDateValueType;
import jp.sf.amateras.mirage.type.StringValueType;
import jp.sf.amateras.mirage.type.TimeValueType;
import jp.sf.amateras.mirage.type.TimestampValueType;
import jp.sf.amateras.mirage.type.UtilDateValueType;
import jp.sf.amateras.mirage.type.ValueType;
import jp.sf.amateras.mirage.util.IOUtil;
import jp.sf.amateras.mirage.util.MirageUtil;
import jp.sf.amateras.mirage.util.Validate;

public class SqlManagerImpl implements SqlManager {

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

	protected ConnectionProvider connectionProvider;

	protected NameConverter nameConverter = new DefaultNameConverter();

	protected Dialect dialect = new StandardDialect();

	protected SqlExecutor sqlExecutor = new SqlExecutor();

	protected CallExecutor callExecutor = new CallExecutor();

	protected Map<String, Node> nodeCache = new ConcurrentHashMap<String, Node>();

	protected boolean cacheMode = false;

	public SqlManagerImpl(){
		addValueType(new StringValueType());
		addValueType(new IntegerValueType());
		addValueType(new IntegerPrimitiveValueType());
		addValueType(new LongValueType());
		addValueType(new LongPrimitiveValueType());
		addValueType(new ShortValueType());
		addValueType(new ShortPrimitiveValueType());
		addValueType(new DoubleValueType());
		addValueType(new DoublePrimitiveValueType());
		addValueType(new FloatValueType());
		addValueType(new FloatPrimitiveValueType());
		addValueType(new BooleanValueType());
		addValueType(new BooleanPrimitiveValueType());
		addValueType(new BigDecimalValueType());
		addValueType(new SqlDateValueType());
		addValueType(new UtilDateValueType());
		addValueType(new TimeValueType());
		addValueType(new TimestampValueType());
		addValueType(new ByteArrayValueType());
//		addValueType(new jp.sf.amateras.mirage.type.DefaultValueType());

		setNameConverter(nameConverter);
		setDialect(dialect);
		setEntityCreator(new DefaultResultEntityCreator());
	}

	public void setCacheMode(boolean cacheMode){
		this.cacheMode = cacheMode;
	}

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

	public NameConverter getNameConverter(){
		return this.nameConverter;
	}

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

	public void setEntityCreator(ResultEntityCreator entityCreator){
		this.sqlExecutor.setEntityCreator(entityCreator);
		this.callExecutor.setEntityCreator(entityCreator);
	}

	public ConnectionProvider getConnectionProvider(){
		return this.connectionProvider;
	}

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

	public Dialect getDialect(){
		return this.dialect;
	}

	protected Node prepareNode(String sqlPath) {

		if(cacheMode && nodeCache.containsKey(sqlPath)){
			return nodeCache.get(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 {
			sql = new String(IOUtil.readStream(in), "UTF-8");
		} catch (IORuntimeException ex){
			throw new IORuntimeException(String.format("Failed to load SQL from: %s", sqlPath), ex.getCause());

		} catch (UnsupportedEncodingException e) {
			// must not to be reached here
			throw new RuntimeException(e);

		} finally{}

		sql = sql.trim();
		if(sql.endsWith(";")){
			sql = sql.substring(0, sql.length() - 1);
		}

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

		if(cacheMode){
			nodeCache.put(sqlPath, node);
		}

		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);
	}

	/**
	 *
	 * @param valueTypes
	 * @throws IllegalArgumentException if the {@code valueTypes} is {@code null} or
	 * an element in the {@code valueTypes} is {@code null}
	 * @author daisuke
	 */
	public void setValueTypes(List<ValueType<?>> valueTypes) {
		Validate.noNullElements(valueTypes);
		this.sqlExecutor.setValueTypes(valueTypes);
		this.callExecutor.setValueTypes(valueTypes);
	}

//	@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());

		Integer result = sqlExecutor.getSingleResult(Integer.class, sql, context.getBindVariables());
		if(result == null){
			return 0;
		}
		return result.intValue();
	}

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

	public int getCountBySql(String sql, Object... params) {
		return getSingleResultBySql(Integer.class, sql, params);
	}

//	@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);
//	}

}
