package org.seasar.dao.impl;

import java.lang.reflect.Field;
import java.sql.DatabaseMetaData;
import java.util.ArrayList;
import java.util.Iterator;
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.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 autoSelectList_;

	private boolean relation_;
	
	private IdentifierGenerator identifierGenerator_;
	
	private String versionNoPropertyName_ = "versionNo";
	
	private String timestampPropertyName_ = "timestamp";

	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);
		setupVersionNoPropertyName(beanDesc);
		setupTimestampPropertyName(beanDesc);
		setupProperty(beanDesc, dbMetaData, dbms);
		setupDatabaseMetaData(beanDesc, dbMetaData, dbms);
		setupPropertiesByColumnName();
	}

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

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

		return getPropertyType(versionNoPropertyName_);
	}

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

		return getPropertyType(timestampPropertyName_);
	}
	
	public String getVersionNoPropertyName() {
		return versionNoPropertyName_;
	}
	
	public String getTimestampPropertyName() {
		return timestampPropertyName_;
	}

	/**
	 * @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(versionNoPropertyName_);
	}
	
	/**
	 * @see org.seasar.dao.BeanMetaData#hasTimestampPropertyType()
	 */
	public boolean hasTimestampPropertyType() {
		return hasPropertyType(timestampPropertyName_);
	}

	/**
	 * @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)) {
			Field field = beanDesc.getField(TABLE);
			tableName_ = (String) FieldUtil.get(field, null);
		} else {
			tableName_ = ClassUtil.getShortClassName(getBeanClass());
		}
	}
	
	private void setupVersionNoPropertyName(BeanDesc beanDesc) {
		if (beanDesc.hasField(VERSION_NO_PROPERTY)) {
			Field field = beanDesc.getField(VERSION_NO_PROPERTY);
			versionNoPropertyName_ = (String) FieldUtil.get(field, null);
		}
	} 
	
	private void setupTimestampPropertyName(BeanDesc beanDesc) {
		if (beanDesc.hasField(TIMESTAMP_PROPERTY)) {
			Field field = beanDesc.getField(TIMESTAMP_PROPERTY);
			timestampPropertyName_ = (String) FieldUtil.get(field, null);
		}
	}


	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_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_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[]{pt.getColumnName()};
					pt.setPrimaryKey(true);
				}
			}
		}
	}

	private void setupDatabaseMetaData(BeanDesc beanDesc, DatabaseMetaData dbMetaData, Dbms dbms) {
		setupPrimaryKey(dbMetaData, dbms);
		setupPropertyPersistentAndColumnName(beanDesc, dbMetaData);
	}
	
	private void setupPrimaryKey(DatabaseMetaData dbMetaData, Dbms dbms) {
		if (identifierGenerator_ == null) {
			List pkeyList = new ArrayList();
			Set primaryKeySet = DatabaseMetaDataUtil.getPrimaryKeySet(dbMetaData,
					tableName_);
			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 setupPropertyPersistentAndColumnName(BeanDesc beanDesc, DatabaseMetaData dbMetaData) {
		Set columnSet = DatabaseMetaDataUtil.getColumnSet(dbMetaData,
				tableName_);
		for (Iterator i = columnSet.iterator(); i.hasNext(); ) {
			String columnName = (String) i.next();
			String columnName2 = StringUtil.replace(columnName, "_", "");
			for (int j = 0; j < getPropertyTypeSize(); ++j) {
				PropertyType pt = getPropertyType(j);
				if (pt.getColumnName().equalsIgnoreCase(columnName2)) {
					pt.setColumnName(columnName);
				}
			}
		}
		if (beanDesc.hasField(NO_PERSISTENT_PROPS)) {
			Field field = beanDesc.getField(NO_PERSISTENT_PROPS);
			String str = (String) FieldUtil.get(field, null);
			String[] props = StringUtil.split(str, ", ");
			for (int i = 0; i < props.length; ++i) {
				PropertyType pt = getPropertyType(props[i].trim());
				pt.setPersistent(false);
			}
		} else {
			for (int i = 0; i < getPropertyTypeSize(); ++i) {
				PropertyType pt = getPropertyType(i);
				if (!columnSet.contains(pt.getColumnName())) {
					pt.setPersistent(false);
				}
			}
		}
	}
	
	private void setupPropertiesByColumnName() {
		for (int i = 0; i < getPropertyTypeSize(); ++i) {
			PropertyType pt = getPropertyType(i);
			propertyTypesByColumnName_.put(pt.getColumnName(), pt);
		}
	}

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

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

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

	private void setupAutoSelectList() {
		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#isRelation()
	 */
	public boolean isRelation() {
		return relation_;
	}
}