/**
 * Copyright Terracotta, Inc.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.ehcache.integrations.shiro;

import org.apache.shiro.ShiroException;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.io.ResourceUtils;
import org.apache.shiro.util.Destroyable;
import org.apache.shiro.util.Initializable;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.xml.XmlConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URL;

/**
 * Shiro {@link CacheManager} implementation using the Ehcache 3.x framework for all cache functionality
 */
public class EhcacheShiroManager implements CacheManager, Initializable, Destroyable {

  private static final Logger log = LoggerFactory.getLogger(EhcacheShiroManager.class);

  private volatile org.ehcache.CacheManager manager;

  private volatile String cacheManagerConfigFile = "classpath:org/ehcache/integrations/shiro/ehcache.xml";
  private volatile boolean cacheManagerImplicitlyCreated = false;

  private volatile XmlConfiguration cacheConfiguration = null;

  /**
   * Returns the wrapped {@link org.ehcache.CacheManager} instance
   *
   * @return the wrapped {@link org.ehcache.CacheManager} instance
   */
  public org.ehcache.CacheManager getCacheManager() {
    return manager;
  }

  /**
   * Sets the wrapped {@link org.ehcache.CacheManager} instance
   *
   * @param cacheManager the {@link org.ehcache.CacheManager} to be used
   */
  public void setCacheManager(org.ehcache.CacheManager cacheManager) {
    try {
      destroy();
    } catch (Exception e) {
      log.warn("The Shiro managed CacheManager threw an Exception while closing", e);
    }
    manager = cacheManager;
    cacheManagerImplicitlyCreated = false;
  }

  /**
   * Returns the resource location of the config file used to initialize a new
   * EhCache CacheManager instance.  The string can be any resource path supported by the
   * {@link org.apache.shiro.io.ResourceUtils#getInputStreamForPath(String)} call.
   * <P>
   * This property is ignored if the CacheManager instance is injected directly - that is, it is only used to
   * lazily create a CacheManager if one is not already provided.
   * </P>
   *
   * @return the resource location of the config file used to initialize the wrapped
   * EhCache CacheManager instance.
   */
  public String getCacheManagerConfigFile() {
    return cacheManagerConfigFile;
  }

  /**
   * Sets the resource location of the config file used to initialize the wrapped
   * EhCache CacheManager instance.  The string can be any resource path supported by the
   * {@link org.apache.shiro.io.ResourceUtils#getInputStreamForPath(String)} call.
   * <P>
   * This property is ignored if the CacheManager instance is injected directly - that is, it is only used to
   * lazily create a CacheManager if one is not already provided.
   * </P>
   *
   * @param cacheManagerConfigFile resource location of the config file used to create the wrapped
   *                               EhCache CacheManager instance.
   */
  public void setCacheManagerConfigFile(String cacheManagerConfigFile) {
    this.cacheManagerConfigFile = cacheManagerConfigFile;
  }

  /**
   * {@inheritDoc}
   */
  public <K, V> Cache<K, V> getCache(String name) throws CacheException {
    log.trace("Acquiring EhcacheShiro instance named [{}]", name);

    try {
      org.ehcache.Cache<Object, Object> cache = ensureCacheManager().getCache(name, Object.class, Object.class);

      if (cache == null) {
        log.info("Cache with name {} does not yet exist.  Creating now.", name);
        cache = createCache(name);
        log.info("Added EhcacheShiro named [{}]", name);
      } else {
        log.info("Using existing EhcacheShiro named [{}]", name);
      }

      return new EhcacheShiro<K, V>(cache);
    } catch (MalformedURLException e) {
      throw new CacheException(e);
    }
  }

  private org.ehcache.Cache<Object, Object> createCache(String name) {
    try {
      XmlConfiguration xmlConfiguration = getConfiguration();
      CacheConfigurationBuilder<Object, Object> configurationBuilder = xmlConfiguration.newCacheConfigurationBuilderFromTemplate(
              "defaultCacheConfiguration", Object.class, Object.class);
      CacheConfiguration<Object, Object> cacheConfiguration = configurationBuilder.build();
      return ensureCacheManager().createCache(name, cacheConfiguration);
    } catch (InstantiationException e) {
      throw new CacheException(e);
    } catch (IllegalAccessException e) {
      throw new CacheException(e);
    } catch (ClassNotFoundException e) {
      throw new CacheException(e);
    } catch (MalformedURLException e) {
      throw new CacheException(e);
    }
  }

  private org.ehcache.CacheManager ensureCacheManager() throws MalformedURLException {
    if (manager == null) {
      manager = CacheManagerBuilder.newCacheManager(getConfiguration());
      manager.init();

      cacheManagerImplicitlyCreated = true;
    }

    return manager;
  }

  private URL getResource() throws MalformedURLException {
    String cacheManagerConfigFile = getCacheManagerConfigFile();
    String configFileWithoutPrefix = stripPrefix(cacheManagerConfigFile);
    if (cacheManagerConfigFile.startsWith(ResourceUtils.CLASSPATH_PREFIX)) {
      return ClassUtils.getResource(configFileWithoutPrefix);
    }

    String url = ResourceUtils.hasResourcePrefix(cacheManagerConfigFile) ? configFileWithoutPrefix
            : cacheManagerConfigFile;

    return new URL(url);
  }

  private static String stripPrefix(String resourcePath) {
    return resourcePath.substring(resourcePath.indexOf(":") + 1);
  }

  private XmlConfiguration getConfiguration() throws MalformedURLException {
    if (cacheConfiguration == null) {
      cacheConfiguration = new XmlConfiguration(getResource());
    }

    return cacheConfiguration;
  }

  public void destroy() throws Exception {
    if (cacheManagerImplicitlyCreated && manager != null) {
      manager.close();
      manager = null;
    }
  }

  /**
   * Initializes this instance.
   * <P>
   * If a {@link #setCacheManager CacheManager} has been
   * explicitly set (e.g. via Dependency Injection or programatically) prior to calling this
   * method, this method does nothing.
   * </P>
   * <P>
   * However, if no {@code CacheManager} has been set a new {@link org.ehcache.Cache} will be initialized.
   * It will use {@code ehcache.xml} configuration file at the root of the classpath.
   * </P>
   *
   * @throws org.apache.shiro.cache.CacheException if there are any CacheExceptions thrown by EhCache.
   */
  public void init() throws ShiroException {
    try {
      ensureCacheManager();
    } catch (MalformedURLException e) {
      throw new ShiroException(e);
    }
  }
}