/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package kr.co.kihyun.spring;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import javax.jdo.PersistenceManagerFactory;
import kr.co.kihyun.beans.entity.AssignType;
import kr.co.kihyun.beans.entity.MoumiEntity;
import kr.co.kihyun.beans.entity.TotDoc;
import kr.co.kihyun.beans.entity.util.PMF;
import kr.co.kihyun.moumi.MoumiConfig;
import org.datanucleus.ClassLoaderResolver;
import org.datanucleus.OMFContext;
import org.datanucleus.jdo.JDOPersistenceManagerFactory;
import org.datanucleus.metadata.AbstractClassMetaData;
import org.datanucleus.metadata.AbstractMemberMetaData;
import org.datanucleus.metadata.MetaDataManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

/**
 *
 * @author bhs
 */
public class MoumiJdbcDao<T extends MoumiEntity> extends JdbcDaoSupport implements MoumiDao<T> {

	private static Logger LOG = LoggerFactory.getLogger(MoumiJdbcDao.class);
	private AbstractClassMetaData metaData;
	private Set<String> dirtyFields = new HashSet<String>(2);
	private MetaDataManager metaDataManager;
	private ClassLoaderResolver classLoaderResolver;

	public AbstractClassMetaData getMetaData() {
		return metaData;
	}

	public Set<String> getDirtyFields() {
		return Collections.unmodifiableSet(dirtyFields);
	}

	public void addDirtyField(String field) {
		this.dirtyFields.add(field);
	}

	private Properties getPmfProperties() {
		Properties result = new Properties();
		InputStream stream = MoumiConfig.class.getResourceAsStream("/moumi-pmf.properties");
		try {
			result.load(stream);
			return result;
		} catch (IOException e) {
			throw new RuntimeException("unable to read moumi-pmf.properties file.");
		}
	}

	private void init(Class<T> c, PersistenceManagerFactory pmf) {
		OMFContext omfContext = new JDOPersistenceManagerFactory(pmf.getProperties()).getOMFContext();
		this.metaDataManager = omfContext.getMetaDataManager();
		this.classLoaderResolver = omfContext.getClassLoaderResolver(null);
		this.metaData = metaDataManager.getMetaDataForClass((Class<T>) c, classLoaderResolver);
	}

	private class MoumiRowMapper implements ParameterizedRowMapper<T> {

		private Class clazz;

		public <T extends MoumiEntity<Object>> MoumiRowMapper(Class<T> c) {
			this.clazz = c;
		}

		public T mapRow(ResultSet rs, int rownum) throws SQLException {
			T pc = null;
			try {
				pc = (T) clazz.newInstance();
			} catch (Exception ex) {
				throw new RuntimeException("Could not instantiate class " + clazz.getSimpleName());
			}

			int skipCount = 0;
			pc.setId(rs.getObject(1));
			for (AbstractMemberMetaData member : MoumiJdbcDao.this.metaData.getManagedMembers()) {
				Field field = null;
				Object fieldValue = null;
				try {
					if (member.isRelationOwner(classLoaderResolver)) {
						field = clazz.getDeclaredField(member.getMemberRepresented().getName());
						field.setAccessible(true);
						fieldValue = rs.getObject(member.getAbsoluteFieldNumber() - skipCount);

						if (field.getType().isEnum()) {
							int ordinal = ((Integer) fieldValue).intValue();
							Object[] values = (Object[]) field.getType().getMethod("values").invoke(fieldValue);
							field.set(pc, field.getType().cast(values[ordinal]));
						} else if (MoumiEntity.class.isInstance(field.getType().newInstance())) {
							MoumiEntity e = (MoumiEntity) field.getType().newInstance();
							e.setId((Long) fieldValue);
							TotDoc doc = (TotDoc) field.getType().cast(e);
							field.set(pc, field.getType().cast(e));
						} else {
							field.set(pc, field.getType().cast(fieldValue));
						}
					} else {
						skipCount++;
					}
				} catch (IllegalArgumentException ex) {
					throw new IllegalArgumentException(this.clazz.getSimpleName()
							+ " is incompatible type or unwrapping conversion fails."
							+ " field: " + member.getMemberRepresented().getName()
							+ " field number: " + member.getAbsoluteFieldNumber()
							+ " field value: " + fieldValue);
				} catch (IllegalAccessException ex) {
					throw new RuntimeException(member.getMemberRepresented().getName()
							+ " field is set to be acceessable but could not access to this field.", ex);
				} catch (NoSuchFieldException ex) {
					throw new RuntimeException(member.getMemberRepresented().getName()
							+ " is present at metadata but could not be found at class."
							+ " It seems to be not enhanced.", ex);
				} catch (ClassCastException cce) {
//					throw new ClassCastException(cce.getMessage() + ": field '" + field.getName() + "'");
					LOG.warn("{}: field '{}'", cce.getMessage(), field.getName());
				} catch (Exception ex) {
//					throw new RuntimeException("Could not map row for field "
//							+ member.getMemberRepresented().getName(), ex);
					LOG.warn("Could not map row for field '{}'", member.getMemberRepresented().getName());
				}
			}
			return pc;
		}
	}

	public MoumiJdbcDao(Class<T> c) {
		Properties props = getPmfProperties();
		init(c, new JDOPersistenceManagerFactory(props));
	}

	public MoumiJdbcDao(Class<T> c, PersistenceManagerFactory pmf) {
		init(c, pmf);
	}

	public T load(Class<T> c, Long id) {
		// TODO Apply fetch plan.
		return (T) this.getJdbcTemplate().queryForObject("select * from " + this.metaData.getTable() + " where id = ?",
//		return (T) this.getJdbcTemplate().queryForObject("select id from " + this.metaData.getTable() + " where id = ?",
				new Object[]{id}, new MoumiRowMapper(c));
	}

	public void store(Class<T> c, T pc) {
		if (dirtyFields.isEmpty())
			return;

		List<Object> params = new ArrayList<Object>();
		StringBuilder sql = new StringBuilder(100);

		sql.append("update ").append(this.metaData.getTable()).append(" set ");
		int i = 0;
		for (Iterator<String> iter = dirtyFields.iterator(); iter.hasNext(); i++) {
			String dirtyField = iter.next();
			if (this.metaData.getMetaDataForMember(dirtyField).getColumnMetaData()[0].getName() != null) {
				sql.append(this.metaData.getMetaDataForMember(dirtyField).getColumnMetaData()[0].getName());
			} else {
				sql.append(dirtyField);
			}
			sql.append(" = ?");
			try {
				Field field = c.getDeclaredField(dirtyField);
				field.setAccessible(true);
				if (field.get(pc) instanceof Enum) {
					params.add(((Enum) field.get(pc)).ordinal());
				} else {
					params.add(field.get(pc));
				}
			} catch (IllegalArgumentException ex) {
				throw new IllegalArgumentException(ex);
			} catch (IllegalAccessException ex) {
				throw new RuntimeException(ex);
			} catch (NoSuchFieldException ex) {
				throw new RuntimeException("Field " + dirtyField + " does not exists in class "
						+ c.getSimpleName() + ". "
						+ "Check addDirtyField(String field) method invocations.");
			} catch (SecurityException ex) {
				throw new SecurityException(ex);
			}
			if (i != dirtyFields.size() - 1) {
				sql.append(", ");
			}
		}
		sql.append(" where id = ?");
		params.add(pc.getId());

		LOG.debug(sql.toString().replaceAll("\\?", "{}"), params.toArray(new Object[params.size()]));
		this.getJdbcTemplate().update(sql.toString(), params.toArray(new Object[params.size()]));
	}
}
