package org.infinispan.jcache;

import static org.infinispan.jcache.RIMBeanServerRegistrationUtility.ObjectNameType.CONFIGURATION;
import static org.infinispan.jcache.RIMBeanServerRegistrationUtility.ObjectNameType.STATISTICS;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.cache.Cache;
import javax.cache.CacheException;
import javax.cache.CacheManager;
import javax.cache.configuration.CacheEntryListenerConfiguration;
import javax.cache.configuration.CompleteConfiguration;
import javax.cache.configuration.Configuration;
import javax.cache.configuration.Factory;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.Duration;
import javax.cache.expiry.ExpiryPolicy;
import javax.cache.integration.CacheLoader;
import javax.cache.integration.CacheWriter;
import javax.cache.integration.CompletionListener;
import javax.cache.management.CacheMXBean;
import javax.cache.processor.EntryProcessor;
import javax.cache.processor.EntryProcessorResult;
import javax.management.MBeanServer;

import org.infinispan.commons.CacheListenerException;
import org.infinispan.commons.api.BasicCache;
import org.infinispan.commons.logging.LogFactory;
import org.infinispan.commons.util.ReflectionUtil;
import org.infinispan.jcache.logging.Log;

public abstract class AbstractJCache<K, V> implements Cache<K, V> {
   private static final Log log =
         LogFactory.getLog(AbstractJCache.class, Log.class);
   private static final boolean trace = log.isTraceEnabled();

   protected final MutableConfiguration<K, V> configuration;
   protected final ExpiryPolicy expiryPolicy;
   protected final AbstractJCacheNotifier<K, V> notifier;

   private final CacheManager cacheManager;

   protected CacheLoader<K, V> jcacheLoader;
   protected CacheWriter<? super K, ? super V> jcacheWriter;

   private final CacheMXBean mxBean;

   public AbstractJCache(MutableConfiguration<K, V> configuration, CacheManager cacheManager, AbstractJCacheNotifier<K, V> notifier) {
      this.configuration = configuration;
      this.cacheManager = cacheManager;
      this.notifier = notifier;
      this.expiryPolicy = configuration.getExpiryPolicyFactory().create();

      this.mxBean = new RIDelegatingCacheMXBean<K, V>(this);
   }

   protected void addConfigurationListeners() {
      for (CacheEntryListenerConfiguration<K, V> r
            : configuration.getCacheEntryListenerConfigurations())
         notifier.addListener(r, this, notifier);
   }

   @Override
   public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
      if (clazz.isInstance(configuration))
         return clazz.cast(configuration);
      throw log.configurationClassNotSupported(clazz);
   }

   public AbstractJCache<K, V> checkNotNull(Object obj, String name) {
      if (obj == null) {
         throw log.parameterMustNotBeNull(name);
      }
      return this;
   }

   protected void setCacheLoader(CompleteConfiguration<K, V> c) {
      // Plug user-defined cache loader into adaptor
      Factory<CacheLoader<K, V>> cacheLoaderFactory = c.getCacheLoaderFactory();
      if (cacheLoaderFactory != null) {
         jcacheLoader = cacheLoaderFactory.create();
         addCacheLoaderAdapter(jcacheLoader);
      }
   }

   protected void setCacheWriter(CompleteConfiguration<K, V> c) {
      // Plug user-defined cache writer into adaptor
      Factory<CacheWriter<? super K, ? super V>> cacheWriterFactory = c.getCacheWriterFactory();
      if (cacheWriterFactory != null) {
         jcacheWriter = cacheWriterFactory.create();
         addCacheWriterAdapter(jcacheWriter);
      }
   }

   protected abstract void addCacheLoaderAdapter(CacheLoader<K, V> cacheLoader);
   protected abstract void addCacheWriterAdapter(CacheWriter<? super K, ? super V> cacheWriter);

   protected void setListenerCompletion(CompletionListener listener) {
      if (listener != null)
         listener.onCompletion();
   }

   protected void setListenerException(CompletionListener listener, Throwable t) {
      if (listener !=  null) {
         if (t instanceof Exception)
            listener.onException((Exception) t);
         else
            listener.onException(new CacheException(t));
      }
   }

   protected List<K> filterLoadAllKeys(Set<? extends K> keys, boolean replaceExistingValues, boolean cacheEvict) {
      if (trace)
         log.tracef("Before filtering, keys to load: %s", keys);

      // Filter null keys out and keys to be overridden optionally
      final List<K> keysToLoad = new ArrayList<K>();
      for (K key : keys) {
         if (key == null)
            throw log.parameterMustNotBeNull("Key");

         // Evict from memory if value needs to be replaced (instead of flag)
         if (cacheEvict && replaceExistingValues && containsKey(key))
            evict(key);

         if (replaceExistingValues || !containsKey(key))
            keysToLoad.add(key);
      }

      if (trace)
         log.tracef("After filtering, keys to load: %s", keysToLoad);

      return keysToLoad;
   }

   protected Map<K, V> loadAllKeys(Collection<? extends K> keysToLoad) {
      try {
         return jcacheLoader.loadAll(keysToLoad);
      } catch (Exception e) {
         throw Exceptions.launderCacheLoaderException(e);
      }
   }

   protected void loadAllFromJCacheLoader(Set<? extends K> keys, boolean replaceExistingValues, final CompletionListener listener,
         BasicCache<K, V> cache, BasicCache<K, V> createCheckCache) {
      final List<K> keysToLoad = filterLoadAllKeys(keys, replaceExistingValues, false);
      if (keysToLoad.isEmpty()) {
         setListenerCompletion(listener);
         return;
      }

      try {
         Map<K, V> loaded = loadAllKeys(keysToLoad);
         Iterator<Map.Entry<K, V>> it = loaded.entrySet().iterator();
         while (it.hasNext()) {
            Map.Entry<K, V> entry = it.next();
            if (entry.getValue() == null)
               it.remove();
         }

         for (Map.Entry<K, V> entry : loaded.entrySet()) {
            K loadedKey = entry.getKey();
            V loadedValue = entry.getValue();
            put(cache, createCheckCache, loadedKey, loadedValue, false);
         }
         setListenerCompletion(listener);
      } catch (Throwable t) {
         setListenerException(listener, t);
      }
   }

   protected <T> T processEntryProcessor(MutableJCacheEntry<K, V> mutable,
         EntryProcessor<K, V, T> entryProcessor, Object[] arguments) {
      try {
         return entryProcessor.process(mutable, arguments);
      } catch (Exception e) {
         throw Exceptions.launderEntryProcessorException(e);
      }
   }

   protected abstract AbstractJCache<K, V> checkNotClosed();

   protected AbstractJCache<K, V> verifyKeys(Set<? extends K> keys) {
      // spec required
      if (keys == null || keys.contains(null))
         throw new NullPointerException("keys is null or keys contains a null: " + keys);

      return this;
   }

   @Override
   public <T> Map<K, EntryProcessorResult<T>> invokeAll(
         Set<? extends K> keys, EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
      checkNotClosed().checkNotNull(entryProcessor, "entryProcessor").verifyKeys(keys);

      Map<K, EntryProcessorResult<T>> map = new HashMap<K, EntryProcessorResult<T>>(keys.size());
      for (K key : keys) {
         EntryProcessorResult<T> result;
         try {
            T t = invoke(key, entryProcessor, arguments);
            result = t == null ? null : new SuccessEntryProcessorResult<T>(t);
         } catch (Throwable t) {
            result = new FailureEntryProcessorResult<T>(t);
         }
         if (result != null)
            map.put(key, result);
      }

      return map;
   }

   protected abstract void evict(K key);

   protected V put(BasicCache<K, V> cache, BasicCache<K, V> createCheckCache,
         K key, V value, boolean isPutIfAbsent) {
      // Use a separate cache reference to check whether entry is created or
      // not. A separate reference allows for listener notifications to be
      // skipped selectively.
      V prev = createCheckCache.get(key);
      boolean isCreated = prev == null;

      // If putIfAbsent and entry already present, skip early
      if (!isCreated && isPutIfAbsent)
         return prev;

      V ret;
      Duration ttl = isCreated
            ? Expiration.getExpiry(expiryPolicy, Expiration.Operation.CREATION)
            : Expiration.getExpiry(expiryPolicy, Expiration.Operation.UPDATE);

      try {
         if (ttl == null || ttl.isEternal()) {
            ret = isPutIfAbsent
                  ? cache.putIfAbsent(key, value)
                  : cache.put(key, value);
         } else if (ttl.equals(Duration.ZERO)) {
            // TODO: Can this be avoided?
            // Special case for ZERO because the Infinispan remove()
            // implementation returns true if entry was expired in the removal
            // (since it was previously stored). JSR-107 TCK expects that if
            // ZERO is passed, the entry is not stored and removal returns false.
            // So, if entry is created, do not store it in the cache.
            // If the entry is modified, explicitly remove it.
            if (!isCreated)
               ret = cache.remove(key);
            else
               ret = null;
         } else {
            long duration = ttl.getDurationAmount();
            TimeUnit timeUnit = ttl.getTimeUnit();
            ret = isPutIfAbsent
                  ? cache.putIfAbsent(key, value, duration, timeUnit)
                  : cache.put(key, value, duration, timeUnit);
         }

         return ret;
      } catch (CacheListenerException e) {
         throw Exceptions.launderCacheListenerException(e);
      }
   }

   protected void updateTTLForAccessed(BasicCache<K, V> cache, K key, V value) {
      Duration ttl = Expiration.getExpiry(expiryPolicy, Expiration.Operation.ACCESS);

      if (ttl != null) {
         if (ttl.equals(Duration.ZERO)) {
            // TODO: Expiry of 0 does not seem to remove entry when next accessed.
            // Hence, explicitly removing the entry.
            cache.remove(key);
         } else {
            // The expiration policy could potentially return different values
            // every time, so don't think we can rely on maxIdle.
            long durationAmount = ttl.getDurationAmount();
            TimeUnit timeUnit = ttl.getTimeUnit();
            cache.put(key, value, durationAmount, timeUnit);
         }
      }
   }

   protected boolean replace(BasicCache<K, V> cache, BasicCache<K, V> existsCheckCache,
         K key, V oldValue, V value, boolean isConditional) {
      checkNotNull(value, "value");
      if (isConditional) {
         // Even if replace fails, values have to be validated (required by TCK)
         checkNotNull(oldValue, "oldValue");
      }

      V current = existsCheckCache.get(key);
      if (current != null) {
         if (isConditional && !current.equals(oldValue)) {
            updateTTLForAccessed(cache, key, value);
            return false;
         }

         Duration ttl = Expiration.getExpiry(expiryPolicy, Expiration.Operation.UPDATE);

         if (ttl == null || ttl.isEternal()) {
            return isConditional
                  ? cache.replace(key, oldValue, value)
                  : cache.replace(key, value) != null;
         } else if (ttl.equals(Duration.ZERO)) {
            // TODO: Can this be avoided?
            // Remove explicitly
            return cache.remove(key) != null;
         } else {
            long duration = ttl.getDurationAmount();
            TimeUnit timeUnit = ttl.getTimeUnit();
            return isConditional
                  ? cache.replace(key, oldValue, value, duration, timeUnit)
                  : cache.replace(key, value, duration, timeUnit) != null;
         }
      }

      return false;
   }

   protected V replace(BasicCache<K, V> cache, K key, V value) {
      checkNotNull(value, "value");
      boolean exists = cache.containsKey(key);
      if (exists) {
         Duration ttl = Expiration.getExpiry(expiryPolicy, Expiration.Operation.UPDATE);

         if (ttl == null || ttl.isEternal()) {
            return cache.replace(key, value);
         } else if (ttl.equals(Duration.ZERO)) {
            // TODO: Can this be avoided?
            // Remove explicitly
            return cache.remove(key);
         } else {
            long duration = ttl.getDurationAmount();
            TimeUnit timeUnit = ttl.getTimeUnit();
            return cache.replace(key, value, duration, timeUnit);
         }
      }

      return null;
   }

   protected boolean remove(BasicCache<K, V> cache, K key, V oldValue) {
      V current = cache.get(key);
      if (current != null && !current.equals(oldValue)) {
         updateTTLForAccessed(cache, key, current);
         return false;
      }

      return cache.remove(key, oldValue);
   }

   @Override
   public CacheManager getCacheManager() {
      return cacheManager;
   }

   //TODO: these were initially package-level
   protected abstract MBeanServer getMBeanServer();

   //TODO: these were initially package-level
   protected Object getCacheMXBean() {
      return mxBean;
   }

   //TODO: these were initially package-level
   protected abstract Object getCacheStatisticsMXBean();

   //TODO: was package-level initially
   public void setManagementEnabled(boolean enabled) {
      configuration.setManagementEnabled(enabled);
      if (enabled)
         RIMBeanServerRegistrationUtility.registerCacheObject(this, CONFIGURATION);
      else
         RIMBeanServerRegistrationUtility.unregisterCacheObject(this, CONFIGURATION);
   }

   //TODO: was package-level initially
   public void setStatisticsEnabled(boolean enabled) {
      configuration.setStatisticsEnabled(enabled);
      if (enabled)
         RIMBeanServerRegistrationUtility.registerCacheObject(this, STATISTICS);
      else
         RIMBeanServerRegistrationUtility.unregisterCacheObject(this, STATISTICS);
   }

   protected abstract void addListener(AbstractJCacheListenerAdapter<K, V> listenerAdapter);

   protected abstract void removeListener(AbstractJCacheListenerAdapter<K, V> listenerAdapter);

   @Override
   public <T> T unwrap(Class<T> clazz) {
      return ReflectionUtil.unwrap(this, clazz);
   }

   protected void addCacheEntryListenerConfiguration(
         CacheEntryListenerConfiguration<K, V> listenerCfg) {
      if (listenerCfg == null) {
         throw new NullPointerException("CacheEntryListenerConfiguration can't be null");
      }

      boolean alreadyExists = false;
      for (CacheEntryListenerConfiguration<? super K, ? super V> c
            : configuration.getCacheEntryListenerConfigurations()) {
         if (c.equals(listenerCfg)) {
            alreadyExists = true;
         }
      }

      if (!alreadyExists) {
         configuration.addCacheEntryListenerConfiguration(listenerCfg);
      } else {
         throw new IllegalArgumentException("A CacheEntryListenerConfiguration can " +
               "be registered only once");
      }
   }

   protected void removeCacheEntryListenerConfiguration(CacheEntryListenerConfiguration<K, V> listenerCfg) {
      configuration.removeCacheEntryListenerConfiguration(listenerCfg);
   }

   protected boolean statisticsEnabled() {
      return getConfiguration(CompleteConfiguration.class).isStatisticsEnabled();
   }

   public boolean managementEnabled() {
      return getConfiguration(CompleteConfiguration.class).isManagementEnabled();
   }

   @Override
   public void close() {
      notifier.close();
      Closeables.close(this.expiryPolicy);
      Closeables.close(this.jcacheLoader);
      Closeables.close(this.jcacheWriter);
   }
}
