/*******************************************************************************
 * 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.dao.annotations.DAOManaged;
import se.unlogic.standardutils.dao.annotations.ManyToOne;
import se.unlogic.standardutils.reflection.ReflectionUtils;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;


public class DefaultOneToManyRelation<LocalType,RemoteType> implements OneToManyRelation<LocalType, RemoteType> {

   private final Field field;
   private Field remoteField;
   private final AnnotatedDAOFactory daoFactory;
   private AnnotatedDAO<RemoteType> annotatedDAO;
   private QueryParameterFactory<RemoteType, LocalType> queryParameterFactory;
   private final Class<LocalType> beanClass;
   private final Class<RemoteType> remoteClass;
   private boolean initialized;

   public DefaultOneToManyRelation(final Class<LocalType> beanClass, final Class<RemoteType> remoteClass, final Field field, final AnnotatedDAOFactory daoFactory, final DAOManaged daoManaged) {
      super();
      this.beanClass = beanClass;
      this.remoteClass = remoteClass;
      this.field = field;
      this.daoFactory = daoFactory;

      List<Field> fields = ReflectionUtils.getFields(remoteClass);

      for(Field remoteField : fields){

         if(remoteField.getType().equals(beanClass) && remoteField.isAnnotationPresent(DAOManaged.class) && remoteField.isAnnotationPresent(ManyToOne.class)){

            this.remoteField = remoteField;

            ReflectionUtils.fixFieldAccess(this.remoteField);

            break;
         }
      }

      if(this.remoteField == null){

         throw new RuntimeException("Unable to to find corresponding @ManyToOne field in class " + remoteClass + " for @OneToMany annotated field " + field.getName() + " in " + beanClass);
      }
   }

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

      if(!initialized){
         init();
      }

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

         query.addRelations(relationQuery);

         if(relationQuery != null){
            query.disableAutoRelations(relationQuery.isDisableAutoRelations());
         }

         query.addParameter(queryParameterFactory.getParameter(bean));

         field.set(bean, annotatedDAO.getAll(query, connection));

      } catch (IllegalArgumentException e) {

         throw new RuntimeException(e);

      } catch (IllegalAccessException e) {

         throw new RuntimeException(e);
      }
   }

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

      if(!initialized){
         init();
      }

      try {
         List<RemoteType> remoteBeans = (List<RemoteType>) field.get(bean);

         if(remoteBeans != null){

            this.fixReferences(remoteBeans, bean);

            annotatedDAO.addAll(remoteBeans, connection, relationQuery);
         }

      } catch (IllegalArgumentException e) {

         throw new RuntimeException(e);

      } catch (IllegalAccessException e) {

         throw new RuntimeException(e);
      }
   }

   private void fixReferences(List<RemoteType> remoteBeans, LocalType bean) throws IllegalArgumentException, IllegalAccessException {

      for(RemoteType remoteBean : remoteBeans){

         remoteField.set(remoteBean, bean);
      }
   }

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

      if(!initialized){
         init();
      }

      try {
         List<RemoteType> remoteBeans = (List<RemoteType>) field.get(bean);

         if(remoteBeans == null ||  remoteBeans.isEmpty()){

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

            query.addRelations(relationQuery);

            if(relationQuery != null){
               query.disableAutoRelations(relationQuery.isDisableAutoRelations());
            }

            query.addParameter(queryParameterFactory.getParameter(bean));

            annotatedDAO.delete(query, connection);

         }else{

            this.fixReferences(remoteBeans, bean);

            //TODO exclude current parameter

            QueryParameter<RemoteType,LocalType> queryParameter = queryParameterFactory.getParameter(bean);

            if(!annotatedDAO.deleteWhereNotIn(remoteBeans, connection, this.remoteField, queryParameter)){

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

               query.addParameter(queryParameter);

               annotatedDAO.delete(query, connection);
            }

            annotatedDAO.addOrUpdateAll(remoteBeans, connection, relationQuery);
         }

      } catch (IllegalArgumentException e) {

         throw new RuntimeException(e);

      } catch (IllegalAccessException e) {

         throw new RuntimeException(e);
      }
   }

   private void init() {

      if(annotatedDAO == null){
         annotatedDAO = this.daoFactory.getDAO(remoteClass);
         queryParameterFactory = annotatedDAO.getParamFactory(remoteField, beanClass);
      }

      this.initialized = true;
   }

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

      return new DefaultOneToManyRelation<LT,RT>(beanClass,remoteClass,field,daoFactory,daoManaged);
   }
}
