/*******************************************************************************
 * 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.collections.CollectionUtils;
import se.unlogic.standardutils.dao.annotations.DAOManaged;
import se.unlogic.standardutils.dao.annotations.Key;
import se.unlogic.standardutils.dao.annotations.SimplifiedRelation;
import se.unlogic.standardutils.dao.enums.Order;
import se.unlogic.standardutils.dao.querys.ArrayListQuery;
import se.unlogic.standardutils.dao.querys.PreparedStatementQuery;
import se.unlogic.standardutils.dao.querys.UpdateQuery;
import se.unlogic.standardutils.populators.BeanStringPopulator;
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.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

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

   private final AnnotatedDAO<LocalType> localDAO;
   private final Field field;

   private String selectSQL;
   private String insertSQL;
   private String deleteSQL;

   private boolean preserveListOrder;
   private String indexColumnName;

   private QueryParameterPopulator<RemoteType> queryParameterPopulator;
   private Method preparedStatementMethod;

   private BeanResultSetPopulator<RemoteType> beanResultSetPopulator;

   private Field keyField;
   private Column<LocalType, ?> keyColumn;

   private final String remoteTableName;
   private String remoteKeyColumnName;
   private final String remoteValueColumnName;
   private Order order;

   private boolean initialized;

   @SuppressWarnings("unchecked")
   public SimplifiedOneToManyRelation(Class<LocalType> beanClass, Class<RemoteType> remoteClass, Field field, AnnotatedDAO<LocalType> localDAO, List<? extends BeanStringPopulator<?>> typePopulators, List<? extends QueryParameterPopulator<?>> queryParameterPopulators) {

      super();
      this.localDAO = localDAO;
      this.field = field;

      SimplifiedRelation simplifiedRelation = field.getAnnotation(SimplifiedRelation.class);

      remoteKeyColumnName = simplifiedRelation.remoteKeyColumnName();
      remoteValueColumnName = simplifiedRelation.remoteValueColumnName();
      order = simplifiedRelation.order();

      if(simplifiedRelation.addTablePrefix()){

         if(simplifiedRelation.deplurifyTablePrefix() && localDAO.getTableName().endsWith("s")){

            remoteTableName = localDAO.getTableName().substring(0, localDAO.getTableName().length()-1) + simplifiedRelation.table();

         }else{

            remoteTableName = localDAO.getTableName() + simplifiedRelation.table();
         }
      }else{

         remoteTableName = simplifiedRelation.table();
      }

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

         try {
            keyField = ReflectionUtils.getField(beanClass, simplifiedRelation.keyField());

            if (keyField == null) {

               throw new RuntimeException("Unable to find field " + simplifiedRelation.keyField() + " in " + beanClass.getClass());
            }

         } catch (SecurityException e) {

            throw new RuntimeException(e);

         }

      } else {

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

         for (Field localBeanField : fields) {

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

               if (this.keyField == null) {

                  keyField = localBeanField;

               } else {

                  throw new RuntimeException("Multiple fields marked with @Key annotation found in class " + beanClass + " therefore keyField has to set on the @SimplifiedRelation annotation of field " + field.getName());
               }
            }
         }
      }

      if (queryParameterPopulators != null) {

         for (QueryParameterPopulator<?> queryParameterPopulator : queryParameterPopulators) {

            if (queryParameterPopulator.getType().equals(remoteClass)) {

               this.queryParameterPopulator = (QueryParameterPopulator<RemoteType>) queryParameterPopulator;
            }
         }
      }

      if (this.queryParameterPopulator == null) {

         preparedStatementMethod = PreparedStatementQueryMethods.getQueryMethod(remoteClass);

         if (preparedStatementMethod == null) {

            throw new RuntimeException("Unable to to find a query parameter populator or prepared statement method matching " + remoteClass + " of @SimplfiedRelation and @OneToMany annotated field " + field.getName() + " in " + beanClass);
         }
      }

      if (typePopulators != null) {

         for (BeanStringPopulator<?> typePopulator : typePopulators) {

            if (typePopulator.getType().equals(remoteClass)) {

               beanResultSetPopulator = new TypeBasedResultSetPopulator<RemoteType>((BeanStringPopulator<RemoteType>) typePopulator, remoteValueColumnName);
            }
         }
      }

      if (beanResultSetPopulator == null) {

         Method resultSetMethod = ResultSetMethods.getColumnNameMethod(remoteClass);

         if (resultSetMethod != null) {

            beanResultSetPopulator = new MethodBasedResultSetPopulator<RemoteType>(resultSetMethod, remoteValueColumnName);

         } else {

            throw new RuntimeException("Unable to to find a type populator or resultset method matching " + remoteClass + " of @SimplfiedRelation and @OneToMany annotated field " + field.getName() + " in " + beanClass);
         }
      }

      if(simplifiedRelation.preserveListOrder()){

         if(StringUtils.isEmpty(simplifiedRelation.indexColumn())){

            throw new RuntimeException("Preserve list order enabled but no index column specified for @SimplifiedRelation annotated field " + field.getName() + " in " + beanClass);
         }

         preserveListOrder = true;
         indexColumnName = simplifiedRelation.indexColumn();
      }
   }

   private void init() {

      this.keyColumn = localDAO.getColumn(keyField);

      if (StringUtils.isEmpty(remoteKeyColumnName)) {

         remoteKeyColumnName = keyColumn.getColumnName();
      }

      this.deleteSQL = "DELETE FROM " + remoteTableName + " WHERE " + remoteKeyColumnName + "=?";

      if(preserveListOrder){

         this.selectSQL = "SELECT * FROM " + remoteTableName + " WHERE " + remoteKeyColumnName + " = ? ORDER BY " + indexColumnName + " " + order;
         this.insertSQL = "INSERT INTO " + remoteTableName + "(" + remoteKeyColumnName + "," + remoteValueColumnName + "," + indexColumnName + ") VALUES (?,?,?)";

      }else{

         this.selectSQL = "SELECT * FROM " + remoteTableName + " WHERE " + remoteKeyColumnName + " = ? ORDER BY " + remoteValueColumnName + " " + order;
         this.insertSQL = "INSERT INTO " + remoteTableName + "(" + remoteKeyColumnName + "," + remoteValueColumnName + ") VALUES (?,?)";
      }

      this.initialized = true;
   }

   /*
    * (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 {
         ArrayListQuery<RemoteType> query = new ArrayListQuery<RemoteType>(connection, false, selectSQL, beanResultSetPopulator);

         setKey(query, bean);

         ArrayList<RemoteType> list = query.executeQuery();

         if (list != null) {

            CollectionUtils.removeNullValues(list);
         }

         field.set(bean, list);

      } catch (IllegalArgumentException e) {

         throw new RuntimeException(e);

      } catch (IllegalAccessException e) {

         throw new RuntimeException(e);
      }
   }

   private void setKey(PreparedStatementQuery query, LocalType bean) throws SQLException {

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

         keyColumn.getQueryParameterPopulator().populate(query, 1, bean);

      } else {

         try {
            keyColumn.getQueryMethod().invoke(query, 1, keyColumn.getBeanValue(bean));

         } catch (IllegalArgumentException e) {

            throw new RuntimeException(e);

         } catch (IllegalAccessException e) {

            throw new RuntimeException(e);

         } catch (InvocationTargetException e) {

            throw new RuntimeException(e);
         }
      }
   }

   private void setValue(RemoteType value, UpdateQuery query) throws SQLException {

      if (queryParameterPopulator != null) {

         queryParameterPopulator.populate(query, 2, value);

      } else {

         try {
            preparedStatementMethod.invoke(query, 2, value);

         } catch (IllegalArgumentException e) {

            throw new RuntimeException(e);

         } catch (IllegalAccessException e) {

            throw new RuntimeException(e);

         } catch (InvocationTargetException 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> values = (List<RemoteType>) field.get(bean);

         if (values != null) {

            int listIndex = 0;

            for (RemoteType value : values) {

               UpdateQuery query = new UpdateQuery(connection, false, insertSQL);

               setKey(query, bean);

               setValue(value, query);

               if(preserveListOrder){

                  query.setInt(3, listIndex);
                  listIndex++;
               }

               query.executeUpdate();
            }
         }

      } catch (IllegalArgumentException e) {

         throw new RuntimeException(e);

      } catch (IllegalAccessException e) {

         throw new RuntimeException(e);
      }
   }

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

      if (!initialized) {
         init();
      }

      UpdateQuery query = new UpdateQuery(connection, false, deleteSQL);

      setKey(query, bean);

      query.executeUpdate();

      this.add(bean, connection, relationQuery);
   }

   public static <LT, RT> OneToManyRelation<LT, RT> getGenericInstance(Class<LT> beanClass, Class<RT> remoteClass, Field field, AnnotatedDAO<LT> localDAO, List<? extends BeanStringPopulator<?>> typePopulators, List<? extends QueryParameterPopulator<?>> queryParameterPopulators) {

      return new SimplifiedOneToManyRelation<LT, RT>(beanClass, remoteClass, field, localDAO, typePopulators, queryParameterPopulators);
   }
}
