/*******************************************************************************
 * Copyright (c) 2010 Robert "Unlogic" Olofsson (unlogic@unlogic.se).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v3
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-3.0-standalone.html
 ******************************************************************************/
package se.unlogic.standardutils.dao;

import se.unlogic.standardutils.annotations.UnsupportedFieldTypeException;
import se.unlogic.standardutils.dao.annotations.DAOManaged;
import se.unlogic.standardutils.dao.annotations.ManyToOne;
import se.unlogic.standardutils.populators.BeanStringPopulator;
import se.unlogic.standardutils.populators.BeanStringPopulatorRegistery;
import se.unlogic.standardutils.populators.QueryParameterPopulator;
import se.unlogic.standardutils.reflection.ReflectionUtils;
import se.unlogic.standardutils.string.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DefaultManyToOneRelation<LocalType,RemoteType, RemoteKeyType> implements ManyToOneRelation<LocalType, RemoteType, RemoteKeyType>{

	private final String columnName;
	private final Field field;
	private QueryParameterPopulator<RemoteKeyType> queryParameterPopulator;
	private Method queryMethod;
	private final BeanResultSetPopulator<RemoteKeyType> remoteKeyPopulator;
	private final Field remoteKeyField;

	private final AnnotatedDAOFactory daoFactory;
	private AnnotatedDAO<RemoteType> annotatedDAO;
	private QueryParameterFactory<RemoteType,RemoteKeyType> queryParameterFactory;
	private final Class<RemoteType> remoteClass;
	private final Class<RemoteKeyType> remoteRemoteKeyClass;

	private boolean initialized;

	public DefaultManyToOneRelation(Class<LocalType> beanClass, Class<RemoteType> remoteClass, Class<RemoteKeyType> remoteKeyClass, Field field, Field remoteKeyField, DAOManaged daoManaged, AnnotatedDAOFactory daoFactory) {

		this.remoteClass = remoteClass;
		this.remoteRemoteKeyClass = remoteKeyClass;
		this.field = field;
		this.remoteKeyField = remoteKeyField;
		this.daoFactory = daoFactory;

		if(!StringUtils.isEmpty(daoManaged.columnName())){
			this.columnName = daoManaged.columnName();
		}else{
			this.columnName = field.getName();
		}

		ReflectionUtils.fixFieldAccess(remoteKeyField);

		//TODO use column instead! somehow...
		Method resultSetMethod = ResultSetMethods.getColumnNameMethod(remoteKeyClass);

		if(resultSetMethod != null){
			
			remoteKeyPopulator = new MethodBasedResultSetPopulator<RemoteKeyType>(resultSetMethod, columnName);
			
		}else{
			
			BeanStringPopulator<RemoteKeyType> typePopulator = BeanStringPopulatorRegistery.getBeanStringPopulator(remoteKeyClass);
			
			if(typePopulator != null){
				
				remoteKeyPopulator = new TypeBasedResultSetPopulator<RemoteKeyType>(typePopulator, columnName);
			}else{

				throw new UnsupportedFieldTypeException("Unable to find resultset method or type populator for field " + remoteKeyField.getName() + " in " + remoteClass + " when creating many to one relation for field " + field.getName() + " in " + beanClass, field, ManyToOne.class, beanClass);
			}
		}
	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#getColumnName()
	 */
	public String getColumnName(){

		return columnName;
	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#getQueryParameterPopulator()
	 */
	public QueryParameterPopulator<RemoteKeyType> getQueryParameterPopulator(){

		if(queryParameterPopulator == null && queryMethod == null){

			if(!initialized){

				this.init();
			}

			this.queryParameterPopulator = annotatedDAO.getQueryParameterPopulator(remoteRemoteKeyClass);
		}

		return queryParameterPopulator;
	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#getQueryMethod()
	 */
	public Method getQueryMethod(){

		if(this.queryMethod == null){
			this.queryMethod = PreparedStatementQueryMethods.getObjectQueryMethod();
		}

		return queryMethod;
	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#getBeanValue(LocalType)
	 */
	@SuppressWarnings("unchecked")
	public RemoteKeyType getBeanValue(LocalType bean){

		try {
			RemoteType subBean = (RemoteType) field.get(bean);

			if(subBean == null){
				return null;
			}

			return (RemoteKeyType)remoteKeyField.get(subBean);

		} catch (IllegalArgumentException e) {

			throw new RuntimeException(e);

		} catch (IllegalAccessException e) {

			throw new RuntimeException(e);
		}
	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#getParamValue(java.lang.Object)
	 */
	@SuppressWarnings("unchecked")
	public RemoteKeyType getParamValue(Object bean) {

		try {
			if(bean == null){
				return null;
			}

			return (RemoteKeyType) remoteKeyField.get(bean);

		} catch (IllegalArgumentException e) {

			throw new RuntimeException(e);

		} catch (IllegalAccessException e) {

			throw new RuntimeException(e);
		}
	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#setValue(LocalType, java.sql.ResultSet, java.sql.Connection, java.lang.reflect.Field[])
	 */
	public void getRemoteValue(LocalType bean, ResultSet resultSet, Connection connection, RelationQuery relationQuery) throws SQLException{

		try {

			if(!initialized){
				this.init();
			}

			RemoteKeyType keyValue = remoteKeyPopulator.populate(resultSet);

			if(keyValue != null){

				HighLevelQuery<RemoteType> query = new HighLevelQuery<RemoteType>();

				query.addParameter(queryParameterFactory.getParameter(keyValue));
				
				if(relationQuery != null){
					query.disableAutoRelations(relationQuery.isDisableAutoRelations());
				}
				
				query.addRelations(relationQuery);

				RemoteType remoteBeanInstance = annotatedDAO.get(query ,connection);

				this.getField().set(bean, remoteBeanInstance);
			}

		} catch (IllegalArgumentException e) {

			throw new RuntimeException(e);

		} catch (IllegalAccessException e) {

			throw new RuntimeException(e);
		}
	}



	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#add(LocalType, java.sql.Connection, java.lang.reflect.Field)
	 */
	@SuppressWarnings("unchecked")
	public void add(LocalType bean, Connection connection, RelationQuery relationQuery) throws SQLException{

		if(!initialized){
			this.init();
		}

		try {
			RemoteType remoteBean = (RemoteType) field.get(bean);

			if(remoteBean != null){

				this.annotatedDAO.add(remoteBean, connection, relationQuery);
			}

		} catch (IllegalArgumentException e) {

			throw new RuntimeException(e);

		} catch (IllegalAccessException e) {

			throw new RuntimeException(e);
		}

	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#update(LocalType, java.sql.Connection, java.lang.reflect.Field)
	 */
	@SuppressWarnings("unchecked")
	public void update(LocalType bean, Connection connection, RelationQuery relationQuery) throws SQLException{

		if(!initialized){
			this.init();
		}

		try {
			RemoteType remoteBean = (RemoteType) field.get(bean);

			if(remoteBean != null){

				this.annotatedDAO.addOrUpdate(remoteBean, connection, relationQuery);
			}

		} catch (IllegalArgumentException e) {

			throw new RuntimeException(e);

		} catch (IllegalAccessException e) {

			throw new RuntimeException(e);
		}

	}

	private void init() {

		this.annotatedDAO = this.daoFactory.getDAO(remoteClass);
		this.queryParameterFactory = annotatedDAO.getParamFactory(remoteKeyField, remoteRemoteKeyClass);

		this.initialized = true;
	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#getField()
	 */
	public Field getField() {

		return field;
	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#getBeanField()
	 */
	public Field getBeanField() {

		return this.field;
	}

	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#getParamType()
	 */
	public Class<RemoteType> getParamType() {

		return remoteClass;
	}


	/* (non-Javadoc)
	 * @see se.unlogic.utils.dao.ManyToOneRelation#isAutoGenerated()
	 */
	public boolean isAutoGenerated() {

		return false;
	}

	public static <LT,RT,RKT> DefaultManyToOneRelation<LT, RT, RKT> getGenericInstance(Class<LT> beanClass, Class<RT> remoteClass, Class<RKT> remoteKeyClass, Field field, Field remoteField, DAOManaged daoManaged, AnnotatedDAOFactory daoFactory){

		return new DefaultManyToOneRelation<LT, RT, RKT>(beanClass, remoteClass, remoteKeyClass, field, remoteField, daoManaged, daoFactory);
	}
}
