/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import static org.jboss.cache.AbstractNode.NodeFlags.*;
import org.jboss.cache.commands.write.CreateNodeCommand;
import org.jboss.cache.factories.CommandsFactory;
import org.jboss.cache.lock.IdentityLock;
import org.jboss.cache.lock.LockStrategyFactory;
import org.jboss.cache.marshall.MarshalledValue;
import org.jboss.cache.optimistic.DataVersion;
import org.jboss.cache.transaction.GlobalTransaction;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Basic data node class.  Throws {@link UnsupportedOperationException} for version-specific methods like {@link #getVersion()} and
 * {@link #setVersion(org.jboss.cache.optimistic.DataVersion)}, defined in {@link org.jboss.cache.NodeSPI}.
 *
 * @author Manik Surtani (<a href="mailto:manik@jboss.org">manik@jboss.org</a>)
 * @since 2.0.0
 */
@SuppressWarnings("unchecked")
public class UnversionedNode<K, V> extends AbstractNode<K, V>
{
   /**
    * Debug log.
    */
   protected static Log log = LogFactory.getLog(UnversionedNode.class);
   protected static final boolean trace = log.isTraceEnabled();

   /**
    * Lock manager that manages locks to be acquired when accessing the node inside a transaction. Lazy set just in case
    * locking is not needed.
    */
   protected transient IdentityLock lock = null;

   /**
    * A reference of the CacheImpl instance.
    */
   private transient CacheSPI cache;

   /**
    * Map of general data keys to values.
    */
   private final Map data = new HashMap();

   protected NodeSPI delegate;
   private CommandsFactory commandsFactory;
   protected LockStrategyFactory lockStrategyFactory;

   /**
    * Constructs a new node with an FQN of Root.
    */
   public UnversionedNode()
   {
      this.fqn = Fqn.ROOT;
      initFlags();
   }

   /**
    * Constructs a new node with a name, etc.
    *
    * @param mapSafe <code>true</code> if param <code>data</code> can safely be directly assigned to this object's
    *                {@link #data} field; <code>false</code> if param <code>data</code>'s contents should be copied into
    *                this object's {@link #data} field.
    */
   protected UnversionedNode(Object child_name, Fqn fqn, Map data, boolean mapSafe, CacheSPI cache)
   {
      if (cache == null)
      {
         throw new IllegalArgumentException("no cache init for " + fqn);
      }
      if (!fqn.isRoot() && !child_name.equals(fqn.getLastElement()))
      {
         throw new IllegalArgumentException("Child " + child_name + " must be last part of " + fqn);
      }

      initFlags();
      this.cache = cache;
      this.fqn = fqn;

      init();
      setInternalState(data);
   }

   /**
    * This method initialises flags on the node, by setting DATA_LOADED to true and VALID to true and all other flags to false.
    * The flags are defined in the {@link NodeFlags} enum.
    */
   protected void initFlags()
   {
      setFlag(DATA_LOADED);
      setFlag(VALID);
   }

   public NodeSPI getDelegate()
   {
      return delegate;
   }

   public void setDelegate(NodeSPI delegate)
   {
      this.delegate = delegate;
   }

   public void injectDependencies(CacheSPI spi, CommandsFactory commandsFactory, LockStrategyFactory lockStrategyFactory)
   {
      this.cache = spi;
      this.commandsFactory = commandsFactory;
      this.lockStrategyFactory = lockStrategyFactory;
      init();
   }

   /**
    * Initializes with a name and FQN and cache.
    */
   private void init()
   {
      if (cache != null && cache.getConfiguration() != null)
         setLockForChildInsertRemove(cache.getConfiguration().isLockParentForChildInsertRemove());
   }

   /**
    * Returns a parent by checking the TreeMap by name.
    */
   public NodeSPI getParent()
   {
      if (fqn.isRoot())
      {
         return null;
      }
      return cache.peek(fqn.getParent(), true);
   }

   protected synchronized void initLock()
   {
      if (lock == null)
      {
         lock = new IdentityLock(lockStrategyFactory, delegate);
      }
   }

   private synchronized Map<Object, Node<K, V>> children()
   {
      if (children == null)
      {
         if (getFqn().isRoot())
         {
            children = new ConcurrentHashMap<Object, Node<K, V>>(64, .5f, 16);
         }
         else
         {
            // Less segments to save memory
            children = new ConcurrentHashMap<Object, Node<K, V>>(4, .75f, 4);
         }
      }
      return children;
   }

   public CacheSPI getCache()
   {
      return cache;
   }

   public boolean isChildrenLoaded()
   {
      return isFlagSet(CHILDREN_LOADED);
   }

   public void setChildrenLoaded(boolean childrenLoaded)
   {
      setFlag(CHILDREN_LOADED, childrenLoaded);
   }

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

   public Object get(Object key)
   {
      assertValid();
      return cache.get(getFqn(), key);
   }

   public Object getDirect(Object key)
   {
      return data.get(key);
   }


   private boolean isReadLocked()
   {
      return lock != null && lock.isReadLocked();
   }

   private boolean isWriteLocked()
   {
      return lock != null && lock.isWriteLocked();
   }

   public IdentityLock getLock()
   {
      initLock();
      return lock;
   }

   public Map getDataDirect()
   {
      if (data == null) return Collections.emptyMap();
//      return Collections.unmodifiableMap(data);
      return data;
   }

   public Object put(Object key, Object value)
   {
      assertValid();
      return cache.put(getFqn(), key, value);
   }

   public Object putDirect(Object key, Object value)
   {
      return data.put(key, value);
   }

   public NodeSPI getOrCreateChild(Object child_name, GlobalTransaction gtx, boolean notify)
   {
      return getOrCreateChild(child_name, gtx, true, notify);
   }

   private NodeSPI getOrCreateChild(Object child_name, GlobalTransaction gtx, boolean createIfNotExists, boolean notify)
   {
      NodeSPI child;
      if (child_name == null)
      {
         throw new IllegalArgumentException("null child name");
      }

      child = (NodeSPI) children().get(child_name);
      InvocationContext ctx = cache.getInvocationContext();
      if (createIfNotExists && child == null)
      {
         // construct the new child outside the synchronized block to avoid
         // spending any more time than necessary in the synchronized section
         Fqn child_fqn = Fqn.fromRelativeElements(this.fqn, child_name);
         NodeSPI newChild = (NodeSPI) cache.getConfiguration().getRuntimeConfig().getNodeFactory().createNode(child_name, delegate, null);
         if (newChild == null)
         {
            throw new IllegalStateException();
         }
         synchronized (this)
         {
            // check again to see if the child exists
            // after acquiring exclusive lock
            child = (NodeSPI) children().get(child_name);
            if (child == null)
            {
               if (notify) cache.getNotifier().notifyNodeCreated(child_fqn, true, ctx);
               child = newChild;
               children.put(child_name, child);

               if (gtx != null)
               {
                  CreateNodeCommand createNodeCommand = commandsFactory.buildCreateNodeCommand(child_fqn);
                  ctx.getTransactionEntry().addModification(createNodeCommand);
               }
            }
         }

         // notify if we actually created a new child
         if (newChild == child)
         {
            if (trace)
            {
               log.trace("created child: fqn=" + child_fqn);
            }
            if (notify) cache.getNotifier().notifyNodeCreated(child_fqn, false, ctx);
         }
      }
      return child;

   }

   public Object remove(Object key)
   {
      assertValid();
      return cache.remove(getFqn(), key);
   }

   public Object removeDirect(Object key)
   {
      if (data == null) return null;
      return data.remove(key);
   }

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

   /**
    * Returns a debug string.
    */
   @Override
   public String toString()
   {
      StringBuilder sb = new StringBuilder();
      sb.append(getClass().getSimpleName());
      if (!isValid()) sb.append(" (INVALID!) ");

      if (isDeleted())
      {
         sb.append(" (deleted) [ ").append(fqn);
      }
      else
      {
         sb.append("[ ").append(fqn);
      }
      if (data != null)
      {
         synchronized (data)
         {
            if (trace)
            {
               sb.append(" data=").append(data.keySet());
            }
            else
            {
               sb.append(" data=[");
               Set keys = data.keySet();
               int i = 0;
               for (Object o : keys)
               {
                  i++;
                  sb.append(o);

                  if (i == 5)
                  {
                     int more = keys.size() - 5;
                     if (more > 1)
                     {
                        sb.append(", and ");
                        sb.append(more);
                        sb.append(" more");
                        break;
                     }
                  }
                  else
                  {
                     sb.append(", ");
                  }
               }
               sb.append("]");
            }
         }
      }
      if (children != null && !children.isEmpty())
      {
         if (trace)
         {
            sb.append(" children=").append(getChildrenNamesDirect());
         }
         else
         {
            sb.append(" children=[");
            Set names = getChildrenNamesDirect();
            int i = 0;
            for (Object o : names)
            {
               i++;
               sb.append(o);

               if (i == 5)
               {
                  int more = names.size() - 5;
                  if (more > 1)
                  {
                     sb.append(", and ");
                     sb.append(more);
                     sb.append(" more");
                     break;
                  }
               }
               else
               {
                  sb.append(", ");
               }
            }
            sb.append("]");
         }
      }
      if (lock != null)
      {
         if (isReadLocked())
         {
            sb.append(" RL");
         }
         if (isWriteLocked())
         {
            sb.append(" WL");
         }
      }
      sb.append("]");
      return sb.toString();
   }

   public void addChildDirect(NodeSPI child)
   {
      Fqn childFqn = child.getFqn();
      if (childFqn.isDirectChildOf(fqn))
      {
         synchronized (this)
         {
            children().put(childFqn.getLastElement(), child);
         }
      }
      else
         throw new CacheException("Attempting to add a child [" + child.getFqn() + "] to [" + getFqn() + "].  Can only add direct children.");
   }

   public NodeSPI addChildDirect(Fqn f)
   {
      return addChildDirect(f, true);
   }

   public NodeSPI addChildDirect(Fqn f, boolean notify)
   {
      if (f.size() == 1)
      {
         GlobalTransaction gtx = cache.getInvocationContext().getGlobalTransaction();
         return getOrCreateChild(f.getLastElement(), gtx, true, notify);
      }
      else
      {
         throw new UnsupportedOperationException("Cannot directly create children which aren't directly under the current node.");
      }

   }

   public NodeSPI addChildDirect(Object childName, boolean notify)
   {
      GlobalTransaction gtx = cache.getInvocationContext().getGlobalTransaction();
      return getOrCreateChild(childName, gtx, true, notify);
   }

   public void clearDataDirect()
   {
      if (data != null) data.clear();
   }

   public NodeSPI getChildDirect(Fqn fqn)
   {
      if (fqn.size() == 1)
      {
         return getChildDirect(fqn.getLastElement());
      }
      else
      {
         NodeSPI currentNode = delegate;
         for (int i = 0; i < fqn.size(); i++)
         {
            Object nextChildName = fqn.get(i);
            currentNode = currentNode.getChildDirect(nextChildName);
            if (currentNode == null) return null;
         }
         return currentNode;
      }
   }

   public Set<Object> getChildrenNamesDirect()
   {
      return children == null ? Collections.emptySet() : new HashSet<Object>(children.keySet());
   }

   public Set<Object> getKeysDirect()
   {
      if (data == null)
      {
         return Collections.emptySet();
      }
      return Collections.unmodifiableSet(new HashSet(data.keySet()));
   }

   public boolean removeChildDirect(Object childName)
   {
      return children != null && children.remove(childName) != null;
   }

   public boolean removeChildDirect(Fqn f)
   {
      if (f.size() == 1)
      {
         return removeChildDirect(f.getLastElement());
      }
      else
      {
         NodeSPI child = getChildDirect(f);
         return child != null && child.getParent().removeChildDirect(f.getLastElement());
      }
   }

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

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

   public void putAll(Map data)
   {
      assertValid();
      cache.put(fqn, data);
   }

   public void putAllDirect(Map data)
   {
      if (data == null) return;
      this.data.putAll(data);
   }

   public void removeChildrenDirect()
   {
      if (children != null)
      {
         children.clear();
      }
      children = null;
   }

   // versioning

   public void setVersion(DataVersion version)
   {
      throw new UnsupportedOperationException("Versioning not supported");
   }

   public DataVersion getVersion()
   {
      throw new UnsupportedOperationException("Versioning not supported");
   }

   private void printIndent(StringBuilder sb, int indent)
   {
      if (sb != null)
      {
         for (int i = 0; i < indent; i++)
         {
            sb.append(" ");
         }
      }
   }

   public void addChild(Object child_name, Node n)
   {
      if (child_name != null)
      {
         children().put(child_name, n);
      }
   }

   /**
    * Returns the name of this node.
    */
   private Object getName()
   {
      return fqn.getLastElement();
   }

   /**
    * Returns the name of this node.
    */
   public Fqn getFqn()
   {
      return fqn;
   }

   public void setFqn(Fqn fqn)
   {
      if (trace)
      {
         log.trace(getFqn() + " set FQN " + fqn);
      }
      this.fqn = fqn;

      if (children == null)
      {
         return;
      }

      // invoke children
      for (Map.Entry<Object, ? extends Node> me : children.entrySet())
      {
         NodeSPI n = (NodeSPI) me.getValue();
         Fqn cfqn = Fqn.fromRelativeElements(fqn, me.getKey());
         n.setFqn(cfqn);
      }
   }

   public NodeSPI getChildDirect(Object childName)
   {
      if (childName == null) return null;
      return (NodeSPI) (children == null ? null : children.get(childName));
   }

   public Set<NodeSPI> getChildrenDirect()
   {
      // strip out deleted child nodes...
      if (children == null || children.size() == 0) return Collections.emptySet();

      Set<NodeSPI> exclDeleted = new HashSet<NodeSPI>();
      for (Node n : children.values())
      {
         NodeSPI spi = (NodeSPI) n;
         if (!spi.isDeleted()) exclDeleted.add(spi);
      }
      return Collections.unmodifiableSet(exclDeleted);
   }

   public boolean hasChildrenDirect()
   {
      return children != null && children.size() != 0;
   }

   public Set<NodeSPI> getChildrenDirect(boolean includeMarkedForRemoval)
   {
      if (includeMarkedForRemoval)
      {
         if (children != null && !children.isEmpty())
         {
            return Collections.unmodifiableSet(new HashSet<NodeSPI>((Collection) children.values()));
         }
         else
         {
            return Collections.emptySet();
         }
      }
      else
      {
         return getChildrenDirect();
      }
   }

   /**
    * Adds details of the node into a map as strings.
    */
   private void printDetailsInMap(StringBuilder sb, int indent)
   {
      printIndent(sb, indent);
      indent += 2;// increse it
      if (!(getFqn()).isRoot())
      {
         sb.append(Fqn.SEPARATOR);
      }
      sb.append(getName());
      sb.append("  ");
      sb.append(data);
      if (children != null)
      {
         for (Node n : children.values())
         {
            sb.append("\n");
            ((NodeSPI) n).printDetails(sb, indent);
         }
      }
   }

   /**
    * Returns true if the data was loaded from the cache loader.
    */
   public boolean isDataLoaded()
   {
      return isFlagSet(DATA_LOADED);
   }

   /**
    * Sets if the data was loaded from the cache loader.
    */
   public void setDataLoaded(boolean dataLoaded)
   {
      setFlag(DATA_LOADED, dataLoaded);
   }

   public boolean isValid()
   {
      return isFlagSet(VALID);
   }

   public void setValid(boolean valid, boolean recursive)
   {
      setFlag(VALID, valid);

      if (trace) log.trace("Marking node " + getFqn() + " as " + (valid ? "" : "in") + "valid");
      if (recursive)
      {
         for (Node child : children().values())
         {
            ((NodeSPI) child).setValid(valid, recursive);
         }
      }
   }

   public boolean isLockForChildInsertRemove()
   {
      return isFlagSet(LOCK_FOR_CHILD_INSERT_REMOVE);
   }

   public void setLockForChildInsertRemove(boolean lockForChildInsertRemove)
   {
      setFlag(LOCK_FOR_CHILD_INSERT_REMOVE, lockForChildInsertRemove);
   }

   public void setInternalState(Map state)
   {
      // don't bother doing anything here
      putAllDirect(state);
   }

   public Map getInternalState(boolean onlyInternalState)
   {
      if (onlyInternalState)
         return new HashMap(0);
      // don't bother doing anything here
      if (data == null) return new HashMap(0);
      return new HashMap(data);
   }

   public void releaseObjectReferences(boolean recursive)
   {
      if (recursive && children != null)
      {
         for (Node<?, ?> child : children.values())
         {
            child.releaseObjectReferences(recursive);
         }
      }

      if (data != null)
      {
         for (Object key : data.keySet())
         {
            // get the key first, before attempting to serialize stuff since data.get() may deserialize the key if doing
            // a hashcode() or equals().

            Object value = data.get(key);
            if (key instanceof MarshalledValue)
            {
               ((MarshalledValue) key).compact(true, true);
            }

            if (value instanceof MarshalledValue)
            {
               ((MarshalledValue) value).compact(true, true);
            }

         }
      }
   }
}
