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