package basics.caching;

import java.util.Iterator;
import java.util.Set;
import basics.testing.Test;
import basics.time.LongSleep;

public class ExpiringCache
{
// protected static final Logger LOGGER = Logger.getLogger(ExpiringCache.class);
private long        millisUntilExpiration;
private LRUMap      map;
// Clear out old entries every few queries
private int         queryCount;
private static int  QUERY_OVERFLOW = 250;
private static int  MAX_ENTRIES    = 100;
private static long TIMEOUT        = 60 * 1000;

static class Entry
{
private long   timestamp;
private Object val;

Entry(long timestamp, Object val)
{
   this.timestamp = timestamp;
   this.val = val;
}

long timestamp()
{
   return timestamp;
}

void setTimestamp(long timestamp)
{
   this.timestamp = timestamp;
}

Object val()
{
   return val;
}

void setVal(Object val)
{
   this.val = val;
}
}

public ExpiringCache()
{
   this(TIMEOUT);
}

public void setTimeoutMillis(long millis)
{
   TIMEOUT = millis;
}

public ExpiringCache(int maxEntries, int maxQueriesUntilOverflow, long millisUntilExpiration)
{
   this.millisUntilExpiration = millisUntilExpiration;
   map = new LRUMap(maxEntries);
   ExpiringCache.QUERY_OVERFLOW = maxQueriesUntilOverflow;
   ExpiringCache.TIMEOUT = millisUntilExpiration;
}

private ExpiringCache(long millisUntilExpiration)
{
   this.millisUntilExpiration = millisUntilExpiration;
   map = new LRUMap(MAX_ENTRIES);
}

public synchronized Object get(String key)
{
   if(++queryCount >= QUERY_OVERFLOW)
   {
      cleanup();
   }
   Entry entry = entryFor(key);
   if(entry != null)
   {
      return entry.val();
   }
   return null;
}

public synchronized void put(String key, Object val)
{
   if(++queryCount >= QUERY_OVERFLOW)
   {
      cleanup();
   }
   Entry entry = entryFor(key);
   if(entry != null)
   {
      entry.setTimestamp(System.currentTimeMillis());
      entry.setVal(val);
   }
   else
   {
      map.put(key, new Entry(System.currentTimeMillis(), val));
   }
}

synchronized void clear()
{
   map.clear();
}

private Entry entryFor(String key)
{
   Entry entry = (Entry)map.get(key);
   if(entry != null)
   {
      long delta = System.currentTimeMillis() - entry.timestamp();
      if(delta < 0 || delta >= millisUntilExpiration)
      {
         map.remove(key);
         entry = null;
      }
   }
   return entry;
}

@SuppressWarnings("unchecked")
private void cleanup()
{
   Set keySet = map.keySet();
   // Avoid ConcurrentModificationExceptions
   String [] keys = new String [keySet.size()];
   int i = 0;
   for(Iterator iter = keySet.iterator(); iter.hasNext();)
   {
      String key = (String)iter.next();
      keys[i++] = key;
   }
   for(int j = 0; j < keys.length; j++)
   {
      entryFor(keys[j]);
   }
   queryCount = 0;
}

public String toString()
{
   StringBuilder s = new StringBuilder();
   s.append("size: " + map.size() + "/" + map.getMaximumSize());
   s.append(";queries: " + queryCount + "/" + MAX_ENTRIES);
   s.append(";timeout: " + TIMEOUT);
   return s.toString();
}

public static void unittest()
{
   LongSleep pause = new LongSleep();
   ExpiringCache ec = new ExpiringCache(3, 5, 10L);
   ec.put("a", new String("AAA"));
   ec.put("b", new String("BBB"));
   ec.put("c", new String("CCC"));
   Test.assertNotNull(ec.get("a"), "a missing");
   Test.assertNotNull(ec.get("b"), "b missing");
   Test.assertNotNull(ec.get("c"), "c missing");
   Test.assertNull(ec.get("d"), "having non existing d???");
   pause.sleep(4);
   ec.put("d", new String("DDD"));
   Test.assertNotNull(ec.get("d"), "new d missing");
   Test.assertNull(ec.get("a"), "a should be gone");
   pause.sleep(6);
   Test.assertNotNull(ec.get("d"), "d should still be there after 6 millis");
   Test.assertNull(ec.get("b"), "b should be gone after 11 ms");
   Test.assertNull(ec.get("c"), "c should be gone after 11 ms");
}
}
