package org.seasar.dao.impl;

import java.lang.reflect.Field;
import java.sql.DatabaseMetaData;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.seasar.dao.BeanMetaData;
import org.seasar.dao.Dbms;
import org.seasar.dao.IdentifierGenerator;
import org.seasar.dao.PrimaryKeyNotFoundRuntimeException;
import org.seasar.dao.RelationPropertyType;
import org.seasar.dao.id.IdentifierGeneratorFactory;
import org.seasar.extension.jdbc.ColumnNotFoundRuntimeException;
import org.seasar.extension.jdbc.PropertyType;
import org.seasar.framework.beans.BeanDesc;
import org.seasar.framework.beans.PropertyDesc;
import org.seasar.framework.beans.PropertyNotFoundRuntimeException;
import org.seasar.framework.beans.factory.BeanDescFactory;
import org.seasar.framework.util.CaseInsensitiveMap;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.DatabaseMetaDataUtil;
import org.seasar.framework.util.FieldUtil;
import org.seasar.framework.util.StringUtil;

/**
 * @author higa
 *  
 */
public class BeanMetaDataImpl extends DtoMetaDataImpl implements BeanMetaData {

	private String tableName_;

	private Map propertyTypesByColumnName_ = new CaseInsensitiveMap();

	private List relationPropertyTypes_ = new ArrayList();

	private String[] primaryKeys_ = new String[0];

	private String autoInsertSql_;

	private String autoUpdateSql_;

	private String autoDeleteSql_;

	private String autoSelectList_;

	private boolean relation_;
	
	private IdentifierGenerator identifierGenerator_;

	public BeanMetaDataImpl(Class beanClass, DatabaseMetaData dbMetaData, Dbms dbms) {
		this(beanClass, dbMetaData, dbms, false);
	}

	public BeanMetaDataImpl(Class beanClass, DatabaseMetaData dbMetaData,
			Dbms dbms, boolean relation) {

		setBeanClass(beanClass);
		relation_ = relation;
		BeanDesc beanDesc = BeanDescFactory.getBeanDesc(beanClass);
		setupTableName(beanDesc);
		setupProperty(beanDesc, dbMetaData, dbms);
		setupDatabaseMetaData(beanDesc, dbMetaData, dbms);
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getTableName()
	 */
	public String getTableName() {
		return tableName_;
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getVersionNoPropertyType()
	 */
	public PropertyType getVersionNoPropertyType()
			throws PropertyNotFoundRuntimeException {

		return getPropertyType(VERSION_NO_PROPERTY_NAME);
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getTimestampPropertyType()
	 */
	public PropertyType getTimestampPropertyType()
			throws PropertyNotFoundRuntimeException {

		return getPropertyType(TIMESTAMP_PROPERTY_NAME);
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getPropertyTypeByColumnName(java.lang.String)
	 */
	public PropertyType getPropertyTypeByColumnName(String columnName)
			throws ColumnNotFoundRuntimeException {

		PropertyType propertyType = (PropertyType) propertyTypesByColumnName_
				.get(columnName);
		if (propertyType == null) {
			throw new ColumnNotFoundRuntimeException(tableName_, columnName);
		}
		return propertyType;
	}

	public PropertyType getPropertyTypeByAliasName(String alias) {
		if (hasPropertyTypeByColumnName(alias)) {
			return getPropertyTypeByColumnName(alias);
		}
		int index = alias.lastIndexOf('_');
		if (index < 0) {
			throw new ColumnNotFoundRuntimeException(tableName_, alias);
		}
		String columnName = alias.substring(0, index);
		String relnoStr = alias.substring(index + 1);
		int relno = -1;
		try {
			relno = Integer.parseInt(relnoStr);
		} catch (Throwable t) {
			throw new ColumnNotFoundRuntimeException(tableName_, alias);
		}
		RelationPropertyType rpt = getRelationPropertyType(relno);
		if (!rpt.getBeanMetaData().hasPropertyTypeByColumnName(columnName)) {
			throw new ColumnNotFoundRuntimeException(tableName_, alias);
		}
		return rpt.getBeanMetaData().getPropertyTypeByColumnName(columnName);
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#hasPropertyTypeByColumnName(java.lang.String)
	 */
	public boolean hasPropertyTypeByColumnName(String columnName) {
		return propertyTypesByColumnName_.get(columnName) != null;
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#hasPropertyTypeByAliasName(java.lang.String)
	 */
	public boolean hasPropertyTypeByAliasName(String alias) {
		if (hasPropertyTypeByColumnName(alias)) {
			return true;
		}
		int index = alias.lastIndexOf('_');
		if (index < 0) {
			return false;
		}
		String columnName = alias.substring(0, index);
		String relnoStr = alias.substring(index + 1);
		int relno = -1;
		try {
			relno = Integer.parseInt(relnoStr);
		} catch (Throwable t) {
			return false;
		}
		if (relno >= getRelationPropertyTypeSize()) {
			return false;
		}
		RelationPropertyType rpt = getRelationPropertyType(relno);
		return rpt.getBeanMetaData().hasPropertyTypeByColumnName(columnName);
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#hasVersionNoPropertyType()
	 */
	public boolean hasVersionNoPropertyType() {
		return hasPropertyType(VERSION_NO_PROPERTY_NAME);
	}
	
	/**
	 * @see org.seasar.dao.BeanMetaData#hasTimestampPropertyType()
	 */
	public boolean hasTimestampPropertyType() {
		return hasPropertyType(TIMESTAMP_PROPERTY_NAME);
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#convertFullColumnName(java.lang.String)
	 */
	public String convertFullColumnName(String alias) {
		if (hasPropertyTypeByColumnName(alias)) {
			return tableName_ + "." + alias;
		}
		int index = alias.lastIndexOf('_');
		if (index < 0) {
			throw new ColumnNotFoundRuntimeException(tableName_, alias);
		}
		String columnName = alias.substring(0, index);
		String relnoStr = alias.substring(index + 1);
		int relno = -1;
		try {
			relno = Integer.parseInt(relnoStr);
		} catch (Throwable t) {
			throw new ColumnNotFoundRuntimeException(tableName_, alias);
		}
		RelationPropertyType rpt = getRelationPropertyType(relno);
		if (!rpt.getBeanMetaData().hasPropertyTypeByColumnName(columnName)) {
			throw new ColumnNotFoundRuntimeException(tableName_, alias);
		}
		return rpt.getPropertyName() + "." + columnName;
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getRelationPropertyTypeSize()
	 */
	public int getRelationPropertyTypeSize() {
		return relationPropertyTypes_.size();
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getRelationPropertyType(int)
	 */
	public RelationPropertyType getRelationPropertyType(int index) {
		return (RelationPropertyType) relationPropertyTypes_.get(index);
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getRelationPropertyType(java.lang.String)
	 */
	public RelationPropertyType getRelationPropertyType(String propertyName)
			throws PropertyNotFoundRuntimeException {

		for (int i = 0; i < getRelationPropertyTypeSize(); i++) {
			RelationPropertyType rpt = (RelationPropertyType) relationPropertyTypes_
					.get(i);
			if (rpt != null
					&& rpt.getPropertyName().equalsIgnoreCase(propertyName)) {
				return rpt;
			}
		}
		throw new PropertyNotFoundRuntimeException(getBeanClass(), propertyName);
	}

	private void setupTableName(BeanDesc beanDesc) {
		if (beanDesc.hasField(TABLE_KEY)) {
			Field field = beanDesc.getField(TABLE_KEY);
			tableName_ = (String) FieldUtil.get(field, null);
		} else {
			tableName_ = ClassUtil.getShortClassName(getBeanClass());
		}
	}

	private void setupProperty(BeanDesc beanDesc,
			DatabaseMetaData dbMetaData, Dbms dbms) {
		
		for (int i = 0; i < beanDesc.getPropertyDescSize(); ++i) {
			PropertyDesc pd = beanDesc.getPropertyDesc(i);
			PropertyType pt = null;
			String relnoKey = pd.getPropertyName() + RELNO_KEY_SUFFIX;
			if (beanDesc.hasField(relnoKey)) {
				if (!relation_) {
					RelationPropertyType rpt = createRelationPropertyType(
							beanDesc, pd, relnoKey, dbMetaData, dbms);
					addRelationPropertyType(rpt);
				}
			} else {
				pt = createPropertyType(beanDesc, pd);
				addPropertyType(pt);
			}
			if (identifierGenerator_ == null) {
				String idKey = pd.getPropertyName() + ID_KEY_SUFFIX;
				if (beanDesc.hasField(idKey)) {
					Field field = beanDesc.getField(idKey);
					String idAnnotation = (String) FieldUtil.get(field, null);
					identifierGenerator_ = IdentifierGeneratorFactory.createIdentifierGenerator(pd.getPropertyName(), dbms, idAnnotation);
					primaryKeys_ = new String[]{pd.getPropertyName()};
					pt.setPrimaryKey(true);
				}
			}
		}
	}

	private void setupDatabaseMetaData(BeanDesc beanDesc, DatabaseMetaData dbMetaData, Dbms dbms) {
		setupPrimaryKey(dbMetaData, dbms);
		setupPropertyPersistent(beanDesc, dbMetaData);
		setupAutoInsertSql();
		setupAutoUpdateSql();
		setupAutoDeleteSql();
	}
	
	private void setupPrimaryKey(DatabaseMetaData dbMetaData, Dbms dbms) {
		if (identifierGenerator_ == null) {
			List pkeyList = new ArrayList();
			Set primaryKeySet = DatabaseMetaDataUtil.getPrimaryKeySet(dbMetaData,
					tableName_);
			if (primaryKeySet.size() == 0) {
				throw new PrimaryKeyNotFoundRuntimeException(getBeanClass());
			}
			for (int i = 0; i < getPropertyTypeSize(); ++i) {
				PropertyType pt = getPropertyType(i);
				if (primaryKeySet.contains(pt.getColumnName())) {
					pt.setPrimaryKey(true);
					pkeyList.add(pt.getColumnName());
				} else {
					pt.setPrimaryKey(false);
				}
			}
			primaryKeys_ = (String[]) pkeyList
					.toArray(new String[pkeyList.size()]);
			identifierGenerator_ = IdentifierGeneratorFactory.createIdentifierGenerator(null, dbms);
		}
	}
	
	private void setupPropertyPersistent(BeanDesc beanDesc, DatabaseMetaData dbMetaData) {
		if (beanDesc.hasField(NO_PERSISTENT_COLUMNS_KEY)) {
			Field field = beanDesc.getField(NO_PERSISTENT_COLUMNS_KEY);
			String str = (String) FieldUtil.get(field, null);
			String[] columns = StringUtil.split(str, ", ");
			for (int i = 0; i < columns.length; ++i) {
				PropertyType pt = getPropertyType(columns[i].trim());
				pt.setPersistent(false);
			}
		} else {
			Set columnSet = DatabaseMetaDataUtil.getColumnSet(dbMetaData,
					tableName_);
			for (int i = 0; i < getPropertyTypeSize(); ++i) {
				PropertyType pt = getPropertyType(i);
				if (!columnSet.contains(pt.getColumnName())) {
					pt.setPersistent(false);
				}
			}
		}
	}

	private RelationPropertyType createRelationPropertyType(BeanDesc beanDesc,
			PropertyDesc propertyDesc, String relnoKey,
			DatabaseMetaData dbMetaData, Dbms dbms) {

		Field field = beanDesc.getField(relnoKey);
		String[] myKeys = new String[0];
		String[] yourKeys = new String[0];
		int relno = FieldUtil.getInt(field, null);
		String relkeysKey = propertyDesc.getPropertyName() + RELKEYS_KEY_SUFFIX;
		if (beanDesc.hasField(relkeysKey)) {
			Field field2 = beanDesc.getField(relkeysKey);
			String relkeys = (String) FieldUtil.get(field2, null);
			StringTokenizer st = new StringTokenizer(relkeys, " \t\n\r\f,");
			List myKeyList = new ArrayList();
			List yourKeyList = new ArrayList();
			while (st.hasMoreTokens()) {
				String token = st.nextToken();
				int index = token.indexOf(':');
				if (index > 0) {
					myKeyList.add(token.substring(0, index));
					yourKeyList.add(token.substring(index + 1));
				} else {
					myKeyList.add(token);
					yourKeyList.add(token);
				}
			}
			myKeys = (String[]) myKeyList.toArray(new String[myKeyList.size()]);
			yourKeys = (String[]) yourKeyList.toArray(new String[yourKeyList
					.size()]);
		}
		RelationPropertyType rpt = new RelationPropertyTypeImpl(propertyDesc,
				relno, myKeys, yourKeys, dbMetaData, dbms);
		return rpt;
	}

	private void addRelationPropertyType(RelationPropertyType rpt) {
		for (int i = relationPropertyTypes_.size(); i <= rpt.getRelationNo(); ++i) {
			relationPropertyTypes_.add(null);
		}
		relationPropertyTypes_.set(rpt.getRelationNo(), rpt);
	}

	protected void addPropertyType(PropertyType propertyType) {
		super.addPropertyType(propertyType);
		propertyTypesByColumnName_.put(propertyType.getColumnName(),
				propertyType);
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getPrimaryKeySize()
	 */
	public int getPrimaryKeySize() {
		return primaryKeys_.length;
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getPrimaryKey(int)
	 */
	public String getPrimaryKey(int index) {
		return primaryKeys_[index];
	}
	
	public IdentifierGenerator getIdentifierGenerator() {
		return identifierGenerator_;
	}

	private void setupAutoInsertSql() {
		StringBuffer buf = new StringBuffer(100);
		buf.append("INSERT INTO ");
		buf.append(getTableName());
		buf.append(" (");
		int persistentColumnCount = 0;
		for (int i = 0; i < getPropertyTypeSize(); ++i) {
			PropertyType pt = getPropertyType(i);
			if (!pt.isPersistent()) {
				continue;
			}
			if (pt.isPrimaryKey() && !identifierGenerator_.isSelfGererate()) {
				continue;
			}
			++persistentColumnCount;
			buf.append(pt.getColumnName());
			buf.append(", ");
		}
		buf.setLength(buf.length() - 2);
		buf.append(") VALUES(");
		for (int i = 0; i < persistentColumnCount; ++i) {
			buf.append("?, ");
		}
		buf.setLength(buf.length() - 2);
		buf.append(")");
		autoInsertSql_ = buf.toString();
	}

	protected void setupAutoUpdateSql() {
		StringBuffer buf = new StringBuffer(100);
		buf.append("UPDATE ");
		buf.append(getTableName());
		buf.append(" SET ");
		for (int i = 0; i < getPropertyTypeSize(); ++i) {
			PropertyType pt = getPropertyType(i);
			if (pt.isPersistent() && !pt.isPrimaryKey()) {
				if (pt.getPropertyName().equals(VERSION_NO_PROPERTY_NAME)) {
					buf.append(pt.getColumnName());
					buf.append(" = ");
					buf.append(pt.getColumnName());
					buf.append(" + 1, ");
				} else {
					buf.append(pt.getColumnName());
					buf.append(" = ?, ");
				}
			}
		}
		buf.setLength(buf.length() - 2);
		setupAutoUpdateWhere(buf);
		autoUpdateSql_ = buf.toString();
	}

	protected void setupAutoDeleteSql() {
		StringBuffer buf = new StringBuffer(100);
		buf.append("DELETE FROM ");
		buf.append(getTableName());
		setupAutoUpdateWhere(buf);
		autoDeleteSql_ = buf.toString();
	}

	protected void setupAutoUpdateWhere(StringBuffer buf) {
		buf.append(" WHERE ");
		for (int i = 0; i < getPrimaryKeySize(); ++i) {
			buf.append(getPrimaryKey(i));
			buf.append(" = ? AND ");
		}
		buf.setLength(buf.length() - 5);
		if (hasVersionNoPropertyType()) {
			PropertyType pt = getVersionNoPropertyType();
			buf.append(" AND ");
			buf.append(pt.getColumnName());
			buf.append(" = ?");
		}
		if (hasTimestampPropertyType()) {
			PropertyType pt = getTimestampPropertyType();
			buf.append(" AND ");
			buf.append(pt.getColumnName());
			buf.append(" = ?");
		}
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getAutoInsertSql()
	 */
	public String getAutoInsertSql() {
		return autoInsertSql_;
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getAutoUpdateSql()
	 */
	public String getAutoUpdateSql() {
		return autoUpdateSql_;
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getAutoDeleteSql()
	 */
	public String getAutoDeleteSql() {
		return autoDeleteSql_;
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getAutoSelectList()
	 */
	public String getAutoSelectList() {
		if (autoSelectList_ != null) {
			return autoSelectList_;
		}
		setupAutoSelectList();
		return autoSelectList_;
	}

	private synchronized void setupAutoSelectList() {
		if (autoSelectList_ != null) {
			return;
		}
		StringBuffer buf = new StringBuffer(100);
		buf.append("SELECT ");
		for (int i = 0; i < getPropertyTypeSize(); ++i) {
			PropertyType pt = getPropertyType(i);
			if (pt.isPersistent()) {
				buf.append(tableName_);
				buf.append(".");
				buf.append(pt.getColumnName());
				buf.append(", ");
			}
		}
		for (int i = 0; i < getRelationPropertyTypeSize(); ++i) {
			RelationPropertyType rpt = getRelationPropertyType(i);
			BeanMetaData bmd = rpt.getBeanMetaData();
			for (int j = 0; j < bmd.getPropertyTypeSize(); ++j) {
				PropertyType pt = bmd.getPropertyType(j);
				if (pt.isPersistent()) {
					String columnName = pt.getColumnName();
					buf.append(rpt.getPropertyName());
					buf.append(".");
					buf.append(columnName);
					buf.append(" AS ");
					buf.append(pt.getColumnName()).append("_").append(
							rpt.getRelationNo());
					buf.append(", ");
				}
			}
		}
		buf.setLength(buf.length() - 2);
		autoSelectList_ = buf.toString();
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getAutoInsertBindVariables(java.lang.Object)
	 */
	public Object[] getAutoInsertBindVariables(Object bean) {
		List variables = new ArrayList();
		for (int i = 0; i < getPropertyTypeSize(); ++i) {
			PropertyType pt = getPropertyType(i);
			if (!pt.isPersistent()) {
				continue;
			}
			if (pt.isPrimaryKey() && !identifierGenerator_.isSelfGererate()) {
				continue;
			}
			if (pt.getPropertyName().equalsIgnoreCase(TIMESTAMP_PROPERTY_NAME)) {
				Timestamp now = new Timestamp(new Date().getTime());
				variables.add(now);
			} else {
				variables.add(pt.getPropertyDesc().getValue(bean));
			}
		}
		return variables.toArray();
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getAutoUpdateBindVariables(java.lang.Object)
	 */
	public Object[] getAutoUpdateBindVariables(Object bean) {
		List bindVariables = new ArrayList();
		for (int i = 0; i < getPropertyTypeSize(); ++i) {
			PropertyType pt = getPropertyType(i);
			if (pt.isPersistent() && !pt.isPrimaryKey() && !pt.getPropertyName().equals(VERSION_NO_PROPERTY_NAME)) {
				if (pt.getPropertyName().equalsIgnoreCase(TIMESTAMP_PROPERTY_NAME)) {
					Timestamp now = new Timestamp(new Date().getTime());
					bindVariables.add(now);
				} else {
					bindVariables.add(pt.getPropertyDesc().getValue(bean));
				}
			}
		}
		addAutoUpdateWhereBindVariables(bindVariables, bean);
		return bindVariables.toArray();
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#getAutoDeleteBindVariables(java.lang.Object)
	 */
	public Object[] getAutoDeleteBindVariables(Object bean) {
		List bindVariables = new ArrayList();
		addAutoUpdateWhereBindVariables(bindVariables, bean);
		return bindVariables.toArray();
	}

	private void addAutoUpdateWhereBindVariables(List bindVariables, Object bean) {
		
		for (int i = 0; i < getPrimaryKeySize(); ++i) {
			PropertyType pt = getPropertyTypeByColumnName(getPrimaryKey(i));
			bindVariables.add(pt.getPropertyDesc().getValue(bean));
		}
		if (hasVersionNoPropertyType()) {
			PropertyType pt = getVersionNoPropertyType();
			bindVariables.add(pt.getPropertyDesc().getValue(bean));
		}
		if (hasTimestampPropertyType()) {
			PropertyType pt = getTimestampPropertyType();
			bindVariables.add(pt.getPropertyDesc().getValue(bean));
		}
	}

	/**
	 * @see org.seasar.dao.BeanMetaData#isRelation()
	 */
	public boolean isRelation() {
		return relation_;
	}
}