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

package org.jboss.cache.pojo.impl;

import java.util.Collection;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Cache;
import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Version;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.factories.XmlConfigurationParser;
import org.jboss.cache.pojo.PojoCache;
import org.jboss.cache.pojo.PojoCacheException;
import org.jboss.cache.pojo.annotation.Attach;
import org.jboss.cache.pojo.annotation.Detach;
import org.jboss.cache.pojo.annotation.Find;

/**
 * Implementation class for PojoCache interface
 *
 * @author Ben Wang
 * @version $Id: PojoCacheImpl.java 4496 2007-09-21 03:15:01Z jason.greene@jboss.com $
 */
public class PojoCacheImpl implements PojoCache
{
   private CacheSPI<Object, Object> cache = null;
   protected final Log log_ = LogFactory.getLog(PojoCacheImpl.this.getClass());
   private PojoCacheDelegate delegate_;
   // Class -> CachedType
   // use WeakHashMap to allow class reloading
   private Map cachedTypes_ = new WeakHashMap();
   private boolean hasCreate_ = false;
   private CacheListenerAdaptor listenerAdaptor = new CacheListenerAdaptor(this);

   public PojoCacheImpl(String configStr, boolean toStart)
   {
      try
      {
         //         cache_ = new PojoTreeCache();
         //         cache_.setConfiguration(new XmlConfigurationParser().parseFile(configStr));

         XmlConfigurationParser parser = new XmlConfigurationParser();
         Configuration expected = parser.parseFile(configStr);

         init(expected, toStart);
      }
      catch (Exception e)
      {
         throw new PojoCacheException("Failed to start " + configStr, e);
      }
   }

   public PojoCacheImpl(Configuration config, boolean toStart)
   {
      init(config, toStart);
   }

   private void init(Configuration config, boolean toStart)
   {
      try
      {
         cache = (CacheSPI<Object, Object>) DefaultCacheFactory.getInstance().createCache(config, toStart);
      }
      catch (Exception e)
      {
         throw new PojoCacheException("init " + config + " failed", e);
      }

      delegate_ = new PojoCacheDelegate(this);
   }

   public CacheSPI<Object, Object> getCacheSPI()
   {
      return cache;
   }

   public Object attach(String id, Object pojo) throws PojoCacheException
   {
      return attach(Fqn.fromString(id), pojo);
   }

   @Attach
   public Object attach(Fqn<?> id, Object pojo) throws PojoCacheException
   {
      try
      {
         Object obj = putObject(id, pojo, null);
         return obj;
      }
      catch (CacheException e)
      {
         throw new PojoCacheException("putObject failed " + id, e);
      }
   }

   @Attach
   public Object attach(Fqn<?> id, Object pojo, String field) throws PojoCacheException
   {
      try
      {
         Object obj = putObject(id, pojo, field);
         return obj;
      }
      catch (CacheException e)
      {
         throw new PojoCacheException("putObject failed " + id, e);
      }
   }

   /**
    * This public API is called from internal package only.
    */
   public Object putObject(Fqn<?> id, Object pojo, String field)
           throws CacheException
   {
      Object obj = null;

      // Maybe this is the same instance already.
      obj = delegate_.putObjectI(id, pojo, field);
      if (obj != null) return obj;

      obj = delegate_.putObjectII(id, pojo, field);
      return obj;
   }

   public Object detach(String id) throws PojoCacheException
   {
      return detach(Fqn.fromString(id));
   }

   @Detach
   public Object detach(Fqn<?> id, String field) throws PojoCacheException
   {
      try
      {
         Object pojo = getObject(id, field);// TODO need optimization here since it will be redundant here
         if (pojo == null) return pojo;

         Object obj = removeObject(id, field);
         return obj;
      }
      catch (CacheException e)
      {
         throw new PojoCacheException("detach " + id + " failed", e);
      }
   }

   public Object detach(Fqn<?> id) throws PojoCacheException
   {
      return detach(id, null);
   }

   public Object removeObject(Fqn<?> id, String field) throws CacheException
   {
      delegate_.setBulkRemove(false);
      return delegate_.removeObject(id, field);
   }

   public String getPojoID(Object pojo)
   {
      throw new PojoCacheException("getPojoID not yet implemented");
   }

   public boolean exists(Fqn<?> id)
   {
      return delegate_.exists(id);
   }

   public Object find(String id) throws PojoCacheException
   {
      return find(Fqn.fromString(id));
   }

   @Find
   public Object find(Fqn<?> id) throws PojoCacheException
   {
      try
      {
         return getObject(id);
      }
      catch (CacheException e)
      {
         throw new PojoCacheException("find " + id + " failed ", e);
      }
   }

   public Object getObject(Fqn<?> id) throws CacheException
   {
      return getObject(id, null);
   }

   public Object getObject(Fqn<?> id, String field) throws CacheException
   {
      return delegate_.getObject(id, field);
   }


   public Map<Fqn<?>, Object> findAll(String id) throws PojoCacheException
   {
      return findAll(Fqn.fromString(id));
   }

   @Find
   public Map<Fqn<?>, Object> findAll(Fqn<?> id) throws PojoCacheException
   {
      // Should produce "/"
      if (id == null) id = Fqn.ROOT;

      try
      {
         return delegate_.findObjects(id);
      }
      catch (CacheException e)
      {
         throw new PojoCacheException("findAll " + id + " failed", e);
      }
   }

   public String getVersion()
   {
      return Version.printVersion();
   }

   public void create() throws PojoCacheException
   {
      log_.info("PojoCache version: " + getVersion());
      try
      {
         cache.create();
      }
      catch (Exception e)
      {
         throw new PojoCacheException("PojoCache create exception", e);
      }

      hasCreate_ = true;
   }

   public void start() throws PojoCacheException
   {
      if (!hasCreate_)
      {
         create();
      }

      try
      {
         log_.info("PojoCache version: " + getVersion());
         cache.start();
      }
      catch (Exception e)
      {
         throw new PojoCacheException("Failed starting " + e, e);
      }
   }

   public void stop() throws PojoCacheException
   {
      cache.stop();
   }

   public void destroy() throws PojoCacheException
   {
      cache.destroy();
   }

   public Collection<Object> getListeners()
   {
      return listenerAdaptor.getListeners();
   }

   public void addListener(Object listener)
   {
      addListener(listener, null);
   }

   public void addListener(Object listener, Pattern pattern)
   {
      // Add and remove listner operations must be serialized to ensure that
      // the adaptor is always present only once, when at least one listener
      // is registered.
      synchronized (listenerAdaptor)
      {
         try
         {
            listenerAdaptor.addListener(listener, pattern);
         }
         catch (IllegalArgumentException e)
         {
            // simplify stack trace for user
            e.fillInStackTrace();
            throw e;
         }
         cache.addCacheListener(listenerAdaptor);
      }
   }

   public void removeListener(Object listener)
   {
      synchronized (listenerAdaptor)
      {
         listenerAdaptor.removeListener(listener);
         if (listenerAdaptor.isEmpty())
            cache.removeCacheListener(listenerAdaptor);
      }
   }

   public Cache<Object,Object> getCache()
   {
      return cache;
   }

   /**
    * Obtain a cache aop type for user to traverse the defined "primitive" types in aop.
    * Note that this is not a synchronized call now for speed optimization.
    *
    * @param clazz The original pojo class
    * @return CachedType
    */
   public synchronized CachedType getCachedType(Class clazz)
   {
      CachedType type = (CachedType) cachedTypes_.get(clazz);
      if (type == null)
      {
         type = new CachedType(clazz);
         cachedTypes_.put(clazz, type);
         return type;
      }
      else
      {
         return type;
      }
   }

   public String toString()
   {
      return getClass().getName() +
              " cache=" + cache +
              " delegate=" + delegate_ +
              " types=" + cachedTypes_.size();
   }
}
