package org.apache.lucene.search;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeSet;

import org.apache.lucene.util.ThreadInterruptedException;

/**
 * Filter caching singleton. It can be used to save filters locally for reuse.
 * This class makes it possible to cache Filters even when using RMI, as it
 * keeps the cache on the searcher side of the RMI connection.
 * 
 * Also could be used as a persistent storage for any filter as long as the
 * filter provides a proper hashCode(), as that is used as the key in the cache.
 * 
 * The cache is periodically cleaned up from a separate thread to ensure the
 * cache doesn't exceed the maximum size.
 * 
 * @deprecated used by remote package which is deprecated as well. You should
 *             use {@link CachingWrapperFilter} if you wish to cache
 *             {@link Filter}s.
 */
@Deprecated
public class FilterManager {

  protected static FilterManager manager;
  
  /** The default maximum number of Filters in the cache */
  protected static final int  DEFAULT_CACHE_CLEAN_SIZE = 100;
  /** The default frequency of cache cleanup */
  protected static final long DEFAULT_CACHE_SLEEP_TIME = 1000 * 60 * 10;

  /** The cache itself */
  protected Map<Integer,FilterItem>           cache;
  /** Maximum allowed cache size */
  protected int           cacheCleanSize;
  /** Cache cleaning frequency */
  protected long          cleanSleepTime;
  /** Cache cleaner that runs in a separate thread */
  protected FilterCleaner filterCleaner;

  public synchronized static FilterManager getInstance() {
    if (manager == null) {
      manager = new FilterManager();
    }
    return manager;
  }

  /**
   * Sets up the FilterManager singleton.
   */
  protected FilterManager() {
    cache            = new HashMap<Integer,FilterItem>();
    cacheCleanSize   = DEFAULT_CACHE_CLEAN_SIZE; // Let the cache get to 100 items
    cleanSleepTime   = DEFAULT_CACHE_SLEEP_TIME; // 10 minutes between cleanings

    filterCleaner   = new FilterCleaner();
    Thread fcThread = new Thread(filterCleaner);
    // set to be a Daemon so it doesn't have to be stopped
    fcThread.setDaemon(true);
    fcThread.start();
  }
  
  /**
   * Sets the max size that cache should reach before it is cleaned up
   * @param cacheCleanSize maximum allowed cache size
   */
  public void setCacheSize(int cacheCleanSize) {
    this.cacheCleanSize = cacheCleanSize;
  }

  /**
   * Sets the cache cleaning frequency in milliseconds.
   * @param cleanSleepTime cleaning frequency in milliseconds
   */
  public void setCleanThreadSleepTime(long cleanSleepTime) {
    this.cleanSleepTime  = cleanSleepTime;
  }

  /**
   * Returns the cached version of the filter.  Allows the caller to pass up
   * a small filter but this will keep a persistent version around and allow
   * the caching filter to do its job.
   * 
   * @param filter The input filter
   * @return The cached version of the filter
   */
  public Filter getFilter(Filter filter) {
    synchronized(cache) {
      FilterItem fi = null;
      fi = cache.get(Integer.valueOf(filter.hashCode()));
      if (fi != null) {
        fi.timestamp = new Date().getTime();
        return fi.filter;
      }
      cache.put(Integer.valueOf(filter.hashCode()), new FilterItem(filter));
      return filter;
    }
  }

  /**
   * Holds the filter and the last time the filter was used, to make LRU-based
   * cache cleaning possible.
   * TODO: Clean this up when we switch to Java 1.5
   */
  protected class FilterItem {
    public Filter filter;
    public long   timestamp;

    public FilterItem (Filter filter) {        
      this.filter = filter;
      this.timestamp = new Date().getTime();
    }
  }


  /**
   * Keeps the cache from getting too big.
   * If we were using Java 1.5, we could use LinkedHashMap and we would not need this thread
   * to clean out the cache.
   * 
   * The SortedSet sortedFilterItems is used only to sort the items from the cache,
   * so when it's time to clean up we have the TreeSet sort the FilterItems by
   * timestamp.
   * 
   * Removes 1.5 * the numbers of items to make the cache smaller.
   * For example:
   * If cache clean size is 10, and the cache is at 15, we would remove (15 - 10) * 1.5 = 7.5 round up to 8.
   * This way we clean the cache a bit more, and avoid having the cache cleaner having to do it frequently.
   */
  protected class FilterCleaner implements Runnable  {

    private boolean running = true;
    private TreeSet<Map.Entry<Integer,FilterItem>> sortedFilterItems;

    public FilterCleaner() {
      sortedFilterItems = new TreeSet<Map.Entry<Integer,FilterItem>>(new Comparator<Map.Entry<Integer,FilterItem>>() {
        public int compare(Map.Entry<Integer,FilterItem> a, Map.Entry<Integer,FilterItem> b) {
            FilterItem fia = a.getValue();
            FilterItem fib = b.getValue();
            if ( fia.timestamp == fib.timestamp ) {
              return 0;
            }
            // smaller timestamp first
            if ( fia.timestamp < fib.timestamp ) {
              return -1;
            }
            // larger timestamp last
            return 1;
          
        }
      });
    }

    public void run () {
      while (running) {

        // sort items from oldest to newest 
        // we delete the oldest filters 
        if (cache.size() > cacheCleanSize) {
          // empty the temporary set
          sortedFilterItems.clear();
          synchronized (cache) {
            sortedFilterItems.addAll(cache.entrySet());
            Iterator<Map.Entry<Integer,FilterItem>> it = sortedFilterItems.iterator();
            int numToDelete = (int) ((cache.size() - cacheCleanSize) * 1.5);
            int counter = 0;
            // loop over the set and delete all of the cache entries not used in a while
            while (it.hasNext() && counter++ < numToDelete) {
              Map.Entry<Integer,FilterItem> entry = it.next();
              cache.remove(entry.getKey());
            }
          }
          // empty the set so we don't tie up the memory
          sortedFilterItems.clear();
        }
        // take a nap
        try {
          Thread.sleep(cleanSleepTime);
        } catch (InterruptedException ie) {
          throw new ThreadInterruptedException(ie);
        }
      }
    }
  }
}
