package org.jboss.cache.invocation;

import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.NodeNotValidException;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.UnversionedNode;
import org.jboss.cache.config.Option;
import org.jboss.cache.lock.NodeLock;
import org.jboss.cache.optimistic.DataVersion;
import org.jboss.cache.transaction.GlobalTransaction;

import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * The delegate that users (and interceptor authors) interact with when they obtain a node from the cache or another node.
 * This wrapper delegates calls down the interceptor chain.
 *
 * @author Manik Surtani (<a href="mailto:manik@jboss.org">manik@jboss.org</a>)
 * @since 2.1.0
 */
@SuppressWarnings("unchecked")
public class NodeInvocationDelegate<K, V> extends AbstractInvocationDelegate implements NodeSPI<K, V>
{
   private final UnversionedNode node;
   private CacheSPI<K, V> spi;

   public NodeInvocationDelegate(UnversionedNode node)
   {
      this.node = node;
   }

   public Object getDelegationTarget()
   {
      return node;
   }

   public void injectDependencies(CacheSPI spi)
   {
      this.spi = spi;
   }

   public boolean isChildrenLoaded()
   {
      return node.isChildrenLoaded();
   }

   public void setChildrenLoaded(boolean loaded)
   {
      node.setChildrenLoaded(loaded);
   }

   public boolean isDataLoaded()
   {
      return node.isDataLoaded();
   }

   public void setDataLoaded(boolean dataLoaded)
   {
      node.setDataLoaded(dataLoaded);
   }

   public Map<Object, Node<K, V>> getChildrenMapDirect()
   {
      return node.getChildrenMapDirect();
   }

   public void setChildrenMapDirect(Map<Object, Node<K, V>> children)
   {
      node.setChildrenMapDirect(children);
   }

   public NodeSPI<K, V> getOrCreateChild(Object name, GlobalTransaction tx)
   {
      return node.getOrCreateChild(name, tx, true);
   }

   public NodeLock getLock()
   {
      return node.getLock();
   }

   public void setFqn(Fqn<?> f)
   {
      node.setFqn(f);
   }

   public boolean isDeleted()
   {
      return node.isDeleted();
   }

   public void markAsDeleted(boolean marker)
   {
      node.markAsDeleted(marker);
   }

   public void markAsDeleted(boolean marker, boolean recursive)
   {
      node.markAsDeleted(marker, recursive);
   }

   public void addChild(Object nodeName, Node<K, V> nodeToAdd)
   {
      node.addChild(nodeName, nodeToAdd);
   }

   public void printDetails(StringBuilder sb, int indent)
   {
      node.printDetails(sb, indent);
   }

   public void print(StringBuilder sb, int indent)
   {
      throw new CacheException("This method is deprecated!");
   }

   public void setVersion(DataVersion version)
   {
      node.setVersion(version);
   }

   public DataVersion getVersion()
   {
      return node.getVersion();
   }

   public Set<NodeSPI<K, V>> getChildrenDirect()
   {
      return node.getChildrenDirect();
   }

   public void removeChildrenDirect()
   {
      node.removeChildrenDirect();
   }

   public Set<NodeSPI<K, V>> getChildrenDirect(boolean includeMarkedAsDeleted)
   {
      return node.getChildrenDirect(includeMarkedAsDeleted);
   }

   public NodeSPI<K, V> getChildDirect(Object childName)
   {
      return node.getChildDirect(childName);
   }

   public NodeSPI<K, V> addChildDirect(Fqn childName)
   {
      return node.addChildDirect(childName);
   }

   public NodeSPI<K, V> addChildDirect(Fqn f, boolean notify)
   {
      return node.addChildDirect(f, notify);
   }

   public NodeSPI<K, V> addChildDirect(Object childName, boolean notify)
   {
      return node.addChildDirect(childName, notify);
   }

   public void addChildDirect(NodeSPI<K, V> child)
   {
      node.addChildDirect(child);
   }

   public NodeSPI<K, V> getChildDirect(Fqn childName)
   {
      return node.getChildDirect(childName);
   }

   public boolean removeChildDirect(Fqn fqn)
   {
      return node.removeChildDirect(fqn);
   }

   public boolean removeChildDirect(Object childName)
   {
      return node.removeChildDirect(childName);
   }

   public V removeDirect(K key)
   {
      return (V) node.removeDirect(key);
   }

   public V putDirect(K key, V value)
   {
      return (V) node.putDirect(key, value);
   }

   public void putAllDirect(Map<K, V> data)
   {
      node.putAllDirect(data);
   }

   public Map<K, V> getDataDirect()
   {
      return node.getDataDirect();
   }

   public V getDirect(K key)
   {
      return (V) node.getDirect(key);
   }

   public void clearDataDirect()
   {
      node.clearDataDirect();
   }

   public Set<K> getKeysDirect()
   {
      return node.getKeysDirect();
   }

   public Set<Object> getChildrenNamesDirect()
   {
      return node.getChildrenNamesDirect();
   }

   public CacheSPI<K, V> getCache()
   {
      return spi;
   }

   public NodeSPI<K, V> getParent()
   {
      return node.getParent();
   }

   public Set<Node<K, V>> getChildren()
   {
      assertValid();
      if (spi == null) return Collections.emptySet();
      Set<Node<K, V>> children = new HashSet<Node<K, V>>();
      for (Object c : spi.getChildrenNames(getFqn()))
      {
         Node n = spi.getNode(Fqn.fromRelativeElements(getFqn(), c));
         if (n != null) children.add(n);
      }
      return Collections.unmodifiableSet(children);
   }

   public Set<Object> getChildrenNames()
   {
      assertValid();
      return spi.getChildrenNames(getFqn());
   }

   public Map<K, V> getData()
   {
      assertValid();
      if (spi == null) return Collections.emptyMap();
      return spi.getData(getFqn());
   }

   public Set<K> getKeys()
   {
      assertValid();
      Set keys = spi.getKeys(getFqn());
      return keys == null ? Collections.emptySet() : Collections.unmodifiableSet(keys);
   }

   public Fqn getFqn()
   {
      return node.getFqn();
   }

   public Node<K, V> addChild(Fqn<?> f)
   {
      // TODO: Revisit.  Is this really threadsafe?  See comment in putIfAbsent() - same solution should be applied here too.
      assertValid();
      Fqn nf = Fqn.fromRelativeFqn(getFqn(), f);
      Option o1;
      try
      {
         o1 = spi.getInvocationContext().getOptionOverrides().clone();
      }
      catch (CloneNotSupportedException e)
      {
         // should never happen
         throw new RuntimeException(e);
      }
      Node<K, V> child = getChild(f);

      if (child == null)
      {
         Option o2;
         try
         {
            o2 = o1.clone();
         }
         catch (CloneNotSupportedException e)
         {
            // should never happen
            throw new RuntimeException(e);
         }
         spi.getInvocationContext().setOptionOverrides(o1);
         spi.put(nf, null);

         spi.getInvocationContext().setOptionOverrides(o2);
         child = getChild(f);
      }
      return child;
   }

   public boolean removeChild(Fqn<?> f)
   {
      assertValid();
      return spi.removeNode(Fqn.fromRelativeFqn(getFqn(), f));
   }

   public boolean removeChild(Object childName)
   {
      assertValid();
      return spi.removeNode(Fqn.fromRelativeElements(getFqn(), childName));
   }

   public Node<K, V> getChild(Fqn<?> f)
   {
      assertValid();
      return spi.getNode(Fqn.fromRelativeFqn(getFqn(), f));
   }

   public Node<K, V> getChild(Object name)
   {
      assertValid();
      return spi.getNode(Fqn.fromRelativeElements(getFqn(), name));
   }

   public V put(K key, V value)
   {
      assertValid();
      return spi.put(getFqn(), key, value);
   }

   public V putIfAbsent(K k, V v)
   {
      assertValid();
      // TODO: Refactor this!!  Synchronized block here sucks, this could lead to a deadlock since the locking interceptors will not use the same mutex.
      // will only work once we make calls directly on the UnversionedNode in the CallInterceptor rather than multiple calls via the CacheImpl.
      synchronized (this)
      {
         if (!getKeys().contains(k))
            return put(k, v);
         else
            return get(k);
      }
   }

   public V replace(K key, V value)
   {
      assertValid();
      // TODO: Refactor this!!  Synchronized block here sucks, this could lead to a deadlock since the locking interceptors will not use the same mutex.
      // will only work once we make calls directly on the UnversionedNode in the CallInterceptor rather than multiple calls via the CacheImpl.
      synchronized (this)
      {
         if (getKeys().contains(key))
         {
            return put(key, value);
         }
         else
            return null;
      }
   }

   public boolean replace(K key, V oldValue, V newValue)
   {
      assertValid();
      // TODO: Refactor this!!  Synchronized block here sucks, this could lead to a deadlock since the locking interceptors will not use the same mutex.
      // will only work once we make calls directly on the UnversionedNode in the CallInterceptor rather than multiple calls via the CacheImpl.
      synchronized (this)
      {
         if (oldValue.equals(get(key)))
         {
            put(key, newValue);
            return true;
         }
         else
            return false;
      }
   }

   public void putAll(Map<K, V> data)
   {
      assertValid();
      spi.put(getFqn(), data);
   }

   public void replaceAll(Map<K, V> data)
   {
      assertValid();
      spi.clearData(getFqn());
      spi.put(getFqn(), data);
   }

   public V get(K key)
   {
      assertValid();
      return spi.get(getFqn(), key);
   }

   public V remove(K key)
   {
      assertValid();
      return spi.remove(getFqn(), key);
   }

   public void clearData()
   {
      assertValid();
      spi.clearData(getFqn());
   }

   public int dataSize()
   {
      assertValid();
      return spi.getKeys(getFqn()).size();
   }

   public boolean hasChild(Fqn<?> f)
   {
      // TODO: This could be made more efficient when calls are made directly on the node
      assertValid();
      return getChild(f) != null;
   }

   public boolean hasChild(Object o)
   {
      // TODO: This could be made more efficient when calls are made directly on the node
      assertValid();
      return getChild(o) != null;
   }

   public boolean isValid()
   {
      return node.isValid();
   }

   public boolean isResident()
   {
      return node.isResident();
   }

   public void setResident(boolean resident)
   {
      node.setResident(resident);
   }

   public boolean isLockForChildInsertRemove()
   {
      return node.isLockForChildInsertRemove();
   }

   public void setLockForChildInsertRemove(boolean lockForChildInsertRemove)
   {
      node.setLockForChildInsertRemove(lockForChildInsertRemove);
   }

   public void releaseObjectReferences(boolean recursive)
   {
      node.releaseObjectReferences(recursive);
   }

   public boolean hasChildrenDirect()
   {
      return node.hasChildrenDirect();
   }

   public Map getInternalState(boolean onlyInternalState)
   {
      return node.getInternalState(onlyInternalState);
   }

   public void setInternalState(Map state)
   {
      node.setInternalState(state);
   }

   public void setValid(boolean valid, boolean recursive)
   {
      node.setValid(valid, recursive);
   }

   protected void assertValid()
   {
      if (!getFqn().isRoot() && !node.isValid())
         throw new NodeNotValidException("Node " + getFqn() + " is not valid.  Perhaps it has been moved or removed.");
   }

   @Override
   public String toString()
   {
      return node == null ? "null" : node.toString();
   }
}
