package gov.va.med.cds.util;

import static org.springframework.orm.hibernate4.SessionFactoryUtils.getDataSource;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.sql.DataSource;

import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.hibernate.Session;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;


public class QueryCacheManager
    implements
        QueryCacheManagerMBeanInterface,
        ApplicationContextAware

{
    public static final int ONE_DAY_IN_SECONDS = 86400;
    public static final int TWELVE_HOURS_IN_SECONDS = 43200;
    public static final int ONE_HOUR_IN_SECONDS = 3600;
    public static final int TWO_HOURS_IN_SECONDS = 7200;
    public static final int FOUR_HOURS_IN_SECONDS = 14400;

    public static final int DEFAULT_MAX_ENTRIES_PER_DATASOURCE = 2000;
    public static final long DEFAULT_MAX_ENTRY_AGE_IN_SECONDS = ONE_HOUR_IN_SECONDS;

    private static QueryCacheManager instance;

    private HashMap<DataSource, QueryCache> dataSourceQueryCacheMap = new HashMap<DataSource, QueryCache>();

    private boolean enabled = true;
    private long maxEntriesPerDataSource = DEFAULT_MAX_ENTRIES_PER_DATASOURCE;
    private long maxDataAgeSeconds = DEFAULT_MAX_ENTRY_AGE_IN_SECONDS;
    private static ApplicationContext applicationContext;
    private long lastOptimizationTime = System.currentTimeMillis();


    public void setApplicationContext( ApplicationContext appContext )
        throws BeansException
    {
        applicationContext = appContext;
        instance = null; // / in the event that the application context reloads,
                         // then the instance will also need to be reloaded
    }

    /**
     * in order to eliminate the overhead of the spring lookup, the
     * PerformanceLog.getInstance() provides access to the singleton
     * performanceLog bean loaded in the application's spring context.
     * 
     * @return
     */
    public static QueryCacheManager getInstance( )
    {
        if ( instance != null )
        {
            return instance;
        }

        // / perform a lookup in the Spring context just one time and set it as
        // the instance for use in future lookups
        if ( applicationContext == null )
        {
            instance = new QueryCacheManager(); // / for testing purposes, when
                                                // the application context is
                                                // not available, then the
                                                // singleton is just given a new
                                                // instance without spring
                                                // awareness...
            return instance;
        }
        
        instance = ( QueryCacheManager )applicationContext.getBean( "queryCacheManager" );
        
        return instance;
    }

    public void put( Session session, String queryName, String paramName, Object paramValue, List<Object> results )
    {
        if ( !enabled )
        {
            return;
        }
        
        HashMap<String, Object> queryParams = new HashMap<String, Object>();
        queryParams.put( paramName, paramValue );
        put( session, queryName, queryParams, results );
    }

    public void put( Session session, String queryName, HashMap<String, Object> queryParams, List<Object> results )
    {
        if ( ( !enabled ) ||
             ( results == null ) || results.isEmpty() ||
             ( session == null ) )
        {
            return;
        }

        DataSource sessionDataSource = getDataSource( session.getSessionFactory() );
        if ( dataSourceQueryCacheMap.containsKey( sessionDataSource ) )
        {
            // / datasource already in map
            QueryCache existingCache = dataSourceQueryCacheMap.get( sessionDataSource );
            synchronized ( existingCache )
            {
                existingCache.put( queryName, queryParams, results );
            }

        }
        else
        {
            QueryCache newCacheForDatasource = new QueryCache();
            newCacheForDatasource.setMaxEntries( maxEntriesPerDataSource );
            newCacheForDatasource.setMaxEntryAgeInSeconds( maxDataAgeSeconds );
            synchronized ( newCacheForDatasource )
            {
                newCacheForDatasource.put( queryName, queryParams, results );
            }
            dataSourceQueryCacheMap.put( sessionDataSource, newCacheForDatasource );
        }
    }

    private void optimize( )
    {
        // /TODO perform this operation in a separate thread so it does not slow
        // the get() methods...
        if ( Math.random() > 0.05 )
        {
            return; // // only perform this check occasionally
        }
        
        long maxDataAgeTime = maxDataAgeSeconds * 1000;
        long timeSinceLastOptimization = System.currentTimeMillis() - lastOptimizationTime;
        if ( maxDataAgeTime > timeSinceLastOptimization )
        {
            return;
        }
        
        removeEntriesExceedingMaxEntryAge();
        setLastOptimizationTime( System.currentTimeMillis() );
    }

    public List<Object> get( Session session, String queryName, String paramName, Object paramValue )
    {
        if ( !enabled )
        {
            return null;
        }

        DataSource sessionDataSource = getDataSource( session.getSessionFactory() );
        QueryCache cache = dataSourceQueryCacheMap.get( sessionDataSource );
        if ( cache == null )
        {
            return null;
        }
        
        optimize();
        synchronized ( cache )
        {
            return cache.get( queryName, paramName, paramValue );
        }
    }

    public List<Object> get( Session session, String queryName, HashMap<String, Object> params )
    {
        if ( !enabled )
        {
            return null;
        }

        DataSource sessionDataSource = getDataSource( session.getSessionFactory() );
        QueryCache cache = dataSourceQueryCacheMap.get( sessionDataSource );
        if ( cache == null )
        {
            return null;
        }
        
        optimize();
        synchronized ( cache )
        {
            return cache.get( queryName, params );
        }
    }


    public boolean isEnabled( )
    {
        return enabled;
    }

    public void setEnabled( boolean enabled )
    {
        if ( this.enabled == enabled )
        {
            return; // / no change in status
        }

        this.enabled = enabled;

        if ( enabled )
        {
            if ( dataSourceQueryCacheMap == null )
            {
                dataSourceQueryCacheMap = new HashMap<DataSource, QueryCache>();
            }
        }
        else
        {
            for ( QueryCache cache : dataSourceQueryCacheMap.values() )
            {
                cache.clear();
                cache = null;
            }
            dataSourceQueryCacheMap.clear();
            dataSourceQueryCacheMap = null;
        }
    }

    public long getMaxEntriesPerDataSource( )
    {
        return maxEntriesPerDataSource;
    }


    public void setMaxEntriesPerDataSource( long maxEntriesPerDataSource )
    {
        if ( maxEntriesPerDataSource < 1 )
        {
            throw new IllegalArgumentException(
                            "maxEntriesPerDataSource cannot be less than 1. To disable the cacheing mechanism, call setEnabled() with a value of 'false'" );
        }

        this.maxEntriesPerDataSource = maxEntriesPerDataSource;
        
        if ( !enabled )
        {
            return;
        }
        
        for ( QueryCache cache : dataSourceQueryCacheMap.values() )
        {
            cache.setMaxEntries( maxEntriesPerDataSource );
        }
    }


    public long getMaxEntryAgeSeconds( )
    {
        return maxDataAgeSeconds;
    }

    public void setMaxEntryAgeSeconds( long maxDataAgeSeconds )
    {
        if ( maxDataAgeSeconds < 1 )
        {
            throw new IllegalArgumentException(
                            "The MaxDataAgeSeconds cannot be set to a number less than 1. To disable the cacheing mechanism, call setEnabled() with a value of 'false'" );
        }
        
        this.maxDataAgeSeconds = maxDataAgeSeconds;
        
        if ( !enabled )
        {
            return;
        }
        
        for ( QueryCache cache : dataSourceQueryCacheMap.values() )
        {
            cache.setMaxEntryAgeInSeconds( maxDataAgeSeconds );
        }
    }

    public long getCacheHitCount( )
    {
        if ( !enabled )
        {
            return -1;
        }
        
        int count = 0;
        for ( QueryCache cache : dataSourceQueryCacheMap.values() )
        {
            count += cache.getCacheHitCount();
        }
        
        return count;
    }

    public long getCacheMissCount( )
    {
        if ( !enabled )
        {
            return -1;
        }
        
        int count = 0;
        for ( QueryCache cache : dataSourceQueryCacheMap.values() )
        {
            count += cache.getCacheMissCount();
        }
        
        return count;
    }

    public long getCacheEntryCount( )
    {
        if ( !enabled )
        {
            return -1;
        }
        
        int count = 0;
        for ( QueryCache cache : dataSourceQueryCacheMap.values() )
        {
            count += cache.size();
        }
        
        return count;
    }

    public long getDatasourceCacheCount( )
    {
        if ( !enabled )
        {
            return -1;
        }
        
        return dataSourceQueryCacheMap.size();
    }

    public String getQueryCacheManagerDump( )
    {
        return toString();
    }

    public String emptyCache( )
    {
        if ( !enabled )
        {
            return "Since caching is not enabled, the caches are already empty.";
        }
        long entryCount = getCacheEntryCount();
        long dataSourceCount = getDatasourceCacheCount();
        dataSourceQueryCacheMap.clear();
        dataSourceQueryCacheMap = null;
        dataSourceQueryCacheMap = new HashMap<DataSource, QueryCache>();
        
        return "The Query caches are now empty. " + entryCount + ( ( entryCount == 1 ) ? " entry was " : " entries were " ) + "removed from "
                        + dataSourceCount + ( ( dataSourceCount == 1 ) ? " DataSource Cache." : " DataSource Caches." );
    }

    public String removeEntriesExceedingMaxEntryAge( )
    {
        if ( !enabled )
        {
            return "Since caching is not enabled, the caches are empty.";
        }

        int count = 0;
        for ( QueryCache cache : dataSourceQueryCacheMap.values() )
        {
            count += cache.removeEntriesExceedingMaxEntryAge();
        }
        // / remove empty caches

        // / this is conceived of a self cleaning feature. If the Spring context
        // "retires" Datasources while opening new ones,
        // / then this code can clear out the "retired" datasource caches and
        // remove the memory footprint
        // / without this, it is possible that the memory consumed by the
        // QueryCacheManager will grow indefinitely
        List<DataSource> keysToRemove = new ArrayList<DataSource>();
        for ( DataSource ds : dataSourceQueryCacheMap.keySet() )
        {
            if ( dataSourceQueryCacheMap.get( ds ).size() == 0 )
            {
                keysToRemove.add( ds );
            }
        }
        for ( DataSource ds : keysToRemove )
        {
            dataSourceQueryCacheMap.remove( ds );
        }
        
        return ( count == 1 ) ? "1 cached entry"
                        : ( count + " cached entries" ) + " exceeding the max age" + ( ( count == 1 ) ? " was" : " were" ) + " removed.";
    }

    public Element toElement( )
    {
        DocumentFactory df = new DocumentFactory();
        Element e = df.createElement( getClass().getSimpleName() );

        if ( !enabled )
        {
            e.add( df.createComment( "Query Caching is currently not enabled" ) );
            return e;
        }

        e.add( df.createAttribute( e, "numberOfCaches", "" + dataSourceQueryCacheMap.size() ) );

        long hitCount = getCacheHitCount();
        long missCount = getCacheMissCount();
        long hitPercentage = Math.round( ( hitCount ) * 100.0 / ( hitCount + missCount ) );

        e.add( df.createAttribute( e, "overallCachingHitCount", "" + hitCount ) );
        e.add( df.createAttribute( e, "overallCachingMissCount", "" + missCount ) );
        e.add( df.createAttribute( e, "overallCachingHitPercentage", "" + hitPercentage + "%" ) );

        Element entries = df.createElement( "entries" );
        e.add( entries );
        for ( DataSource key : dataSourceQueryCacheMap.keySet() )
        {
            Element dsqc = df.createElement( "dataSourceQueryCache" );
            dsqc.add( df.createAttribute( dsqc, "dataSourceIdentifier", key.toString() ) );
            dsqc.add( dataSourceQueryCacheMap.get( key ).toElement() );
            entries.add( dsqc );
        }
        
        return e;
    }

    public String toString( )
    {
        if ( !enabled )
        {
            return "Query Caching is currently not enabled";
        }

        return toElement().asXML();
    }

    /** returns a estimate of the Amount of Memory that the QueryCacheManager is using.  
     * 
     */
    @Override
    public String getMemoryUseEstimate( )
    {
        if ( !enabled )
        {
            return "Query Caching is currently not enabled. Neglible use of memory.";
        }
        long byteEstimate = toString().getBytes().length;
        DecimalFormat decimalFormat = new DecimalFormat( "###,###" );
        String bytesString = decimalFormat.format( byteEstimate );
        
        return bytesString + " bytes.";
    }

    private synchronized void setLastOptimizationTime( long aLastOptimizationTime )
    {
        this.lastOptimizationTime = aLastOptimizationTime;
    }

}

