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