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.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;

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.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) {
		StringBuilder sb = new StringBuilder();
		sb.append("DELETE FROM ").append(nameConverter.entityToTable(entity.getClass().getName()));
		sb.append(" WHERE ");

		List<Object> params = new ArrayList<Object>();
		boolean hasPrimaryKey = false;

		BeanDesc beanDesc = BeanDescFactory.getBeanDesc(entity.getClass());

		for(int i=0;i<beanDesc.getPropertyDescSize();i++){
			PropertyDesc pd = beanDesc.getPropertyDesc(i);
			if(pd.getAnnotation(PrimaryKey.class) != null && pd.isReadable()){
				if(!params.isEmpty()){
					sb.append(" AND ");
				}
				sb.append(nameConverter.propertyToColumn(pd.getPropertyName())).append("=?");
				try {
					params.add(pd.getValue(entity));
				} catch(Exception ex){
					throw new RuntimeException(ex);
				}
				hasPrimaryKey = true;
			}
		}

		if(hasPrimaryKey == false){
			throw new RuntimeException(
					"Primary key is not found: " + entity.getClass().getName());
		}

		return executor.executeUpdateSql(sb.toString(), params.toArray());
	}

	@Override
	public int insertEntity(Object entity) {
		List<Object> params = new ArrayList<Object>();
		StringBuilder sb = new StringBuilder();
		BeanDesc beanDesc = BeanDescFactory.getBeanDesc(entity.getClass());

		sb.append("INSERT INTO ").append(nameConverter.entityToTable(entity.getClass().getName())).append(" (");
		{
			int count = 0;
			for(int i=0;i<beanDesc.getPropertyDescSize();i++){
				PropertyDesc pd = beanDesc.getPropertyDesc(i);
				PrimaryKey primaryKey = pd.getAnnotation(PrimaryKey.class);
				if((primaryKey == null || primaryKey.persistent()) && !pd.isTransient() && pd.isReadable() ){
					if(count != 0){
						sb.append(", ");
					}
					sb.append(nameConverter.propertyToColumn(pd.getPropertyName()));
					count++;
				}
			}
		}
		sb.append(") VALUES (");
		{
			int count = 0;
			for(int i=0;i<beanDesc.getPropertyDescSize();i++){
				PropertyDesc pd = beanDesc.getPropertyDesc(i);
				PrimaryKey primaryKey = pd.getAnnotation(PrimaryKey.class);
				if((primaryKey == null || primaryKey.persistent()) && !pd.isTransient() && pd.isReadable() ){
					if(count != 0){
						sb.append(", ");
					}
					sb.append("?");

					try {
						params.add(pd.getValue(entity));
					} catch(Exception ex){
						throw new RuntimeException(ex);
					}

					count++;
				}
			}
		}
		sb.append(")");

		return executor.executeUpdateSql(sb.toString(), params.toArray());
	}

	@Override
	public int updateEntity(Object entity) {
		List<Object> params = new ArrayList<Object>();
		StringBuilder sb = new StringBuilder();

		sb.append("UPDATE ").append(nameConverter.entityToTable(entity.getClass().getName())).append(" SET ");

		BeanDesc beanDesc = BeanDescFactory.getBeanDesc(entity.getClass());
		{
			int count = 0;
			for (int i = 0; i < beanDesc.getPropertyDescSize(); i++) {
				PropertyDesc pd = beanDesc.getPropertyDesc(i);
				if(pd.getAnnotation(PrimaryKey.class) == null && !pd.isTransient() && pd.isReadable() ){
					if (count != 0) {
						sb.append(", ");
					}
					sb.append(nameConverter.propertyToColumn(pd.getPropertyName())).append(" = ?");
					try {
						params.add(pd.getValue(entity));
					} catch (Exception ex) {
						throw new RuntimeException(ex);
					}
					count++;
				}
			}
		}
		sb.append(" WHERE ");
		{
			int count = 0;
			for (int i = 0; i < beanDesc.getPropertyDescSize(); i++) {
				PropertyDesc pd = beanDesc.getPropertyDesc(i);
				if(pd.getAnnotation(PrimaryKey.class) != null && pd.isReadable() ){
					if(count != 0){
						sb.append(" AND ");
					}
					sb.append(nameConverter.propertyToColumn(pd.getPropertyName())).append(" = ? ");
					try {
						params.add(pd.getValue(entity));
					} catch(Exception ex){
						throw new RuntimeException(ex);
					}
					count++;
				}
			}
			if(count == 0){
				throw new RuntimeException(
						"Primary key is not found: " + entity.getClass().getName());
			}
		}

		return executor.executeUpdateSql(sb.toString(), params.toArray());
	}

	@Override
	public <T> T findEntity(Class<T> clazz, Object... id) {
		// assemble SQL
		StringBuilder sb = new StringBuilder();
		BeanDesc beanDesc = BeanDescFactory.getBeanDesc(clazz);

		sb.append("SELECT * FROM ");
		sb.append(nameConverter.entityToTable(clazz.getName()));
		sb.append(" WHERE ");

		int count = 0;

		for(int i=0; i<beanDesc.getPropertyDescSize(); i++){
			PropertyDesc pd = beanDesc.getPropertyDesc(i);
			if(pd.getAnnotation(PrimaryKey.class) != null){
				if(count != 0){
					sb.append(" AND ");
				}
				sb.append(nameConverter.propertyToColumn(pd.getPropertyName()));
				sb.append(" = ?");
				count++;
			}
		}
		if(count == 0){
			throw new RuntimeException(
					"Primary key is not found: " + clazz.getName());
		}

		// execute SQL
		return executor.getSingleResult(clazz, sb.toString(), 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 parameter){
		String sql = toCallString(functionName, parameter, true);
		return callExecutor.call(resultClass, sql, parameter);
	}

	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 parameter){
		String sql = toCallString(functionName, parameter, true);
		return callExecutor.callForList(resultClass, sql, parameter);
	}

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

}
