package org.jboss.cache;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.buddyreplication.BuddyFqnTransformer;
import org.jboss.cache.config.Configuration;
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.factories.annotations.Stop;
import org.jboss.cache.invocation.NodeInvocationDelegate;
import org.jboss.cache.lock.LockManager;
import org.jboss.cache.marshall.NodeData;
import org.jboss.cache.optimistic.DataVersion;
import org.jboss.cache.transaction.GlobalTransaction;

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

/**
 * A container for the root node in the cache, which also provides helpers for efficiently accessing nodes, walking trees, etc.
 *
 * @author Mircea.Markus@jboss.com
 * @since 2.2
 */
@NonVolatile
public class DataContainerImpl implements DataContainer
{
   private static final Log log = LogFactory.getLog(DataContainerImpl.class);
   private static boolean trace = log.isTraceEnabled();

   private Configuration configuration;

   /**
    * Root node.
    */
   private NodeSPI root;

   /**
    * Set<Fqn> of Fqns of the topmost node of internal regions that should
    * not included in standard state transfers.
    */
   private final Set<Fqn> internalFqns = new HashSet<Fqn>();
   private NodeFactory nodeFactory;
   private LockManager lockManager;
   private BuddyFqnTransformer buddyFqnTransformer;

   @Inject
   public void injectDependencies(Configuration configuration, NodeFactory nodeFactory, LockManager lockManager, BuddyFqnTransformer transformer)
   {
      setDependencies(configuration, nodeFactory, lockManager);

      // We need to create a root node even at this stage since certain components rely on this being available before
      // start() is called.
      // TODO: Investigate which components rely on this being available before start(), and why!
      //TODO - remove setDependencies method at this point
      createRootNode();
      this.buddyFqnTransformer = transformer;
   }

   public void setDependencies(Configuration configuration, NodeFactory nodeFactory, LockManager lockManager)
   {
      this.configuration = configuration;
      this.nodeFactory = nodeFactory;
      this.lockManager = lockManager;
   }

   @Start(priority = 12)
   public void createRootNode()
   {
      if (trace) log.trace("Starting data container");
      // create a new root temporarily.
      NodeSPI tempRoot = nodeFactory.createRootDataNode();
      // if we don't already have a root or the new (temp) root is of a different class (optimistic vs pessimistic) to
      // the current root, then we use the new one.

      Class currentRootType = root == null ? null : ((NodeInvocationDelegate) root).getDelegationTarget().getClass();
      Class tempRootType = ((NodeInvocationDelegate) tempRoot).getDelegationTarget().getClass();

      if (!tempRootType.equals(currentRootType))
      {
         if (trace) log.trace("Setting root node to an instance of " + tempRootType);
         setRoot(tempRoot);
      }
      root.setChildrenLoaded(true);
      root.setLockForChildInsertRemove(configuration.isLockParentForChildInsertRemove());
   }

   @Stop(priority = 100)
   public void stop()
   {
      // empty in-memory state
      root.clearDataDirect();
      root.removeChildrenDirect();
   }

   public NodeSPI getRoot()
   {
      return root;
   }

   /**
    * Sets the root node reference to the node passed in.
    *
    * @param root node
    */
   public void setRoot(NodeSPI root)
   {
      if (root == null || !root.getFqn().isRoot())
         throw new CacheException("Attempting to set an invalid node [" + root + "] as a root node!");
      this.root = root;
   }

   public void registerInternalFqn(Fqn fqn)
   {
      internalFqns.add(fqn);
   }

   /**
    * Finds a node given a fully qualified name, directly off the interceptor chain.  In the event of an exception,
    * returns null.  Does not include invalid or deleted nodes.
    *
    * @param fqn Fully qualified name for the corresponding node.
    * @return Node referenced by the given Fqn, or null if the node cannot be found or if there is an exception.
    */
   public NodeSPI peek(Fqn fqn)
   {
      try
      {
         return peekVersioned(fqn, null);
      }
      catch (CacheException e)
      {
         log.warn("Unexpected error", e);
         return null;
      }
   }

   public NodeSPI peekStrict(GlobalTransaction gtx, Fqn fqn, boolean includeInvalid)
   {
      NodeSPI n = peekVersioned(fqn, null, includeInvalid);
      if (n == null)
      {
         StringBuilder builder = new StringBuilder();
         builder.append("Node ").append(fqn).append(" not found");
         String errStr = builder.toString();
         if (trace) log.trace(errStr);
         throw new NodeNotExistsException(errStr);
      }
      return n;
   }

   public NodeSPI peekVersioned(Fqn fqn, DataVersion version)
   {
      return peekVersioned(fqn, version, false);
   }

   public NodeSPI peekVersioned(Fqn fqn, DataVersion version, boolean includeInvalidNodes)
   {
      if (fqn == null) return null;

      NodeSPI toReturn = peek(fqn, false, includeInvalidNodes);

      if (toReturn != null && version != null && configuration.isNodeLockingOptimistic())
      {
         // we need to check the version of the data node...
         DataVersion nodeVersion = toReturn.getVersion();
         if (trace)
         {
            log.trace("looking for optimistic node [" + fqn + "] with version [" + version + "].  My version is [" + nodeVersion + "]");
         }
         if (nodeVersion.newerThan(version))
         {
            // we have a versioning problem; throw an exception!
            throw new CacheException("Unable to validate versions.");
         }
      }
      return toReturn;
   }

   public NodeSPI peek(Fqn<?> fqn, boolean includeDeletedNodes)
   {
      return peek(fqn, includeDeletedNodes, false);
   }

   public NodeSPI peek(Fqn<?> fqn, boolean includeDeletedNodes, boolean includeInvalidNodes)
   {
      if (fqn == null || fqn.size() == 0) return getRoot();
      NodeSPI n = getRoot();
      int fqnSize = fqn.size();
      for (int i = 0; i < fqnSize; i++)
      {
         Object obj = fqn.get(i);
         n = n.getChildDirect(obj);
         if (n == null)
         {
            return null;
         }
         else if (!includeDeletedNodes && n.isDeleted())
         {
            return null;
         }
         else if (!includeInvalidNodes && !n.isValid())
         {
            return null;
         }
      }
      return n;
   }

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

   public boolean hasChildren(Fqn fqn)
   {
      if (fqn == null) return false;

      NodeSPI n = peek(fqn);
      return n != null && n.hasChildrenDirect();
   }

   public List<NodeData> buildNodeData(List<NodeData> list, NodeSPI node)
   {
      NodeData data = new NodeData(buddyFqnTransformer.getActualFqn(node.getFqn()), node.getDataDirect());
      list.add(data);
      for (Object childNode : node.getChildrenDirect())
      {
         buildNodeData(list, (NodeSPI) childNode);
      }
      return list;
   }

   public List<Fqn> getNodesForEviction(Fqn fqn, boolean recursive)
   {
      List<Fqn> result = new ArrayList<Fqn>();
      NodeSPI node = peek(fqn, false);
      if (recursive)
      {
         if (node != null) recursiveAddEvictionNodes(node, result);
      }
      else
      {
         if (node == null)
         {
            result.add(fqn);
            return result;
         }
         if (fqn.isRoot())
         {
            for (Object childName : node.getChildrenNamesDirect())
            {
               if (!node.isResident()) result.add(Fqn.fromRelativeElements(fqn, childName));
            }
         }
         else if (!node.isResident())
         {
            result.add(fqn);
         }
      }
      return result;
   }

   private void recursiveAddEvictionNodes(NodeSPI<?, ?> node, List<Fqn> result)
   {
      for (NodeSPI<?, ?> child : node.getChildrenDirect())
      {
         recursiveAddEvictionNodes(child, result);
      }
      Fqn fqn = node.getFqn();
      if (!fqn.isRoot() && !node.isResident())
      {
         result.add(fqn);
      }
   }

   @Override
   public String toString()
   {
      return toString(false);
   }

   public Set<Fqn> getInternalFqns()
   {
      return Collections.unmodifiableSet(internalFqns);
   }

   /**
    * Returns a debug string with optional details of contents.
    *
    * @param details if true, details are printed
    * @return detailed contents of the container
    */
   @SuppressWarnings("deprecation")
   public String toString(boolean details)
   {
      StringBuilder sb = new StringBuilder();
      int indent = 0;

      if (!details)
      {
         sb.append(getClass().getName()).append(" [").append(getNumberOfNodes()).append(" nodes, ");
         sb.append(getNumberOfLocksHeld()).append(" locks]");
      }
      else
      {
         if (root == null)
            return sb.toString();
         for (Object n : root.getChildrenDirect())
         {
            ((NodeSPI) n).print(sb, indent);
            sb.append("\n");
         }
      }
      return sb.toString();
   }

   public int getNumberOfLocksHeld()
   {
      return numLocks(root);
   }

   private int numLocks(NodeSPI n)
   {
      int num = 0;
      if (n != null)
      {
         if (lockManager.isLocked(n))
         {
            num++;
         }
         for (Object cn : n.getChildrenDirect(true))
         {
            num += numLocks((NodeSPI) cn);
         }
      }
      return num;
   }

   public int getNumberOfNodes()
   {
      return numNodes(root) - 1;
   }

   private int numNodes(NodeSPI n)
   {
      int count = 1;// for n
      if (n != null)
      {
         for (Object child : n.getChildrenDirect())
         {
            count += numNodes((NodeSPI) child);
         }
      }
      return count;
   }

   /**
    * Prints information about the contents of the nodes in the cache's current
    * in-memory state.  Does not load any previously evicted nodes from a
    * cache loader, so evicted nodes will not be included.
    *
    * @return details
    */
   public String printDetails()
   {
      StringBuilder sb = new StringBuilder();
      root.printDetails(sb, 0);
      sb.append("\n");
      return sb.toString();
   }


   /**
    * Returns lock information.
    *
    * @return lock info
    */
   public String printLockInfo()
   {
      return lockManager.printLockInfo(root);
   }

   public int getNumberOfAttributes(Fqn fqn)
   {
      return numAttributes(peek(fqn));
   }

   private int numAttributes(NodeSPI n)
   {
      int count = 0;
      for (Object child : n.getChildrenDirect())
      {
         count += numAttributes((NodeSPI) child);
      }
      count += n.getDataDirect().size();
      return count;
   }

   /**
    * Returns an <em>approximation</em> of the total number of attributes in
    * the cache. Since this method doesn't acquire any locks, the number might
    * be incorrect, or the method might even throw a
    * ConcurrentModificationException
    *
    * @return number of attribs
    */
   public int getNumberOfAttributes()
   {
      return numAttributes(root);
   }

   public boolean removeFromDataStructure(Fqn f, boolean skipMarkerCheck)
   {
      NodeSPI n = peek(f, true);
      if (n == null)
      {
         return false;
      }

      if (trace) log.trace("Performing a real remove for node " + f + ", marked for removal.");
      if (skipMarkerCheck || n.isDeleted())
      {
         if (n.getFqn().isRoot())
         {
            // do not actually delete; just remove deletion marker
            n.markAsDeleted(true);

            // mark the node to be removed (and all children) as invalid so anyone holding a direct reference to it will
            // be aware that it is no longer valid.
            n.setValid(false, true);
            n.setValid(true, false);

            // but now remove all children, since the call has been to remove("/")
            n.removeChildrenDirect();
            return true;
         }
         else
         {
            // mark the node to be removed (and all children) as invalid so anyone holding a direct reference to it will
            // be aware that it is no longer valid.
            n.setValid(false, true);
            return n.getParent().removeChildDirect(n.getFqn().getLastElement());
         }
      }
      else
      {
         if (log.isDebugEnabled()) log.debug("Node " + f + " NOT marked for removal as expected, not removing!");
         return false;
      }
   }

   public void evict(Fqn fqn, boolean recursive)
   {
      List<Fqn> toEvict = getNodesForEviction(fqn, recursive);
      for (Fqn aFqn : toEvict)
      {
         evict(aFqn);
      }
   }

   public boolean evict(Fqn fqn)
   {
      if (peek(fqn, false, true) == null) return true;
      if (hasChildren(fqn))
      {
         if (trace)
            log.trace("removing DATA as node has children: evict(" + fqn + ")");
         removeData(fqn);
         return false;
      }
      else
      {
         if (trace) log.trace("removing NODE as it is a leaf: evict(" + fqn + ")");
         removeNode(fqn);
         return true;
      }
   }

   private void removeNode(Fqn fqn)
   {
      NodeSPI targetNode = peekVersioned(fqn, null, true);
      if (targetNode == null) return;
      NodeSPI parentNode = targetNode.getParent();
      targetNode.setValid(false, false);
      if (parentNode != null)
      {
         parentNode.removeChildDirect(fqn.getLastElement());
         parentNode.setChildrenLoaded(false);
      }
   }

   protected void removeData(Fqn fqn)
   {
      NodeSPI n = peekVersioned(fqn, null);
      if (n == null)
      {
         log.warn("node " + fqn + " not found");
         return;
      }
      n.clearDataDirect();
      n.setDataLoaded(false);
   }

   public Object[] createNodes(Fqn fqn)
   {
      List<NodeSPI> result = new ArrayList<NodeSPI>(fqn.size());
      Fqn tmpFqn = Fqn.ROOT;

      int size = fqn.size();

      // root node
      NodeSPI n = root;
      for (int i = 0; i < size; i++)
      {
         Object childName = fqn.get(i);
         tmpFqn = Fqn.fromRelativeElements(tmpFqn, childName);

         NodeSPI childNode;
         Map children = n.getChildrenMapDirect();
         childNode = children == null ? null : (NodeSPI) children.get(childName);

         if (childNode == null)
         {
            childNode = n.addChildDirect(Fqn.fromElements(childName));
            result.add(childNode);
         }

         n = childNode;
      }
      return new Object[]{result, n};
   }

   public void setBuddyFqnTransformer(BuddyFqnTransformer buddyFqnTransformer)
   {
      this.buddyFqnTransformer = buddyFqnTransformer;
   }
}
