/*
 * 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.
 */
package org.exoplatform.services.jcr.impl.core.query.lucene;

import org.apache.commons.collections.map.LRUMap;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

/**
 * Implements a Document number cache with a fixed size and a LRU strategy.
 */
final class DocNumberCache
{

   /**
    * Logger instance for this class.
    */
   private static final Log log                 = ExoLogger.getLogger("exo.jcr.component.core.DocNumberCache");

   /**
    * Log cache statistics at most every 10 seconds.
    */
   private static final long LOG_INTERVAL = 1000 * 10;

   /**
    * The number of cache segments.
    */
   private static final int CACHE_SEGMENTS = 0x10;

   /**
    * Mask to calculate segment number.
    */
   private static final int CACHE_SEGMENTS_MASK = CACHE_SEGMENTS - 1;

   /**
    * LRU Maps where key=uuid value=reader;docNumber
    */
   private final LRUMap[] docNumbers = new LRUMap[CACHE_SEGMENTS];

   /**
    * Timestamp of the last cache statistics log.
    */
   private long lastLog;

   /**
    * Cache misses.
    */
   private long misses;

   /**
    * Cache accesses;
    */
   private long accesses;

   /**
    * Creates a new <code>DocNumberCache</code> with a limiting
    * <code>size</code>.
    *
    * @param size the cache limit.
    */
   DocNumberCache(int size)
   {
      size = size / CACHE_SEGMENTS;
      if (size < 0x40)
      {
         // minimum size is 0x40 * 0x10 = 1024
         size = 0x40;
      }
      for (int i = 0; i < docNumbers.length; i++)
      {
         docNumbers[i] = new LRUMap(size);
      }
   }

   /**
    * Puts a document number into the cache using a uuid as key. An entry is
    * only overwritten if the according reader is younger than the reader
    * associated with the existing entry.
    *
    * @param uuid the key.
    * @param reader the index reader from where the document number was read.
    * @param n the document number.
    */
   void put(String uuid, CachingIndexReader reader, int n)
   {
      LRUMap cacheSegment = docNumbers[getSegmentIndex(uuid.charAt(0))];
      //UUID key = UUID.fromString(uuid);
      String key = uuid;
      synchronized (cacheSegment)
      {
         Entry e = (Entry)cacheSegment.get(key);
         if (e != null)
         {
            // existing entry
            // ignore if reader is older than the one in entry
            if (reader.getCreationTick() <= e.creationTick)
            {
               if (log.isDebugEnabled())
               {
                  log.debug("Ignoring put(). New entry is not from a newer reader. " + "existing: " + e.creationTick
                     + ", new: " + reader.getCreationTick());
               }
               e = null;
            }
         }
         else
         {
            // entry did not exist
            e = new Entry(reader.getCreationTick(), n);
         }

         if (e != null)
         {
            cacheSegment.put(key, e);
         }
      }
   }

   /**
    * Returns the cache entry for <code>uuid</code>, or <code>null</code> if
    * no entry exists for <code>uuid</code>.
    *
    * @param uuid the key.
    * @return cache entry or <code>null</code>.
    */
   Entry get(String uuid)
   {
      LRUMap cacheSegment = docNumbers[getSegmentIndex(uuid.charAt(0))];
      // uuid may be invalid
      //        if (uuid.length() != UUID.UUID_FORMATTED_LENGTH) {
      //            return null;
      //        }
      String key = uuid;
      Entry entry;
      synchronized (cacheSegment)
      {
         entry = (Entry)cacheSegment.get(key);
      }
      if (log.isDebugEnabled())
      {
         accesses++;
         if (entry == null)
         {
            misses++;
         }
         // log at most after 1000 accesses and every 10 seconds
         if (accesses > 1000 && System.currentTimeMillis() - lastLog > LOG_INTERVAL)
         {
            long ratio = 100;
            if (misses != 0)
            {
               ratio -= misses * 100L / accesses;
            }
            StringBuilder statistics = new StringBuilder();
            int inUse = 0;
            for (int i = 0; i < docNumbers.length; i++)
            {
               inUse += docNumbers[i].size();
            }
            statistics.append("size=").append(inUse);
            statistics.append("/").append(docNumbers[0].maxSize() * CACHE_SEGMENTS);
            statistics.append(", #accesses=").append(accesses);
            statistics.append(", #hits=").append((accesses - misses));
            statistics.append(", #misses=").append(misses);
            statistics.append(", cacheRatio=").append(ratio).append("%");
            log.debug(statistics.toString());
            accesses = 0;
            misses = 0;
            lastLog = System.currentTimeMillis();
         }
      }
      return entry;
   }

   /**
    * Returns the segment index for character c.
    */
   private static int getSegmentIndex(char c)
   {
      if (c > '9')
      {
         c += 9;
      }
      return c & CACHE_SEGMENTS_MASK;
   }

   public static final class Entry
   {

      /**
       * The creation tick of the IndexReader.
       */
      final long creationTick;

      /**
       * The document number.
       */
      final int doc;

      Entry(long creationTick, int doc)
      {
         this.creationTick = creationTick;
         this.doc = doc;
      }
   }
}
