/*
 * JBoss, the OpenSource WebOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */

package org.jboss.web.tomcat.tc5.session;

import java.io.IOException;
import java.util.*;
import javax.management.ObjectName;

import org.jboss.cache.CacheException;
import org.jboss.cache.Fqn;
import org.jboss.cache.TreeCache;
import org.jboss.cache.TreeCacheListener;
import org.jboss.cache.TreeCacheMBean;
import org.jboss.cache.lock.TimeoutException;
import org.jboss.invocation.MarshalledValue;
import org.jboss.logging.Logger;
import org.jboss.mx.util.MBeanProxyExt;
import org.jboss.web.tomcat.tc5.Tomcat5;
import org.jgroups.View;

/**
 * A wrapper class to JBossCache. This is currently needed to handle various operations such as
 * <ul>
 * <li>Using MarshalledValue to replace Serializable used inside different web app class loader context.</li>
 * <li>Stripping out any id string after ".". This is to handle the JK failover properly with
 * Tomcat JvmRoute.</li>
 * <li>Cache exception retry.</li>
 * <li>Helper APIS.</li>
 * </ul>
 * TODO We will need to handle the TreeCacheListener for node events.
 */
public class JBossCacheService implements TreeCacheListener
{
   private TreeCacheMBean proxy_;
   private ObjectName cacheServiceName_;
   private static Logger log_ = Logger.getLogger(JBossCacheService.class);
   public static final String SESSION = "JSESSION";
   public static final String ATTRIBUTE = "ATTRIBUTE";
   public static final String KEY = "ATRR_KEY";
   private static final int RETRY = 3;

   // Class loader for this web app.
   private ClassLoader tcl_;
   private JBossCacheManager manager_;

   public JBossCacheService() throws ClusteringNotSupportedException
   {
      // Find JBossCacheService
      try
      {
         cacheServiceName_ = new ObjectName(Tomcat5.DEFAULT_CACHE_NAME);
         // Create Proxy-Object for this service
         proxy_ = (TreeCacheMBean) MBeanProxyExt.create(TreeCacheMBean.class, cacheServiceName_);
         if (proxy_ == null)
         {
            throw new RuntimeException("JBossCacheService: locate null TomcatCacheMbean");
         }

      }
      catch (Throwable e)
      {
         String str = cacheServiceName_ + " service to Tomcat clustering not found";
         log_.error(str);
         throw new ClusteringNotSupportedException(str);
      }
   }

   public void start(ClassLoader tcl, JBossCacheManager manager)
   {
      tcl_ = tcl;
      manager_ = manager;
      proxy_.addTreeCacheListener(this);
   }

   public void stop()
   {
      proxy_.removeTreeCacheListener(this);
   }

   public Object getSession(String id)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getSessionFqn(realId);
      return getUnMarshalledValue(_get(fqn, realId));
   }

   public Object putSession(String id, Object session)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getSessionFqn(realId);
      return _put(fqn, realId, getMarshalledValue(session));
   }

   public Object removeSession(String id)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getSessionFqn(realId);
      if (log_.isDebugEnabled())
      {
         log_.debug("Remove session from distributed store. Fqn: " + fqn);
      }
      return getUnMarshalledValue(_remove(fqn, realId));
   }

   public void removeSessionLocal(String id)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getSessionFqn(realId);
      if (log_.isDebugEnabled())
      {
         log_.debug("Remove session from my own distributed store only. Fqn: " + fqn);
      }
      _evict(fqn);
   }

   public List findSessionIDs() {
      List ids = new ArrayList();
      try {
         Set names = proxy_.getChildrenNames(SESSION);
         if(names == null || names.size() == 0) return ids;

         for(Iterator it = names.iterator(); it.hasNext();) {
            ids.add(it.next());
         }
      } catch (CacheException e) {
         e.printStackTrace();
         throw new RuntimeException("JBossCacheService: exception occurred in cache getChildrenNames ... ", e);
      }
      return ids;
   }

   public boolean exists(String id)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getSessionFqn(realId);
      return proxy_.exists(fqn);
   }

   public Object getAttribute(String id, String key)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getAttributeFqn(realId);
      return getUnMarshalledValue(_get(fqn, key));
   }

   public Object putAttribute(String id, String key, Object value)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getAttributeFqn(realId);
      return _put(fqn, key, getMarshalledValue(value));
   }

   public void putAttribute(String id, Map map)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getAttributeFqn(realId);
      Set set = map.keySet();
      Iterator it = set.iterator();
      while (it.hasNext())
      {
         String key = (String) it.next();
         _put(fqn, key, getMarshalledValue(map.get(key)));
      }
   }

   public void removeAttributes(String id)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getAttributeFqn(realId);
      _remove(fqn);
   }

   public Object removeAttribute(String id, String key)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getAttributeFqn(realId);
      if (log_.isDebugEnabled())
      {
         log_.debug("Remove attribute from distributed store. Fqn: " + fqn + " key: " + key);
      }
      return getUnMarshalledValue(_remove(fqn, key));
   }

   public void removeAttributeLocal(String id)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getAttributeFqn(realId);
      if (log_.isDebugEnabled())
      {
         log_.debug("Remove attributes from my own distributed store only. Fqn: " + fqn);
      }
      _evict(fqn);
   }

   /**
    * Obtain the keys associated with this fqn. Note that it is not the fqn children.
    *
    * @return
    */
   public Set getAttributeKeys(String id)
   {
      if (id == null || id.length() == 0)
         throw new IllegalArgumentException("JBossCacheService: id is either null or empty");

      String realId = stripJvmRoute(id);
      Fqn fqn = getAttributeFqn(realId);
      try
      {
         return proxy_.getKeys(fqn);
      }
      catch (CacheException e)
      {
         e.printStackTrace();
      }
      return null;
   }

   /**
    * Return all attributes associated with this session id. Return empty map if not found.
    *
    * @param id
    * @return
    */
   public Map getAttributes(String id)
   {
      if (id == null || id.length() == 0) return new HashMap();
      Set set = getAttributeKeys(id);
      String realId = stripJvmRoute(id);
      Fqn fqn = getAttributeFqn(realId);
      Map map = new HashMap();
      for (Iterator it = set.iterator(); it.hasNext();)
      {
         String key = (String) it.next();
         Object value = getAttribute(id, key);
         map.put(key, value);
      }
      return map;
   }

   /**
    * Wrapper to embed retyr logic.
    *
    * @param fqn
    * @param id
    * @return
    */
   protected Object _get(Fqn fqn, String id)
   {
      Exception ex = null;
      for (int i = 0; i < RETRY; i++)
      {
         try
         {
            return proxy_.get(fqn, id);
         }
         catch (TimeoutException e)
         {
            e.printStackTrace();
            ex = e;
         }
         catch (Exception e)
         {
            e.printStackTrace();
            throw new RuntimeException("JBossCacheService: exception occurred in cache get ... ", e);
         }
      }
      throw new RuntimeException("JBossCacheService: exception occurred in cache get after retry ... ", ex);
   }

   /**
    * Wrapper to embed retyr logic.
    *
    * @param fqn
    * @param id
    * @param value
    * @return
    */
   protected Object _put(Fqn fqn, String id, Object value)
   {
      Exception ex = null;
      for (int i = 0; i < RETRY; i++)
      {
         try
         {
            return proxy_.put(fqn, id, value);
         }
         catch (TimeoutException e)
         {
            e.printStackTrace();
            ex = e;
         }
         catch (Exception e)
         {
            e.printStackTrace();
            throw new RuntimeException("JBossCacheService: exception occurred in cache put ... ", e);
         }
      }
      throw new RuntimeException("JBossCacheService: exception occurred in cache put after retry ... ", ex);
   }

   /**
    * Wrapper to embed retyr logic.
    *
    * @param fqn
    * @param id
    * @return
    */
   protected Object _remove(Fqn fqn, String id)
   {
      Exception ex = null;
      for (int i = 0; i < RETRY; i++)
      {
         try
         {
            return proxy_.remove(fqn, id);
         }
         catch (TimeoutException e)
         {
            e.printStackTrace();
            ex = e;
         }
         catch (Exception e)
         {
            e.printStackTrace();
            throw new RuntimeException("JBossCacheService: exception occurred in cache remove ... ", e);
         }
      }
      throw new RuntimeException("JBossCacheService: exception occurred in cache remove after retry ... ", ex);
   }

   /**
    * Wrapper to embed retyr logic.
    *
    * @param fqn
    */
   protected void _remove(Fqn fqn)
   {
      Exception ex = null;
      for (int i = 0; i < RETRY; i++)
      {
         try
         {
            proxy_.remove(fqn);
            return;
         }
         catch (TimeoutException e)
         {
            e.printStackTrace();
            ex = e;
         }
         catch (Exception e)
         {
            e.printStackTrace();
            throw new RuntimeException("JBossCacheService: exception occurred in cache remove ... ", e);
         }
      }
      throw new RuntimeException("JBossCacheService: exception occurred in cache remove after retry ... ", ex);
   }

   /**
    * Wrapper to embed retyr logic.
    *
    * @param fqn
    */
   protected void _evict(Fqn fqn)
   {
      Exception ex = null;
      for (int i = 0; i < RETRY; i++)
      {
         try
         {
            proxy_.evict(fqn);
            return;
         }
         catch (TimeoutException e)
         {
            e.printStackTrace();
            ex = e;
         }
         catch (Exception e)
         {
            e.printStackTrace();
            throw new RuntimeException("JBossCacheService: exception occurred in cache evict ... ", e);
         }
      }
      throw new RuntimeException("JBossCacheService: exception occurred in cache evict after retry ... ", ex);
   }


   /**
    * Since we store the base id (i.e., without the JvmRoute) internally while the real session id
    * has the postfix in there if mod_jk is used, we will need to strip it to get the real key. Now this
    * is an aspect.
    *
    * @param id
    * @return
    */
   private String stripJvmRoute(String id)
   {
      int index = id.indexOf(".");
      if (index > 0)
      {
         return id.substring(0, index);
      }
      else
      {
         return id;
      }
   }

   private Fqn getSessionFqn(String id)
   {
      // /SESSION/id
      Object[] objs = new Object[]{SESSION, id};
      return new Fqn(objs);
   }

   private Fqn getAttributeFqn(String id)
   {
      // /SESSION/id/ATTR
      Object[] objs = new Object[]{SESSION, id, ATTRIBUTE};
      return new Fqn(objs);
   }

   private Object getMarshalledValue(Object value)
   {
      try
      {
         return new MarshalledValue(value);
      }
      catch (IOException e)
      {
         e.printStackTrace();
         return null;
      }
   }

   private Object getUnMarshalledValue(Object mv)
   {
      if (mv == null) return null;
      // Swap in/out the tcl for this web app. Needed only for un marshalling.
      ClassLoader prevTCL = Thread.currentThread().getContextClassLoader();
      Thread.currentThread().setContextClassLoader(tcl_);
      try
      {
         return ((MarshalledValue) mv).get();
      }
      catch (IOException e)
      {
         e.printStackTrace();
         return null;
      }
      catch (ClassNotFoundException e)
      {
         e.printStackTrace();
         return null;
      }
      finally
      {
         Thread.currentThread().setContextClassLoader(prevTCL);
      }
   }

   // --------------- TreeCacheListener methods ------------------------------------

   public void nodeCreated(Fqn fqn)
   {
      // No-op
   }

   public void nodeRemoved(Fqn fqn)
   {
      nodeDirty(fqn);
   }

   /**
    * Called when a node is loaded into memory via the CacheLoader. This is not the same
    * as {@link #nodeCreated(Fqn)}.
    */
   public void nodeLoaded(Fqn fqn)
   {

   }

   public void nodeModified(Fqn fqn)
   {
      nodeDirty(fqn);
   }

   protected void nodeDirty(Fqn fqn)
   {
      // TODO no-op now. But will need to do something later.
      // Right now we assume only the failover case.
   }

   public void nodeVisited(Fqn fqn)
   {
      // no-op
   }

   public void cacheStarted(TreeCache cache)
   {
      // TODO will need to synchronize this with local sessions
   }

   public void cacheStopped(TreeCache cache)
   {
      // TODO will need to synchronize this with local sessions
   }

   public void viewChange(View new_view)
   {
      // We don't care for this event.
   }

   public void nodeEvicted(Fqn fqn)
   {
      // We don't care for this event.
   }

}
