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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Modification;
import org.jboss.cache.TreeCache;
import org.jboss.cache.CacheException;
import org.jboss.cache.buddyreplication.BuddyManager;
import org.jboss.cache.config.CacheLoaderConfig.IndividualCacheLoaderConfig;

import java.util.*;

import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;

/**
 * Dummy cache loader that stores data in memory
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
 */
public class DummyInMemoryCacheLoader implements CacheLoader
{
   protected Map nodes = new ConcurrentHashMap();
   protected Log log = LogFactory.getLog(DummyInMemoryCacheLoader.class);
   protected Map transactions = new ConcurrentHashMap();
   protected TreeCache cache;


   public void setConfig(IndividualCacheLoaderConfig config)
   {
   }

   public IndividualCacheLoaderConfig getConfig()
   {
      return null;
   }

   public void setConfig(Properties properties)
   {
   }

   public void setCache(TreeCache c)
   {
      cache = c;
   }

   public Set getChildrenNames(Fqn fqn) throws Exception
   {
      if (log.isDebugEnabled()) log.debug("Calling getChildrenNames on Fqn " + fqn + ".  Data map = " + nodes);
      if (!nodes.containsKey(fqn))
      {
         log.debug("node not in loader");
         return null;
      }

      Set children = findChildren(fqn);
      log.debug("Fqn " + fqn + " has children " + children);
      // to keep in line with the CacheLoader interface contract for this method.
      return children.size() == 0 ? null : children;
   }

   private Set findChildren(Fqn p)
   {
      Set c = new HashSet();
      Fqn f;
      for(Iterator it = nodes.keySet().iterator(); it.hasNext();)
      {
         f = (Fqn)it.next();
         if (!f.isRoot() && f.getParent().equals(p))
         {
            c.add(f.getLast());
         }
      }
      return c;
   }

   public Map get(Fqn name) throws Exception
   {
      DummyNode dn = (DummyNode)nodes.get(name);
      Map d = dn != null ? dn.data : null;

      if (log.isDebugEnabled()) log.debug("Getting data for fqn " + name + " = " + d);
      return d;
   }

   public boolean exists(Fqn name) throws Exception
   {
      return nodes.containsKey(name);
   }

   public Object put(Fqn name, Object key, Object value) throws Exception
   {
      DummyNode n = (DummyNode)nodes.get(name);
      if (n == null)
      {
         n = new DummyNode(name);
      }
      Object old = n.data.put(key, value);
      nodes.put(name, n);
      // we need to make sure parents get put in as well.
      recursivelyPutParentsIfNeeded(name);
      if (log.isDebugEnabled()) log.debug("Did a put on " + name + ", data is " + n.data);
      return old;
   }

   public void put(Fqn name, Map attributes) throws Exception
   {
      DummyNode n = (DummyNode)nodes.get(name);
      if (n == null)
      {
         n = new DummyNode(name);
      }
      if (attributes != null) n.data.putAll(attributes);
      nodes.put(name, n);
      // we need to make sure parents get put in as well.
      recursivelyPutParentsIfNeeded(name);
      if (log.isDebugEnabled()) log.debug("Did a put on " + name + ", data is " + n.data);
   }

   public void put(List modifications) throws Exception
   {
      Modification m;
      for(Iterator it = modifications.iterator(); it.hasNext();)
      {
         m = (Modification)it.next();
         switch (m.getType())
         {
            case Modification.PUT_DATA:
               put(m.getFqn(), m.getData());
               break;
            case Modification.PUT_DATA_ERASE:
               removeData(m.getFqn());
               put(m.getFqn(), m.getData());
               break;
            case Modification.PUT_KEY_VALUE:
               put(m.getFqn(), m.getKey(), m.getValue());
               break;
            case Modification.REMOVE_DATA:
               removeData(m.getFqn());
               break;
            case Modification.REMOVE_KEY_VALUE:
               remove(m.getFqn(), m.getKey());
               break;
            case Modification.REMOVE_NODE:
               remove(m.getFqn());
               break;
            default:
               throw new CacheException("Unknown modificatiobn " + m.getType());
         }
      }
   }

   private void recursivelyPutParentsIfNeeded(Fqn node)
   {
      Fqn parent = node.getParent();
      if (nodes.containsKey(parent)) return; // nothing to do.

      // else put the parent in.
      nodes.put(parent, new DummyNode(parent));
      recursivelyPutParentsIfNeeded(parent);
   }

   public Object remove(Fqn fqn, Object key) throws Exception
   {
      log.debug("Removing data from " + fqn);
      DummyNode n = (DummyNode)nodes.get(fqn);
      if (n == null) n = new DummyNode(fqn);
      Object old = n.data.remove(key);
      nodes.put(fqn, n);
      return old;
   }

   public void remove(Fqn fqn) throws Exception
   {
      log.debug("Removing fqn " + fqn);
      nodes.remove(fqn);
      // remove children.
      recursivelyRemoveChildren(fqn);
   }

   private void recursivelyRemoveChildren(Fqn removedParent)
   {
      Fqn f;
      for(Iterator it = nodes.keySet().iterator(); it.hasNext();)
      {
         f = (Fqn)it.next();
         if (f.getParent().equals(removedParent))
         {
            // remove the child node too
            nodes.remove(f);
            // and it's children.  Depth first.
            recursivelyRemoveChildren(f);
         }
      }
   }

   public void removeData(Fqn fqn) throws Exception
   {
      log.debug("Removing data from " + fqn);
      DummyNode n = (DummyNode)nodes.get(fqn);
      if (n == null) n = new DummyNode(fqn);
      n.data.clear();
      nodes.put(fqn, n);
   }

   public void prepare(Object tx, List modifications, boolean one_phase) throws Exception
   {
      if (one_phase)
      {
         put(modifications);
      }
      else
      {
         transactions.put(tx, modifications);
      }
   }

   public void commit(Object tx) throws Exception
   {
      List modifications = (List)transactions.remove(tx);
      if (modifications == null)
      {
         throw new Exception("transaction " + tx + " not found in transaction table");
      }
      put(modifications);
   }

   public void rollback(Object tx)
   {
      transactions.remove(tx);
   }

   public byte[] loadEntireState() throws Exception
   {
      return loadState(Fqn.ROOT);
   }

   public byte[] loadState(Fqn subtree) throws Exception
   {
      List list = new LinkedList();
      getNodeDataList(subtree, list);
      if (log.isTraceEnabled()) log.trace("Loading state of " + list.size() + " nodes into stream");
      // cache.getMarshaller().objectToObjectStream(list, out, fqn);
      return cache.getMarshaller().objectToByteBuffer(list);
   }

   protected void getNodeDataList(Fqn fqn, List list) throws Exception
   {
      Map attrs;
      Set children_names;
      String child_name;
      Fqn tmp_fqn;
      NodeData nd;

      // first handle the current node
      attrs = get(fqn);
      if (attrs == null || attrs.size() == 0)
      {
         nd = new NodeData(fqn);
      }
      else
      {
         nd = new NodeData(fqn, attrs);
      }
      //out.writeObject(nd);
      list.add(nd);

      // then visit the children
      children_names = getChildrenNames(fqn);
      if (children_names == null)
      {
         return;
      }

      for(Iterator it = children_names.iterator(); it.hasNext();)
      {      
         child_name = (String) it.next();
         tmp_fqn = new Fqn(fqn, child_name);
         //loadStateHelper(tmp_fqn, out);
         getNodeDataList(tmp_fqn, list);
      }      
   }

   public void storeEntireState(byte[] state) throws Exception
   {
      storeState(Fqn.ROOT, state);
   }

   public void storeState(Fqn subtree, byte[] state) throws Exception
   {
      // remove entire existing state
      this.remove(subtree);

      boolean moveToBuddy = subtree.isChildOf(BuddyManager.BUDDY_BACKUP_SUBTREE_FQN) && subtree.size() > 1;

      // store new state
      Fqn fqn = null;
      //NodeData nd = null;
      Object objectFromStream = cache.getMarshaller().objectFromByteBuffer(state);
      List nodeData = (List) objectFromStream;

      for(Iterator it = nodeData.iterator(); it.hasNext();)
      {
         NodeData nd = (NodeData) it.next();

         if (moveToBuddy)
         {
            fqn = BuddyManager.getBackupFqn(subtree, nd.getFqn());
         }
         else
         {
            fqn = nd.getFqn();
         }

         if (nd.getAttributes() != null)
         {
            removeData(fqn);
            this.put(fqn, nd.getAttributes());// creates a node with 0 or more attributes
         }
         else
         {
            this.put(fqn, null);// creates a node with null attributes
         }
      }

   }


   public void create() throws Exception
   {
   }

   public void start() throws Exception
   {
   }

   public void stop()
   {
   }

   public void destroy()
   {
   }


   public class DummyNode
   {
      Map data = new HashMap();
      Fqn fqn;

      public DummyNode(Fqn fqn)
      {
         this.fqn = fqn;
      }

      public String toString()
      {
         return "Node{" +
                "data=" + data +
                ", fqn=" + fqn +
                '}';
      }
   }


   public String toString()
   {
      return "DummyInMemoryCacheLoader{" +
             "nodes=" + nodes +
             '}';
   }
}
