/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * Copyright (c) 2006 - 2009 Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.resourceloader;

import java.net.URL;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.resourceloader.cache.BundleCacheResourceWrapper;
import org.pentaho.reporting.libraries.resourceloader.cache.NullResourceBundleDataCache;
import org.pentaho.reporting.libraries.resourceloader.cache.NullResourceDataCache;
import org.pentaho.reporting.libraries.resourceloader.cache.NullResourceFactoryCache;
import org.pentaho.reporting.libraries.resourceloader.cache.ResourceBundleDataCache;
import org.pentaho.reporting.libraries.resourceloader.cache.ResourceBundleDataCacheEntry;
import org.pentaho.reporting.libraries.resourceloader.cache.ResourceBundleDataCacheProvider;
import org.pentaho.reporting.libraries.resourceloader.cache.ResourceDataCache;
import org.pentaho.reporting.libraries.resourceloader.cache.ResourceDataCacheEntry;
import org.pentaho.reporting.libraries.resourceloader.cache.ResourceDataCacheProvider;
import org.pentaho.reporting.libraries.resourceloader.cache.ResourceFactoryCache;
import org.pentaho.reporting.libraries.resourceloader.cache.ResourceFactoryCacheProvider;
import org.pentaho.reporting.libraries.resourceloader.modules.cache.ehcache.EHCacheModule;

/**
 * The resource manager takes care about the loaded resources, performs caching, if needed and is the central instance
 * when dealing with resources. Resource loading is a two-step process. In the first step, the {@link ResourceLoader}
 * accesses the physical storage or network connection to read in the binary data. The loaded {@link ResourceData}
 * carries versioning information with it an can be cached indendently from the produced result. Once the loading is
 * complete, a {@link ResourceFactory} interprets the binary data and produces a Java-Object from it.
 * <p/>
 * Resources are identified by an Resource-Key and some optional loader parameters (which can be used to parametrize the
 * resource-factories).
 *
 * @author Thomas Morgner
 * @see ResourceData
 * @see ResourceLoader
 * @see ResourceFactory
 */
public final class ResourceManager
{
  private static final Log logger = LogFactory.getLog(ResourceManager.class);
  private ResourceManagerBackend backend;

  public static final String BUNDLE_LOADER_PREFIX = "org.pentaho.reporting.libraries.resourceloader.bundle.loader.";
  public static final String LOADER_PREFIX = "org.pentaho.reporting.libraries.resourceloader.loader.";
  public static final String FACTORY_TYPE_PREFIX = "org.pentaho.reporting.libraries.resourceloader.factory.type.";
  public static final String BUNDLE_CACHE_PROVIDER_KEY = "org.pentaho.reporting.libraries.resourceloader.cache.BundleDataCacheProvider";
  public static final String DATA_CACHE_PROVIDER_KEY = "org.pentaho.reporting.libraries.resourceloader.cache.DataCacheProvider";
  public static final String FACTORY_CACHE_PROVIDER_KEY = "org.pentaho.reporting.libraries.resourceloader.cache.FactoryCacheProvider";

  private ResourceDataCache dataCache;
  private ResourceBundleDataCache bundleCache;
  private ResourceFactoryCache factoryCache;

  /**
   * A set that contains the class-names of all cache-modules, which could not be instantiated correctly.
   * This set is used to limit the number of warnings in the log to exactly one per class.
   */
  private static final Set failedModules = new HashSet();

  /**
   * Default Constructor.
   */
  public ResourceManager()
  {
    this(new DefaultResourceManagerBackend());
  }

  public ResourceManager(final ResourceManagerBackend resourceManagerBackend)
  {
    if (resourceManagerBackend == null)
    {
      throw new NullPointerException();
    }
    this.backend = resourceManagerBackend;
    this.bundleCache = new NullResourceBundleDataCache();
    this.dataCache = new NullResourceDataCache();
    this.factoryCache = new NullResourceFactoryCache();
  }

  public ResourceManager(final ResourceManager parent, final ResourceManagerBackend backend)
  {
    if (backend == null)
    {
      throw new NullPointerException();
    }
    if (parent == null)
    {
      throw new NullPointerException();
    }

    this.backend = backend;
    this.bundleCache = parent.getBundleCache();
    this.dataCache = parent.getDataCache();
    this.factoryCache = parent.getFactoryCache();
  }

  public ResourceManagerBackend getBackend()
  {
    return backend;
  }

  /**
   * Creates a ResourceKey that carries no Loader-Parameters from the given object.
   *
   * @param data the key-data
   * @return the generated resource-key, never null.
   * @throws ResourceKeyCreationException if the key-creation failed.
   */
  public ResourceKey createKey(final Object data)
      throws ResourceKeyCreationException
  {
    return createKey(data, null);
  }

  /**
   * Creates a ResourceKey that carries the given Loader-Parameters contained in the optional map.
   *
   * @param data       the key-data
   * @param parameters an optional map of parameters.
   * @return the generated resource-key, never null.
   * @throws ResourceKeyCreationException if the key-creation failed.
   */
  public ResourceKey createKey(final Object data, final Map parameters)
      throws ResourceKeyCreationException
  {
    return backend.createKey(data, parameters);
  }

  /**
   * Derives a new key from the given resource-key. Only keys for a hierarchical storage system (like file-systems or
   * URLs) can have derived keys. Since LibLoader 0.3.0 only hierarchical keys can be derived. For that, the deriving
   * path must be given as String.
   * <p/>
   * Before trying to derive the key, the system tries to interpret the path as absolute key-value.
   *
   * @param parent the parent key, must never be null
   * @param path   the relative path, that is used to derive the key.
   * @return the derived key.
   * @throws ResourceKeyCreationException if deriving the key failed.
   */
  public ResourceKey deriveKey(final ResourceKey parent, final String path)
      throws ResourceKeyCreationException
  {
    return deriveKey(parent, path, null);
  }

  /**
   * Derives a new key from the given resource-key. Only keys for a hierarchical storage system (like file-systems or
   * URLs) can have derived keys. Since LibLoader 0.3.0 only hierarchical keys can be derived. For that, the deriving
   * path must be given as String.
   * <p/>
   * The optional parameter-map will be applied to the derived key after the parent's parameters have been copied to
   * the new key.
   * <p/>
   * Before trying to derive the key, the system tries to interpret the path as absolute key-value.
   *
   * @param parent     the parent key, or null to interpret the path as absolute key.
   * @param path       the relative path, that is used to derive the key.
   * @param parameters a optional map containing resource-key parameters.
   * @return the derived key.
   * @throws ResourceKeyCreationException if deriving the key failed.
   */
  public ResourceKey deriveKey(final ResourceKey parent, final String path, final Map parameters)
      throws ResourceKeyCreationException
  {
    return backend.deriveKey(parent, path, parameters);
  }

  /**
   * Tries to convert the resource-key into an URL. Not all resource-keys have an URL representation. This method
   * exists to make it easier to connect LibLoader to other resource-loading frameworks.
   *
   * @param key the resource-key
   * @return the URL for the key, or null if there is no such key.
   */
  public URL toURL(final ResourceKey key)
  {
    return backend.toURL(key);
  }

  public Resource createDirectly(final Object keyValue, final Class target)
      throws ResourceLoadingException,
      ResourceCreationException,
      ResourceKeyCreationException
  {
    final ResourceKey key = createKey(keyValue);
    return create(key, null, target);
  }


  /**
   * Tries to find the first resource-bundle-loader that would be able to process the key.
   *
   * @param key the resource-key.
   * @return the resourceloader for that key, or null, if no resource-loader is able to process the key.
   * @throws ResourceLoadingException if an error occured.
   */
  public synchronized ResourceBundleData loadResourceBundle(final ResourceKey key) throws ResourceLoadingException
  {
    final ResourceBundleDataCache bundleCache = getBundleCache();
    final ResourceBundleDataCacheEntry cached = bundleCache.get(key);
    if (cached != null)
    {
      final ResourceBundleData data = cached.getData();
      // check, whether it is valid.

      final long version = data.getVersion(this);
      if ((cached.getStoredVersion() < 0) ||
          (version >= 0 && cached.getStoredVersion() == version))
      {
        // now also make sure that the underlying data has not changed.
        // This may look a bit superfluous, but the repository may not provide
        // sensible cacheable information.
        final ResourceDataCacheEntry bundleRawDataCacheEntry = getDataCache().get(data.getBundleKey());
        if (bundleRawDataCacheEntry != null)
        {
          final ResourceData bundleRawData = bundleRawDataCacheEntry.getData();
          if (bundleRawData != null)
          {
            if (isValidData(bundleRawDataCacheEntry, bundleRawData))
            {
              return data;
            }
            getDataCache().remove(bundleRawData);
          }
        }
      }
      bundleCache.remove(data);
    }

    final ResourceBundleData data = backend.loadResourceBundle(this, key);
    if (data != null && isResourceDataCacheable(data))
    {
      bundleCache.put(this, data);
    }
    return data;
  }

  private boolean isResourceDataCacheable(final ResourceData data)
  {
    try
    {
      return data.getVersion(this) != -1;
    }
    catch (ResourceLoadingException e)
    {
      return false;
    }
  }

  public ResourceData load(final ResourceKey key) throws ResourceLoadingException
  {
    final ResourceBundleData bundle = loadResourceBundle(key);
    if (bundle != null)
    {
      logger.debug("Loaded bundle for key " + key);
      return bundle;
    }
    final ResourceKey parent = key.getParent();
    if (parent != null)
    {
      // try to load the bundle data of the parent
      final ResourceBundleData parentData = loadResourceBundle(parent);
      if (parentData != null)
      {
        logger.debug("Loaded bundle for key (derivate) " + key);
        return parentData.deriveData(key);
      }
    }
    return loadRawData(key);
  }

  private boolean isValidData(final ResourceDataCacheEntry cached,
                              final ResourceData data) throws ResourceLoadingException
  {
    // check, whether it is valid.
    if (cached.getStoredVersion() < 0)
    {
      // a non versioned entry is always valid. (Maybe this is from a Jar-URL?)
      return true;
    }

    final long version = data.getVersion(this);
    if (version < 0)
    {
      // the system is no longer able to retrieve the version information?
      // (but versioning information must have been available in the past)
      // oh, that's bad. Assume the worst and re-read the data.
      return false;
    }
    if (cached.getStoredVersion() == version)
    {
      return true;
    }
    else
    {
      return false;
    }
  }

  public synchronized ResourceData loadRawData(final ResourceKey key)
      throws UnrecognizedLoaderException, ResourceLoadingException
  {
    final ResourceDataCache dataCache = getDataCache();
    // Alternative 3: This is a plain resource and not contained in a bundle. Load as binary data
    final ResourceDataCacheEntry cached = dataCache.get(key);
    if (cached != null)
    {
      final ResourceData data = cached.getData();
      if (data != null)
      {
        if (isValidData(cached, data))
        {
          return data;
        }
        dataCache.remove(data);
      }
    }

    final ResourceData data = backend.loadRawData(this, key);
    if (data != null && isResourceDataCacheable(data))
    {
      dataCache.put(this, data);
    }
    return data;
  }

  public Resource create(final ResourceKey key, final ResourceKey context, final Class target)
      throws ResourceLoadingException, ResourceCreationException
  {
    if (target == null)
    {
      throw new NullPointerException("Target must not be null");
    }
    if (key == null)
    {
      throw new NullPointerException("Key must not be null.");
    }
    return create(key, context, new Class[]{target});
  }

  public Resource create(final ResourceKey key, final ResourceKey context)
      throws ResourceLoadingException, ResourceCreationException
  {
    return create(key, context, (Class[]) null);
  }

  public Resource create(final ResourceKey key, final ResourceKey context, final Class[] target)
      throws ResourceLoadingException, ResourceCreationException
  {
    if (key == null)
    {
      throw new NullPointerException();
    }

    final ResourceFactoryCache factoryCache = getFactoryCache();
    // ok, we have a handle to the data, and the data is current.
    // Lets check whether we also have a cached result.
    final Resource resource = factoryCache.get(key, target);
    if (resource != null)
    {
      if (backend.isResourceUnchanged(this, resource))
      {
        // mama, look i am a good cache manager ...
        return resource;
      }
      else
      {
        // someone evil changed one of the dependent resources ...
        factoryCache.remove(resource);
      }
    }

    final ResourceData loadedData = load(key);
    final Resource newResource;
    if (loadedData instanceof ResourceBundleData)
    {
      final ResourceBundleData resourceBundleData = (ResourceBundleData) loadedData;
      final ResourceManager derivedManager = resourceBundleData.deriveManager(this);
      newResource = backend.create(derivedManager, resourceBundleData, context, target);
      if (isResourceCacheable(newResource))
      {
        if (EHCacheModule.CACHE_MONITOR.isDebugEnabled())
        {
          EHCacheModule.CACHE_MONITOR.debug("Storing created bundle-resource for key: " + key);
        }
        factoryCache.put(newResource);
        if (key != newResource.getSource())
        {
          factoryCache.put(new BundleCacheResourceWrapper(newResource, key));
        }
      }
      else
      {
        if (EHCacheModule.CACHE_MONITOR.isDebugEnabled())
        {
          EHCacheModule.CACHE_MONITOR.debug("Created bundle-resource is not cacheable for " + key);
        }
      }
    }
    else
    {
      newResource = backend.create(this, loadedData, context, target);
      if (isResourceCacheable(newResource))
      {
        if (EHCacheModule.CACHE_MONITOR.isDebugEnabled())
        {
          EHCacheModule.CACHE_MONITOR.debug("Storing created resource for key: " + key);
        }
        factoryCache.put(newResource);
      }
      else
      {
        if (EHCacheModule.CACHE_MONITOR.isDebugEnabled())
        {
          EHCacheModule.CACHE_MONITOR.debug("Created resource is not cacheable for " + key);
        }
      }
    }
    return newResource;
  }

  private boolean isResourceCacheable(final Resource newResource)
  {
    final ResourceKey source = newResource.getSource();
    if (newResource.isTemporaryResult())
    {
      return false;
    }
    if (newResource.getVersion(source) == -1)
    {
      return false;
    }
    final ResourceKey[] keys = newResource.getDependencies();
    for (int i = 0; i < keys.length; i++)
    {
      if (newResource.getVersion(keys[i]) == -1)
      {
        return false;
      }
    }
    return true;
  }


  public ResourceDataCache getDataCache()
  {
    return dataCache;
  }

  public void setDataCache(final ResourceDataCache dataCache)
  {
    if (dataCache == null)
    {
      throw new NullPointerException();
    }
    this.dataCache = dataCache;
  }

  public ResourceFactoryCache getFactoryCache()
  {
    return factoryCache;
  }

  public void setFactoryCache(final ResourceFactoryCache factoryCache)
  {
    if (factoryCache == null)
    {
      throw new NullPointerException();
    }
    this.factoryCache = factoryCache;
  }

  public ResourceBundleDataCache getBundleCache()
  {
    return bundleCache;
  }

  public void setBundleCache(final ResourceBundleDataCache bundleCache)
  {
    if (bundleCache == null)
    {
      throw new NullPointerException();
    }
    this.bundleCache = bundleCache;
  }

  public void registerDefaults()
  {
    // Create all known resource loaders ...
    registerDefaultLoaders();

    // Register all known factories ...
    registerDefaultFactories();
    // add the caches ..
    registerDataCache();
    registerBundleDataCache();
    registerFactoryCache();
  }

  public void registerDefaultFactories()
  {
    backend.registerDefaultFactories();
  }

  public void registerBundleDataCache()
  {
    final Configuration config = LibLoaderBoot.getInstance().getGlobalConfig();
    final String dataCacheProviderClass =
        config.getConfigProperty(BUNDLE_CACHE_PROVIDER_KEY);
    if (dataCacheProviderClass == null)
    {
      return;
    }
    final Object maybeDataCacheProvider =
        ObjectUtilities.loadAndInstantiate
            (dataCacheProviderClass, ResourceManager.class, ResourceBundleDataCacheProvider.class);
    if (maybeDataCacheProvider == null)
    {
      return;
    }

    final ResourceBundleDataCacheProvider provider = (ResourceBundleDataCacheProvider) maybeDataCacheProvider;
    try
    {
      final ResourceBundleDataCache cache = provider.createBundleDataCache();
      if (cache != null)
      {
        setBundleCache(cache);
      }
    }
    catch (Throwable e)
    {
      // ok, did not work ...
      synchronized (failedModules)
      {
        if (failedModules.contains(dataCacheProviderClass) == false)
        {
          logger.warn("Failed to create data cache: " + e.getLocalizedMessage());
          failedModules.add(dataCacheProviderClass);
        }
      }
    }
  }

  public void registerDataCache()
  {
    final Configuration config = LibLoaderBoot.getInstance().getGlobalConfig();
    final String dataCacheProviderClass =
        config.getConfigProperty(DATA_CACHE_PROVIDER_KEY);
    if (dataCacheProviderClass == null)
    {
      return;
    }
    final Object maybeDataCacheProvider =
        ObjectUtilities.loadAndInstantiate
            (dataCacheProviderClass, ResourceManager.class, ResourceDataCacheProvider.class);
    if (maybeDataCacheProvider instanceof ResourceDataCacheProvider)
    {
      final ResourceDataCacheProvider provider = (ResourceDataCacheProvider) maybeDataCacheProvider;
      try
      {
        final ResourceDataCache cache = provider.createDataCache();
        if (cache != null)
        {
          setDataCache(cache);
        }
      }
      catch (Throwable e)
      {
        // ok, did not work ...
        synchronized (failedModules)
        {
          if (failedModules.contains(dataCacheProviderClass) == false)
          {
            logger.warn("Failed to create data cache: " + e.getLocalizedMessage());
            failedModules.add(dataCacheProviderClass);
          }
        }
      }
    }
  }

  public void registerFactoryCache()
  {
    final Configuration config = LibLoaderBoot.getInstance().getGlobalConfig();
    final String cacheProviderClass = config.getConfigProperty
        (FACTORY_CACHE_PROVIDER_KEY);
    if (cacheProviderClass == null)
    {
      return;
    }
    final Object maybeCacheProvider = ObjectUtilities.loadAndInstantiate
        (cacheProviderClass, ResourceManager.class, ResourceFactoryCacheProvider.class);

    if (maybeCacheProvider != null)
    {
      final ResourceFactoryCacheProvider provider = (ResourceFactoryCacheProvider) maybeCacheProvider;
      try
      {
        final ResourceFactoryCache cache = provider.createFactoryCache();
        if (cache != null)
        {
          setFactoryCache(cache);
        }
      }
      catch (Throwable e)
      {
        synchronized (failedModules)
        {
          if (failedModules.contains(cacheProviderClass) == false)
          {
            logger.warn("Failed to create factory cache: " + e.getLocalizedMessage());
            failedModules.add(cacheProviderClass);
          }
        }
      }
    }
  }

  public void registerDefaultLoaders()
  {
    backend.registerDefaultLoaders();
  }

  public void registerBundleLoader(final ResourceBundleLoader loader)
  {
    if (loader == null)
    {
      throw new NullPointerException();
    }
    backend.registerBundleLoader(loader);
  }

  public void registerLoader(final ResourceLoader loader)
  {
    if (loader == null)
    {
      throw new NullPointerException();
    }
    backend.registerLoader(loader);
  }

  public void registerFactory(final ResourceFactory factory)
  {
    if (factory == null)
    {
      throw new NullPointerException();
    }
    backend.registerFactory(factory);
  }

  public void shutDown()
  {
    factoryCache.shutdown();
    dataCache.shutdown();
  }

  /**
   * Creates a String version of the <code>ResourceKey</code> that can be deserialized with the
   * <code>deserialize()</code> method.
   *
   * @param bundleKey the key to the bundle containing the resource, or null if no bundle exists.
   * @param key       the key to be serialized
   * @throws ResourceException    indicates an error trying to serialize the key
   * @throws NullPointerException indicates the supplied key is <code>null</code>
   */
  public String serialize(final ResourceKey bundleKey, final ResourceKey key) throws ResourceException
  {
    return backend.serialize(bundleKey, key);
  }

  /**
   * Converts a serialized version of a <code>ResourceKey</code> into an actual <code>ResourceKey</code>
   * by locating the proper <code>ResourceLoader</code> that can perform the deserialization.
   *
   * @param serializedKey the String serialized key to be deserialized
   * @return the <code>ResourceKey</code> that has been deserialized
   * @throws ResourceKeyCreationException indicates an error trying to create the <code>ResourceKey</code>
   *                                      from the deserialized version
   */
  public ResourceKey deserialize(final ResourceKey bundleKey,
                                 final String serializedKey) throws ResourceKeyCreationException
  {
    return backend.deserialize(bundleKey, serializedKey);
  }
}
