/*******************************************************************************
 * 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.Key;
import se.unlogic.standardutils.dao.annotations.ManyToMany;
import se.unlogic.standardutils.dao.querys.UpdateQuery;
import se.unlogic.standardutils.reflection.ReflectionUtils;
import se.unlogic.standardutils.string.StringUtils;

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


public class DefaultManyToManyRelation<LocalType,RemoteType> implements ManyToManyRelation<LocalType, RemoteType> {

   private final Field field;
   private final Field localKeyField;
   private final Field remoteKeyField;

   private final String linkTable;
   private final String localLinkTableColumnName;
   private final String remoteKeyColumnName;
   private final String remoteLinkTableColumnName;

   private String linkTableLinkSQL;
   private String linkTableDeleteSQL;
   private String linkTableInsertSQL;

   private Column<LocalType,?> localColumn;
   private Column<RemoteType, ?> remoteColumn;

   private final AnnotatedDAOFactory daoFactory;
   private AnnotatedDAO<RemoteType> annotatedDAO;
   private final Class<LocalType> beanClass;
   private final Class<RemoteType> remoteClass;

   private boolean initialized;

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


      //Validate linkTable name
      ManyToMany localAnnotation = field.getAnnotation(ManyToMany.class);

      this.linkTable = localAnnotation.linkTable();

      //Get remote field
      Field matchingRemoteField = null;

      Field[] fields = remoteClass.getDeclaredFields();

      for(Field remoteField : fields){

         if(ReflectionUtils.getGenericlyTypeCount(remoteField) == 1 && ReflectionUtils.getGenericType(remoteField).equals(beanClass) && remoteField.isAnnotationPresent(DAOManaged.class) && remoteField.isAnnotationPresent(ManyToMany.class) && remoteField.getAnnotation(ManyToMany.class).linkTable().equals(linkTable)){

            matchingRemoteField = remoteField;

            break;
         }

      }

      if(matchingRemoteField == null){

         throw new RuntimeException("Unable to to find corresponding @ManyToMany field in " + remoteClass + " while parsing field " + field.getName() + " in " + beanClass);
      }

      //Get remote annotation
      ManyToMany remoteAnnotation = matchingRemoteField.getAnnotation(ManyToMany.class);

      //Get local key field
      this.localKeyField = getKeyField(localAnnotation, beanClass, field);

      //Get local localColumn name
      this.localLinkTableColumnName = getColumnName(daoManaged, localKeyField);

      //Get remote key field
      remoteKeyField = getKeyField(remoteAnnotation, remoteClass, matchingRemoteField);

      //Get remote localColumn name
      this.remoteLinkTableColumnName = getColumnName(remoteKeyField.getAnnotation(DAOManaged.class), remoteKeyField);

      String remoteColumnName = remoteKeyField.getAnnotation(DAOManaged.class).columnName();

      //Get remote table localColumn name
      if(!StringUtils.isEmpty(remoteColumnName)){

         this.remoteKeyColumnName = remoteColumnName;

      }else{

         this.remoteKeyColumnName = remoteKeyField.getName();
      }

      this.linkTableDeleteSQL = "DELETE FROM " + linkTable + " WHERE " + localLinkTableColumnName + " = ?";
      this.linkTableInsertSQL = "INSERT INTO " + linkTable + " (" + localLinkTableColumnName + "," + remoteLinkTableColumnName + ") VALUES (?,?)";

   }

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

      try {
         if(!initialized){
            init();
         }

         CustomQueryParameter<LocalType> queryParameter = new CustomQueryParameter<LocalType>(localColumn, bean);

         if(queryParameter.getParamValue() != null){

            field.set(bean, annotatedDAO.getAll(linkTableLinkSQL, queryParameter, connection, relationQuery));
         }

      } catch (IllegalArgumentException e) {

         throw new RuntimeException(e);

      } catch (IllegalAccessException e) {

         throw new RuntimeException(e);
      }
   }

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

      //TODO cascading add like OneToMany relations

      try {
         if(!initialized){
            init();
         }

         //Check if there are any relations to set
         List<RemoteType> remoteBeans = (List<RemoteType>) field.get(bean);

         if(remoteBeans == null){

            return;
         }

         for(RemoteType remoteBean : remoteBeans){

            //Set new relations
            UpdateQuery insertQuery;

            insertQuery = new UpdateQuery(connection, false, linkTableInsertSQL);

            setQueryParameter(insertQuery, this.localColumn, bean, 1);
            setQueryParameter(insertQuery, this.remoteColumn, remoteBean, 2);

            insertQuery.executeUpdate();
         }

      } catch (IllegalArgumentException e) {

         throw new RuntimeException(e);

      } catch (IllegalAccessException e) {

         throw new RuntimeException(e);
      }
   }

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

      //TODO cascading add like OneToMany relations

      try {
         if(!initialized){
            init();
         }

         //Clear old relations
         UpdateQuery deleteQuery;

         deleteQuery = new UpdateQuery(connection, false, linkTableDeleteSQL);

         setQueryParameter(deleteQuery, this.localColumn, bean, 1);

         deleteQuery.executeUpdate();

         //Check if there are any new relations to set
         List<RemoteType> remoteBeans = (List<RemoteType>) field.get(bean);

         if(remoteBeans == null){

            return;
         }

         for(RemoteType remoteBean : remoteBeans){

            //Set new relations
            UpdateQuery insertQuery;

            insertQuery = new UpdateQuery(connection, false, linkTableInsertSQL);

            setQueryParameter(insertQuery, this.localColumn, bean, 1);
            setQueryParameter(insertQuery, this.remoteColumn, remoteBean, 2);

            insertQuery.executeUpdate();
         }

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

      if(this.linkTableLinkSQL == null){
         this.linkTableLinkSQL = "SELECT " + annotatedDAO.getTableName() + ".* FROM " + annotatedDAO.getTableName() + " INNER JOIN " + linkTable + " ON (" + annotatedDAO.getTableName() + "." + remoteKeyColumnName + "=" + linkTable + "." + remoteLinkTableColumnName + ") WHERE " + linkTable + "." + localLinkTableColumnName + " = ?";
      }

      if(this.localColumn == null){
         this.localColumn = this.daoFactory.getDAO(beanClass).getColumn(this.localKeyField);
      }

      if(this.remoteColumn == null){
         this.remoteColumn = this.annotatedDAO.getColumn(remoteKeyField);
      }

      this.initialized = true;
   }

   private static <Type> void setQueryParameter(UpdateQuery query, Column<Type,?> column, Type bean, int index) throws SQLException{

      if(column.getQueryParameterPopulator() != null){

         column.getQueryParameterPopulator().populate(query, index, column.getBeanValue(bean));

      }else{

         try {
            column.getQueryMethod().invoke(query, index, column.getBeanValue(bean));

         } catch (IllegalArgumentException e) {

            throw new RuntimeException(e);

         } catch (IllegalAccessException e) {

            throw new RuntimeException(e);

         } catch (InvocationTargetException e) {

            throw new RuntimeException(e);
         }
      }
   }

   public static Field getKeyField(ManyToMany manyToManyAnnotation, Class<?> clazz, Field annotatedField){

      if(!StringUtils.isEmpty(manyToManyAnnotation.keyField())){

         try {
            Field keyField = clazz.getDeclaredField(manyToManyAnnotation.keyField());

            DAOManaged keyDAOPopulate = keyField.getAnnotation(DAOManaged.class);

            if(keyDAOPopulate == null){

               throw new RuntimeException("Specified keyField " + manyToManyAnnotation.keyField() + " for @ManyToMany annotation for field " + annotatedField.getName() + "  in " + clazz + " is missing the @DAOManaged annotation");
            }

            return keyField;


         } catch (SecurityException e) {

            throw new RuntimeException("Unable to find specified keyField " + manyToManyAnnotation.keyField() + " for @ManyToMany annotation for field " + annotatedField.getName() + "  in " + clazz);

         } catch (NoSuchFieldException e) {

            throw new RuntimeException("Unable to find specified keyField " + manyToManyAnnotation.keyField() + " for @ManyToMany annotation for field " + annotatedField.getName() + "  in " + clazz);
         }

      }else{

         ArrayList<Field> keyFields = getKeyFields(clazz);

         if(keyFields.size() == 0){

            throw new RuntimeException("Unable to find any @Key annotated fields in " + clazz);

         }else if(keyFields.size() > 1){

            throw new RuntimeException("keyField needs to be specified for @ManyToMany annotated field " + annotatedField.getName() + " in " + clazz + " since the class contains multiple @Key annotated fields");
         }

         return keyFields.get(0);
      }
   }

   public static ArrayList<Field> getKeyFields(Class<?> clazz){

      ArrayList<Field> keyFields = new ArrayList<Field>();

      Field[] fields = clazz.getDeclaredFields();

      for(Field field : fields){

         if(field.isAnnotationPresent(Key.class) && field.isAnnotationPresent(DAOManaged.class)){

            keyFields.add(field);
         }
      }

      return keyFields;
   }

   private static String getColumnName(DAOManaged manyToManyDAOPopulate, Field keyField){

      String keyColumnName;

      if(!StringUtils.isEmpty(manyToManyDAOPopulate.columnName())){

         return manyToManyDAOPopulate.columnName();

      }else if(!StringUtils.isEmpty(keyColumnName = keyField.getAnnotation(DAOManaged.class).columnName())){

         return keyColumnName;

      }else{

         return keyField.getName();
      }
   }

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

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