/**
 * 
 */


package gov.va.med.cds.persistence.hibernate;


import gov.va.med.cds.exceptionframework.ExceptionHandler;
import gov.va.med.cds.filter.EntryFilterInterface;
import gov.va.med.cds.persistence.QueryAssociationInterface;
import gov.va.med.cds.persistence.QueryWorkInterface;
import gov.va.med.cds.template.generated.JaxBMarshallerUnmarshaller;
import gov.va.med.cds.template.generated.JaxBMarshallerUnmarshallerInterface;
import gov.va.med.cds.util.PerformanceLog;
import gov.va.med.cds.util.TimeoutUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;


/**
 * @author vhaislegberb
 *
 *  The <code>DefaultQueryWork</code> class implements the convention-based named 
 *  query name building mechanism defined originally in CDS v2.0. This convention
 *  based approach append to the entry point name optionally the 'pids' or patient
 *  identifiers, the start and end dates, and the record identifiers. 
 */
public class DefaultQueryWork
    implements
        QueryWorkInterface
{
    protected static final Log LOGGER = LogFactory.getLog( DefaultQueryWork.class );
    
    protected Session session;

    protected Element results = DocumentHelper.createElement( "results" );

    protected EntryFilterInterface entryFilter;

    protected List<String> personIdentifiers;

    protected QueryAssociationInterface queryAssociation;

    private List<Exception> exceptions = new ArrayList<Exception>();

    protected Class<?> pointInTimeUserType;

    protected String applicationName;
    
    protected String siteId;
    
    private Map<String, String> templateTimeoutMap = null;
    
    private QueryNameBuilderInterface queryNameBuilder = null;
    
    private FilterParameterExtractorInterface filterParameterExtractor = null;
    
    private Map<String, QueryParameterTransformerInterface> parameterTransformerMap = null;
    
    protected QueryParameterBinderInterface queryParameterBinder = null;

    protected static PerformanceLog performanceLog = PerformanceLog.getInstance();
    
    protected JaxBMarshallerUnmarshallerInterface marshallerUnmarshaller;
   
    
    /**
     * Constructs a new instance of DefaultQueryWork for use in processing filtering requests and querying information 
     * from the data store.
     * @param session The Hibernate Session instance.
     * @param queryAssociation Support for multi query domain reads.
     * @param entryFilter Entry filter defines what the client is asking for and how that data should be filtered.
     * @param queryNameBuilder Defines the algorithm for building or resolving a filter to a Hibernate named query name.
     * @param filterParameterExtractor Contains logic for how to extract parameter values form the filter.
     * @param parameterTransformerMap A map of parameter names to parameter transformer instances to use to transform parameters prior
     * to use in the query.
     * @param personIdentifiers The list of person identifiers to query for for Patient Centric queries.
     * @param pointInTimeUserType The Point In Time user type to use in setting date parameters on the query.
     * @param applicationName The application name used for audit and logging purposes.
     * @param siteId The site identifier of the application issuing the query.
     */
    public DefaultQueryWork( Session session, QueryAssociationInterface queryAssociation, EntryFilterInterface entryFilter,
                    QueryNameBuilderInterface queryNameBuilder, FilterParameterExtractorInterface filterParameterExtractor, 
                    Map<String, QueryParameterTransformerInterface> parameterTransformerMap, QueryParameterBinderInterface queryParameterBinder, 
                    List<String> personIdentifiers, Class<?> pointInTimeUserType, String applicationName, String siteId )
    {
        
        this(session, queryAssociation, entryFilter, queryNameBuilder, filterParameterExtractor, parameterTransformerMap, 
                        queryParameterBinder, null, personIdentifiers, pointInTimeUserType, applicationName, siteId, null );
    }
    
	/**
     * Constructs a new instance of DefaultQueryWork for use in processing filtering requests and querying information 
     * from the data store.
     * @param session The Hibernate Session instance.
     * @param queryAssociation Support for multi query domain reads.
     * @param entryFilter Entry filter defines what the client is asking for and how that data should be filtered.
     * @param queryNameBuilder Defines the algorithm for building or resolving a filter to a Hibernate named query name.
     * @param filterParameterExtractor Contains logic for how to extract parameter values form the filter.
     * @param parameterTransformerMap A map of parameter names to parameter transformer instances to use to transform parameters prior
     * to use in the query.
     * @param personIdentifiers The list of person identifiers to query for for Patient Centric queries.
     * @param pointInTimeUserType The Point In Time user type to use in setting date parameters on the query.
     * @param applicationName The application name used for audit and logging purposes.
     * @param siteId The site identifier of the application issuing the query.
     * @param marshallerUnmarshaller The JaxBMarshallerUnmarshaller object used in unmarshalling and marshalling XML to and from Java POJO objects.
     */
    public DefaultQueryWork( Session session, QueryAssociationInterface queryAssociation, EntryFilterInterface entryFilter,
                    QueryNameBuilderInterface queryNameBuilder, FilterParameterExtractorInterface filterParameterExtractor, 
                    Map<String, QueryParameterTransformerInterface> parameterTransformerMap, QueryParameterBinderInterface queryParameterBinder, 
                    List<String> personIdentifiers, Class<?> pointInTimeUserType, String applicationName, String siteId, JaxBMarshallerUnmarshallerInterface marshallerUnmarshaller )
    {
        
        this(session, queryAssociation, entryFilter, queryNameBuilder, filterParameterExtractor, parameterTransformerMap, 
                        queryParameterBinder, null, personIdentifiers, pointInTimeUserType, applicationName, siteId, marshallerUnmarshaller );
    }
    
    /**
     * Constructs a new instance of DefaultQueryWork for use in processing filtering requests and querying information 
     * from the data store.
     * @param session The Hibernate Session instance.
     * @param queryAssociation Support for multi query domain reads.
     * @param entryFilter Entry filter defines what the client is asking for and how that data should be filtered.
     * @param queryNameBuilder Defines the algorithm for building or resolving a filter to a Hibernate named query name.
     * @param filterParameterExtractor Contains logic for how to extract parameter values form the filter.
     * @param parameterTransformerMap A map of parameter names to parameter transformer instances to use to transform parameters prior
     * to use in the query.
     * @param templateTimeoutMap The map of timeout values for templates for use in constraining how long queries can run for 
     * particular domains.
     * @param personIdentifiers The list of person identifiers to query for for Patient Centric queries.
     * @param pointInTimeUserType The Point In Time user type to use in setting date parameters on the query.
     * @param applicationName The application name used for audit and logging purposes.
     * @param siteId The site identifier of the application issuing the query.
     */
    public DefaultQueryWork( Session session, QueryAssociationInterface queryAssociation, EntryFilterInterface entryFilter,
                    QueryNameBuilderInterface queryNameBuilder, FilterParameterExtractorInterface filterParameterExtractor, 
                    Map<String, QueryParameterTransformerInterface> parameterTransformerMap, QueryParameterBinderInterface queryParameterBinder, 
                    Map<String, String> templateTimeoutMap, List<String> personIdentifiers, Class<?> pointInTimeUserType, String applicationName, String siteId, JaxBMarshallerUnmarshallerInterface marshallerUnmarshaller )
    {
        this.session = session;
        this.entryFilter = entryFilter;
        this.queryAssociation = queryAssociation;
        this.personIdentifiers = personIdentifiers;
        this.pointInTimeUserType = pointInTimeUserType;
        
        this.queryNameBuilder = queryNameBuilder;
        this.filterParameterExtractor = filterParameterExtractor;
        this.parameterTransformerMap = parameterTransformerMap;
        this.queryParameterBinder = queryParameterBinder;
        
        this.templateTimeoutMap = templateTimeoutMap;
        this.applicationName = applicationName;
        this.siteId = siteId;
        this.marshallerUnmarshaller= marshallerUnmarshaller;
    }
    

    /* (non-Javadoc)
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run( )
    {
        long startTime = System.currentTimeMillis();

        try
        {
            Query query = prepareQuery( session, queryAssociation.getAssociationName(), entryFilter, personIdentifiers );
            if ( query != null )
            {
                if( getTemplateTimeoutMap() != null )
                {
                    long queryTimeout = TimeoutUtil.processTimeout( getTemplateTimeoutMap(), entryFilter, this.applicationName );
                    query.setTimeout( (int)queryTimeout );
                }
                
                startTime = System.currentTimeMillis();
                for ( Object element : query.list() )
                {
                		Element marshalledElement = marshalObjectToElement(element);
                		this.results.add( marshalledElement );                   	
                }
                long finishTime = System.currentTimeMillis();
                if ( performanceLog.isInfoEnabled() )
                {
                    performanceLog.logFinishTime( startTime, finishTime, "DefaultQueryWork.run()", entryFilter.getDomainEntryPoint(),
                                    entryFilter.getTemplateId(), entryFilter.getRequestId(), siteId, true, "jdbc" );
                }
            }
        }
        catch ( Exception ex )
        {
        	long finishTime = System.currentTimeMillis();
            if ( performanceLog.isInfoEnabled() )
            {
                performanceLog.logFinishTime( startTime, finishTime, "DefaultQueryWork.run()", entryFilter.getDomainEntryPoint(),
                                entryFilter.getTemplateId(), entryFilter.getRequestId(), siteId, false, "jdbc" );
            }

            //LOGGER.error( "************************************* TIMEOUT EXCEPTION *********************************************************" );
            addException( ex, applicationName );
        }

    }


    /**
     * Handles completing the prepared hibernate statements.
     * 
     * @param aSession
     *            - hibernate session used to obtain named prepared statement.
     * @param aEntryFilter
     *            hibernate HDRII filters.
     * @param aPatient
     *            patient
     * @return Populated named prepared statement.
     */
    @SuppressWarnings( "rawtypes" )
    protected Query prepareQuery( Session aSession, String associationName, EntryFilterInterface aEntryFilter, List<String> personIdentifiers )
    {
        // Initialize the query parameter map with the filter parameter values.
        Map<String, Object> queryParameterMap = getFilterParameters( aEntryFilter );
        
        if( aEntryFilter.isPatientCentricFilter() )
        {
            if( personIdentifiers == null || personIdentifiers.size() == 0 )
            {
                // If this is a patient centric query but there are no person identifier in the list, return a null query 
                // which will short circuit the query work.
                return null;
            }
            else
            {
                // Patient identifiers come into the query work as a parameter. Add the patient identifiers to the 
                // query parameters map, transforming as is necessary, in preparation for query execution.
                addPatientIdentifiers(aEntryFilter, personIdentifiers, queryParameterMap);
            }
        }
        
        // Use the Query Name Builder instance to build the query name based on a pre-determined algorithm.
        String queryName = queryNameBuilder.buildQueryName( aEntryFilter, associationName);
        
        Query query = null;
        try{
        	 // Lookup the named query from the Hibernate session.
            query = session.getNamedQuery( queryName );
        }
        catch(HibernateException he){
        	//some hbm files have defined a name query of 'templateId.default' query based on several optional parameters - it would be difficult to construct an exact named query for each
        	query = session.getNamedQuery(aEntryFilter.getTemplateId()+".default");
        }       
        
        // Bind the query parameters to the bind variables in the query using the query binder configured in the query work.
        queryParameterBinder.bindQueryParameters(query, queryParameterMap, pointInTimeUserType);
        
        // return the query, ready for use.
        return query;
    }
    
    //prototype experiment code
    protected Element marshalObjectToElement(Object data) throws Exception{
    	
    	//discover if the instance Object (data) is a top level domain Class (clazz) or if it is a subquery Class we need to retrieve
    	Class<?> clazz = data.getClass();
    	
    	//TODO: This should never be null...
    	//if(this.marshallerUnmarshaller == null)
    	//{
    		//this.marshallerUnmarshaller = new JaxBMarshallerUnmarshaller();
    	//}
    	
    	return this.marshallerUnmarshaller.marshalToXmlElement(data, clazz);
       	
    }


    /* (non-Javadoc)
     * @see commonj.work.Work#isDaemon()
     */
    @Override
    public boolean isDaemon( )
    {
        return false;
    }


    /* (non-Javadoc)
     * @see commonj.work.Work#release()
     */
    @Override
    public void release( )
    {
        try
        {
            if ( this.session != null && this.session.isOpen() )
            {
                this.session.close();
            }
        }
        catch ( HibernateException e )
        {
            addException( e, applicationName );
        }
    }


    /*
     *  @see gov.va.med.cds.persistence.QueryWork#getResults()
     */
    @Override
    public Element getResults( )
    {
        return results;
    }


    /*
     * (non-Javadoc)
     * @see gov.va.med.cds.persistence.QueryWork#getExceptions( )
     */
    @Override
    public List<Exception> getExceptions( )
    {
        return exceptions;
    }


    /*
     * (non-Javadoc)
     * @see gov.va.med.cds.persistence.QueryWork#addException(java.lang.Exception)
     */
    @Override
    public void addException( Exception ex, String applicationName )
    {
        ExceptionHandler.logRootException( ex, entryFilter.getTemplateId(), entryFilter.getRequestId(), applicationName );
        this.exceptions.add( ex );
    }


    public void setTemplateTimeoutMap( Map<String, String> templateTimeoutMap )
    {
        this.templateTimeoutMap = templateTimeoutMap;
    }


    public Map<String, String> getTemplateTimeoutMap( )
    {
        return templateTimeoutMap;
    }
    
    public void setQueryNameBuilder( QueryNameBuilderInterface queryNameBuilder )
    {
        this.queryNameBuilder = queryNameBuilder;
    }
    
    /* (non-Javadoc)
     * @see gov.va.med.cds.persistence.hibernate.QueryWork#getQueryAssociation()
     */
    public QueryAssociationInterface getQueryAssociation( )
    {
        return queryAssociation;
    }
    
    protected final void addPatientIdentifiers( EntryFilterInterface entryFilter, List<String> personIdentifiersArg, Map<String, Object> queryParameterMap )
    {
        QueryParameterTransformerInterface personIdentifierParameterTransformer = parameterTransformerMap.get( FilterParameterExtractorInterface.PN_PERSON_IDENTIFIERS );
        if(personIdentifierParameterTransformer != null)
        {
            personIdentifierParameterTransformer.transformAndSetParameter( FilterParameterExtractorInterface.PN_PERSON_IDENTIFIERS, personIdentifiers, queryParameterMap );
        }
        else {
            queryParameterMap.put( FilterParameterExtractorInterface.PN_PERSON_IDENTIFIERS, personIdentifiers );
        }
    }
    
    protected final Map<String, Object> getFilterParameters(EntryFilterInterface aEntryFilter)
    {
        return filterParameterExtractor.extractNamedParameters(parameterTransformerMap, aEntryFilter);
    }
    
    public JaxBMarshallerUnmarshallerInterface getMarshallerUnmarshaller() {
		return marshallerUnmarshaller;
	}

	public void setMarshallerUnmarshaller(
			JaxBMarshallerUnmarshallerInterface marshallerUnmarshaller) {
		this.marshallerUnmarshaller = marshallerUnmarshaller;
	}
}