package org.jboss.cache.invocation;

import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.CacheStatus;
import org.jboss.cache.DataContainer;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.NodeNotExistsException;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.RPCManager;
import org.jboss.cache.Region;
import org.jboss.cache.RegionManager;
import org.jboss.cache.Version;
import org.jboss.cache.buddyreplication.BuddyManager;
import org.jboss.cache.buddyreplication.GravitateResult;
import org.jboss.cache.commands.read.GetChildrenNamesCommand;
import org.jboss.cache.commands.read.GetDataMapCommand;
import org.jboss.cache.commands.read.GetKeyValueCommand;
import org.jboss.cache.commands.read.GetKeysCommand;
import org.jboss.cache.commands.read.GetNodeCommand;
import org.jboss.cache.commands.read.GravitateDataCommand;
import org.jboss.cache.commands.write.EvictCommand;
import org.jboss.cache.commands.write.MoveCommand;
import org.jboss.cache.commands.write.PutDataMapCommand;
import org.jboss.cache.commands.write.PutForExternalReadCommand;
import org.jboss.cache.commands.write.PutKeyValueCommand;
import org.jboss.cache.commands.write.RemoveKeyCommand;
import org.jboss.cache.commands.write.RemoveNodeCommand;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.Option;
import org.jboss.cache.factories.CommandsFactory;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.factories.annotations.NonVolatile;
import org.jboss.cache.interceptors.base.CommandInterceptor;
import org.jboss.cache.loader.CacheLoaderManager;
import org.jboss.cache.marshall.Marshaller;
import org.jboss.cache.notifications.Notifier;
import org.jboss.cache.statetransfer.StateTransferManager;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jboss.cache.transaction.TransactionTable;
import org.jgroups.Address;

import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * The delegate that users (and ChainedInterceptor authors) interact with when they create a cache by using a cache factory.
 * 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")
@NonVolatile
public class CacheInvocationDelegate<K, V> extends AbstractInvocationDelegate implements CacheSPI<K, V>
{
   // this stuff is needed since the SPI has methods to retrieve these.
   private StateTransferManager stateTransferManager;
   private CacheLoaderManager cacheLoaderManager;
   private Notifier notifier;
   private TransactionManager transactionManager;
   private BuddyManager buddyManager;
   private TransactionTable transactionTable;
   private RPCManager rpcManager;
   private RegionManager regionManager;
   private Marshaller marshaller;
   private DataContainer dataContainer;
   private CommandsFactory commandsFactory;

   @Inject
   public void initialize(StateTransferManager stateTransferManager, CacheLoaderManager cacheLoaderManager, Notifier notifier,
                          TransactionManager transactionManager, BuddyManager buddyManager, TransactionTable transactionTable,
                          RPCManager rpcManager, RegionManager regionManager, Marshaller marshaller,
                          CommandsFactory commandsFactory, DataContainer dataContainer)
   {
      this.stateTransferManager = stateTransferManager;
      this.cacheLoaderManager = cacheLoaderManager;
      this.notifier = notifier;
      this.transactionManager = transactionManager;
      this.buddyManager = buddyManager;
      this.transactionTable = transactionTable;
      this.rpcManager = rpcManager;
      this.regionManager = regionManager;
      this.marshaller = marshaller;
      this.dataContainer = dataContainer;
      this.commandsFactory = commandsFactory;
   }

   private void reset()
   {
      this.stateTransferManager = null;
      this.cacheLoaderManager = null;
      this.transactionManager = null;
      this.buddyManager = null;
      this.transactionTable = null;
      this.rpcManager = null;
      this.marshaller = null;
      this.dataContainer = null;
      this.commandsFactory = null;
   }

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

   public Configuration getConfiguration()
   {
      return configuration;
   }

   public NodeSPI<K, V> getRoot()
   {
      return (NodeSPI<K, V>) dataContainer.getRoot();
   }

   public TransactionManager getTransactionManager()
   {
      return transactionManager;
   }

   public void addInterceptor(CommandInterceptor i, int position)
   {
      invoker.addInterceptor(i, position);
   }

   public void addInterceptor(CommandInterceptor i, Class<? extends CommandInterceptor> afterInterceptor)
   {
      invoker.addInterceptor(i, afterInterceptor);
   }

   public List<CommandInterceptor> getInterceptorChain()
   {
      return invoker.asList();
   }

   public void removeInterceptor(int position)
   {
      invoker.removeInterceptor(position);
   }

   public void removeInterceptor(Class<? extends CommandInterceptor> interceptorType)
   {
      invoker.removeInterceptor(interceptorType);
   }

   public CacheLoaderManager getCacheLoaderManager()
   {
      return cacheLoaderManager;
   }

   public BuddyManager getBuddyManager()
   {
      return buddyManager;
   }

   public TransactionTable getTransactionTable()
   {
      return transactionTable;
   }

   public RPCManager getRPCManager()
   {
      return rpcManager;
   }

   public StateTransferManager getStateTransferManager()
   {
      return stateTransferManager;
   }

   public String getClusterName()
   {
      return configuration.getClusterName();
   }

   public int getNumberOfAttributes()
   {
      return dataContainer.getNumberOfAttributes();
   }

   public int getNumberOfNodes()
   {
      return dataContainer.getNumberOfNodes();
   }

   public RegionManager getRegionManager()
   {
      return regionManager;
   }

   public GlobalTransaction getCurrentTransaction(Transaction tx, boolean createIfNotExists)
   {
      return transactionTable.getCurrentTransaction(tx, createIfNotExists);
   }

   public GlobalTransaction getCurrentTransaction()
   {
      return transactionTable.getCurrentTransaction();
   }

   public Set<Fqn> getInternalFqns()
   {
      return dataContainer.getInternalFqns();
   }

   public int getNumberOfLocksHeld()
   {
      return dataContainer.getNumberOfLocksHeld();
   }

   public boolean exists(String fqn)
   {
      return exists(Fqn.fromString(fqn));
   }

   public boolean exists(Fqn<?> fqn)
   {
      return peek(fqn, false, false) != null;
   }

   public Notifier getNotifier()
   {
      return notifier;
   }

   public Marshaller getMarshaller()
   {
      return marshaller;
   }

   public GravitateResult gravitateData(Fqn fqn, boolean searchBuddyBackupSubtrees, InvocationContext ctx)
   {
      cacheStatusCheck(ctx);
      GravitateDataCommand command = commandsFactory.buildGravitateDataCommand(fqn, searchBuddyBackupSubtrees);
      return (GravitateResult) invoker.invoke(ctx, command);
//      return (GravitateResult) command.perform(null);
   }

   public NodeSPI<K, V> peek(Fqn fqn, boolean includeDeletedNodes, boolean includeInvalidNodes)
   {
      return (NodeSPI<K, V>) dataContainer.peek(fqn, includeDeletedNodes, includeInvalidNodes);
   }

   public NodeSPI<K, V> peek(Fqn fqn, boolean includeDeletedNodes)
   {
      return (NodeSPI<K, V>) dataContainer.peek(fqn, includeDeletedNodes);
   }

   public void addCacheListener(Object listener)
   {
      notifier.addCacheListener(listener);
   }

   public void removeCacheListener(Object listener)
   {
      notifier.removeCacheListener(listener);
   }

   public Set<Object> getCacheListeners()
   {
      return notifier.getCacheListeners();
   }

   public void create() throws CacheException
   {
      componentRegistry.create();
   }

   public void start() throws CacheException
   {
      componentRegistry.start();
   }

   public void stop()
   {
      componentRegistry.stop();
   }

   public void destroy()
   {
      reset();
      componentRegistry.destroy();
   }

   public CacheStatus getCacheStatus()
   {
      return componentRegistry.getState();
   }

   public InvocationContext getInvocationContext()
   {
      assertIsConstructed();
      return invocationContextContainer.get();
   }

   public void setInvocationContext(InvocationContext ctx)
   {
      assertIsConstructed();
      // assume a null ctx is meant to "un-set" the context?
      if (ctx == null) invocationContextContainer.remove();
      else invocationContextContainer.set(ctx);
   }

   public Address getLocalAddress()
   {
      if (rpcManager == null) return null;
      return rpcManager.getLocalAddress();
   }

   public List<Address> getMembers()
   {
      if (rpcManager == null) return null;
      return rpcManager.getMembers();
   }

   public String getVersion()
   {
      return Version.printVersion();
   }

   public void move(Fqn<?> nodeToMove, Fqn<?> newParent) throws NodeNotExistsException
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      MoveCommand command = commandsFactory.buildMoveCommand(nodeToMove, newParent);
      invoker.invoke(ctx, command);
   }

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

   public boolean removeRegion(Fqn fqn)
   {
      return regionManager.removeRegion(fqn);
   }

   public Region getRegion(Fqn fqn, boolean createIfAbsent)
   {
      return regionManager.getRegion(fqn, createIfAbsent);
   }

   public void evict(Fqn<?> fqn, boolean recursive)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      EvictCommand c = commandsFactory.buildEvictFqnCommand(fqn);
      c.setRecursive(recursive);
      invoker.invoke(ctx, c);
   }

   public void evict(Fqn<?> fqn)
   {
      evict(fqn, false);
   }

   public V get(Fqn<?> fqn, K key)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      GetKeyValueCommand command = commandsFactory.buildGetKeyValueCommand(fqn, key, true);
      return (V) invoker.invoke(ctx, command);
   }

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

   public boolean removeNode(Fqn<?> fqn)
   {
      // special case if we are removing the root.  Remove all children instead.
      if (fqn.isRoot())
      {
         boolean result = true;
         // we need to preserve options
         InvocationContext ctx = getInvocationContext();
         Option o = ctx.getOptionOverrides();
         Set<Fqn> internalFqns = getInternalFqns();
         for (Object childName : peek(fqn, false, false).getChildrenNames())
         {
            if (!internalFqns.contains(Fqn.fromElements(childName)))
            {
               ctx.setOptionOverrides(o);
               result = removeNode(Fqn.fromRelativeElements(fqn, childName)) && result;
            }
         }
         return result;
      }
      else
      {
         InvocationContext ctx = invocationContextContainer.get();
         cacheStatusCheck(ctx);
         GlobalTransaction tx = transactionTable.getCurrentTransaction();
         RemoveNodeCommand command = commandsFactory.buildRemoveNodeCommand(tx, fqn);
         Object retval = invoker.invoke(ctx, command);
         return retval != null && (Boolean) retval;
      }
   }

   public boolean removeNode(String fqn)
   {
      return removeNode(Fqn.fromString(fqn));
   }

   public NodeSPI<K, V> getNode(Fqn<?> fqn)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      GetNodeCommand command = commandsFactory.buildGetNodeCommand(fqn);
      return (NodeSPI) invoker.invoke(ctx, command);
   }

   public NodeSPI<K, V> getNode(String fqn)
   {
      return getNode(Fqn.fromString(fqn));
   }

   public V remove(Fqn<?> fqn, K key) throws CacheException
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      GlobalTransaction tx = transactionTable.getCurrentTransaction();
      RemoveKeyCommand command = commandsFactory.buildRemoveKeyCommand(tx, fqn, key);
      return (V) invoker.invoke(ctx, command);
   }

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

   public void put(Fqn<?> fqn, Map<K, V> data)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      PutDataMapCommand command = commandsFactory.buildPutDataMapCommand(null, fqn, data);
      invoker.invoke(ctx, command);
   }

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

   public void putForExternalRead(Fqn<?> fqn, K key, V value)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      // if the node exists then this should be a no-op.
      if (peek(fqn, false, false) == null)
      {
         getInvocationContext().getOptionOverrides().setFailSilently(true);
         getInvocationContext().getOptionOverrides().setForceAsynchronous(true);
         PutForExternalReadCommand command = commandsFactory.buildPutForExternalReadCommand(null, fqn, key, value);
         invoker.invoke(ctx, command);
      }
      else
      {
         if (log.isDebugEnabled())
            log.debug("putForExternalRead() called with Fqn " + fqn + " and this node already exists.  This method is hence a no op.");
      }
   }

   public V put(Fqn<?> fqn, K key, V value)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      GlobalTransaction tx = transactionTable.getCurrentTransaction();
      PutKeyValueCommand command = commandsFactory.buildPutKeyValueCommand(tx, fqn, key, value);
      return (V) invoker.invoke(ctx, command);
   }

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

   /**
    * Retrieves a defensively copied data map of the underlying node.
    *
    * @param fqn
    * @return map of data, or an empty map
    * @throws CacheException
    */
   public Map<K, V> getData(Fqn<?> fqn)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      GetDataMapCommand command = commandsFactory.buildGetDataMapCommand(fqn);
      return (Map<K, V>) invoker.invoke(ctx, command);
   }

   /**
    * Returns a set of attribute keys for the Fqn.
    * Returns null if the node is not found, otherwise a Set.
    * The set is a copy of the actual keys for this node.
    *
    * @param fqn name of the node
    */
   public Set getKeys(String fqn)
   {
      return getKeys(Fqn.fromString(fqn));
   }

   /**
    * Returns a set of attribute keys for the Fqn.
    * Returns null if the node is not found, otherwise a Set.
    * The set is a copy of the actual keys for this node.
    *
    * @param fqn name of the node
    */
   public Set<K> getKeys(Fqn<?> fqn)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      GetKeysCommand command = commandsFactory.buildGetKeysCommand(fqn);
      return (Set<K>) invoker.invoke(ctx, command);
   }

   /**
    * Removes the keys and properties from a node.
    */
   public void clearData(String fqn) throws CacheException
   {
      clearData(Fqn.fromString(fqn));
   }

   /**
    * Removes the keys and properties from a named node.
    */
   public void clearData(Fqn fqn)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      GlobalTransaction tx = getCurrentTransaction();
      invoker.invoke(ctx, commandsFactory.buildClearDataCommand(tx, fqn));
   }

   /**
    * Returns all children of a given node.  Returns an empty set if there are no children.
    * The set is unmodifiable.
    *
    * @param fqn The fully qualified name of the node
    * @return Set an unmodifiable set of children names, Object.
    */
   public <E> Set<E> getChildrenNames(Fqn<E> fqn)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      GetChildrenNamesCommand command = commandsFactory.buildGetChildrenNamesCommand(fqn);
      Set<E> retval = (Set<E>) invoker.invoke(ctx, command);
      if (retval != null)
         retval = Collections.unmodifiableSet(new HashSet<E>(retval));
      else
         retval = Collections.emptySet();
      return retval;
   }

   public Set getChildrenNames(String fqn)
   {
      return getChildrenNames(Fqn.fromString(fqn));
   }

   public DataContainer getDataContainer()
   {
      return dataContainer;
   }

   protected void cacheStatusCheck(InvocationContext ctx)
   {
      assertIsConstructed();
      if (!ctx.getOptionOverrides().isSkipCacheStatusCheck() && !componentRegistry.invocationsAllowed(true))
      {
         throw new IllegalStateException("Cache not in STARTED state!");
      }
   }
}
