/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt 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.eviction;

import org.jboss.cache.TreeCache;
import org.jboss.logging.Logger;

import java.util.HashMap;
import java.util.Map;

/**
 * Factory to create region from configuration, to track region,
 * and to resolve naming conflict for regions. Note that in addition to
 * user-specified regions, there is also a global cache <code>_default_</code>
 * region that covers everything else.
 *
 * @author Ben Wang 02-2004
 * @version $Id: RegionManager.java,v 1.4.2.7 2005/12/01 19:48:41 dimitris Exp $
 */
public class RegionManager
{
   private Logger log_ = Logger.getLogger(RegionManager.class);
   public final static int CAPACITY = 200000;
   private Map regionMap_ = new HashMap();
   // optimization
   private Region[] regions_;
   private EvictionPolicy policy_;
   // There is global cache wide default values if no region is found.
   final static String DEFAULT_REGION = "/_default_/";

   public RegionManager(EvictionPolicy policy)
   {
      policy_ = policy;
      regions_ = null;
   }

   /**
    * Create a region based on fqn.
    *
    * @param fqn       The region identifier.
    * @param algorithm EvictionAlgorithm that associates with this region.
    * @throws RegionNameConflictException
    */
   public Region createRegion(String fqn, EvictionAlgorithm algorithm)
   throws RegionNameConflictException
   {
      if (log_.isDebugEnabled())
      {
         log_.debug("createRegion(): creating region for fqn- " + fqn);
      }

      String newFqn = appendFqn(fqn);
      checkConflict(newFqn);
      Region region = new Region(newFqn, policy_, algorithm);
      regionMap_.put(newFqn, region);
      return region;
   }

   public void removeRegion(String fqn)
   {
      regionMap_.remove(fqn);
   }

   /**
    * Append the fqn with "/" if necessary
    *
    * @param fqn
    * @return
    */
   private String appendFqn(String fqn)
   {
      if (!fqn.endsWith(TreeCache.SEPARATOR))
         return fqn + TreeCache.SEPARATOR;
      else
         return fqn;
   }

   public boolean hasRegion(String myFqn)
   {
      String newFqn = appendFqn(myFqn);
      return regionMap_.containsKey(newFqn);
   }

   public Region getRegion(String myFqn)
   {
      Region[] regions = getRegions();
      // TODO. Is not needed if fqn.toString is appended with SEPARATOR.
      String myRFqn = appendFqn(myFqn);
      // TODO need further optimization in regex matching

      // Do it in reverse order such that children fqn gets matched first.
      for (int i = (regions.length - 1); i >= 0; i--)
      {
         String fqn = regions[i].getFqn();
         if (myRFqn.startsWith(fqn)) return regions[i];
      }

      if (log_.isTraceEnabled())
      {
         log_.trace("getRegion(): not user-specified region found for this fqn- " + myFqn
         + " will use the global default region");
      }
      return (Region) regionMap_.get(DEFAULT_REGION);
   }

   public Region[] getRegions()
   {
      // optimization
      if (regions_ != null && regions_.length == regionMap_.size())
         return regions_;

      Object[] objs = regionMap_.values().toArray();
      Region[] regions = new Region[objs.length];
      for (int i = 0; i < objs.length; i++)
      {
         regions[i] = (Region) objs[i];
      }

      if (log_.isDebugEnabled())
      {
         log_.debug("getRegions(): size of region " + regions.length);
      }

      regions_ = regions;
      return regions;
   }

   /**
    * Check for conflict in the current regions. There is a conflict
    * <p/>
    * if fqn is any parent fqn of the current regions.
    *
    * @param myFqn Current fqn for potential new region.
    * @throws RegionNameConflictException to indicate a region name conflict has ocurred.
    */
   public void checkConflict(String myFqn) throws RegionNameConflictException
   {
      // Step x. Loop thru the region map and compare the fqn. Order is important.
      // Not very efficient if there is a lot of regions.
      // First comer wins. E.g., /a/b/c and then /a/b/c will have /a/b in different
      // region than /a/b. But if /a/b and /a/b/c, then we will /a/b/c will be
      // the same region as /a/b. That is, we check if a fqn is within a region
      // by looping thru regions and then check if my fqn is a child of region
      // fqn. It it is, viola, and take this and exit.
      Region[] regions = getRegions();
      for (int i = 0; i < regions.length; i++)
      {
         String fqn = regions[i].getFqn();
         if (myFqn.equals(fqn) || myFqn.startsWith(fqn))
         { // fqn is a child of myFqn.
            throw new RegionNameConflictException("RegionManager.checkConflict(): new region fqn "
            + myFqn + " is in conflict with current region fqn- " + fqn);
         }
      }
      // We are clear then.
   }
}
