package org.seasar.dao.impl;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.seasar.dao.BeanMetaData;
import org.seasar.dao.DaoMetaData;
import org.seasar.dao.DaoNotFoundRuntimeException;
import org.seasar.dao.Dbms;
import org.seasar.dao.DtoMetaData;
import org.seasar.dao.IllegalSignatureRuntimeException;
import org.seasar.dao.SqlCommand;
import org.seasar.dao.dbms.DbmsManager;
import org.seasar.extension.jdbc.PropertyType;
import org.seasar.extension.jdbc.ResultSetFactory;
import org.seasar.extension.jdbc.ResultSetHandler;
import org.seasar.extension.jdbc.impl.ObjectResultSetHandler;
import org.seasar.framework.beans.BeanDesc;
import org.seasar.framework.beans.MethodNotFoundRuntimeException;
import org.seasar.framework.beans.factory.BeanDescFactory;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.ConnectionUtil;
import org.seasar.framework.util.DataSourceUtil;
import org.seasar.framework.util.FieldUtil;
import org.seasar.framework.util.MethodUtil;
import org.seasar.framework.util.ResourceUtil;
import org.seasar.framework.util.StringUtil;
import org.seasar.framework.util.TextUtil;

/**
 * @author higa
 *  
 */
public class DaoMetaDataImpl implements DaoMetaData {

	private static final String[] INSERT_NAMES = new String[] { "insert",
			"create", "add" };

	private static final String[] UPDATE_NAMES = new String[] { "update",
			"modify", "store" };

	private static final String[] DELETE_NAMES = new String[] { "delete",
			"remove" };

	private Class daoClass_;

	private BeanDesc daoBeanDesc_;

	private DataSource dataSource_;

	private ResultSetFactory resultSetFactory_;

	private Dbms dbms_;

	private Class beanClass_;

	private BeanMetaData beanMetaData_;

	private Map sqlCommands_ = new HashMap();

	public DaoMetaDataImpl(Class daoClass, DataSource dataSource,
			ResultSetFactory resultSetFactory) {

		daoClass_ = daoClass;
		daoBeanDesc_ = BeanDescFactory.getBeanDesc(daoClass);
		Field beanField = daoBeanDesc_.getField(BEAN);
		beanClass_ = (Class) FieldUtil.get(beanField, null);
		dataSource_ = dataSource;
		resultSetFactory_ = resultSetFactory;
		Connection con = DataSourceUtil.getConnection(dataSource_);
		try {
			DatabaseMetaData dbMetaData = ConnectionUtil.getMetaData(con);
			dbms_ = DbmsManager.getDbms(dbMetaData);
			beanMetaData_ = new BeanMetaDataImpl(beanClass_, dbMetaData, dbms_);
		} finally {
			ConnectionUtil.close(con);
		}
		setupSqlCommand();
	}

	private void setupSqlCommand() {
		String[] names = daoBeanDesc_.getMethodNames();
		for (int i = 0; i < names.length; ++i) {
			Method[] methods = daoBeanDesc_.getMethods(names[i]);
			if (methods.length == 1 && MethodUtil.isAbstract(methods[0])) {
				setupMethod(methods[0]);
			}
		}
	}

	private void setupMethod(Method method) {
		String base = daoClass_.getName().replace('.', '/') + "_"
				+ method.getName();
		String dbmsPath = base + dbms_.getSuffix() + ".sql";
		String standardPath = base + ".sql";
		if (ResourceUtil.isExist(dbmsPath)) {
			String sql = TextUtil.readText(dbmsPath);
			setupMethodByManual(method, sql);
		} else if (ResourceUtil.isExist(standardPath)) {
			String sql = TextUtil.readText(standardPath);
			setupMethodByManual(method, sql);
		} else {
			setupMethodByAuto(method);
		}
	}

	private void setupMethodByManual(Method method, String sql) {
		if (isSelect(method)) {
			setupSelectMethodByManual(method, sql);
		} else {
			setupUpdateMethodByManual(method, sql);
		}
	}

	private void setupMethodByAuto(Method method) {
		if (isInsert(method.getName())) {
			setupInsertMethodByAuto(method);
		} else if (isUpdate(method.getName())) {
			setupUpdateMethodByAuto(method);
		} else if (isDelete(method.getName())) {
			setupDeleteMethodByAuto(method);
		} else {
			setupSelectMethodByAuto(method);
		}
	}

	private void setupSelectMethodByManual(Method method, String sql) {
		SelectDynamicCommand cmd = createSelectDynamicCommand(createResultSetHandler(method));
		cmd.setSql(sql);
		cmd.setArgNames(getArgNames(method.getName()));
		sqlCommands_.put(method.getName(), cmd);
	}

	private SelectDynamicCommand createSelectDynamicCommand(ResultSetHandler rsh) {
		return new SelectDynamicCommand(dataSource_, rsh, resultSetFactory_);
	}

	private SelectDynamicCommand createSelectDynamicCommand(
			ResultSetHandler resultSetHandler, String query) {

		SelectDynamicCommand cmd = createSelectDynamicCommand(resultSetHandler);
		StringBuffer buf = new StringBuffer(255);
		if (startsWithSelect(query)) {
			buf.append(query);
		} else {
			String sql = dbms_.getAutoSelectSql(getBeanMetaData());
			buf.append(sql);
			if (query != null) {
				if (startsWithOrderBy(query)) {
					buf.append(" ");
				} else if (sql.lastIndexOf("WHERE") < 0) {
					buf.append(" WHERE ");
				} else {
					buf.append(" AND ");
				}
				buf.append(query);
			}
		}
		cmd.setSql(buf.toString());
		return cmd;
	}

	private static boolean startsWithSelect(String query) {
		return StringUtil.startsWith(query, "select");
	}

	private static boolean startsWithOrderBy(String query) {
		return StringUtil.startsWith(query, "order by");
	}

	private ResultSetHandler createResultSetHandler(Method method) {
		if (method.getReturnType().isAssignableFrom(List.class)) {
			return new BeanListMetaDataResultSetHandler(beanMetaData_);
		} else if (method.getReturnType().isAssignableFrom(beanClass_)) {
			return new BeanMetaDataResultSetHandler(beanMetaData_);
		} else if (method.getReturnType().isAssignableFrom(
				Array.newInstance(beanClass_, 0).getClass())) {
			return new BeanArrayMetaDataResultSetHandler(beanMetaData_);
		} else {
			return new ObjectResultSetHandler();
		}
	}

	private void setupUpdateMethodByManual(Method method, String sql) {
		UpdateDynamicCommand cmd = new UpdateDynamicCommand(dataSource_);
		cmd.setSql(sql);
		String[] argNames = getArgNames(method.getName());
		if (argNames.length == 0 && method.getParameterTypes().length == 1
				&& method.getParameterTypes()[0].isAssignableFrom(beanClass_)) {
			argNames = new String[] { StringUtil.decapitalize(ClassUtil
					.getShortClassName(beanClass_)) };
		}
		cmd.setArgNames(argNames);
		sqlCommands_.put(method.getName(), cmd);
	}

	private void setupInsertMethodByAuto(Method method) {
		checkAutoUpdateMethod(method);
		String[] propertyNames = getPersistentPropertyNames(method.getName());
		SqlCommand cmd = null;
		if (method.getParameterTypes()[0].isAssignableFrom(beanClass_)) {
			cmd = new InsertAutoStaticCommand(dataSource_, beanMetaData_,
					propertyNames);
		} else {
			cmd = new InsertBatchAutoStaticCommand(dataSource_, beanMetaData_,
					propertyNames);
		}
		sqlCommands_.put(method.getName(), cmd);
	}

	private void setupUpdateMethodByAuto(Method method) {
		checkAutoUpdateMethod(method);
		String[] propertyNames = getPersistentPropertyNames(method.getName());
		SqlCommand cmd = null;
		if (method.getParameterTypes()[0].isAssignableFrom(beanClass_)) {
			cmd = new UpdateAutoStaticCommand(dataSource_, beanMetaData_,
					propertyNames);
		} else {
			cmd = new UpdateBatchAutoStaticCommand(dataSource_, beanMetaData_,
					propertyNames);
		}
		sqlCommands_.put(method.getName(), cmd);
	}

	private void setupDeleteMethodByAuto(Method method) {
		checkAutoUpdateMethod(method);
		String[] propertyNames = getPersistentPropertyNames(method.getName());
		SqlCommand cmd = null;
		if (method.getParameterTypes()[0].isAssignableFrom(beanClass_)) {
			cmd = new DeleteAutoStaticCommand(dataSource_, beanMetaData_,
					propertyNames);
		} else {
			cmd = new DeleteBatchAutoStaticCommand(dataSource_, beanMetaData_,
					propertyNames);
		}
		sqlCommands_.put(method.getName(), cmd);
	}

	private String[] getPersistentPropertyNames(String methodName) {
		List names = new ArrayList();
		String fieldName = methodName + NO_PERSISTENT_PROPS_SUFFIX;
		if (daoBeanDesc_.hasField(fieldName)) {
			Field field = daoBeanDesc_.getField(fieldName);
			String s = (String) FieldUtil.get(field, null);
			String[] props = StringUtil.split(s, ", ");
			for (int i = 0; i < beanMetaData_.getPropertyTypeSize(); ++i) {
				PropertyType pt = beanMetaData_.getPropertyType(i);
				if (pt.isPersistent()
						&& !isPropertyExist(props, pt.getPropertyName())) {
					names.add(pt.getPropertyName());
				}
			}
		} else {
			fieldName = methodName + PERSISTENT_PROPS_SUFFIX;
			if (daoBeanDesc_.hasField(fieldName)) {
				Field field = daoBeanDesc_.getField(fieldName);
				String s = (String) FieldUtil.get(field, null);
				String[] props = StringUtil.split(s, ", ");
				names.addAll(Arrays.asList(props));
				for (int i = 0; i < beanMetaData_.getPrimaryKeySize(); ++i) {
					String pk = beanMetaData_.getPrimaryKey(i);
					PropertyType pt = beanMetaData_
							.getPropertyTypeByColumnName(pk);
					names.add(pt.getPropertyName());
				}
				if (beanMetaData_.hasVersionNoPropertyType()) {
					names.add(BeanMetaData.VERSION_NO_PROPERTY_NAME);
				}
				if (beanMetaData_.hasTimestampPropertyType()) {
					names.add(BeanMetaData.TIMESTAMP_PROPERTY_NAME);
				}
			}
		}
		if (names.size() == 0) {
			for (int i = 0; i < beanMetaData_.getPropertyTypeSize(); ++i) {
				PropertyType pt = beanMetaData_.getPropertyType(i);
				if (pt.isPersistent()) {
					names.add(pt.getPropertyName());
				}
			}
		}
		return (String[]) names.toArray(new String[names.size()]);
	}
	
	private boolean isPropertyExist(String[] props, String propertyName) {
		for (int i = 0; i < props.length; ++i) {
			if (props[i].equalsIgnoreCase(propertyName)) {
				return true;
			}
		}
		return false;
	}

	private void setupSelectMethodByAuto(Method method) {
		String query = getQuery(method.getName());
		ResultSetHandler handler = createResultSetHandler(method);
		SelectDynamicCommand cmd = null;
		String[] argNames = getArgNames(method.getName());
		if (query != null && !startsWithOrderBy(query)) {
			cmd = createSelectDynamicCommand(handler, query);
		} else {
			cmd = createSelectDynamicCommand(handler);
			String sql = null;
			if (argNames.length == 0 && method.getParameterTypes().length == 1) {
				argNames = new String[] { "dto" };
				sql = createAutoSelectSqlByDto(method.getParameterTypes()[0]);
			} else {
				sql = createAutoSelectSql(argNames);
			}
			if (query != null) {
				sql = sql + " " + query;
			}
			cmd.setSql(sql);
		}
		cmd.setArgNames(argNames);
		sqlCommands_.put(method.getName(), cmd);
	}

	private String createAutoSelectSqlByDto(Class dtoClass) {
		String sql = dbms_.getAutoSelectSql(getBeanMetaData());
		StringBuffer buf = new StringBuffer(sql);
		DtoMetaData dmd = new DtoMetaDataImpl(dtoClass);
		boolean began = false;
		if (!(sql.lastIndexOf("WHERE") > 0)) {
			buf.append("/*BEGIN*/ WHERE ");
			began = true;
		}
		for (int i = 0; i < dmd.getPropertyTypeSize(); ++i) {
			PropertyType pt = dmd.getPropertyType(i);
			String aliasName = pt.getColumnName();
			if (!beanMetaData_.hasPropertyTypeByAliasName(aliasName)) {
				continue;
			}
			if (!beanMetaData_.getPropertyTypeByAliasName(aliasName)
					.isPersistent()) {
				continue;
			}
			String columnName = beanMetaData_.convertFullColumnName(aliasName);
			String propertyName = "dto." + pt.getPropertyName();
			buf.append("/*IF ");
			buf.append(propertyName);
			buf.append(" != null*/");
			buf.append(" ");
			if (!began || i != 0) {
				buf.append("AND ");
			}
			buf.append(columnName);
			buf.append(" = /*");
			buf.append(propertyName);
			buf.append("*/null");
			buf.append("/*END*/");
		}
		if (began) {
			buf.append("/*END*/");
		}
		return buf.toString();
	}

	private String createAutoSelectSql(String[] argNames) {
		String sql = dbms_.getAutoSelectSql(getBeanMetaData());
		StringBuffer buf = new StringBuffer(sql);
		if (argNames.length != 0) {
			boolean began = false;
			if (!(sql.lastIndexOf("WHERE") > 0)) {
				buf.append("/*BEGIN*/ WHERE ");
				began = true;
			}
			for (int i = 0; i < argNames.length; ++i) {
				String columnName = beanMetaData_
						.convertFullColumnName(argNames[i]);
				buf.append("/*IF ");
				buf.append(argNames[i]);
				buf.append(" != null*/");
				buf.append(" ");
				if (!began || i != 0) {
					buf.append("AND ");
				}
				buf.append(columnName);
				buf.append(" = /*");
				buf.append(argNames[i]);
				buf.append("*/null");
				buf.append("/*END*/");
			}
			if (began) {
				buf.append("/*END*/");
			}
		}
		return buf.toString();
	}

	private void checkAutoUpdateMethod(Method method) {
		if (method.getParameterTypes().length != 1
				|| !method.getParameterTypes()[0].isAssignableFrom(beanClass_)
				&& !method.getParameterTypes()[0].isAssignableFrom(List.class)
				&& !method.getParameterTypes()[0].isArray()) {
			throw new IllegalSignatureRuntimeException("EDAO0006", method
					.toString());
		}
	}

	private boolean isSelect(Method method) {
		if (isInsert(method.getName())) {
			return false;
		}
		if (isUpdate(method.getName())) {
			return false;
		}
		if (isDelete(method.getName())) {
			return false;
		}
		return true;
	}

	private boolean isInsert(String methodName) {
		for (int i = 0; i < INSERT_NAMES.length; ++i) {
			if (methodName.startsWith(INSERT_NAMES[i])) {
				return true;
			}
		}
		return false;
	}

	private boolean isUpdate(String methodName) {
		for (int i = 0; i < UPDATE_NAMES.length; ++i) {
			if (methodName.startsWith(UPDATE_NAMES[i])) {
				return true;
			}
		}
		return false;
	}

	private boolean isDelete(String methodName) {
		for (int i = 0; i < DELETE_NAMES.length; ++i) {
			if (methodName.startsWith(DELETE_NAMES[i])) {
				return true;
			}
		}
		return false;
	}

	private String[] getArgNames(String methodName) {
		String argsKey = methodName + ARGS_SUFFIX;
		if (daoBeanDesc_.hasField(argsKey)) {
			Field argNamesField = daoBeanDesc_.getField(argsKey);
			String argNames = (String) FieldUtil.get(argNamesField, null);
			return StringUtil.split(argNames, " ,");
		} else {
			return new String[0];
		}
	}

	private String getQuery(String methodName) {
		String key = methodName + QUERY_SUFFIX;
		if (daoBeanDesc_.hasField(key)) {
			Field queryField = daoBeanDesc_.getField(key);
			return (String) FieldUtil.get(queryField, null);
		} else {
			return null;
		}
	}

	/**
	 * @see org.seasar.dao.DaoMetaData#getBeanClass()
	 */
	public Class getBeanClass() {
		return daoClass_;
	}

	/**
	 * @see org.seasar.dao.DaoMetaData#getBeanMetaData()
	 */
	public BeanMetaData getBeanMetaData() {
		return beanMetaData_;
	}

	/**
	 * @see org.seasar.dao.DaoMetaData#getSqlCommand(java.lang.String)
	 */
	public SqlCommand getSqlCommand(String methodName)
			throws MethodNotFoundRuntimeException {

		SqlCommand cmd = (SqlCommand) sqlCommands_.get(methodName);
		if (cmd == null) {
			throw new MethodNotFoundRuntimeException(daoClass_, methodName,
					null);
		}
		return cmd;
	}

	/**
	 * @see org.seasar.dao.DaoMetaData#hasSqlCommand(java.lang.String)
	 */
	public boolean hasSqlCommand(String methodName) {
		return sqlCommands_.containsKey(methodName);
	}

	/**
	 * @see org.seasar.dao.DaoMetaData#createFindCommand(java.lang.String)
	 */
	public SqlCommand createFindCommand(String query) {
		return createSelectDynamicCommand(new BeanListMetaDataResultSetHandler(
				beanMetaData_), query);
	}

	public SqlCommand createFindArrayCommand(String query) {
		return createSelectDynamicCommand(
				new BeanArrayMetaDataResultSetHandler(beanMetaData_), query);
	}

	/**
	 * @see org.seasar.dao.DaoMetaData#createFindBeanCommand(java.lang.String)
	 */
	public SqlCommand createFindBeanCommand(String query) {
		return createSelectDynamicCommand(new BeanMetaDataResultSetHandler(
				beanMetaData_), query);
	}

	/**
	 * @see org.seasar.dao.DaoMetaData#createFindObjectCommand(java.lang.String)
	 */
	public SqlCommand createFindObjectCommand(String query) {
		return createSelectDynamicCommand(new ObjectResultSetHandler(), query);
	}

	public static Class getDaoInterface(Class clazz) {
		if (clazz.isInterface()) {
			return clazz;
		}
		for (Class target = clazz; target != AbstractDao.class; target = target
				.getSuperclass()) {
			Class[] interfaces = target.getInterfaces();
			for (int i = 0; i < interfaces.length; ++i) {
				Class intf = interfaces[i];
				if (intf.getName().endsWith("Dao")) {
					return intf;
				}
			}
		}
		throw new DaoNotFoundRuntimeException(clazz);
	}

	public void setDbms(Dbms dbms) {
		dbms_ = dbms;
	}
}