package org.infinispan.tree.impl;

import java.util.Map;
import java.util.Set;

import org.infinispan.AdvancedCache;
import org.infinispan.Cache;
import org.infinispan.atomic.AtomicMap;
import org.infinispan.commons.CacheConfigurationException;
import org.infinispan.commons.CacheException;
import org.infinispan.context.Flag;
import org.infinispan.tree.Fqn;
import org.infinispan.tree.Node;
import org.infinispan.tree.NodeNotExistsException;
import org.infinispan.tree.TreeCache;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * @author Manik Surtani (<a href="mailto:manik AT jboss DOT org">manik AT jboss DOT org</a>)
 * @since 4.0
 */
public class TreeCacheImpl<K, V> extends TreeStructureSupport implements TreeCache<K, V> {
   private static final Log log = LogFactory.getLog(TreeCacheImpl.class);
   private static final boolean trace = log.isTraceEnabled();

   public TreeCacheImpl(Cache<?, ?> cache) {
      this(cache.getAdvancedCache());
   }

   public TreeCacheImpl(AdvancedCache<?, ?> cache) {
      super(cache, cache.getBatchContainer());
      if (cache.getCacheConfiguration().indexing().index().isEnabled())
         throw new CacheConfigurationException("TreeCache cannot be used with a Cache instance configured to use indexing!");
      assertBatchingSupported(cache.getCacheConfiguration());
      createRoot();
   }

   @Override
   public Node<K, V> getRoot() {
      return getRoot(cache);
   }

   @Override
   public Node<K, V> getRoot(Flag... flags) {
      return getRoot(cache.withFlags(flags));
   }

   private Node<K, V> getRoot(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache) {
      return new NodeImpl<K, V>(Fqn.ROOT, cache, batchContainer);
   }

   @Override
   public V put(String fqn, K key, V value) {
      return put(cache, Fqn.fromString(fqn), key, value);
   }

   @Override
   public V put(String fqn, K key, V value, Flag... flags) {
      return put(cache.withFlags(flags), Fqn.fromString(fqn), key, value);
   }

   @Override
   public void put(Fqn fqn, Map<? extends K, ? extends V> data) {
      put(cache, fqn, data);
   }

   @Override
   public void put(Fqn fqn, Map<? extends K, ? extends V> data, Flag... flags) {
      put(cache.withFlags(flags), fqn, data);
   }

   private void put(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn fqn, Map<? extends K, ? extends V> data) {
      startAtomic();
      try {
         Node<K, V> n = getNode(cache, fqn);
         if (n == null) createNodeInCache(cache, fqn);
         n = getNode(cache, fqn);
         n.putAll(data);
      } finally {
         endAtomic();
      }
   }

   @Override
   public void put(String fqn, Map<? extends K, ? extends V> data) {
      put(cache, Fqn.fromString(fqn), data);
   }

   @Override
   public void put(String fqn, Map<? extends K, ? extends V> data, Flag... flags) {
      put(cache.withFlags(flags), Fqn.fromString(fqn), data);
   }

   @Override
   public V remove(Fqn fqn, K key) {
      return remove(cache, fqn, key);
   }

   @Override
   public V remove(Fqn fqn, K key, Flag... flags) {
      return remove(cache.withFlags(flags), fqn, key);
   }

   private V remove(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn fqn, K key) {
      startAtomic();
      try {
         AtomicMap<K, V> map = getAtomicMap(cache, new NodeKey(fqn, NodeKey.Type.DATA));
         return map == null ? null : map.remove(key);
      } finally {
         endAtomic();
      }
   }

   @Override
   public V remove(String fqn, K key) {
      return remove(cache, Fqn.fromString(fqn), key);
   }

   @Override
   public V remove(String fqn, K key, Flag... flags) {
      return remove(cache.withFlags(flags), Fqn.fromString(fqn), key);
   }

   @Override
   public boolean removeNode(Fqn fqn) {
      return removeNode(cache, fqn);
   }

   @Override
   public boolean removeNode(Fqn fqn, Flag... flags) {
      return removeNode(cache.withFlags(flags), fqn);
   }

   private boolean removeNode(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn fqn) {
      if (fqn.isRoot()) return false;
      startAtomic();
      boolean result;
      try {
         if (trace) log.tracef("About to remove node %s", fqn);
         Node<K, V> n = getNode(cache, fqn.getParent());
         result = n != null && n.removeChild(fqn.getLastElement());
      } finally {
         endAtomic();
      }
      if (trace) log.trace("Node successfully removed");
      return result;
   }

   @Override
   public boolean removeNode(String fqn) {
      return removeNode(cache, Fqn.fromString(fqn));
   }

   @Override
   public boolean removeNode(String fqn, Flag... flags) {
      return removeNode(cache.withFlags(flags), Fqn.fromString(fqn));
   }

   @Override
   public Node<K, V> getNode(Fqn fqn) {
      return getNode(cache, fqn);
   }

   @Override
   public Node<K, V> getNode(Fqn fqn, Flag... flags) {
      return getNode(cache.withFlags(flags), fqn);
   }

   private Node<K, V> getNode(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn fqn) {
      startAtomic();
      try {
         if (exists(cache, fqn))
            return new NodeImpl<K, V>(fqn, cache, batchContainer);
         else return null;
      } finally {
         endAtomic();
      }
   }

   @Override
   public Node<K, V> getNode(String fqn) {
      return getNode(cache, Fqn.fromString(fqn));
   }

   @Override
   public Node<K, V> getNode(String fqn, Flag... flags) {
      return getNode(cache.withFlags(flags), Fqn.fromString(fqn));
   }

   @Override
   public V get(Fqn fqn, K key) {
      return get(cache, fqn, key);
   }

   @Override
   public V get(Fqn fqn, K key, Flag... flags) {
      return get(cache.withFlags(flags), fqn, key);
   }

   private V get(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn fqn, K key) {
      Map<K, V> m = getAtomicMap(cache, new NodeKey(fqn, NodeKey.Type.DATA));
      if (m == null) return null;
      return m.get(key);
   }

   @Override
   public boolean exists(String f) {
      return exists(cache, Fqn.fromString(f));
   }

   @Override
   public boolean exists(String fqn, Flag... flags) {
      return exists(cache.withFlags(flags), Fqn.fromString(fqn));
   }

   @Override
   public boolean exists(Fqn fqn, Flag... flags) {
      return exists(cache.withFlags(flags), fqn);
   }

   @Override
   public V get(String fqn, K key) {
      return get(cache, Fqn.fromString(fqn), key);
   }

   @Override
   public V get(String fqn, K key, Flag... flags) {
      return get(cache.withFlags(flags), Fqn.fromString(fqn), key);
   }

   @Override
   public void move(Fqn nodeToMoveFqn, Fqn newParentFqn) throws NodeNotExistsException {
      move(cache, nodeToMoveFqn, newParentFqn);
   }

   @Override
   public void move(Fqn nodeToMoveFqn, Fqn newParentFqn, Flag... flags) throws NodeNotExistsException {
      move(cache.withFlags(flags), nodeToMoveFqn, newParentFqn);
   }

   private void move(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn nodeToMoveFqn, Fqn newParentFqn) throws NodeNotExistsException {
      if (trace) log.tracef("Moving node '%s' to '%s'", nodeToMoveFqn, newParentFqn);
      if (nodeToMoveFqn == null || newParentFqn == null)
         throw new NullPointerException("Cannot accept null parameters!");

      if (nodeToMoveFqn.getParent().equals(newParentFqn)) {
         if (trace) log.trace("Not doing anything as this node is equal with its parent");
         // moving onto self!  Do nothing!
         return;
      }

      // Depth first.  Lets start with getting the node we want.
      startAtomic();
      boolean success = false;
      try {
         // Use the FORCE_WRITE_LOCK for the first read operation on the source, the source's parent,
         // and the destination.
         AdvancedCache<NodeKey, AtomicMap<?, ?>> cacheForWrite = cache.withFlags(Flag.FORCE_WRITE_LOCK);

         // check that parent's structure map contains the node to be moved. in case of optimistic locking this
         // ensures the write skew is properly detected if some other thread removes the child
         Node<K, V> parent = getNode(cacheForWrite, nodeToMoveFqn.getParent());
         if (!parent.hasChild(nodeToMoveFqn.getLastElement())) {
            if (trace) log.trace("The parent does not have the child that needs to be moved. Returning...");
            return;
         }
         Node<K, V> nodeToMove = getNode(cacheForWrite, nodeToMoveFqn);
         if (nodeToMove == null) {
            if (trace) log.trace("Did not find the node that needs to be moved. Returning...");
            return; // nothing to do here!
         }
         if (!exists(cacheForWrite, newParentFqn)) {
            // then we need to silently create the new parent
            createNodeInCache(cache, newParentFqn);
            if (trace) log.tracef("The new parent (%s) did not exists, was created", newParentFqn);
         }

         // create an empty node for this new parent
         Fqn newFqn = Fqn.fromRelativeElements(newParentFqn, nodeToMoveFqn.getLastElement());
         createNodeInCache(cache, newFqn);
         Node<K, V> newNode = getNode(cache, newFqn);
         Map<K, V> oldData = nodeToMove.getData();
         if (oldData != null && !oldData.isEmpty()) newNode.putAll(oldData);
         for (Object child : nodeToMove.getChildrenNames()) {
            // move kids
            if (trace) log.tracef("Moving child %s", child);
            Fqn oldChildFqn = Fqn.fromRelativeElements(nodeToMoveFqn, child);
            move(cache, oldChildFqn, newFqn);
         }
         removeNode(cache, nodeToMoveFqn);
         success = true;
      } finally {
         if (success) {
            endAtomic();
         } else {
            failAtomic();
         }
      }
      log.tracef("Successfully moved node '%s' to '%s'", nodeToMoveFqn, newParentFqn);
   }

   @Override
   public void move(String nodeToMove, String newParent) throws NodeNotExistsException {
      move(cache, Fqn.fromString(nodeToMove), Fqn.fromString(newParent));
   }

   @Override
   public void move(String nodeToMove, String newParent, Flag... flags) throws NodeNotExistsException {
      move(cache.withFlags(flags), Fqn.fromString(nodeToMove), Fqn.fromString(newParent));
   }

   @Override
   public Map<K, V> getData(Fqn fqn) {
      return getData(cache, fqn);
   }

   @Override
   public Map<K, V> getData(Fqn fqn, Flag... flags) {
      return getData(cache.withFlags(flags), fqn);
   }

   private Map<K, V> getData(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn fqn) {
      startAtomic();
      try {
         Node<K, V> node = getNode(cache, fqn);
         if (node == null)
            return null;
         else
            return node.getData();
      } finally {
         endAtomic();
      }
   }

   @Override
   public Set<K> getKeys(String fqn) {
      return getKeys(cache, Fqn.fromString(fqn));
   }

   @Override
   public Set<K> getKeys(String fqn, Flag... flags) {
      return getKeys(cache.withFlags(flags), Fqn.fromString(fqn));
   }

   @Override
   public Set<K> getKeys(Fqn fqn) {
      return getKeys(cache, fqn);
   }

   @Override
   public Set<K> getKeys(Fqn fqn, Flag... flags) {
      return getKeys(cache.withFlags(flags), fqn);
   }

   private Set<K> getKeys(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn fqn) {
      startAtomic();
      try {
         Node<K, V> node = getNode(cache, fqn);
         if (node == null)
            return null;
         else
            return node.getKeys();
      } finally {
         endAtomic();
      }
   }

   @Override
   public void clearData(String fqn) {
      clearData(cache, Fqn.fromString(fqn));
   }

   @Override
   public void clearData(String fqn, Flag... flags) {
      clearData(cache.withFlags(flags), Fqn.fromString(fqn));
   }

   @Override
   public void clearData(Fqn fqn) {
      clearData(cache, fqn);
   }

   @Override
   public void clearData(Fqn fqn, Flag... flags) {
      clearData(cache.withFlags(flags), fqn);
   }

   public void clearData(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn fqn) {
      startAtomic();
      try {
         Node<K, V> node = getNode(cache, fqn);
         if (node != null)
            node.clearData();
      } finally {
         endAtomic();
      }
   }

   @Override
   public V put(Fqn fqn, K key, V value) {
      return put(cache, fqn, key, value);
   }

   @Override
   public V put(Fqn fqn, K key, V value, Flag... flags) {
      return put(cache.withFlags(flags), fqn, key, value);
   }

   private V put(AdvancedCache<NodeKey, AtomicMap<?, ?>> cache, Fqn fqn, K key, V value) {
      startAtomic();
      try {
         createNodeInCache(cache, fqn);
         Map<K, V> m = getAtomicMap(cache, new NodeKey(fqn, NodeKey.Type.DATA));
         return m.put(key, value);
      } finally {
         endAtomic();
      }
   }

   @Override
   public Cache<?, ?> getCache() {
      // Retrieve the advanced cache as a way to retrieve
      // the cache behind the cache adapter.
      return cache.getAdvancedCache();
   }

   // ------------------ nothing different; just delegate to the cache
   @Override
   public void start() throws CacheException {
      cache.start();
      createRoot();
   }

   @Override
   public void stop() {
      cache.stop();
   }

   private void createRoot() {
      if (!exists(Fqn.ROOT)) createNodeInCache(Fqn.ROOT);
   }

   public String toString() {
      return cache.toString();
   }
}
