

package gov.va.med.cds.filter;


import gov.va.med.cds.clinicaldata.EntryFilter;
import gov.va.med.cds.clinicaldata.FilterMetaData;
import gov.va.med.cds.clinicaldata.FilterMetaDataInterface;
import gov.va.med.cds.exception.ErrorCodeEnum;
import gov.va.med.cds.exception.FilterCacheException;
import gov.va.med.cds.exceptionframework.ExceptionHandler;
import gov.va.med.cds.persistence.hibernate.tfs.AbstractTfsHibernatePersistenceManager;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;


public class HibernateOracleFilterCachePersistence
    extends
        AbstractTfsHibernatePersistenceManager
    implements
        FilterCachePersistenceInterface
{
    public HibernateOracleFilterCachePersistence( )
    {
    }


    private Query buildFilterStatusQuery( Session aSession )
    {
        String queryName = "FilterSchemaIdsByStatus";
        Query query = aSession.getNamedQuery( queryName );
        query.setParameter( "status", "ACTIVE" );
        return query;
    }


    private Query buildFilterIdQuery( Session aSession, String aFilterId )
    {
        String queryName = "FilterSchemaId";
        Query query = aSession.getNamedQuery( queryName );

        query.setParameter( "filterId", aFilterId );
        return query;
    }


    /**
     * Retrieves all the database Filter Cache identifiers from FILTER_SCHEMA table.
     * 
     * @return a list of filter identifiers from the FILTER_SCHEMA table.
     */
    @SuppressWarnings( "unchecked" )
    public List<String> getFilterIdentifiers( )
    {
        Session session = this.sessionFactory.getCurrentSession();
        List<String> filterIdentifiers = null;
        try
        {
            Query query = buildFilterStatusQuery( session );

            filterIdentifiers = query.list();
        }
        catch ( Exception exception )
        {
            ExceptionHandler.handleException( exception, null, null, null );
        }

        return filterIdentifiers;
    }


    /**
     * Replaces Filter Schema Table Entries by deleting and then populating with meta files and passed VHIM version and
     * domain entry point lists. If any error occurs, it will rollback changes and swallow exception. Note: This method
     * is only used for testing.  SJB update - now propagates error.
     * 
     * @param Collection<FilterMetaDataInterface> filterMetaDatas - list of filter meta data to persist.
     * @param List<String> domainEntryPointsList - list of domain entry points to persist.
     * @param List<String> vhimVersionList - list of vhim versions to persist.
     */
    public void replaceFilterPersistence( Collection<FilterMetaDataInterface> filterMetaDatas )
    {
        Session session = this.sessionFactory.getCurrentSession();

        deleteAllFilterSchemaTableEntries( session );

        for ( FilterMetaDataInterface filterMetaData : filterMetaDatas )
        {
            persistFilterSchema( filterMetaData, session );
        }
    }


    /**
     * Deletes all Filter Schema Table Entries.
     * 
     * @param Session aSession - is a hibernate session to be used with the persistence request.
     * @throws FilterCacheException - Thrown if the exception occurs during the deleting of filter schema tables
     *             entries.
     */
    @SuppressWarnings( "unchecked" )
    private void deleteAllFilterSchemaTableEntries( Session aSession )
        throws FilterCacheException
    {
        try
        {
            List<FilterMetaData> filters = aSession.createCriteria( FilterMetaData.class ).list();

            for ( FilterMetaData filterMetaData : filters )
            {
                aSession.delete( filterMetaData );
            }
        }
        catch ( HibernateException e )
        {
            throw new FilterCacheException( ErrorCodeEnum.ERROR_DELETING_FILTER_DOMAIN_TABLES, e, e.getMessage() );
        }
    }


    /**
     * This method persists a single Filter Schema to the FILTER_SCHEMA table.
     * 
     * @param FilterMetaDataInterface filterMetaData - contains all of the Filter Schema information.
     * @param Session aSession - is a database reference to be used with the persistence request.
     * @throws FilterCacheException - thrown if the exception occurs while the database is storing the filter schema.
     */
    private void persistFilterSchema( FilterMetaDataInterface filterMetaData, Session aSession )
        throws FilterCacheException
    {
        try
        {
            persistifyVhimVersions( aSession, filterMetaData.getVhimVersionWrappers() );

            persistifyEntryFilters( aSession, filterMetaData.getEntryFilters() );

            persistifyFilter( aSession, filterMetaData );
        }
        catch ( NullPointerException e )
        {
            throw new FilterCacheException( ErrorCodeEnum.CANNOT_SAVE_FILTER_SCHEMA, e, filterMetaData.getFilterId(), e.getMessage() );
        }
        catch ( HibernateException e )
        {
            throw new FilterCacheException( ErrorCodeEnum.CANNOT_SAVE_FILTER_SCHEMA, e, filterMetaData.getFilterId(), e.getMessage() );
        }
    }


    /**
     * Entry Filters are many-to-many to FilterMetaData.  Each EntryFilter has a DomainEntryPoint and an Xpath.
     * I assume the combination of both makes a unique EntryFilter. 
     * @param aSession hibernate session to persist with
     * @param entryFilters set of entry filters to persist
     */
    private void persistifyEntryFilters( Session aSession, Set<EntryFilter> entryFilters )
    {
        EntryFilter[] entryFiltersArray = entryFilters.toArray( new EntryFilter[] {} );
        EntryFilter queryResult = null;

        Query query;

        for ( EntryFilter entryFilter : entryFiltersArray )
        {
            entryFilter.setDomainEntryPointWrapper( persistifyDomainEntryPoint( aSession, entryFilter.getDomainEntryPointWrapper() ) );

            if ( entryFilter.getXpath() != null )
            {
                query = aSession.getNamedQuery( "EntryFilterDomainXpath" ).setParameter( "xpath", entryFilter.getXpath() )
                                .setParameter( "depw", entryFilter.getDomainEntryPointWrapper() );
            }
            else
            {
                query = aSession.getNamedQuery( "EntryFilterDomainNullXpath" ).setParameter( "depw", entryFilter.getDomainEntryPointWrapper() );
            }

            queryResult = ( EntryFilter )query.uniqueResult();

            if ( queryResult != null )
            {
                entryFilters.remove( entryFilter );
                entryFilters.add( queryResult );
            }
        }
    }


    private void persistifyFilter( Session aSession, FilterMetaDataInterface aFilterMetaData )
    {
        FilterMetaData queryResult = null;

        // check to see if that template already exists in the database
        // if it does, replace the one in the TMD collection with the persistent version.
        Query query = aSession.getNamedQuery( "FilterSchemaId" ).setParameter( "filterId", aFilterMetaData.getFilterId() );
        queryResult = ( FilterMetaData )query.uniqueResult();

        if ( queryResult != null )
        {
            aSession.delete( queryResult );
        }

        aSession.flush();
        aSession.save( aFilterMetaData );
    }


    /**
     * This method persists a single Filter Schema to the FILTER_SCHEMA table.
     * 
     * @param FilterMetaDataInterface filterMetaData - contains all of the Filter Schema information.
     * @throws FilterCacheException - thrown if the exception occurs while the database is storing the filter schema.
     */
    public void persistFilterSchema( FilterMetaDataInterface aFilterMetaData )
        throws FilterCacheException
    {
        Session session = this.sessionFactory.getCurrentSession();
        persistFilterSchema( aFilterMetaData, session );
    }


    /**
     * Loads filters from filter_schema table into memory.
     * 
     * @param aSession the hibernate session with which to make the database call.
     * @param aFilterId the filter id.
     */
    private FilterMetaData loadFilterFromPersistence( Session aSession, String aFilterId )
    {
        FilterMetaData queryResults = null;
        try
        {
            Query query;

            if ( aFilterId == null )
            {
                throw new FilterCacheException( ErrorCodeEnum.CANNOT_LOAD_FILTER_SCHEMA_FROM_PERSISTENCE, aFilterId, "aFilterId cannot be null" );
            }
            else
            {
                query = buildFilterIdQuery( aSession, aFilterId );
            }

            queryResults = ( FilterMetaData )query.uniqueResult();
        }
        catch ( Exception exception )
        {
            ExceptionHandler.handleException( exception, null, null, null );
        }

        return queryResults;
    }


    /**
     * Loads filters from filter_schema table into memory.
     * 
     * @param aSession the hibernate session with which to make the database call.
     * @param aFilterId the filter id. If (@code aFilterId) is null, all filters will be retrieved.
     */
    @SuppressWarnings( "unchecked" )
    private List<FilterMetaData> loadFiltersFromPersistence( Session aSession )
    {
        List<FilterMetaData> filterMetaDatas = null;
        List<String> filterIds = null;
        FilterMetaData filter;
        try
        {
            Query query = buildFilterStatusQuery( aSession );
            filterIds = query.list();
            filterMetaDatas = new ArrayList<FilterMetaData>( filterIds.size() );
            for ( String filterId : filterIds )
            {
                filter = loadFilterFromPersistence( aSession, filterId );
                if ( filter != null )
                {
                    filterMetaDatas.add( filter );
                }
            }
        }
        catch ( Exception exception )
        {
            ExceptionHandler.handleException( exception, null, null, null );
        }

        return filterMetaDatas;
    }


    /**
     * This method loads either a single Filter Schema by filterIdentifier from the database and inserts it into the
     * FilterCache or it loads all of the filter schemas into the FilterCache. It is limited by the sized of the
     * FilterCache.
     * 
     * @param FilterMemoryCacheInterface filterMemoryCache - is the object to load with the Filter Schema.
     * @param String filterId - is the unique id assigned for this Filter Schema.
     * @param aSession the hibernate session with which to make the database call.
     */
    private void loadFilterMemoryCacheFromPersistence( FilterMemoryCacheInterface aFilterMemoryCache, String aFilterId, Session aSession )
    {
        if ( aFilterMemoryCache == null )
        {
            throw new FilterCacheException( ErrorCodeEnum.FILTER_MEMORY_CACHE_IS_NOT_PROVIDED, "The filterMemoryCache passed in is null." );
        }

        try
        {
            if ( aFilterId == null )
            {
                List<FilterMetaData> hibernateQueryResults = loadFiltersFromPersistence( aSession );

                for ( FilterMetaData filterMetaData : hibernateQueryResults )
                {
                    aFilterMemoryCache.loadFilterMetaDataIntoMemoryCache( filterMetaData.getFilterId(), filterMetaData );
                }
            }
            else
            {
                FilterMetaData filterMetaData = loadFilterFromPersistence( aSession, aFilterId );
                if ( filterMetaData != null )
                {
                    aFilterMemoryCache.loadFilterMetaDataIntoMemoryCache( aFilterId, filterMetaData );
                }
            }
        }
        catch ( HibernateException e )
        {
            throw new FilterCacheException( ErrorCodeEnum.CANNOT_LOAD_FILTER_SCHEMA_FROM_PERSISTENCE, e, aFilterId, e.getMessage() );
        }
    }


    /**
     * Method to load the Filter Memory Cache with as many Filter Schemas as it can hold. If there are more schemas than
     * room in the filter memory cache it loads the maximum amount. Currently there is no frequency or priority in the
     * database for each schema. So this method will just load the first N schemas. In the future it might be useful to
     * either:
     * <p>
     * A) not load any schemas just let the system add them when they are needed or
     * <p>
     * B) add a field to the table indicating the frequency of use then sort and load by frequency.
     * 
     * @param FilterMemoryCacheInterface filterMemoryCache - is the cache to load.
     */
    //calling class needs to be synchronized to protect its class level variable
    public void loadAllFilterSchemasIntoMemoryCache( FilterMemoryCacheInterface filterMemoryCache )
    {
        Session session = this.sessionFactory.getCurrentSession();
        loadFilterMemoryCacheFromPersistence( filterMemoryCache, null, session );
    }


    /**
     * Loads the provided Filter Schema in FilterMemoryCache
     * 
     * @param String filterId - is the identifier for a specific Filter Schema.
     * @param FilterMemoryCacheInterface filterMemoryCache - is the cache to load.
     */
    //calling class needs to be synchronized to protect its class level variable
    public void loadFilterIntoMemoryCache( String filterId, FilterMemoryCacheInterface filterMemoryCache )
    {
        Session session = this.sessionFactory.getCurrentSession();
        loadFilterMemoryCacheFromPersistence( filterMemoryCache, filterId, session );
    }


    /**
     * Remove this Filter Schema from both the database and the Filter Memory Cache itself.
     * 
     * @param String filterId - is the FILTER_IDENTIFIER which is a string for this Filter Schema.
     * 
     * @throws FilterCacheException - thrown if an exception occurs during the removal of a Filter Schema.
     */
    public void removeFilterSchema( String aFilterId )
    {
        Session session = this.sessionFactory.getCurrentSession();
        FilterMetaData filter = null;

        try
        {
            filter = ( FilterMetaData )buildFilterIdQuery( session, aFilterId ).uniqueResult();
            // TODO remove from filter cache
            if ( filter != null )
            {
                session.delete( filter );
            }
        }
        catch ( HibernateException e )
        {
            throw new FilterCacheException( ErrorCodeEnum.CANNOT_DELETE_FILTER, e, aFilterId, e.getMessage() );
        }
    }
}
