/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * 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.exoplatform.services.cache.concurrent;

import org.exoplatform.services.cache.CacheListener;
import org.exoplatform.services.cache.CachedObjectSelector;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.log.Log;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * An {@link org.exoplatform.services.cache.ExoCache} implementation based on {@link java.util.concurrent.ConcurrentHashMap}
 * that minimize locking. Cache entries are maintained in a fifo list that is used for the fifo eviction policy.
 *
 * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
 * @version $Revision$
 */
public class ConcurrentFIFOExoCache<K extends Serializable, V> implements ExoCache<K, V>
{

   private static int DEFAULT_MAX_SIZE = 50;

   private final Log log;

   private volatile long liveTimeMillis;

   volatile int maxSize;

   private CopyOnWriteArrayList<ListenerContext<K, V>> listeners;

   private CacheState<K, V> state;

   AtomicInteger hits = new AtomicInteger();

   AtomicInteger misses = new AtomicInteger();

   private String label;

   private String name;

   private boolean logEnabled = false;

   public ConcurrentFIFOExoCache()
   {
      this(DEFAULT_MAX_SIZE);
   }

   public ConcurrentFIFOExoCache(Log log)
   {
      this(DEFAULT_MAX_SIZE, log);
   }

   public ConcurrentFIFOExoCache(int maxSize)
   {
      this(null, maxSize);
   }

   public ConcurrentFIFOExoCache(int maxSize, Log log)
   {
      this(null, maxSize, log);
   }

   public ConcurrentFIFOExoCache(String name, int maxSize)
   {
      this(name, maxSize, null);
   }

   public ConcurrentFIFOExoCache(String name, int maxSize, Log log)
   {
      this.maxSize = maxSize;
      this.name = name;
      this.state = new CacheState<K, V>(this, log);
      this.liveTimeMillis = -1;
      this.log = log;
      this.listeners = new CopyOnWriteArrayList<ListenerContext<K, V>>();
   }

   public void assertConsistent()
   {
      state.assertConsistency();
   }

   public String getName()
   {
      return name;
   }

   public void setName(String s)
   {
      name = s;
   }

   public String getLabel()
   {
      if (label == null)
      {
         if (name.length() > 30)
         {
            String shortLabel = name.substring(name.lastIndexOf(".") + 1);
            setLabel(shortLabel);
            return shortLabel;
         }
         return name;
      }
      return label;
   }

   public void setLabel(String name)
   {
      label = name;
   }

   public long getLiveTime()
   {
      long tmp = getLiveTimeMillis();
      return tmp == -1 ? -1 : tmp / 1000;
   }

   public void setLiveTime(long period)
   {
      setLiveTimeMillis(period * 1000);
   }

   public long getLiveTimeMillis()
   {
      return liveTimeMillis;
   }

   public void setLiveTimeMillis(long liveTimeMillis)
   {
      if (liveTimeMillis < 0)
      {
         liveTimeMillis = -1;
      }
      this.liveTimeMillis = liveTimeMillis;
   }

   public int getMaxSize()
   {
      return maxSize;
   }

   public void setMaxSize(int max)
   {
      this.maxSize = max;
   }

   public V get(Serializable name)
   {
      if (name == null)
      {
         return null;
      }
      return state.get(name);
   }

   public void put(K name, V obj)
   {
      if (name == null)
      {
         throw new IllegalArgumentException("No null cache key accepted");
      }
      if (liveTimeMillis != 0)
      {
         long expirationTime = liveTimeMillis > 0 ? System.currentTimeMillis() + liveTimeMillis : Long.MAX_VALUE;
         state.put(expirationTime, name, obj, false);
      }
   }

   public void putLocal(K name, V obj)
   {
      if (name == null)
      {
         throw new IllegalArgumentException("No null cache key accepted");
      }
      if (liveTimeMillis != 0)
      {
         long expirationTime = liveTimeMillis > 0 ? System.currentTimeMillis() + liveTimeMillis : Long.MAX_VALUE;
         state.put(expirationTime, name, obj, true);
      }
   }

   public void putMap(Map<? extends K, ? extends V> objs)
   {
      if (objs == null)
      {
         throw new IllegalArgumentException("No null map accepted");
      }
      long expirationTime = liveTimeMillis > 0 ? System.currentTimeMillis() + liveTimeMillis : Long.MAX_VALUE;
      for (Serializable name : objs.keySet())
      {
         if (name == null)
         {
            throw new IllegalArgumentException("No null cache key accepted");
         }
      }
      for (Map.Entry<? extends K, ? extends V> entry : objs.entrySet())
      {
         state.put(expirationTime, entry.getKey(), entry.getValue(), false);
      }
   }

    public V remove(Serializable name)
   {
      if (name == null)
      {
         throw new IllegalArgumentException("No null cache key accepted");
      }
      return state.remove(name);
   }

   public List<? extends V> getCachedObjects()
   {
      LinkedList<V> list = new LinkedList<V>();
      for (ObjectRef<K, V> objectRef : state.map.values())
      {
         V object = objectRef.getObject();
         if (objectRef.isValid())
         {
            list.add(object);
         }
      }
      return list;
   }

   public List<? extends V> removeCachedObjects()
   {
      List<? extends V> list = getCachedObjects();
      clearCache();
      return list;
   }

   public void clearCache()
   {
      state = new CacheState<K, V>(this, log);
   }

   public void select(CachedObjectSelector<? super K, ? super V> selector) throws Exception
   {
      if (selector == null)
      {
         throw new IllegalArgumentException("No null selector");
      }
      for (Map.Entry<K, ObjectRef<K, V>> entry : state.map.entrySet())
      {
         K key = entry.getKey();
         ObjectRef<K, V> info = entry.getValue();
         if (selector.select(key, info))
         {
            selector.onSelect(this, key, info);
         }
      }
   }

   public int getCacheSize()
   {
      return state.queue.size();
   }

   public int getCacheHit()
   {
      return hits.get();
   }

   public int getCacheMiss()
   {
      return misses.get();
   }

   public synchronized void addCacheListener(CacheListener<? super K, ? super V> listener)
   {
      if (listener == null)
      {
         throw new IllegalArgumentException("The listener cannot be null");
      }
      listeners.add(new ListenerContext<K, V>(listener, this));
   }

   public boolean isLogEnabled()
   {
      return logEnabled;
   }

   public void setLogEnabled(boolean logEnabled)
   {
      this.logEnabled = logEnabled;
   }

   //

   public void onExpire(K key, V obj)
   {
      if (!listeners.isEmpty())
         for (ListenerContext<K, V> context : listeners)
            context.onExpire(key, obj);
   }

   public void onRemove(K key, V obj)
   {
      if (!listeners.isEmpty())
         for (ListenerContext<K, V> context : listeners)
            context.onRemove(key, obj);
   }

   public void onPut(K key, V obj)
   {
      if (!listeners.isEmpty())
         for (ListenerContext<K, V> context : listeners)
            context.onPut(key, obj);
   }

   public void onPutLocal(K key, V obj)
   {
      if (!listeners.isEmpty())
         for (ListenerContext<K, V> context : listeners)
            context.onPutLocal(key, obj);
   }

   public void onGet(K key, V obj)
   {
      if (!listeners.isEmpty())
         for (ListenerContext<K, V> context : listeners)
            context.onGet(key, obj);
   }

   public void onClearCache()
   {
      if (!listeners.isEmpty())
         for (ListenerContext<K, V> context : listeners)
            context.onClearCache();
   }

   public List<ListenerContext<K, V>> getListeners() {
      return listeners;
   }

}