/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2000 - 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache.invocation;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.*;
import org.jboss.cache.batch.BatchContainer;
import org.jboss.cache.buddyreplication.BuddyManager;
import org.jboss.cache.buddyreplication.GravitateResult;
import org.jboss.cache.commands.CommandsFactory;
import org.jboss.cache.commands.read.ExistsCommand;
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.Configuration.NodeLockingScheme;
import org.jboss.cache.config.ConfigurationException;
import org.jboss.cache.config.Option;
import org.jboss.cache.factories.ComponentRegistry;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.factories.annotations.NonVolatile;
import org.jboss.cache.factories.annotations.Start;
import org.jboss.cache.interceptors.base.CommandInterceptor;
import org.jboss.cache.loader.CacheLoaderManager;
import org.jboss.cache.marshall.Marshaller;
import org.jboss.cache.mvcc.MVCCNodeHelper;
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.jboss.cache.util.Immutables;
import org.jgroups.Address;

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

/**
 * 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 AT jboss DOT org">manik AT jboss DOT org</a>)
 * @since 2.1.0
 */
@NonVolatile
public class CacheInvocationDelegate<K, V> extends AbstractInvocationDelegate implements CacheSPI<K, V>
{
   private static final Log log = LogFactory.getLog(CacheInvocationDelegate.class);

   // 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;
   private MVCCNodeHelper mvccHelper;
   private boolean usingMvcc;
   private BatchContainer batchContainer;

   @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, MVCCNodeHelper mvccHelper, BatchContainer batchContainer)
   {
      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;
      this.mvccHelper = mvccHelper;
      this.batchContainer = batchContainer;
   }

   @Start
   void setNodeLockingScheme()
   {
      usingMvcc = configuration.getNodeLockingScheme() == NodeLockingScheme.MVCC;
   }

   private void reset()
   {
      this.usingMvcc = false;
      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 getNode(Fqn.ROOT);
   }

   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.addAfterInterceptor(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)
   {
      if (usingMvcc)
      {
         InvocationContext ctx = invocationContextContainer.get();
         cacheStatusCheck(ctx);
         ExistsCommand command = commandsFactory.buildExistsNodeCommand(fqn);
         return (Boolean) invoker.invoke(ctx, command);
      }
      else
      {
         return peek(fqn, 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);
   }

   @SuppressWarnings("unchecked")
   public NodeSPI<K, V> peek(Fqn fqn, boolean includeDeletedNodes, boolean includeInvalidNodes)
   {
      // TODO: clean this up somehow!  Anyway, this method should NOT be used outside of testing frameworks.
      return (usingMvcc)
            ? mvccPeek(fqn)
            : (NodeSPI<K, V>) dataContainer.peek(fqn, includeDeletedNodes, includeInvalidNodes);
   }

   @SuppressWarnings("unchecked")
   public NodeSPI<K, V> peek(Fqn fqn, boolean includeDeletedNodes)
   {
      // TODO: clean this up somehow!  Anyway, this method should NOT be used outside of testing frameworks.
      return (usingMvcc)
            ? mvccPeek(fqn)
            : (NodeSPI<K, V>) dataContainer.peek(fqn, includeDeletedNodes);
   }

   @SuppressWarnings("unchecked")
   private NodeSPI<K, V> mvccPeek(Fqn f)
   {
      NodeSPI<K, V> n;
      try
      {
         n = mvccHelper.wrapNodeForReading(getInvocationContext(), f, false);
      }
      catch (InterruptedException e)
      {
         throw new CacheException(e);
      }
      if (n == null || n.isNullNode()) return null;
      return n;
   }

   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);
   }

   @SuppressWarnings("unchecked")
   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().copy();
         Set<Fqn> internalFqns = getInternalFqns();
         for (Object childName : peek(fqn, false, false).getChildrenNames())
         {
            if (!internalFqns.contains(Fqn.fromElements(childName)))
            {
               ctx.setOptionOverrides(o.copy());
               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));
   }

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

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

   @SuppressWarnings("unchecked")
   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<? extends K, ? extends V> data)
   {
      invokePut(fqn, data, false);
   }

   public void put(String fqn, Map<? extends K, ? extends 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.");
         }
      }
   }

   @SuppressWarnings("unchecked")
   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);
   }


   @SuppressWarnings("unchecked")
   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);
   }

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

   @SuppressWarnings("unchecked")
   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));
   }

   public void startBatch()
   {
      if (!configuration.isInvocationBatchingEnabled())
      {
         throw new ConfigurationException("Invocation batching not enabled in current configuration!  Please use the <invocationBatching /> element.");
      }
      batchContainer.startBatch();
   }

   public void endBatch(boolean successful)
   {
      if (!configuration.isInvocationBatchingEnabled())
      {
         throw new ConfigurationException("Invocation batching not enabled in current configuration!  Please use the <invocationBatching /> element.");
      }
      batchContainer.endBatch(successful);
   }

   public boolean isLeaf(String fqn) throws NodeNotExistsException
   {
      return isLeaf(Fqn.fromString(fqn));
   }

   public boolean isLeaf(Fqn fqn) throws NodeNotExistsException
   {
      Set<Object> names = getChildrenNamesInternal(fqn);
      if (names == null) throw new NodeNotExistsException("Node " + fqn + " does not exist!");
      return names.isEmpty();
   }


   public Set<Object> getChildrenNames(Fqn fqn)
   {
      Set<Object> names = getChildrenNamesInternal(fqn);
      return names == null ? Collections.emptySet() : names;
   }

   /**
    * Will return a null if the node doesnt exist!
    * @param fqn to check
    * @return set or null
    */
   @SuppressWarnings("unchecked")
   private Set<Object> getChildrenNamesInternal(Fqn fqn)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      GetChildrenNamesCommand command = commandsFactory.buildGetChildrenNamesCommand(fqn);
      Set<Object> retval = (Set<Object>) invoker.invoke(ctx, command);

      // this is needed to work around JBCACHE-1480
      if (retval != null && !retval.isEmpty())
      {
         for (Iterator i = retval.iterator(); i.hasNext();)
         {
            Object child = getNode(Fqn.fromRelativeElements(fqn, i.next()));
            if (child == null) i.remove();
         }
      }

      if (retval != null)
      {
         retval = Immutables.immutableSetWrap(retval); // this is already copied in the command
      }

      return retval;
   }

   @SuppressWarnings("unchecked")
   public Set<String> getChildrenNames(String fqn)
   {
      return (Set) getChildrenNames(Fqn.fromString(fqn));
   }

   public ComponentRegistry getComponentRegistry()
   {
      return componentRegistry;
   }

   public DataContainer getDataContainer()
   {
      return dataContainer;
   }

   protected void cacheStatusCheck(InvocationContext ctx)
   {
      assertIsConstructed();
      if (!ctx.getOptionOverrides().isSkipCacheStatusCheck() && !componentRegistry.invocationsAllowed(true)) // this should always be true since we should never wait for the cache to start at this stage
      {
         throw new CacheNotReadyException("Cache not in a valid STARTED state!  Cache state is " + componentRegistry.getState());
      }
   }

   private void invokePut(Fqn fqn, Map<? extends K, ? extends V> data, boolean erase)
   {
      InvocationContext ctx = invocationContextContainer.get();
      cacheStatusCheck(ctx);
      PutDataMapCommand command = commandsFactory.buildPutDataMapCommand(null, fqn, data);
      command.setErase(erase);
      invoker.invoke(ctx, command);
   }
}
