/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.empire.spring;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.sql.DataSource;

import org.apache.empire.db.DBColumnExpr;
import org.apache.empire.db.DBCommand;
import org.apache.empire.db.DBReader;
import org.apache.empire.db.DBRecord;
import org.apache.empire.db.DBRecordData;
import org.apache.empire.db.DBRowSet;
import org.apache.empire.db.DBTable;
import org.apache.empire.db.exceptions.RecordNotFoundException;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.Assert;

/**
 * This class simplifies database access methods with Empire. Database queries
 * are executed with the help of callback interfaces, while hides connection
 * handling from the developer. Only the query logic has to be implemented and
 * leave getting and releasing the connection to the template.
 * 
 * Some methods (delete, update) delegates to DBRecord, DBDatabase and DBCommand
 * methods while hiding connection handling from developers.
 * 
 * This class is working with a wrapped JdbcTemplate, so either jdbcTemplate or
 * dataSource must be set.
 * 
 * Allows customization of creating DBReader and DBRecord instances through
 * ObjectFactories.
 * 
 * This class is based on {@link org.springframework.jdbc.core.JdbcTemplate}.
 * 
 *
 */

public class EmpireTemplate implements InitializingBean {

	/** Wrapped JdbcTemplate instance */
	private JdbcTemplate jdbcTemplate;

	/** Factory to create a new DBReader instance, by default returns DBReader */
	private ObjectFactory<DBReader> readerFactory = new ObjectFactory<DBReader>() {

		@Override
		public DBReader getObject() throws BeansException {
			return new DBReader();
		}

	};

	/** Factory to create a new DBRecord instance, by default returns DBRecord */
	private ObjectFactory<DBRecord> recordFactory = new ObjectFactory<DBRecord>() {

		@Override
		public DBRecord getObject() throws BeansException {
			return new DBRecord();
		}

	};

	/** Default constructor */
	public EmpireTemplate() {
		super();
	}

	/**
	 * Setting a custom ObjectFactory to allow custom DBRecord create with
	 * newRecord().
	 * 
	 * @param recordFactory
	 */

	public void setDBRecordFactory(ObjectFactory<DBRecord> recordFactory) {
		this.recordFactory = recordFactory;
	}

	/**
	 * Setting a custom DBRecord class to use in newRecord().
	 * 
	 * @param recordClass
	 *            the class which extends DBRecord.class
	 */
	public void setDBRecordClass(final Class<? extends DBRecord> recordClass) {
		this.recordFactory = new ObjectFactory<DBRecord>() {

			@Override
			public DBRecord getObject() throws BeansException {
				try {
					return recordClass.newInstance();
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		};
	}

	/**
	 * Setting a custom ObjectFactory to allow custom DBReaders to use in
	 * queries.
	 * 
	 * @param readerFactory
	 */

	public void setDBReaderFactory(ObjectFactory<DBReader> readerFactory) {
		this.readerFactory = readerFactory;
	}

	/**
	 * Setting a custom DBReader class to use in queries.
	 * 
	 * @param readerClass
	 *            the class which extends DBReader.class
	 */
	public void setDBReaderClass(final Class<? extends DBReader> readerClass) {
		this.readerFactory = new ObjectFactory<DBReader>() {

			@Override
			public DBReader getObject() throws BeansException {
				try {
					return readerClass.newInstance();
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		};
	}

	/** Setting the wrapped JdbcTemplate */
	public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	public JdbcTemplate getJdbcTemplate() {
		return this.jdbcTemplate;
	}

	/** Setting the datasource */
	public final void setDataSource(DataSource dataSource) {
		if (this.jdbcTemplate == null
				|| dataSource != this.jdbcTemplate.getDataSource()) {
			this.jdbcTemplate = new JdbcTemplate(dataSource);
			this.jdbcTemplate.afterPropertiesSet();
		}
	}

	public void afterPropertiesSet() {
		if (getJdbcTemplate() == null) {
			throw new IllegalArgumentException(
					"Property 'jdbcTemplate' is required, either jdbcTemplate or dataSource must be set.");
		}

	}

	/**
	 * Executes a given DBCommand, mapping each row to a Java object via a
	 * DBRecordMapper.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param recordMapper
	 *            the mapper which maps each DBRecordData to a Java object
	 * @return the extracted list
	 */
	public <K> List<K> query(final DBCommand cmd,
			final DBRecordMapper<K> recordMapper) {
		return query(cmd, new DbRecordMapperExtractor<K>(recordMapper));

	}

	/**
	 * Executes a given DBCommand, mapping a single row to a Java object using
	 * the provided DBRecordMapper.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param recordMapper
	 *            the DBRecordMapper to map
	 * @return the single Object
	 * @throws IncorrectResultSizeDataAccessException
	 *             if more than one result object has been found
	 */
	public <K> K queryForObject(final DBCommand cmd,
			final DBRecordMapper<K> recordMapper) {

		return DataAccessUtils.uniqueResult(query(cmd, recordMapper));

	}

	/**
	 * Executes a given DBCommand, mapping a single column to a Java object
	 * using on DBRecordData.getValue() method.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param col
	 *            the column to map
	 * @return the extracted list
	 */
	public List<Object> queryForList(final DBCommand cmd, final DBColumnExpr col) {
		class SingleValueMapper implements DBRecordMapper<Object> {

			@Override
			public Object mapRecord(DBRecordData record, int rowNum) {
				return record.getValue(col);
			}

		}
		return query(cmd, new SingleValueMapper());

	}

	/**
	 * Executes a given DBCommand, mapping a single column to a single Long. If
	 * the value in the database is null, defaultValue is returned.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param col
	 *            the column to map
	 * @param defaultValue
	 *            the value to return in case of the database value is null
	 * @return the single Long value
	 * @throws IncorrectResultSizeDataAccessException
	 *             if more than one result object has been found
	 */
	public Long queryForLong(final DBCommand cmd, final DBColumnExpr col,
			Long defaultValue) {
		return DataAccessUtils.uniqueResult(queryForLongList(cmd, col,
				defaultValue));
	}

	/**
	 * Executes a given DBCommand, extracting a single column to a List of Long.
	 * If the value in the database is null, defaultValue is added to the list.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param col
	 *            the column to map
	 * @param defaultValue
	 *            the value to return in case of the database value is null
	 * @return the extracted list
	 */
	public List<Long> queryForLongList(final DBCommand cmd,
			final DBColumnExpr col, final Long defaultValue) {
		class SingleLongMapper implements DBRecordMapper<Long> {

			@Override
			public Long mapRecord(DBRecordData record, int rowNum) {
				return record.isNull(col) ? defaultValue : record.getLong(col);
			}

		}
		return query(cmd, new SingleLongMapper());

	}

	/**
	 * Executes a given DBCommand, mapping a single column to a single Integer.
	 * If the value in the database is null, defaultValue is returned.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param col
	 *            the column to map
	 * @param defaultValue
	 *            the value to return in case of the database value is null
	 * @return the single Integer value
	 * @throws IncorrectResultSizeDataAccessException
	 *             if more than one result object has been found
	 */
	public Integer queryForInteger(final DBCommand cmd, final DBColumnExpr col,
			Integer defaultValue) {
		return DataAccessUtils.uniqueResult(queryForIntegerList(cmd, col,
				defaultValue));
	}

	/**
	 * Executes a given DBCommand, extracting a single column to a List of
	 * Integers. If the value in the database is null, defaultValue is added to
	 * the list.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param col
	 *            the column to map
	 * @param defaultValue
	 *            the value to return in case of the database value is null
	 * @return the extracted list
	 */
	public List<Integer> queryForIntegerList(final DBCommand cmd,
			final DBColumnExpr col, final Integer defaultValue) {
		class SingleIntegerMapper implements DBRecordMapper<Integer> {

			@Override
			public Integer mapRecord(DBRecordData record, int rowNum) {
				return record.isNull(col) ? defaultValue : record.getInt(col);
			}

		}
		return query(cmd, new SingleIntegerMapper());
	}

	/**
	 * Executes a given DBCommand, mapping a single column to a single String.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param col
	 *            the column to map
	 * @return the single String value
	 * @throws IncorrectResultSizeDataAccessException
	 *             if more than one result object has been found
	 */
	public String queryForString(final DBCommand cmd, final DBColumnExpr col) {
		return DataAccessUtils.uniqueResult(queryForStringList(cmd, col));
	}

	/**
	 * Executes a given DBCommand, extracting a single column to a List of
	 * Strings.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param col
	 *            the column to map
	 * @return the extracted list
	 */
	public List<String> queryForStringList(final DBCommand cmd,
			final DBColumnExpr col) {
		class SingleStringMapper implements DBRecordMapper<String> {

			@Override
			public String mapRecord(DBRecordData record, int rowNum) {
				return record.getString(col);
			}

		}
		return query(cmd, new SingleStringMapper());
	}

	/**
	 * Executes a given DBCommand and handles the DBReader with the provided
	 * DBReaderExtractor.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param readerExtractor
	 * @return the result returned by the readerExtractor
	 */
	public <K> K query(final DBCommand cmd,
			final DBReaderExtractor<K> readerExtractor) {

		class QueryCallback implements ConnectionCallback<K> {
			public K doInConnection(Connection connection) throws SQLException,
					DataAccessException {
				return query(connection, cmd, readerExtractor);
			}
		}
		return getJdbcTemplate().execute(new QueryCallback());

	}

	/**
	 * Executes a given DBCommand and handles each row of the DBReader with the
	 * provided DBRecordCallbackHandler.
	 * 
	 * @param cmd
	 *            the DBCommand to execute
	 * @param recordCallbackHandler
	 */
	public void query(final DBCommand cmd,
			final DBRecordCallbackHandler recordCallbackHandler) {
		query(cmd, new DbRecordCallbackHandlerExtractor(recordCallbackHandler));
	}

	/**
	 * Deletes a given record from the database. Calls
	 * DBRecord.delete(connection).
	 * 
	 * @see org.apache.empire.db.DBRecord#delete(Connection)
	 * @param record
	 *            to delete
	 */
	public void deleteRecord(final DBRecord record) {

		class DeleteRecordCallback implements ConnectionCallback<Object> {
			public Object doInConnection(Connection connection)
					throws SQLException, DataAccessException {
				record.delete(connection);
				return null;
			}
		}
		getJdbcTemplate().execute(new DeleteRecordCallback());

	}

	/**
	 * Deletes a record from a table with a given single primary key.
	 * 
	 * @param table
	 *            the table to delete from
	 * @param key
	 *            the primary key
	 */

	public void deleteRecord(final DBTable table, final Object key) {
		deleteRecord(table, new Object[] { key });
	}

	/**
	 * Deletes a record from a table with a given multiple primary key. Calls
	 * DBTable.deleteRecord(Object[], Connection).
	 * 
	 * @see org.apache.empire.db.DBTable.deleteRecord(Object[], Connection)
	 * @param table
	 *            the table to delete from
	 * @param keys
	 *            the primary keys array
	 */
	public void deleteRecord(final DBTable table, final Object[] keys) {

		class DeleteRecordCallback implements ConnectionCallback<Object> {
			public Object doInConnection(Connection connection)
					throws SQLException, DataAccessException {
				table.deleteRecord(keys, connection);
				return null;
			}
		}
		getJdbcTemplate().execute(new DeleteRecordCallback());

	}

	/**
	 * Updates the record and saves all changes in the database. Calls
	 * DBRecord.update(connection).
	 *
	 * @see org.apache.empire.db.DBRecord#update(Connection)
	 * @param record
	 *            to update
	 * @return the updated record
	 */
	public DBRecord updateRecord(final DBRecord record) {

		class UpdateRecordCallback implements ConnectionCallback<DBRecord> {
			public DBRecord doInConnection(Connection connection)
					throws SQLException, DataAccessException {
				record.update(connection);
				return record;
			}
		}

		return getJdbcTemplate().execute(new UpdateRecordCallback());

	}

	/**
	 * Executes an Update statement from a command object. This method delegates
	 * to DBDatabase.executeUpdate(cmd, connection), getting the DBDatabase
	 * instance from the DBCOmmand object.
	 *
	 * @see org.apache.empire.db.DBDatabase#executeUpdate(DBCommand, Connection)
	 * 
	 * @param cmd
	 *            the command object containing the update command
	 * @return the number of records that have been updated with the supplied
	 *         statement
	 */
	public int executeUpdate(final DBCommand cmd) {

		class UpdateRecordCallback implements ConnectionCallback<Integer> {
			public Integer doInConnection(Connection connection)
					throws SQLException, DataAccessException {
				return cmd.getDatabase().executeUpdate(cmd, connection);
			}
		}

		return getJdbcTemplate().execute(new UpdateRecordCallback());

	}

	/**
	 * Executes a Delete statement from a command object. This method delegates
	 * to DBDatabase.executeDelete(cmd, connection), getting the DBDatabase
	 * instance from the DBCOmmand object.
	 *
	 * @see org.apache.empire.db.DBDatabase#executeDelete(DBCommand, Connection)
	 * 
	 * @param cmd
	 *            the command object containing the delete command
	 * @return the number of records that have been deleted with the supplied
	 *         statement
	 */
	public int executeDelete(final DBTable table, final DBCommand cmd) {

		class DeleteRecordCallback implements ConnectionCallback<Integer> {
			public Integer doInConnection(Connection connection)
					throws SQLException, DataAccessException {
				return cmd.getDatabase().executeDelete(table, cmd, connection);
			}
		}

		return getJdbcTemplate().execute(new DeleteRecordCallback());

	}

	/**
	 * Executes an Insert statement from a command object. This method delegates
	 * to DBDatabase.executeInsert(cmd, connection), getting the DBDatabase
	 * instance from the DBCOmmand object.
	 *
	 * @see org.apache.empire.db.DBDatabase#executeInsert(DBCommand, Connection)
	 * 
	 * @param cmd
	 *            the command object containing the insert command
	 * @return the number of records that have been inserted with the supplied
	 *         statement
	 */
	public int executeInsert(final DBCommand cmd) {

		class InsertRecordCallback implements ConnectionCallback<Integer> {
			public Integer doInConnection(Connection connection)
					throws SQLException, DataAccessException {
				return cmd.getDatabase().executeInsert(cmd, connection);
			}
		}

		return getJdbcTemplate().execute(new InsertRecordCallback());

	}

	/**
	 * Helper method to an create a DBRecord instance. Can be customized through
	 * setRecordFactory or setRecordClass methods.
	 *
	 * @param table
	 *            the table
	 * @return the new DBRecord instance
	 */
	public DBRecord newRecord(final DBRowSet table) {
		DBRecord record = this.recordFactory.getObject();
		record.create(table);
		return record;
	}

	/**
	 * Opens a DBRecord instance with the given multiple primary keys. In
	 * contrast of getRecord(DBRowSet, Object) this method throws
	 * RecordNotFoundExpcetion if matching record exists.
	 *
	 * @param table
	 *            the table to read the record from
	 * @param key
	 *            the primary key
	 * @return the DBRecord instance, never null
	 * @throws org.apache.empire.db.exceptions.RecordNotFoundException
	 *             in case of the record not found
	 */
	public DBRecord openRecord(final DBRowSet table, final Object key) {
		return openRecord(table, new Object[] { key });
	}

	/**
	 * Opens a DBRecord instance with the given multiple primary keys. In
	 * contrast of getRecord(DBRowSet, Object[]) this method throws
	 * RecordNotFoundExpcetion if matching record exists.
	 *
	 * @param table
	 *            the table to read the record from
	 * @param keys
	 *            the primary key array
	 * @return the DBRecord instance, never null
	 * @throws org.apache.empire.db.exceptions.RecordNotFoundException
	 *             in case of the record not found
	 */
	public DBRecord openRecord(final DBRowSet table, final Object[] keys) {

		class ReadRecordCallback implements ConnectionCallback<DBRecord> {
			public DBRecord doInConnection(Connection connection)
					throws SQLException, DataAccessException {
				DBRecord record = EmpireTemplate.this.recordFactory.getObject();
				record.read(table, keys, connection);
				return record;
			}
		}

		return getJdbcTemplate().execute(new ReadRecordCallback());

	}

	/**
	 * Opens a DBRecord instance with the given single primary key. In contrast
	 * of openRecord(DBRowSet, Object) this method returns null if no matching
	 * record exists.
	 *
	 * @param table
	 *            the table to read the record from
	 * @param key
	 *            the primary key
	 * @return the DBRecord instance, can be null
	 */
	public DBRecord getRecord(final DBRowSet table, final Object key) {
		return openRecord(table, new Object[] { key });
	}

	/**
	 * Opens a DBRecord instance with the given multiple primary keys. In
	 * contrast of openRecord(DBRowSet, Object[]) this method returns null if no
	 * matching record exists.
	 *
	 * @param table
	 *            the table to read the record from
	 * @param keys
	 *            the primary keys array
	 * @return the DBRecord instance, can be null
	 */
	public DBRecord getRecord(final DBRowSet table, final Object[] keys) {

		class ReadRecordCallback implements ConnectionCallback<DBRecord> {
			public DBRecord doInConnection(Connection connection)
					throws SQLException, DataAccessException {
				DBRecord record = EmpireTemplate.this.recordFactory.getObject();
				try {
					record.read(table, keys, connection);
				} catch (RecordNotFoundException e) {
					return null;
				}
				return record;
			}
		}

		return getJdbcTemplate().execute(new ReadRecordCallback());

	}

	/**
	 * Executes a given DBCommand query and maps each row to Class<T> object
	 * using DBReader.getBeanList method.
	 * 
	 * @see org.apache.empire.db.DBReader.getBeanList(C, Class<T>, int)
	 * 
	 * @param cmd
	 *            the query command
	 * @param c
	 *            the collection to add the objects to
	 * @param t
	 *            the class type of the objects in the list
	 * @param maxCount
	 *            the maximum number of objects
	 * 
	 * @return the list of T
	 */
	public <C extends Collection<T>, T> C queryForBeanList(final DBCommand cmd,
			final C c, final Class<T> t, final int maxCount) {

		class GetBeanListCallback implements DBReaderExtractor<C> {

			@Override
			public C process(DBReader reader) {
				return reader.getBeanList(c, t, maxCount);
			}
		}

		return query(cmd, new GetBeanListCallback());

	}

	/**
	 * Executes a given DBCommand query and maps each row to Class<T> object
	 * using DBReader.getBeanList method.
	 * 
	 * @see org.apache.empire.db.DBReader.getBeanList(Class<T>, int)
	 * 
	 * @param cmd
	 *            the query command
	 * @param t
	 *            the class type of the objects in the list
	 * @param maxCount
	 *            the maximum number of objects
	 * 
	 * @return the list of T
	 */
	public <T> List<T> queryForBeanList(DBCommand cmd, Class<T> t, int maxItems) {
		return queryForBeanList(cmd, new ArrayList<T>(), t, maxItems);
	}

	/**
	 * Executes a given DBCommand query and maps each row to Class<T> object
	 * using DBReader.getBeanList method.
	 * 
	 * @see org.apache.empire.db.DBReader.getBeanList(Class<T>)
	 * 
	 * @param cmd
	 *            the query command
	 * @param t
	 *            the class type of the objects in the list
	 * 
	 * @return the list of T
	 */

	public <T> List<T> queryForBeanList(DBCommand cmd, Class<T> t) {
		return queryForBeanList(cmd, t, -1);
	}

	/**
	 * Executes a given DBCommand query and maps a single row to Class<T> object
	 * using DBReader.getBeanList method.
	 * 
	 * @param cmd
	 *            the query command
	 * @param t
	 *            the class type of the object to return
	 * 
	 * @return the list of T
	 * @throws IncorrectResultSizeDataAccessException
	 *             if more than one result object has been found
	 */
	public <T> T queryForBean(DBCommand cmd, Class<T> t) {
		return DataAccessUtils.uniqueResult(queryForBeanList(cmd, t, -1));
	}

	/**
	 * Executes a ConnectionCallback. Delegates to the underlying JdbcTemplate.
	 * 
	 * @see 
	 *      org.springframework.jdbc.core.JdbcTemplate.execute(ConnectionCallback
	 *      <K>)
	 * 
	 * @param connectionCallback
	 * @return arbitrary object
	 */
	public <K> K execute(ConnectionCallback<K> connectionCallback) {
		return getJdbcTemplate().execute(connectionCallback);
	}

	private <K> K query(Connection connection, DBCommand command,
			DBReaderExtractor<K> callback) {
		DBReader reader = newDBReader();
		try {
			reader.open(command, connection);

			return callback.process(reader);

		} finally {
			reader.close();
		}
	}

	/**
	 * Creates a new DBReader instance from the readerFactory;
	 * 
	 * @return DBReader instance
	 */

	private DBReader newDBReader() {

		return this.readerFactory.getObject();
	}

	private static class DbRecordCallbackHandlerExtractor implements
			DBReaderExtractor<Object> {

		private final DBRecordCallbackHandler rowCallbackHandler;

		public DbRecordCallbackHandlerExtractor(
				DBRecordCallbackHandler rowCallbackHandler) {
			Assert.notNull(rowCallbackHandler, "RowCallbackHandler is required");
			this.rowCallbackHandler = rowCallbackHandler;
		}

		// @Override
		public Object process(DBReader reader) {
			try {
				while (reader.moveNext()) {
					this.rowCallbackHandler.processRecord(reader);
				}
				return null;
			} finally {
				reader.close();
			}
		}

	}

	private static class DbRecordMapperExtractor<K> implements
			DBReaderExtractor<List<K>> {

		private final DBRecordMapper<K> dataReader;

		public DbRecordMapperExtractor(DBRecordMapper<K> rowMapper) {
			Assert.notNull(rowMapper, "DataReader is required");
			this.dataReader = rowMapper;
		}

		// @Override
		public List<K> process(DBReader reader) {
			try {
				List<K> results = new ArrayList<K>();
				int rowNum = 0;

				while (reader.moveNext()) {
					results.add(this.dataReader.mapRecord(reader, rowNum));
					rowNum++;
				}

				return results;

			} finally {
				reader.close();
			}
		}

	}

}
