/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.esr.ui.common.service;

// Java Classes
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletContext;

// Library Classes
import org.springframework.web.context.ServletContextAware;

// Framework Classes
import gov.va.med.fw.model.lookup.Lookup;
import gov.va.med.fw.service.AbstractComponent;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.util.StringUtils;

// ESR Classes
import gov.va.med.esr.common.model.lookup.City;
import gov.va.med.esr.common.model.lookup.County;
import gov.va.med.esr.common.model.lookup.State;
import gov.va.med.esr.common.model.lookup.VAFacility;
import gov.va.med.esr.common.model.lookup.ZipCode;
import gov.va.med.esr.service.LookupService;
import gov.va.med.esr.service.UnknownLookupTypeException;
import gov.va.med.esr.service.UnknownLookupCodeException;
import gov.va.med.esr.ui.common.action.ZipCodeLookupBean;
import gov.va.med.esr.ui.common.beans.VAMC;
import gov.va.med.esr.ui.common.dao.UIAbstractLookupDAO;
import gov.va.med.esr.ui.common.dao.VAMCDAO;

/**
 * Lookup cache service caches the lookup objects(list of LabelValueBean objects) in the servlet context on demand
 * basis. The cache is refreshed after the refresh interval.
 *
 * @author Muddaiah Ranga
 * @version 3.0
 */
public final class LookupCacheServiceImpl extends AbstractComponent implements LookupCacheService, ServletContextAware
{
    private ServletContext servletContext;

    //Key for holding the lookup map in servlet context
    private static final String LOOKUP_MAP_KEY = "lookupMapKey";

    //One hour in milli seconds
    private static final long ONE_HOUR = 60 * 60 * 1000;

    //Default refresh interval if not set
    private static final long DEFAULT_REFRESH_INTERVAL = 8;

    //Time stamp when lookup initialized/refreshed
    private long loadTime;

    //Refresh interval
    private long refreshInterval = DEFAULT_REFRESH_INTERVAL * ONE_HOUR;

    //Lookup service bean
    private LookupService lookupService = null;

    //Map of class names to their associated lookup DAOs
    private Map daoMap;

    //Lookups that are not cached - always retrived from the database
    private List nonCachedTypes;

    static
    {
    }

    public void afterPropertiesSet()
        throws Exception
    {
        if (lookupService == null)
        {
            throw new ServiceException("LookupService not set");
        }
        if (nonCachedTypes == null)
        {
            nonCachedTypes = new ArrayList();
        }
    }

    /**
     * Gets the collection of lookup objects either from the ServletContext or from the LookupService.
     *
     * @param className filly qualified name of the lookup class
     *
     * @return collection of BOM objects.
     * @throws UnknownLookupTypeException
     * @throws ClassNotFoundException
     */
    public Collection getFromCache(String className)
        throws UnknownLookupTypeException, ClassNotFoundException
    {
        Map map = getLookupAsMap(className);
        Collection list = null;
        if (map != null)
        {
            list = map.values();
        }
        return list;
    }

    public Lookup getByCodeFromCache(String className, String code)
        throws UnknownLookupTypeException, ClassNotFoundException
    {
        if (StringUtils.isEmpty(className) || StringUtils.isEmpty(code))
        {
            return null;
        }
        Lookup lookup = null;

        if (nonCachedTypes.contains(className))
        {
            // Non-Cached Items (e.g. user name lookup)
            UIAbstractLookupDAO dao = getDAO(className);
            if (dao != null)
            {
                try
                {
                    // If we found a DAO, use it to get the list of lookup data
                    lookup = dao.getByCode(className, code);
                }
                catch (UnknownLookupCodeException e)
                {
                    // Ensure the lookup is null since the code wasn't found.
                    lookup = null;
                }
            }
        }
        else
        {
            // Cached-Items.  This will get the lookup values in the form of a map.  If the items are
            // already cached, the cached map will be returned.  If not, the items will be cached.
            Map map = getLookupAsMap(className);
            if (map != null)
            {
                // Try to get the code from the previously cached map
                lookup = (Lookup)map.get(code);
            }

            // If we didn't find a lookup value from the cache, try getting it directly from the DAO or lookup service.
            // This is needed for inactive items that aren't in the cache
            if (lookup == null)
            {
                try
                {
                    // Get the class based on the string version of it
                    Class clazz = Class.forName(className);

                    // Get the DAO associated with the class
                    UIAbstractLookupDAO dao = getDAO(clazz);
                    if (dao != null)
                    {
                        // If we found a DAO, use it to get the lookup
                        lookup = dao.getByCode(className, code);
                    }
                    else
                    {
                        // If no DAO was found, use the standard lookup service to get the lookup
                        lookup = lookupService.getByCode(Class.forName(className), code);
                    }
                }
                catch (UnknownLookupCodeException e)
                {
                    // Ensure the lookup is null since the code wasn't found.
                    lookup = null;
                }
            }
        }

        // Return the lookup (or null if it wasn't found)
        return lookup;
    }

    public Lookup getByCodeFromCache(Class clazz, String code)
        throws UnknownLookupTypeException, ClassNotFoundException
    {
        if (clazz != null && StringUtils.isNotEmpty(code))
        {
            return getByCodeFromCache(clazz.getName(), code);
        }
        return null;
    }

    /**
     * Gets the look up map from the cache(ServletContext). If not found calls the LookupService and converts the
     * collection in to map keyed by the code and returns. It caches the lookup data obtained from the LookupService.
     *
     * @param className filly qualified name of the lookup class
     *
     * @return map of BOM objects.
     * @throws UnknownLookupTypeException
     * @throws ClassNotFoundException
     */
    private Map getLookupAsMap(String className)
        throws UnknownLookupTypeException, ClassNotFoundException
    {
        long currentTime = System.currentTimeMillis();
        Map lookupMap = (Map)servletContext.getAttribute(LOOKUP_MAP_KEY);

        // Clear the map if the time passed the refresh interval
        if (lookupMap != null && (currentTime > (loadTime + refreshInterval)))
        {
            lookupMap.clear();
            loadTime = currentTime;
        }

        // Create the lookup map for the first time and set it in the servlet context
        if (lookupMap == null)
        {
            lookupMap = new HashMap();
            servletContext.setAttribute(LOOKUP_MAP_KEY, lookupMap);
            loadTime = currentTime;
        }

        // See if the data has been cached
        Map map = (Map)lookupMap.get(className);
        if (map == null)
        {
            List lookupList = null;

            // Get the class based on the string version of it
            Class clazz = Class.forName(className);

            // Get the DAO associated with the class
            UIAbstractLookupDAO dao = getDAO(clazz);
            if (dao != null)
            {
                // If we found a DAO, use it to get the list of lookup data
                lookupList = dao.findAll();
            }
            else
            {
                // If no DAO was found, use the standard lookup service
                lookupList = lookupService.findAll(clazz);
            }

            // Convert the lookup list into a map keyed by the lookup code and cache it
            if (lookupList != null)
            {
                map = new LinkedHashMap();
                Object object = null;
                for (Iterator iter = lookupList.iterator(); iter.hasNext();)
                {
                    object = iter.next();
                    if (object instanceof Lookup)
                    {
                        map.put(((Lookup)object).getCode(), object);
                    }
                }

                //Cache the map only if the type is not nonCached Type
                if (!nonCachedTypes.contains(className))
                    lookupMap.put(className, map);
            }
        }

        // Return the resultant map
        return map;
    }

    public Map getVISNSiteMap() throws UnknownLookupTypeException, ClassNotFoundException
    {
        return this.lookupService.getVISNSiteMap();
    }

    public Map getParentSiteMap() throws UnknownLookupTypeException, ClassNotFoundException
    {
        VAMCDAO vamcDAO = (VAMCDAO)getDAO(VAMC.class);
        return vamcDAO.getParentSiteMap(getLookupService().findAll(VAFacility.class));
    }

    public Map getVISNVAMCMap() throws UnknownLookupTypeException, ClassNotFoundException
    {
        VAMCDAO vamcDAO = (VAMCDAO)getDAO(VAMC.class);
        return vamcDAO.getVISNVAMCMap(new ArrayList(this.getFromCache(VAMC.class.getName())));
    }

    /**
     * Returns the DAO associated with the passed in lookupType class.
     *
     * @param lookupType The lookupType class
     *
     * @return The DAO associated with the lookup type class or null if a DAO wasn't found for the specified class.
     */
    protected UIAbstractLookupDAO getDAO(Class lookupType)
    {
        UIAbstractLookupDAO dao = null;
        if ((this.daoMap != null) && (lookupType != null))
        {
            dao = (UIAbstractLookupDAO)this.daoMap.get(lookupType.getName());
        }
        return dao;
    }

    protected UIAbstractLookupDAO getDAO(String className)
    {
        UIAbstractLookupDAO dao = null;
        if ((this.daoMap != null) && (className != null))
        {
            dao = (UIAbstractLookupDAO)this.daoMap.get(className);
        }
        return dao;
    }

    public List getZipCode(String className, String zipCode) throws UnknownLookupTypeException
    {
        // Create a list of bean object to return
        List results = new ArrayList();

        // Lookup the zip code information
        ZipCode zipCodeObject = lookupService.getPartialZipCodeByCode(zipCode);

        // Return the empty list of no results were found
        if (zipCodeObject == null)
        {
            return results;
        }

        // Get the state
        String state = "";
        State stateObject = zipCodeObject.getState();
        if (stateObject != null)
        {
            state = stateObject.getName();
        }

        // Get the county
        String county = "";
        County countyObject = zipCodeObject.getCounty();
        if (countyObject != null)
        {
            county = countyObject.getName();
        }

        // Loop through the cities for multiple city matches for this zip code
        Set cities = zipCodeObject.getCities();
        if ((cities == null) || (cities.isEmpty()))
        {
            // If no data is present in any field, return the empty list
            if ((StringUtils.isNotEmpty(county)) && (StringUtils.isNotEmpty(state)))
            {
                return results;
            }

            // If no cities, but county or state is present,
            // blank out the city and use the rest of the results
            results.add(new ZipCodeLookupBean(zipCode, "", county, state));
        }
        else
        {
            for (Iterator iterator = cities.iterator(); iterator.hasNext();)
            {
                City city = (City)iterator.next();
                results.add(new ZipCodeLookupBean(zipCode, city.getName(), county, state));
            }
        }

        // Return the results
        return results;
    }

    public List getZipCode(Class clazz, String zipCode) throws UnknownLookupTypeException
    {
        if (clazz != null && StringUtils.isNotEmpty(zipCode))
        {
            return getZipCode(clazz.getName(), zipCode);
        }
        return null;
    }

    /**
     * Sets the refresh interval. Assumes the argument passed in in hours.
     *
     * @param refreshTime
     */
    public void setRefreshInterval(long refreshTime)
    {
        if (refreshTime <= 0)
        {
            refreshTime = DEFAULT_REFRESH_INTERVAL;
        }
        this.refreshInterval = refreshTime * ONE_HOUR;
    }

    public LookupService getLookupService()
    {
        return this.lookupService;
    }

    public void setLookupService(LookupService lookupService)
    {
        this.lookupService = lookupService;
    }

    public ServletContext getServletContext()
    {
        return servletContext;
    }

    public void setServletContext(ServletContext servletContext)
    {
        this.servletContext = servletContext;
    }

    public Map getDaoMap()
    {
        return daoMap;
    }

    public void setDaoMap(Map daoMap)
    {
        this.daoMap = daoMap;
    }

    public List getNonCachedTypes()
    {
        return nonCachedTypes;
    }

    public void setNonCachedTypes(List nonCachedTypes)
    {
        this.nonCachedTypes = nonCachedTypes;
    }
}