/*
 * 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.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.Modification;
import org.jboss.cache.Region;
import org.jboss.cache.RegionManager;
import org.jboss.cache.buddyreplication.BuddyFqnTransformer;
import org.jboss.cache.buddyreplication.BuddyManager;
import org.jboss.cache.marshall.Marshaller;
import org.jboss.cache.marshall.NodeData;
import org.jboss.cache.marshall.NodeDataExceptionMarker;
import org.jboss.cache.marshall.NodeDataMarker;
import org.jboss.cache.util.MapCopy;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A convenience abstract implementation of a {@link CacheLoader}.  Specific methods to note are methods like
 * {@link #storeState(org.jboss.cache.Fqn,java.io.ObjectInputStream)}, {@link #loadState(org.jboss.cache.Fqn,java.io.ObjectOutputStream)},
 * {@link #storeEntireState(java.io.ObjectInputStream)} and {@link #loadEntireState(java.io.ObjectOutputStream)} which have convenience
 * implementations here.
 * <p/>
 * Also useful to note is the implementation of {@link #put(java.util.List)}, used during the prepare phase of a transaction.
 * <p/>
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
 * @since 2.0.0
 */
public abstract class AbstractCacheLoader implements CacheLoader
{
   protected CacheSPI cache;
   protected RegionManager regionManager;
   private static final Log log = LogFactory.getLog(AbstractCacheLoader.class);
   private static final boolean trace = log.isTraceEnabled();
   protected BuddyFqnTransformer buddyFqnTransformer = new BuddyFqnTransformer();
   /**
    * HashMap<Object,List<Modification>>. List of open transactions. Note that this is purely transient, as
    * we don't use a log, recovery is not available
    */
   protected Map<Object, List<Modification>> transactions = new ConcurrentHashMap<Object, List<Modification>>();

   public void put(Fqn fqn, Map<Object, Object> attributes, boolean erase) throws Exception
   {
      if (erase)
      {
         removeData(fqn);
      }

      // JBCACHE-769 -- make a defensive copy
      Map<Object, Object> attrs = (attributes == null ? null : new MapCopy<Object, Object>(attributes));
      put(fqn, attrs);
   }

   public void storeEntireState(ObjectInputStream is) throws Exception
   {
      storeState(Fqn.ROOT, is);
   }

   @SuppressWarnings("unchecked")
   public void storeState(Fqn subtree, ObjectInputStream in) throws Exception
   {
      // remove entire existing state
      this.remove(subtree);

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

      // store new state
      Object objectFromStream = cache.getMarshaller().objectFromObjectStream(in);
      if (objectFromStream instanceof NodeDataMarker)
      {
         // no persistent state sent across; return?
         return;
      }
      if (objectFromStream instanceof NodeDataExceptionMarker)
      {
         NodeDataExceptionMarker ndem = (NodeDataExceptionMarker) objectFromStream;
         throw new CacheException("State provider cacheloader at node " + ndem.getCacheNodeIdentity()
               + " threw exception during loadState (see Caused by)", ndem.getCause());
      }

      List nodeData = (List) objectFromStream;
      for (Object aNodeData : nodeData)
      {
         NodeData nd = (NodeData) aNodeData;
         if (nd.isExceptionMarker())
         {
            NodeDataExceptionMarker ndem = (NodeDataExceptionMarker) nd;
            throw new CacheException("State provider cacheloader at node " + ndem.getCacheNodeIdentity()
                  + " threw exception during loadState (see Caused by)", ndem.getCause());
         }
      }
      storeStateHelper(subtree, nodeData, moveToBuddy);
   }

   protected void storeStateHelper(Fqn subtree, List nodeData, boolean moveToBuddy) throws Exception
   {
      for (Object aNodeData : nodeData)
      {
         NodeData nd = (NodeData) aNodeData;
         if (nd.isMarker()) break;
         Fqn fqn;
         if (moveToBuddy)
         {
            fqn = buddyFqnTransformer.getBackupFqn(subtree, nd.getFqn());
         }
         else
         {
            fqn = nd.getFqn();
         }

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

   public void loadEntireState(ObjectOutputStream os) throws Exception
   {
      loadState(Fqn.ROOT, os);
   }

   public void loadState(Fqn subtree, ObjectOutputStream os) throws Exception
   {
//      ClassLoader currentCL = Thread.currentThread().getContextClassLoader();
//      try
//      {
      // Set the TCCL to any classloader registered for subtree
//         setUnmarshallingClassLoader(subtree);
      loadStateHelper(subtree, os);
//      }
//      finally
//      {
//         Thread.currentThread().setContextClassLoader(currentCL);
//      }
   }


   public void setCache(CacheSPI c)
   {
      this.cache = c;
   }

   public void setRegionManager(RegionManager regionManager)
   {
      this.regionManager = regionManager;
   }

   protected void regionAwareMarshall(Fqn fqn, Object toMarshall) throws Exception
   {
      Region r = regionManager == null ? null : regionManager.getValidMarshallingRegion(fqn);
      ClassLoader originalClassLoader = null;
      boolean needToResetLoader = false;
      Thread current = null;

      if (r != null)
      {
         // set the region's class loader as the thread's context classloader
         needToResetLoader = true;
         current = Thread.currentThread();
         originalClassLoader = current.getContextClassLoader();
         current.setContextClassLoader(r.getClassLoader());
      }

      try
      {
         doMarshall(fqn, toMarshall);
      }
      finally
      {
         if (needToResetLoader) current.setContextClassLoader(originalClassLoader);
      }
   }

   protected Object regionAwareUnmarshall(Fqn fqn, Object toUnmarshall) throws Exception
   {
      Region r = regionManager == null ? null : regionManager.getValidMarshallingRegion(fqn);
      ClassLoader originalClassLoader = null;
      boolean needToResetLoader = false;
      Thread current = null;

      if (r != null)
      {
         if (trace)
            log.trace("Using region " + r.getFqn() + ", which has registered class loader " + r.getClassLoader() + " as a context class loader.");
         // set the region's class loader as the thread's context classloader
         needToResetLoader = true;
         current = Thread.currentThread();
         originalClassLoader = current.getContextClassLoader();
         current.setContextClassLoader(r.getClassLoader());
      }

      try
      {
         return doUnmarshall(fqn, toUnmarshall);
      }
      finally
      {
         if (needToResetLoader) current.setContextClassLoader(originalClassLoader);
      }
   }

   protected void doMarshall(Fqn fqn, Object toMarshall) throws Exception
   {
      throw new RuntimeException("Should be overridden");
   }

   protected Object doUnmarshall(Fqn fqn, Object toUnmarshall) throws Exception
   {
      throw new RuntimeException("Should be overridden");
   }


   /**
    * Do a preorder traversal: visit the node first, then the node's children
    */
   protected void loadStateHelper(Fqn fqn, ObjectOutputStream out) throws Exception
   {
      List<NodeData> list = new LinkedList<NodeData>();
      getNodeDataList(fqn, list);
      if (trace) log.trace("Loading state of " + list.size() + " nodes into stream");
      cache.getMarshaller().objectToObjectStream(list, out, fqn);
   }

   protected void getNodeDataList(Fqn fqn, List<NodeData> list) throws Exception
   {
      Map<Object, Object> attrs;
      Set<?> childrenNames;
      String childName;
      Fqn tmpFqn;
      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
      childrenNames = getChildrenNames(fqn);
      if (childrenNames == null)
      {
         return;
      }
      for (Object childrenName : childrenNames)
      {
         childName = (String) childrenName;
         tmpFqn = Fqn.fromRelativeElements(fqn, childName);
         if (!cache.getInternalFqns().contains(tmpFqn)) getNodeDataList(tmpFqn, list);
      }
   }

   public void put(List<Modification> modifications) throws Exception
   {
      for (Modification m : modifications)
      {
         switch (m.getType())
         {
            case PUT_DATA:
               put(m.getFqn(), m.getData());
               break;
            case PUT_DATA_ERASE:
               removeData(m.getFqn());
               put(m.getFqn(), m.getData());
               break;
            case PUT_KEY_VALUE:
               put(m.getFqn(), m.getKey(), m.getValue());
               break;
            case REMOVE_DATA:
               removeData(m.getFqn());
               break;
            case REMOVE_KEY_VALUE:
               remove(m.getFqn(), m.getKey());
               break;
            case REMOVE_NODE:
               remove(m.getFqn());
               break;
            case MOVE:
               //                involve moving all children too,
               _move(m.getFqn(), m.getFqn2());
               break;
            default:
               throw new CacheException("Unknown modificatiobn " + m.getType());
         }
      }
   }

   private void _move(Fqn fqn, Fqn parent) throws Exception
   {
      Object name = fqn.getLastElement();
      Fqn newFqn = Fqn.fromRelativeElements(parent, name);

      // start deep.
      Set childrenNames = getChildrenNames(fqn);
      if (childrenNames != null)
      {
         for (Object c : childrenNames)
         {
            _move(Fqn.fromRelativeElements(fqn, c), newFqn);
         }
      }
      // get data for node.
      Map<Object, Object> data = get(fqn);
      if (data != null)// if null, then the node never existed.  Don't bother removing?
      {
         remove(fqn);
         put(newFqn, data);
      }
   }

   protected Marshaller getMarshaller()
   {
      return cache.getMarshaller();
   }

   // empty implementations for loaders that do not wish to implement lifecycle.
   public void create() throws Exception
   {
   }

   public void start() throws Exception
   {
   }

   public void stop()
   {
   }

   public void destroy()
   {
   }

   // Adds simple transactional capabilities to cache loaders that are inherently non-transactional.  If your cache loader implementation
   // is tansactional though, then override these.

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

   public void commit(Object tx) throws Exception
   {
      List<Modification> modifications = 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);
   }
}
