/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2000 - 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache.loader.jdbm;

import jdbm.helper.Tuple;
import jdbm.helper.TupleBrowser;
import net.jcip.annotations.ThreadSafe;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.config.CacheLoaderConfig.IndividualCacheLoaderConfig;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Optimized version of the {@link JdbmCacheLoader} implementation which is better
 * for storing data under a single node. Speed is improved.
 * <p/>
 * Data is stored using an FQN to Map association, where the depth is prefixed to the FQN:
 * <pre>
 * 0/ = NodeData
 * 1/node1 = Node Data
 * 1/node2 = Node Data
 * 2/node2/node3 = Node Data
 * 2/node2/node4 = Node Data
 * </pre>
 * <p/>
 * Browse operations lock the entire tree; eventually the JDBM team plans to fix this.
 *
 * @author Elias Ross
 * @version $Id: JdbmCacheLoader.java 7086 2008-11-05 18:39:35Z genman $
 */
@ThreadSafe
public class JdbmCacheLoader2 extends JdbmCacheLoader
{
   private static final Log log = LogFactory.getLog(JdbmCacheLoader2.class);
   private static final boolean trace = log.isTraceEnabled();

   /**
    * Max number of dummy parent nodes to cache.
    */
   private static final int PARENT_CACHE_SIZE = 100;

   /**
    * Empty parent nodes whose existence is cached.
    * Empty parents are required to ensure {@link #getChildrenNames(Fqn)}
    * and recursive {@link #remove(Fqn)} work correctly.
    */
   private Set<Fqn> parents = Collections.synchronizedSet(new HashSet<Fqn>());

   @Override
   JdbmCacheLoader2Config createConfig(IndividualCacheLoaderConfig base)
   {
      return new JdbmCacheLoader2Config(base);
   }

   /**
    * Adds a depth number to the start of the FQN.
    */
   private Fqn withDepth(Fqn name) {
       return withDepth(name, name.size());
   }
   
   /**
    * Adds a depth number to the start of the FQN.
    */
   private Fqn withDepth(Fqn name, int depth) {
       Fqn n = Fqn.fromElements(depth);
       return Fqn.fromRelativeList(n, name.peekElements());
   }

   @Override
   Set<Object> getChildrenNames0(Fqn name) throws IOException
   {
      Fqn name2 = withDepth(name, name.size() + 1);
      TupleBrowser browser = tree.browse(name2);
      Tuple t = new Tuple();

      Set<Object> set = new HashSet<Object>();

      while (browser.getNext(t))
      {
         Fqn fqn = (Fqn) t.getKey();
         if (!fqn.isChildOf(name2))
         {
            break;
         }
         set.add(fqn.getLastElement());
      }

      if (set.isEmpty())
      {
         return null;
      }

      return Collections.unmodifiableSet(set);
   }

   @Override
   public Map get(Fqn name)
         throws Exception
   {
      checkOpen();
      checkNonNull(name, "name");

      return (Map) tree.find(withDepth(name));
   }

   @Override
   public boolean exists(Fqn name) throws IOException
   {
      return tree.find(withDepth(name)) != null;
   }

   @Override
   Object put0(Fqn name, Object key, Object value) throws Exception
   {
      checkNonNull(name, "name");
      ensureParent(name);
      Fqn dname = withDepth(name);
      Map map = (Map) tree.find(dname);
      Object oldValue = null;
      if (map != null) {
	      oldValue = map.put(key, value);
	      tree.insert(dname, map, true);
      } else {
          map = new HashMap();
          map.put(key, value);
	      tree.insert(dname, map, false);
      }
      return oldValue;
   }

   @Override
   void put0(Fqn name, Map<?, ?> values) throws IOException
   {
      if (trace)
      {
         log.trace("put " + name + " values=" + values);
      }
      ensureParent(name);
      Fqn dname = withDepth(name);
      if (values == null)
          values = emptyMap();
      else
         values = new HashMap(values);
      tree.insert(dname, values, true);
   }
   
   private Map emptyMap() {
       return new HashMap(0);
   }

   /**
    * Ensures a parent node exists.
    * Calls recursively to initialize parents as necessary.
    */
   private void ensureParent(Fqn name) throws IOException
   {
      if (name.size() <= 1)
         return;
      Fqn parent = name.getParent();
      if (parents.contains(parent))
         return;
      if (!exists(parent))
         put0(parent, emptyMap());
      parents.add(parent);
      if (parents.size() > PARENT_CACHE_SIZE)
      {
         parents.clear();
      }
   }

   @Override
   void erase0(Fqn name, boolean prune)
         throws IOException
   {
      if (trace)
      {
         log.trace("erase " + name + " prune=" + prune);
      }
      if (!prune) {
         put0(name, emptyMap());
         return;
      }
      Set children = getChildrenNames(name);
      if (children != null)
      {
         log.trace("remove children: " + children);
         for (Object child : children)
         {
            erase0(Fqn.fromRelativeElements(name, child), true);
         }
      }
      parents.remove(name);
      try
      {
	      tree.remove(withDepth(name));
      }
      catch (IllegalArgumentException e)
      {
         log.trace("remove non-existant key? " + e);
      }
   }
   
   @Override
   Object eraseKey0(Fqn name, Object key)
         throws IOException
   {
      Fqn dname = withDepth(name);
      Map map = (Map) tree.find(dname);
      Object oldValue = null;
      if (map != null) {
	      oldValue = map.remove(key);
	      tree.insert(dname, map, true);
      }
      return oldValue;
   }

}
