package com.tandbergtv.workflow.driver.search;

import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

import com.tandbergtv.workflow.core.WorkflowProcess;
import com.tandbergtv.workflow.core.WorkflowTemplate;
import com.tandbergtv.workflow.util.SearchCriteria;
import com.tandbergtv.workflow.util.SortingOrder;

/**
 * Performs search for WorkflowProcesses based on the parameters given in a
 * SearchCriteria object.
 * 
 * @author Imran Naqvi
 */
public class WorkflowProcessSearchHelper implements ISearchHelper {
	
	private SessionFactory factory;

	private static final Logger logger = Logger.getLogger(WorkflowProcessSearchHelper.class);

	private static final String PROCESS_ALIAS = SearchParameterBase.PROCESS_ALIAS;

	private static final String WORKFLOW_TEMPLATE_ALIAS = SearchParameterBase.WORKFLOW_TEMPLATE_ALIAS;

	private static final String ORDER_BY = " ORDER BY ";

	private static final String NON_ARCHIVED_WHERE_CLAUSE = 
		" AND " + PROCESS_ALIAS + "." + "active = true";
	
	private static final String WORKORDER_TEMPLATE_WHERE_CLAUSE = 
		PROCESS_ALIAS + ".processDefinition.id = " + WORKFLOW_TEMPLATE_ALIAS + ".id";

	private static final String BASIC_COUNT_CLAUSE = 
		"Select Count(Distinct "+PROCESS_ALIAS+") from WorkflowProcess " + PROCESS_ALIAS;
	
	/**
	 * Creates a WorkflowProcessSearchHelper
	 */
	public WorkflowProcessSearchHelper(SessionFactory factory) {
		this.factory = factory;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.tandbergtv.workflow.driver.search.ISearchHelper#search(com.tandbergtv.workflow.util.SearchCriteria)
	 */
	public List<WorkflowProcess> search(SearchCriteria searchCriteria) {
		List<WorkflowProcess> processes = null;
		Session session = factory.openSession();
		
		try {
			String queryString = null;
			if (searchCriteria.count() == 0)
				queryString = getSearchAllQuery(searchCriteria);
			else
				queryString = buildQuery(searchCriteria);
			logger.debug("Executing Query:" + queryString);
			if (queryString != null) {
				Query query = session.createQuery(queryString);
				if (searchCriteria.getStartingRecordNumber() != 0)
					query.setFirstResult(searchCriteria
							.getStartingRecordNumber());
				if (searchCriteria.getRecordsCount() != 0)
					query.setMaxResults(searchCriteria.getRecordsCount());
				List queryResult = query.list();
				processes = getProcessListFromQueryResult(queryResult);
			}
			logger.debug("Search Result:" + processes.size() + " Processes");
		} finally {
			session.close();
		}
		
		return processes;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.tandbergtv.workflow.driver.search.ISearchHelper#count(com.tandbergtv.workflow.util.SearchCriteria)
	 */
	public int count(SearchCriteria searchCriteria) {
		int count = 0;
		Session session = factory.openSession();
		try {
			String queryString = buildCountQuery(searchCriteria);
			if (queryString != null) {
				logger.debug("Executing Query:" + queryString);
				List queryResult = session.createQuery(queryString).list();
				count = Integer.parseInt(queryResult.get(0).toString());
				logger.debug("Received count:" + count);
			}
		} finally {
			session.close();
		}
		
		return count;
	}

	/**
	 * Generates a list of WorkflowProcesses from the List obtained after
	 * executing the query.
	 * 
	 * @param queryResult
	 */
	private List<WorkflowProcess> getProcessListFromQueryResult(List queryResult) {
		List<WorkflowProcess> processes = new ArrayList<WorkflowProcess>();

		for (Object objects : queryResult) {
			WorkflowProcess process = null;

			/*
			 * In some cases we are going to get multiple classes from the
			 * query, but the first object in each 'row' is going to be a
			 * WorkflowProcess because that's how we construct our query
			 */
			if (objects instanceof WorkflowProcess)
				process = (WorkflowProcess) objects;
			else
				process = (WorkflowProcess) ((Object[]) objects)[0];
				
			processes.add(process);
		}
		return processes;
	}

	/**
	 * Builds the HQL query to retrieve the number of processes that are
	 * retrieved based on the parameters specified in the SearchCriteria. The
	 * query is built based the WorkflowProcess parameters or variableInstance
	 * parameters, and excluded resourcegroup parameters.
	 * 
	 * @param searchCriterion
	 */
	private String buildCountQuery(SearchCriteria searchCriteria) {
		if (searchCriteria.count() == 0){
			return getFromClause(searchCriteria, true) + " WHERE " + getBasicWhereClause();
		}
		return getFromClause(searchCriteria, true) + getWhereClause(searchCriteria);
	}

	/**
	 * Gets the HQL query which would retrieve all WorkflowProcesses.
	 * 
	 * @return
	 */
	private String getSearchAllQuery(SearchCriteria searchCriteria) {
		String orderClause = getOrderClause(searchCriteria);
		String fromClause = getFromClause(searchCriteria, false);
		return fromClause + " WHERE " + getBasicWhereClause()
				+ (orderClause == null ? "" : orderClause);
	}

	/**
	 * Builds the HQL query based on the parameters specified in the
	 * SearchCriteria. The query is built based the WorkflowProcess parameters
	 * or variableInstance parameters, and excluded resourcegroup parameters.
	 * 
	 * @param searchCriterion
	 */
	private String buildQuery(SearchCriteria searchCriteria) {
		String query = getFromClause(searchCriteria, false);
		if (query != null)
			query += getWhereClause(searchCriteria);
		String orderClause = getOrderClause(searchCriteria);
		if (orderClause != null)
			query += orderClause;
		return query;
	}

	/**
	 * Gets the Select Clause of the query. In case we are sorting by some process variables we would need
	 * to include them in our select clause. Wewould also need to include the fields in the select clause
	 * if we are sorting by a property of an bject that has a relationship with WorkflowProcess i.e. node 
	 * name (process.rootToken.node.name).
	 * @param searchCriteria
	 * @return
	 */
	private String getSelectClause(SearchCriteria searchCriteria){
		String selectClause = "Select Distinct "+PROCESS_ALIAS;
		for (SearchParameterBase param : searchCriteria.getSearchList())
			if (param.getSortingOrder() != null){
				if(param.isVarInstance())
					selectClause += ", " + param.getAlias();
				else if(param.getFieldName().indexOf(".") != -1) 
					selectClause += ", " + param.getAlias() +"."+ param.getFieldName();
			}
		return selectClause;
	}
	
	/**
	 * Builds the from clause of the HQL query based on the parameters specified
	 * in the SearchCriteria.
	 * 
	 * @param searchCriteria
	 */
	private String getFromClause(SearchCriteria searchCriteria, boolean countQuery) {
		String fromClause;
		if(!countQuery)
			 fromClause = " from WorkflowProcess " + PROCESS_ALIAS;
		else
			 fromClause = BASIC_COUNT_CLAUSE;
		
		String templateClassName = null;
		int objectCount = 1;
		
		/**
		*We go through the SOrtParameters which are for proces variables first since they require
		*Left Outer Joins, and these need to be done first before adding other tables in the from clause.
		*/
		if(!countQuery)
			for (SearchParameterBase param : searchCriteria.getSearchList()) {
				if (param.isVarInstance() && param instanceof SortParameter) {
					String fromPart = param.getPartialFromClause(objectCount++);
					fromClause += fromPart.equals("") ? "" : fromPart;
				}
			}
		for (SearchParameterBase param : searchCriteria.getSearchList()) {
			if (param.isVarInstance() && param instanceof SortParameter) 
				continue;
			else if (param instanceof TemplateParameter) {
				templateClassName = ((TemplateParameter)param).getClassName();
			}else{
				String fromPart = param.getPartialFromClause(objectCount++);
				fromClause += fromPart.equals("")?"":"," + fromPart;
			}
		}
		
		/* 
		 * All queries need to be filtered by the ProcessDefinition subclass. We use a default
		 * if the search criteria doesn't explicitly tell us.
		 */
		if (templateClassName == null)
			templateClassName = getDefaultTemplateClass().getName();
		
		fromClause += ", " + templateClassName + " " + WORKFLOW_TEMPLATE_ALIAS;
		
		if(!countQuery)
			return getSelectClause(searchCriteria) + fromClause;
		return fromClause;
	}
	
	private Class getDefaultTemplateClass() {
		return WorkflowTemplate.class;
	}

	/**
	 * Builds the where clause of the HQL query based on the parameters
	 * specified in the SearchCriteria.
	 * 
	 * @param searchCriteria
	 */
	private String getWhereClause(SearchCriteria searchCriteria) {
		String whereClause = "";
		for (SearchParameterBase param : searchCriteria.getSearchList()) {
			if (param instanceof TemplateParameter)
				continue;
			String partialWhere = param.getPartialWhereClause();
			if(partialWhere.equals(""))
				continue;
			whereClause += (whereClause.trim() == "" ? " WHERE " : " AND ");
			whereClause += partialWhere;
		}
		whereClause += (whereClause.trim() == "" ? " WHERE " : " AND ");
		whereClause += getBasicWhereClause();
		return whereClause;
	}

	/**
	 * Return the part of the where clause which is appended to all queries.
	 * 
	 * @return
	 */
	private String getBasicWhereClause() {
		/* Only look for processes that are created from WorkOrder Templates */
		String whereClause = WORKORDER_TEMPLATE_WHERE_CLAUSE;

		/* Only look for processes that have not been archived */
		whereClause += NON_ARCHIVED_WHERE_CLAUSE;
		return whereClause;
	}

	/**
	 * Builds the Order by clause of the HQL query based on the parameters
	 * specified in the SearchCriteria.
	 * 
	 * @param searchCriteria
	 */
	private String getOrderClause(SearchCriteria searchCriteria) {
		String orderClause = "";
		for (SearchParameterBase param : searchCriteria.getSearchList()) {
			if(param.getSortingOrder() == null)
				continue;
			if (!orderClause.equals(""))
				orderClause += ",";
			else
				orderClause = ORDER_BY;
			orderClause += param.getAlias() + "." + param.getSortingColumn();
			
			orderClause += param.getSortingOrder() == SortingOrder.ASCENDING ? " ASC" : " DESC";
		}
		return orderClause;
	}
	
}
