/*
 * 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>
 */
public class JBossCacheService implements TreeCacheListener
{
   private TreeCacheMBean proxy_;
   private ObjectName cacheServiceName_;
   protected 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;
   // Needed for cache invalidation
   private static final String VERSION_KEY = "VERSION";
   // Needed to reconstruct the original session id, if JvmRoute is used.
   private String jvmRoute_;
   // web app path (JBAS-1367). Idea is web_app_path + session id is a unique pair.
   private String webAppPath_;
   // List of new session ids that is being replicated but not yet on the in-memory space. Note that it is not synchronzied
   private List newSessionIDList_;

   // 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
         // TODO We can also consider using getInstance to get directly the reference handle.
         proxy_ = (TreeCacheMBean) MBeanProxyExt.create(TreeCacheMBean.class, cacheServiceName_);
         if (proxy_ == null)
         {
            throw new RuntimeException("JBossCacheService: locate null TomcatCacheMbean");
         }

         newSessionIDList_ = new ArrayList();
      }
      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;
      jvmRoute_ = null;
      proxy_.addTreeCacheListener(this);
      String path = manager_.getContainer().getName();
      if( path.length() == 0 || path.equals("/")) {
         // If this is root.
         webAppPath_ = "ROOT";
      } if ( path.startsWith("/") ) {
         webAppPath_ = path.substring(1);
      } else {
         webAppPath_ = path;
      }
      log_.debug("Old and new web app path are: " +path + ", " +webAppPath_);
   }

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

   /**
    * Find session ids for the whole Manager instance. Note that this also depends on the web app
    * path setting.
    *
    * @return Empty list if not found.
    */
   public List findSessionIDs()
   {
      List ids = new ArrayList();
      try {
         // Construct the fqn
         Object[] objs = new Object[]{SESSION, webAppPath_};
         Fqn path = new Fqn( objs );
         // locate children under each web app path
         Set names = proxy_.getChildrenNames(path);

         if( names == null ) return ids;
         for(Iterator it = names.iterator(); it.hasNext();) {
            Object id = it.next();
            if(id==null) continue;
            ids.add(id);
            if(log_.isTraceEnabled()) {
               log_.trace("Retrieving through web app path with fqn: " +path + " and session id: " +id);
            }
         }
      } catch (CacheException e) {
         e.printStackTrace();
         throw new RuntimeException("JBossCacheService: exception occurred in cache getChildrenNames ... ", e);
      }
      return ids;
   }

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

   public void putSession(String id, Object session)
   {
      String realId = stripJvmRoute(id);
      Fqn fqn = getSessionFqn(realId);
// TODO Need to investigate why this causes deadlock??
//      Map map = new HashMap();
//      map.put(realId, getMarshalledValue(session));
      // Put in (VERSION_KEY, version) after the real put for cache invalidation
//      map.put(VERSION_KEY, new Integer(((ClusteredSession)session).getVersion()));
//      _put(fqn, map);
      // This is not very efficient now since it generates two nodeAdded event.
      _put(fqn, realId, getMarshalledValue(session));
      // Put in (VERSION_KEY, version) after the real put for cache invalidation
      _put(fqn, VERSION_KEY, new Integer(((ClusteredSession)session).getVersion()));
   }

   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);
      }
      Object obj = getUnMarshalledValue(_remove(fqn, realId));
      // This needs to go after object removal to support correct cache invalidation.
//      _remove(fqn, VERSION_KEY);
      // Will just remove the whole tree now (include the fqn).
      _remove(fqn);
      return obj;
   }

   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 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();
      if(set == null) return map;
      for (Iterator it = set.iterator(); it.hasNext();)
      {
         String key = (String) it.next();
         Object value = getAttribute(id, key);
         map.put(key, value);
      }
      return map;
   }

   /**
    * Retrieve the new sessions in the underlying cache. Return size 0 if not found.
    *
    */
   public List getNewSessionsInStore()
   {
      List list = new ArrayList();
      synchronized(newSessionIDList_)
      {
         if(newSessionIDList_.size() != 0)
         {
            list.addAll(newSessionIDList_);
            newSessionIDList_.clear();
         }
      }
      return list;
   }

   /**
    * 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 retry 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 retry logic.
    *
    * @param fqn
    * @param map
    */
   protected void _put(Fqn fqn, Map map)
   {
      Exception ex = null;
      for (int i = 0; i < RETRY; i++)
      {
         try
         {
            proxy_.put(fqn, map);
            return;
         }
         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 retry 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)
   {
      // TODO Need optimization later since not every request needs to do this.
      int index = id.indexOf(".");
      if (index > 0)
      {
         if(jvmRoute_ == null)
            jvmRoute_ = id.substring(index);

         return id.substring(0, index);
      }
      else
      {
         return id;
      }
   }

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

   private Fqn getAttributeFqn(String id)
   {
      // /SESSION/id/ATTR
      Object[] objs = new Object[]{SESSION, webAppPath_, 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)
   {
      // Query if we have version value. If we do, we compare the version. Invalidate if necessary.
      Integer version = (Integer)_get(fqn, VERSION_KEY);
      if(version != null)
      {
         String realId = getIdFromFqn(fqn);

         /*
         String id;
         // Need to reconstruct the session id because it could contain jvm route.
         if(jvmRoute_ == null)
         {
            id = realId;
         } else
         {
            id = realId + "." +jvmRoute_;
         }
         */

         ClusteredSession session = (ClusteredSession)manager_.findLocalSession(realId);
         // if session is null, that means the data is fresh. No need to do anything then.
         if( session != null )
         {
            if( session.isNewData(version.intValue()))
            {
               // Need to invalidate
               session.setIsOutdated(true);
               if(log_.isDebugEnabled())
               {
                  log_.debug("nodeDirty(): session in-memory data is invalidated with id: " +realId
                  + " and verion id: " +version.intValue());
               }
            }
         } else
         {
            // fresh data. We will need to populate it to the local in-memory copy.
//            if(log_.isDebugEnabled())
//            {
//               log_.debug("nodeDirty(): session id recovered -- " +realId);
//            }

            synchronized(newSessionIDList_)
            {
               if(!newSessionIDList_.contains(realId))
                  newSessionIDList_.add(realId);
            }
         }
      }
   }

   protected String getIdFromFqn(Fqn fqn)
   {
      return (String)fqn.get(fqn.size()-1);
   }

   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.
   }

}