/*******************************************************************************
 * Copyright © 2005 VHA. All rights reserved
 ******************************************************************************/
package gov.va.med.fw.persistent.hibernate;

// Java Classes
import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;

// Library Classes
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.HibernateException;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.logging.Log;

// Framework Classes
import gov.va.med.fw.service.pagination.SearchQueryInfo;
import gov.va.med.fw.model.AbstractEntity;
import gov.va.med.fw.util.ObjectUtils;
import gov.va.med.fw.persistent.MaxRecordsExceededException;

/**
 * A class that intelligently executes a paginated query and returns its
 * results.
 * 
 * @author Andrew Pach
 * @version 1.0
 */
public class PaginatedQueryExecutor extends AbstractEntity {
	
	/**
	 * An instance of serialVersionUID
	 */
	private static final long serialVersionUID = 5245502870107943655L;

	/**
	 * The query which returns the number of records in the data query.
	 */
	private Query countQuery;

	/**
	 * The data query to issue that returns the desired results.
	 */
	private Query dataQuery;

	/**
	 * The Hibernate Criteria query which returns the number of records in the data query.
	 */
	private Criteria countCriteria;
	
	/**
	 * The Hibernate Criteria query to issue that returns the desired results.
	 */
	private Criteria dataCriteria;
	
	/**
	 * The search query information which defines how the query should be
	 * executed.
	 */
	private SearchQueryInfo searchQueryInfo;

	private boolean sortPerformed = false;
	
	/**
	 * Constructs a paginated query executor.
	 * 
	 * @param countQuery
	 *           The query that performs a count of the number of rows retrieved.
	 *           This query should return one row and one column in that row that
	 *           provides the number of rows that will be retrieved by the data
	 *           query. (i.e. select count(*) from ...).
	 * @param dataQuery
	 *           The query that retrieves the result data.
	 * @param searchQueryInfo
	 *           The paginated query parameters
	 */
	public PaginatedQueryExecutor(Query countQuery, Query dataQuery,
			SearchQueryInfo searchQueryInfo) {
		this.countQuery = countQuery;
		this.dataQuery = dataQuery;
		this.searchQueryInfo = searchQueryInfo;
	}

	/**
	 * Constructs a paginated query executor.
	 * 
	 * @param countCriteria
	 *           The Criteria query that performs a count of the number of rows retrieved.
	 *           This query should return one row and one column in that row that
	 *           provides the number of rows that will be retrieved by the data
	 *           query. (i.e. select count(*) from ...).
	 * @param dataCriteria
	 *           The query that retrieves the result data.
	 * @param searchQueryInfo
	 *           The paginated query parameters
	 * @param sortPerformed
	 * 			 If sorting was performed in the query
	 */
	public PaginatedQueryExecutor(Criteria countCriteria, Criteria dataCriteria,
			SearchQueryInfo searchQueryInfo, boolean sortPerformed) {
		
		this.countCriteria = countCriteria;
		this.dataCriteria = dataCriteria;
		this.searchQueryInfo = searchQueryInfo;
		this.sortPerformed = sortPerformed;
	}

	/**
	 * Executes the query and returns the list of results.
	 * 
	 * @return The list of records from the data query.
	 * @throws HibernateException
	 *            if there are any errors while executing the query.
	 * @throws MaxRecordsExceededException
	 *            if the maximum number of records retrieved exceeds the maximum
	 *            allowed.
	 */
	public List executeQuery() throws HibernateException,
			MaxRecordsExceededException {
		// The results to return
		List results = new ArrayList();

        Log logger = getLogger();

        // Get the total number of records
		int numResults = executeCountQuery();
		searchQueryInfo.setTotalNumberOfEntries(numResults);
		if (numResults == 0) {
			// ***** NO RESULTS RETURNED *****
			// Query returned no results so just return with an empty list
			searchQueryInfo
					.setSearchTypePerformed(SearchQueryInfo.SEARCH_READ_ALL);
			return results;
		}

		// Check to ensure the maximum number of result limit hasn't been reached
		checkMaxRecordsExceeded(numResults);

		// Fetch the type of action the user is performing
		String action = searchQueryInfo.getSearchAction();
		if (logger.isDebugEnabled()) {
			logger.debug("Action is \"" + action + "\".");
		}

		// Determine whether a sort was performed
		boolean sortPerformed = getSortPerformed(); 
		if (logger.isDebugEnabled()) {
			logger.debug("Data query is performing sort flag: \"" + sortPerformed
					+ "\".");
		}
		searchQueryInfo.setSortPerformed(sortPerformed);

		// ***** SORT COLUMN NOT SUPPORTED *****
		//DNS   doank CCR8600: added condition to correctly perform full search
		//Full Search is only perform when there's no Order By Clause and the Number of Sort Elements is greater than 0
		//This means that there're sort column that cannot be performed in the database, e.g some computed column based on a table field
		if ((searchQueryInfo.getSortElements() != null && searchQueryInfo.getSortElements().size() > 0 ) && (!sortPerformed)) {
			if (logger.isDebugEnabled()) {
				logger.debug("A requested GUI sort column is not supported within the database.  "
								+ "SortColumnMap: " + searchQueryInfo.getSortElements());
			}
			// A sort was requested, but not implemented in the query. This means
			// that a sort was requested
			// on a field that is not in the database or not in the form that is
			// correctly sortable
			// (e.g. the database contains a reference value, but the user sees the
			// reference text associated
			// with the value).
			// In this case, perform a full search since the GUI will have to
			// handle the sorting for us.
			// Also, set a flag in SearchQueryInfo in case the caller needs to
			// perform any special processing
			// in this scenario.
		
			searchQueryInfo.setSortColumnNotSupported(true);
			return performFullSearch();
		}

		// ***** PAGING OR SORTING Query *****
		if ((action.equals(SearchQueryInfo.ACTION_PAGE))
				|| (action.equals(SearchQueryInfo.ACTION_SORT))) {
			// For paging and sortin, just return the requested page's worth of
			// data
			return performSinglePageSearch();
		}

		// Get the search optimization type
		String searchOptimizationType = searchQueryInfo
				.getSearchOptimizationType();
		if (logger.isDebugEnabled()) {
			logger.debug("Search Optimization Type is \"" + searchOptimizationType
					+ "\".");
		}

		// ***** NEW SEARCH QUERY - READ ONE PAGE *****
		if (searchOptimizationType.equals(SearchQueryInfo.SEARCH_SINGLE_PAGE)) {
			// The search optimization type is "single page" so retrieve only one
			// page's worth of data
			return performSinglePageSearch();
		}

		// ***** NEW SEARCH QUERY - READ ALL PAGES *****
		if (searchOptimizationType.equals(SearchQueryInfo.SEARCH_READ_ALL)) {
			// The search optimization type is "read all" so retrieve all of the
			// data
			return performFullSearch();
		}

		// ***** NEW SEARCH QUERY - OPTIMIZE QUERY *****

		// First retrieve the first page's worth of data
		if (logger.isDebugEnabled()) {
			logger.debug("Retrieving the first pages worth of data.");
		}
		results = executeSinglePageQuery(1);

		if ((results.size() < searchQueryInfo.getNumberPerPage())
				&& (searchQueryInfo.getPagingPage() == 1)) {
			if (logger.isDebugEnabled()) {
				logger.debug("All " + searchQueryInfo.getTotalNumberOfEntries()
						+ " records have been retrieved.");
			}

			// ***** ALL RECORDS READ IN FIRST PAGE *****
			// We have already read all the records (i.e. the first page is all the
			// data)
			// so just return this one page.
			searchQueryInfo
					.setSearchTypePerformed(SearchQueryInfo.SEARCH_READ_ALL);
			return results;
		}

		// Compute the total estimated size in bytes of the results set.
		// Eventhough we might loose some precision in this calculation, this is
		// not a problem since we're just
		// computing an estimate.
		int firstPageSize = ObjectUtils.getObjectSize(results);
		int totalEstimatedSize = firstPageSize
				/ searchQueryInfo.getNumberPerPage() * numResults;
		if (logger.isDebugEnabled()) {
			logger.debug("The size of the first page is " + firstPageSize
					+ " bytes and the total number of " + "records is " + numResults
					+ " so the total estimated size is " + totalEstimatedSize
					+ " bytes.");
		}

		if (totalEstimatedSize > searchQueryInfo
				.getMaxInMemoryEstimatedResultsSizeInBytes()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Since the estimated size of " + totalEstimatedSize
						+ " bytes is > " + "the maximum allowed of "
						+ searchQueryInfo.getMaxInMemoryEstimatedResultsSizeInBytes()
						+ ", we will fetch only 1 pages worth of data.");
			}

			// The estimated size of the results is greater than what is allowed
			// per our configuration.
			// As a result, we need to read only one pages worth of data
			if (searchQueryInfo.getPagingPage() != 1) {
				// The page we already read is not the one being requested so read
				// the correct page
				results = executeSinglePageQuery(searchQueryInfo.getPagingPage());
			}

			// Set the type of search we performed
			searchQueryInfo
					.setSearchTypePerformed(SearchQueryInfo.SEARCH_SINGLE_PAGE);

			// Return the results
			return results;
		}
		else {
			if (logger.isDebugEnabled()) {
				logger.debug("Since the estimated size of " + totalEstimatedSize
						+ " bytes is <= " + "the maximum allowed of "
						+ searchQueryInfo.getMaxInMemoryEstimatedResultsSizeInBytes()
						+ ", we will perform a full search.");
			}

			// The estimated size of the results is within what is allowed. As a
			// result, read all the data.
			return performFullSearch();
		}
	}

	/**
	 * If Query is used, check to see if ORDER BY exists in the query;
	 * if Criteria query is used, use the stored fiedld that was passed in
	 * via the constructor
	 * 
	 * @return true if sorting is performed in the query
	 */
	private boolean getSortPerformed() 
	{
		if (dataQuery != null)
			return (dataQuery.getQueryString().indexOf("ORDER BY") != -1);
		else
			return this.sortPerformed;
	}
	
	/**
	 * Performs a single page search for the current page's worth of data.
	 * 
	 * @return The list of results
	 * @throws HibernateException
	 *            If any problems were encountered executing the query.
	 */
	protected List performSinglePageSearch() throws HibernateException {
		if (getLogger().isDebugEnabled()) {
			getLogger().debug("Performing a single page search of page "
					+ searchQueryInfo.getPagingPage() + ".");
		}

		// Get the results for the current page's worth of data
		List results = executeSinglePageQuery(searchQueryInfo.getPagingPage());

		// Set the type of search we performed
		searchQueryInfo
				.setSearchTypePerformed(SearchQueryInfo.SEARCH_SINGLE_PAGE);

		// Return the results
		return results;
	}

	/**
	 * Performs a full page search.
	 * 
	 * @return The full list of results.
	 * @throws HibernateException
	 *            If any problems were encountered executing the query.
	 */
	protected List performFullSearch() throws HibernateException {
		if (getLogger().isDebugEnabled()) {
			getLogger().debug("Performing a full search.");
		}

		// If we are being asked to retrieve the full set of results, go get them
		// all.
		List results = executeFullQuery();

		// Set the type of search we performed
		searchQueryInfo.setSearchTypePerformed(SearchQueryInfo.SEARCH_READ_ALL);

		// Return the results
		return results;
	}

	/**
	 * Executes a count query and returns the number of rows.
	 * 
	 * @return The row count.
	 * @throws HibernateException
	 *            If any problems were encountered executing the query.
	 */
	protected int executeCountQuery() throws HibernateException {
		if (getLogger().isDebugEnabled()) {
			getLogger().debug("Retrieving the total number of records in the search.");
		}

		List results = (countQuery!=null) ? countQuery.list() : countCriteria.list();
		
		int count = 0;
		if (results.size() > 0) {
            Object resultCount = results.get(0);
            if (BigDecimal.class.isAssignableFrom(resultCount.getClass())){
                count = ((BigDecimal) resultCount).intValue();
            }
            else {
                count = ((Integer) resultCount).intValue();
            }
		}

		if (getLogger().isDebugEnabled()) {
			getLogger().debug("Total number of entries in search: " + count + ".");
		}

		return count;
	}

	/**
	 * Executes a query that will return the current pages worth of data.
	 * 
	 * @param pageNumber
	 *           the page number for the page to retrieve.
	 * @return The list of records from the data query for the current page.
	 * @throws HibernateException
	 *            If any problems were encountered executing the query.
	 */
	protected List executeSinglePageQuery(int pageNumber)
			throws HibernateException {
		int numberPerPage = searchQueryInfo.getNumberPerPage();
		int firstRowNum = (pageNumber - 1) * numberPerPage;

		// if using Query approach
		if (dataQuery != null) {
			dataQuery.setFirstResult(firstRowNum);
			dataQuery.setMaxResults(numberPerPage);
			return dataQuery.list();
		}
		// else using Criteria query approach
		else {
				dataCriteria.setFirstResult(firstRowNum);
				dataCriteria.setMaxResults(numberPerPage);
				return dataCriteria.list();			
		}
	}

	/**
	 * Executes a query that will return all the data results across all pages
	 * (i.e. the full set of results).
	 * 
	 * @return The full list of records from the data query.
	 * @throws HibernateException
	 *            If any problems were encountered executing the query.
	 */
	protected List executeFullQuery() throws HibernateException {
		// if using Query approach
		if (dataQuery != null) {
			dataQuery.setFirstResult(0);
			dataQuery.setMaxResults(Integer.MAX_VALUE);
			return dataQuery.list();
		}
		// else using Criteria query approach
		else {
			dataCriteria.setFirstResult(0);
			dataCriteria.setMaxResults(Integer.MAX_VALUE);
			return dataCriteria.list();
		}
	}

	/**
	 * Checks to see if the maximum number of records have been exceeded.
	 * 
	 * @param totalResults
	 *           The total number of results from the query
	 * @throws MaxRecordsExceededException
	 *            if the maximum number of records have been exceeded.
	 */
	protected void checkMaxRecordsExceeded(int totalResults)
			throws MaxRecordsExceededException {
		if (totalResults > searchQueryInfo.getMaxAllowedRecords()) {
			throw new MaxRecordsExceededException(
					totalResults,
					searchQueryInfo.getMaxAllowedRecords(),
					totalResults
							+ " records match the search criteria which exceeds the configured maximum of "
							+ searchQueryInfo.getMaxAllowedRecords() + ".");
		}
	}

	/**
	 * @see gov.va.med.fw.model.AbstractEntity#buildToString(org.apache.commons.lang.builder.ToStringBuilder)
	 */
	protected void buildToString(ToStringBuilder builder) {
		builder.append("countQuery", countQuery);
		builder.append("dataQuery", dataQuery);
		builder.append("searchQueryInfo", searchQueryInfo);
		builder.append("countCriteria", countCriteria);
		builder.append("dataCriteria", dataCriteria);
	}
}