package org.infinispan.jcache.remote;

import java.lang.management.ManagementFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.stream.Collectors;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.configuration.CacheEntryListenerConfiguration;
import javax.cache.integration.CacheLoader;
import javax.cache.integration.CacheWriter;
import javax.cache.integration.CompletionListener;
import javax.cache.processor.EntryProcessor;
import javax.cache.processor.EntryProcessorException;
import javax.management.MBeanServer;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.commons.logging.LogFactory;
import org.infinispan.commons.util.CloseableIterator;
import org.infinispan.commons.util.InfinispanCollections;
import org.infinispan.jcache.AbstractJCache;
import org.infinispan.jcache.AbstractJCacheListenerAdapter;
import org.infinispan.jcache.Exceptions;
import org.infinispan.jcache.JCacheEntry;
import org.infinispan.jcache.MutableJCacheEntry;
import org.infinispan.jcache.logging.Log;

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

   private volatile boolean isClosed = false;

   private final RemoteCache<K, V> cacheWithoutStats;
   private final RemoteCache<K, V> cache;
   private final RemoteCache<K, V> cacheWithoutReturnValue;
   private final RemoteCache<K, V> cacheWithCacheStore;

   private final LocalStatistics stats;

   public JCache(RemoteCache<K, V> cache, RemoteCache<K, V> cacheForceReturnValue, CacheManager cacheManager, ConfigurationAdapter<K, V> configurationAdapter) {
      super(configurationAdapter.getConfiguration(), cacheManager, new JCacheNotifier<K, V>());

      setCacheLoader(configuration);
      setCacheWriter(configuration);

      stats = new LocalStatistics();

      this.cacheWithoutStats = new RemoteCacheWithOldValue<>(cache);

      this.cache = new RemoteCacheWithOldValue<>(new RemoteCacheWithStats<>(new RemoteCacheWithSyncListeners<>(cacheForceReturnValue, notifier, this), stats));
      this.cacheWithoutReturnValue = new RemoteCacheWithOldValue<>(new RemoteCacheWithStats<>(cache, stats));
      this.cacheWithCacheStore = new RemoteCacheWithCacheStore<K, V>(this.cache, jcacheLoader, jcacheWriter, configuration) {
         @Override
         protected void onLoad(K key, V value) {
            JCache.this.put(JCache.this.cache, JCache.this.cache, key, value, false);
         }
      };

      addConfigurationListeners();

      if (configuration.isManagementEnabled())
         setManagementEnabled(true);

      if (configuration.isStatisticsEnabled())
         setStatisticsEnabled(true);
   }

   @Override
   public V get(K key) {
      checkNotClosed();
      checkNotNull(key, "key");

      V value = cacheWithCacheStore.get(key);
      if (value != null) {
         updateTTLForAccessed(cache, key, value);
      }
      return value;
   }

   @Override
   public Map<K, V> getAll(Set<? extends K> keys) {
      checkNotClosed();
      InfinispanCollections.assertNotNullEntries(keys, "keys");

      if (keys.isEmpty()) {
         return Collections.emptyMap();
      }

      /**
       * TODO: Just an idea here to consider down the line: if keys.size() is big (TBD...), each of
       * this get calls could maybe be swapped by getAsync() in order to paralelise the retrieval of
       * entries. It'd be interesting to do a small performance test to see after which number of
       * elements doing it in paralel becomes more efficient than sequential :)
       */
      Map<K, V> result = new HashMap<K, V>(keys.size());
      for (K key : keys) {
         V value = get(key);
         if (value != null) {
            result.put(key, value);
         }
      }
      return result;
   }

   @Override
   public boolean containsKey(K key) {
      checkNotClosed();
      checkNotNull(key, "key");
      return cache.containsKey(key);
      //FIXME implement me
   }

   @Override
   public void loadAll(Set<? extends K> keys, boolean replaceExistingValues, CompletionListener completionListener) {
      checkNotClosed();
      checkNotNull(keys, "keys");

      if (jcacheLoader == null) {
         setListenerCompletion(completionListener);
      } else {
         loadAllFromJCacheLoader(keys, replaceExistingValues, completionListener, cache, cache);
      }
   }

   @Override
   public void put(K key, V value) {
      checkNotClosed();
      checkNotNull(key, "key");
      checkNotNull(value, "value");

      writeToCacheWriter(key, value);
      put(cache, cacheWithoutStats, key, value, false);
      //FIXME locks
   }

   private void writeToCacheWriter(K key, V value) {
      if (!configuration.isWriteThrough() || jcacheWriter == null) {
         return;
      }
      try {
         jcacheWriter.write(new JCacheEntry<K, V>(key, value));
      } catch (Exception ex) {
         throw Exceptions.launderCacheWriterException(ex);
      }
   }

   private void removeFromCacheWriter(K key) {
      if (!configuration.isWriteThrough() || jcacheWriter == null) {
         return;
      }
      try {
         jcacheWriter.delete(key);
      } catch (Exception ex) {
         throw Exceptions.launderCacheWriterException(ex);
      }
   }

   @Override
   public V getAndPut(K key, V value) {
      checkNotClosed();
      checkNotNull(key, "key");
      checkNotNull(value, "value");

      //FIXME locks
      V prev = put(cache, cache, key, value, false);

      writeToCacheWriter(key, value);
      return prev;
   }

   @Override
   public void putAll(Map<? extends K, ? extends V> inputMap) {
      checkNotClosed();
      InfinispanCollections.assertNotNullEntries(inputMap, "inputMap");

      //FIXME locks
      for (final Map.Entry<? extends K, ? extends V> e : inputMap.entrySet()) {
         final K key = e.getKey();
         final V value = e.getValue();
         put(key, value);
      }
   }

   @Override
   public boolean putIfAbsent(K key, V value) {
      checkNotClosed();
      checkNotNull(key, "key");
      checkNotNull(value, "value");

      boolean put = put(cache, cache, key, value, true) == null;
      if (put) {
         writeToCacheWriter(key, value);
      }
      return put;
      //FIXME locks
   }

   @Override
   public boolean remove(K key) {
      checkNotClosed();
      checkNotNull(key, "key");

      removeFromCacheWriter(key);
      return cache.remove(key) != null;
      //FIXME locks
   }

   @Override
   public boolean remove(K key, V oldValue) {
      checkNotClosed();
      checkNotNull(key, "key");
      checkNotNull(oldValue, "oldValue");

      boolean removed = remove(cache, key, oldValue);
      if (removed) {
         removeFromCacheWriter(key);
      }
      return removed;
      //FIXME locks
   }

   @Override
   public V getAndRemove(K key) {
      checkNotClosed();
      checkNotNull(key, "key");

      V prev = cache.remove(key);
      removeFromCacheWriter(key);
      if (prev != null) {
         stats.incrementCacheHits();
      } else {
         stats.incrementCacheMisses();
      }

      return prev;
      //FIXME locks
   }

   @Override
   public boolean replace(K key, V oldValue, V newValue) {
      checkNotClosed();
      checkNotNull(key, "key");
      checkNotNull(oldValue, "oldValue");
      checkNotNull(newValue, "newValue");

      boolean replaced = replace(cache, cache, key, oldValue, newValue, true);
      if (replaced) {
         writeToCacheWriter(key, newValue);
      }
      return replaced;
      //FIXME locks
   }

   @Override
   public boolean replace(K key, V value) {
      checkNotClosed();
      checkNotNull(key, "key");
      checkNotNull(value, "value");
      boolean replaced = replace(cache, cacheWithoutStats, key, null, value, false);
      if (replaced) {
         writeToCacheWriter(key, value);
      } else {
         stats.incrementCacheMisses();
      }
      return replaced;
      //FIXME locks
   }

   @Override
   public V getAndReplace(K key, V value) {
      checkNotClosed();
      checkNotNull(key, "key");
      checkNotNull(value, "value");

      V prev = replace(cache, key, value);
      if (prev != null) {
         writeToCacheWriter(key, value);
      } else {
         stats.incrementCacheMisses();
      }

      return prev;
      //FIXME locks
   }

   @Override
   public void removeAll(Set<? extends K> keys) {
      checkNotClosed();
      InfinispanCollections.assertNotNullEntries(keys, "keys");
      for (K k : keys) {
         remove(k);
      }
      //FIXME implement me
   }

   @Override
   public void removeAll() {
      checkNotClosed();

      try (CloseableIterator<K> it = cacheWithoutStats.keySet().iterator()) {
         while (it.hasNext()) {
            remove(it.next());
         }
      }
      //FIXME locks
   }

   @Override
   public void clear() {
      checkNotClosed();

      cache.clear();
   }

   @Override
   public <T> T invoke(K key, EntryProcessor<K, V, T> entryProcessor, Object... arguments) throws EntryProcessorException {
      checkNotClosed();
      checkNotNull(key, "key");
      checkNotNull(entryProcessor, "entryProcessor");

      // Using references for backup copies to provide perceived exclusive
      // read access, and only apply changes if original value was not
      // changed by another thread, the JSR requirements for this method could
      // have been full filled. However, the TCK has some timing checks which
      // verify that under contended access, one of the threads should "wait"
      // for the other, hence the use locks.

      if (trace)
         log.tracef("Invoke entry processor %s for key=%s", entryProcessor, key);

      // Get old value skipping any listeners to impacting
      // listener invocation expectations set by the TCK.
      V oldValue = cache.get(key);

      MutableJCacheEntry<K, V> mutable = createMutableCacheEntry(oldValue, key);
      T ret = processEntryProcessor(mutable, entryProcessor, arguments);

      switch (mutable.getOperation()) {
      case NONE:
         break;
      case ACCESS:
         updateTTLForAccessed(cache, key, oldValue);
         break;
      case UPDATE:
         V newValue = mutable.getNewValue();
         if (newValue == null) {
            throw new EntryProcessorException();
         }
         if (oldValue != null) {
            // Only allow change to be applied if value has not
            // changed since the start of the processing.
            replace(cache, cacheWithoutStats, key, oldValue, newValue, true);
         } else {
            put(cache, cache, key, newValue, true);
         }
         writeToCacheWriter(key, newValue);
         break;
      case REMOVE:
         cache.remove(key);
         removeFromCacheWriter(key);
         break;
      }

      return ret;
   }

   private MutableJCacheEntry<K, V> createMutableCacheEntry(V safeOldValue, K key) {
      return new MutableJCacheEntry<K, V>(configuration.isReadThrough() ? cacheWithCacheStore : cache, cacheWithoutStats, key, safeOldValue);
   }

   @Override
   public String getName() {
      return cache.getName();
   }

   @Override
   public void close() {
      super.close();
      cache.stop();
      isClosed = true;
   }

   @Override
   public boolean isClosed() {
      return isClosed;
   }

   @Override
   public void registerCacheEntryListener(CacheEntryListenerConfiguration<K, V> listenerCfg) {
      notifier.addListener(listenerCfg, this, notifier);
      addCacheEntryListenerConfiguration(listenerCfg);
   }

   @Override
   public void deregisterCacheEntryListener(CacheEntryListenerConfiguration<K, V> listenerCfg) {
      notifier.removeListener(listenerCfg, this);
      removeCacheEntryListenerConfiguration(listenerCfg);
   }

   @Override
   public Iterator<javax.cache.Cache.Entry<K, V>> iterator() {
      checkNotClosed();

      return new Itr();
   }

   @Override
   protected MBeanServer getMBeanServer() {
      return ManagementFactory.getPlatformMBeanServer();
   }

   @Override
   protected Object getCacheStatisticsMXBean() {
      return stats;
   }

   protected AbstractJCache<K, V> checkNotClosed() {
      if (isClosed())
         throw log.cacheClosed();

      return this;
   }

   private class Itr implements Iterator<Cache.Entry<K, V>> {

      // This forces all keys into memory at once
      private final Iterator<K> it = cache.keySet().stream().collect(Collectors.toSet()).iterator();
      private Entry<K, V> current;
      private Entry<K, V> next;

      Itr() {
         fetchNext();
      }

      private void fetchNext() {
         //TODO: fix stats
//         long start = statisticsEnabled() ? System.nanoTime() : 0;
         if (it.hasNext()) {
            K key = it.next();
            V value = cache.get(key);
            next = new JCacheEntry<K, V>(key, value);
//            if (statisticsEnabled()) {
//             stats.incrementCacheHits();
//               stats.addGetTimeNano(System.nanoTime() - start);
//            }
         } else {
            next = null;
         }
      }

      @Override
      public boolean hasNext() {
         return next != null;
      }

      @Override
      public Entry<K, V> next() {
         if (next == null)
            fetchNext();

         if (next == null)
            throw new NoSuchElementException();

         // Set return value
         Entry<K, V> ret = next;

//         // Force expiration if needed
         updateTTLForAccessed(cache, next.getKey(), next.getValue());

         current = next;

         // Fetch next...
         fetchNext();

         return ret;
      }

      @Override
      public void remove() {
         if (current == null)
            throw new IllegalStateException();

         // TODO: Should Infinispan's core iterators be mutable?
         // It can be worked around as shown here for JSR-107 needs
         K k = current.getKey();
         current = null;
         cache.remove(k);
         removeFromCacheWriter(k);
      }
   }
   @Override
   protected void addListener(AbstractJCacheListenerAdapter<K, V> listenerAdapter) {
      cache.addClientListener(listenerAdapter);
   }

   @Override
   protected void removeListener(AbstractJCacheListenerAdapter<K, V> listenerAdapter) {
      cache.removeClientListener(listenerAdapter);
   }

   @Override
   protected void evict(K key) {
      //Nothing to do.
   }

   @Override
   protected void addCacheLoaderAdapter(CacheLoader<K, V> cacheLoader) {
      //Nothing to do.
   }

   @Override
   protected void addCacheWriterAdapter(CacheWriter<? super K, ? super V> cacheWriter) {
      //Nothing to do.
   }
}
