/**
 * 
 */


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


import gov.va.med.cds.filter.EntryFilterInterface;
import gov.va.med.cds.persistence.QueryAssociationInterface;
import gov.va.med.cds.template.generated.JaxBMarshallerUnmarshallerInterface;

import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Map;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.Type;
import org.hibernate.internal.SessionFactoryImpl;


/**
 * @author vhaislegberb
 *
 */
public class PageableCriteriaQueryWork
    extends
        PageableQueryWork
{
    
    public PageableCriteriaQueryWork( 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 )
    {
        super( session, queryAssociation, entryFilter, queryNameBuilder, filterParameterExtractor, parameterTransformerMap, queryParameterBinder,
                        templateTimeoutMap, personIdentifiers, pointInTimeUserType, applicationName, siteId, marshallerUnmarshaller);
    }


    /**
     * 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.
        String entityName = this.queryAssociation.getFilterModelName();
        ClassMetadata entityMetaData = session.getSessionFactory().getClassMetadata( entityName );
        
        Criteria criteria = session.createCriteria( entityName );
        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);
                Object[] pids = ((List<String>)queryParameterMap.remove( FilterParameterExtractorInterface.PN_PERSON_IDENTIFIERS )).toArray();
                
                //TODO: Test to see if this will resolve to collection / association too.
                String patientIdentityColumn  = propertyToColumn( entityName, "patient.identifier.identity" );
                String patientAssigningAuthColumn  = propertyToColumn( entityName, "patient.identifier.assigningFacility" );
                
                String sqlRestriction = String.format("{alias}.%s || '/' || {alias}.%s  in (%s) ", patientAssigningAuthColumn, patientIdentityColumn, arrayToParameterizationString( pids ));
                
                Type[] types = new Type[pids.length];
                Arrays.fill( types, StandardBasicTypes.STRING );
                criteria.add( Restrictions.sqlRestriction(sqlRestriction, pids, types));
            }
        }
        
        // handle date ranges in query next
        Map<String, Object> dateRanges = (Map<String, Object>)queryParameterMap.remove( "dateRanges" );
        if(dateRanges != null && dateRanges.size() > 0)
        {
            for(String key : dateRanges.keySet())
            {
                String columnName = propertyToColumn(entityName, key, 1);
                String sqlRestrictionLeft = String.format("{alias}.%s between SYS_EXTRACT_UTC(?) and SYS_EXTRACT_UTC(?)", columnName);
                String sqlRestrictionRight = String.format("{alias}.%s is null", columnName);
                Map<String, Calendar> dateRange =  (Map<String, Calendar>)dateRanges.get( key );
                Type[] types = new Type[dateRange.values().size()];
                Arrays.fill(types, StandardBasicTypes.TIMESTAMP);
                Object[] params = new Object[]{dateRange.get( "begin" ), dateRange.get( "end" )};
                criteria.add( Restrictions.or( Restrictions.sqlRestriction( sqlRestrictionLeft, params, types  ), Restrictions.sqlRestriction( sqlRestrictionRight ) ));
            }
        }
        
        
        // handle the remaining paramters.
        if(queryParameterMap != null && !queryParameterMap.isEmpty())
        {
            for(String key : queryParameterMap.keySet())
            {
                Object param = queryParameterMap.get( key );
                String columnName = propertyToColumn(entityName, key);
                
                if(param instanceof List)
                {
                    String[] inParams = ((List<String>)param).toArray(new String[]{});
                    inParams = stringArrayToAllUppers( inParams );
                    Type[] types = new Type[inParams.length];
                    Arrays.fill( types, StandardBasicTypes.STRING );
                    String sqlRestriction = String.format( "upper({alias}.%s) in (%s)", columnName, arrayToParameterizationString( inParams ) );
                    criteria.add( Restrictions.sqlRestriction( sqlRestriction, inParams, types ) );
                }
                else 
                {
                    // iterate through the remainder of the keys and set the parameters
                    
                    String sqlRestriction = String.format( "upper({alias}.%s) = upper(?)", columnName);
                    criteria.add( Restrictions.sqlRestriction( sqlRestriction, queryParameterMap.get( key ) , StandardBasicTypes.STRING ));
                }
            }
        }
        
        // return the query, ready for use.
        return new HibernateQueryCriteriaDelegate( criteria );
    }


    private String arrayToParameterizationString( Object[] arr )
    {
        StringBuilder sbParamString = new StringBuilder();
        for ( int i = 0; i < arr.length - 1; i++ )
        {
            sbParamString.append( "?, " );
        }
        sbParamString.append( "?" );
        return sbParamString.toString();
    }
    
    private String propertyToColumn(String entityName, String propertyName)
    {
        return propertyToColumn(entityName, propertyName, 0);
        
    }
    
    private String propertyToColumn(String entityName, String propertyName, int columnIdx)
    {
        ClassMetadata meta = this.session.getSessionFactory().getClassMetadata( entityName );
        if(meta instanceof AbstractEntityPersister)
        {
            AbstractEntityPersister persister = (AbstractEntityPersister)meta;
            String[] columns = persister.toColumns( propertyName );
            if(columns.length >= columnIdx)
            {
                return columns[columnIdx];
            }
        }
        
        throw new IllegalArgumentException("Can't get column name for entity.");   
    }
    
    private String[] stringArrayToAllUppers(String[] strArr)
    {
        String[] retVal = new String[strArr.length];
        for(int i = 0; i < strArr.length; i++)
        {
            retVal[i] = strArr[i].toUpperCase();
        }
        
        return retVal;
    }

}
