/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program 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 3 of the License, or (at your option) any later version.
 * This program 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 program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.services.cache.concurrent;

import org.exoplatform.services.log.Log;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Really the cache state (we need it because of the clear cache consistency).
 */
class CacheState<K extends Serializable, V>
{

   private final Log log;

   private final ConcurrentFIFOExoCache<K, V> config;

   final ConcurrentHashMap<K, ObjectRef<K, V>> map;

   final Queue<ObjectRef<K, V>> queue;

   CacheState(ConcurrentFIFOExoCache<K, V> config, Log log)
   {
      this.log = log;
      this.config = config;
      this.map = new ConcurrentHashMap<K, ObjectRef<K, V>>();
      this.queue = new SynchronizedQueue<ObjectRef<K, V>>(log);
   }

   public void assertConsistency()
   {
      if (queue instanceof SynchronizedQueue)
      {
         ((SynchronizedQueue)queue).assertConsistency();
      }
      int mapSize = map.size();
      int effectiveQueueSize = queue.size();
      if (effectiveQueueSize != mapSize)
      {
         throw new AssertionError("The map size is " + mapSize + " is different from the queue size "
            + effectiveQueueSize);
      }
   }

   public V get(Serializable name)
   {
      ObjectRef<K, V> entry = map.get(name);
      if (entry != null)
      {
         V o = entry.getObject();
         if (entry.isValid())
         {
            config.hits.incrementAndGet();
            config.onGet(entry.name, o);
            return o;
         }
         else
         {
            config.misses.incrementAndGet();
            if (map.remove(name, entry))
            {
               queue.remove(entry);
            }
            config.onExpire(entry.name, o);
         }
      }
      else
      {
         config.misses.incrementAndGet();
      }
      return null;
   }

   private boolean isTraceEnabled()
   {
      return log != null && log.isTraceEnabled();
   }

   private void trace(String message)
   {
      log.trace(message + " [" + Thread.currentThread().getName() + "]");
   }

   /**
    * Do a put with the provided expiration time.
    *
    * @param expirationTime the expiration time
    * @param name the cache key
    * @param obj the cached value
    */
   void put(long expirationTime, K name, V obj, boolean local)
   {
      boolean trace = isTraceEnabled();
      ObjectRef<K, V> nextRef = new SimpleObjectRef<K, V>(expirationTime, name, obj);
      ObjectRef<K, V> previousRef = map.put(name, nextRef);

      // Remove previous (promoted as first element)
      if (previousRef != null)
      {
         queue.remove(previousRef);
         if (trace)
         {
            trace("Replaced item=" + previousRef.serial + " with item=" + nextRef.serial + " in the map");
         }
      }
      else if (trace)
      {
         trace("Added item=" + nextRef.serial + " to map");
      }

      // Add to the queue
      queue.add(nextRef);

      // Perform eviction from queue
      ArrayList<ObjectRef<K, V>> evictedRefs = queue.trim(config.maxSize);
      if (evictedRefs != null)
      {
         for (ObjectRef<K, V> evictedRef : evictedRefs)
         {
            // We remove it from the map only if it was the same entry
            // it could have been removed concurrently by an explicit remove
            // or by a promotion
            map.remove(evictedRef.name, evictedRef);

            // Expiration callback
            config.onExpire(evictedRef.name, evictedRef.getObject());
         }
      }

      // Put callback
      if (local)
      {
         config.onPutLocal(name, obj);
      }
      else
      {
         config.onPut(name, obj);
      }
   }

   public V remove(Serializable name)
   {
      boolean trace = isTraceEnabled();
      ObjectRef<K, V> item = map.remove(name);
      if (item != null)
      {
         if (trace)
         {
            trace("Removed item=" + item.serial + " from the map going to remove it");
         }
         boolean removed = queue.remove(item);
         boolean valid = removed && item.isValid();
         V object = item.getObject();
         if (valid)
         {
            config.onRemove(item.name, object);
            return object;
         }
         else
         {
            config.onExpire(item.name, object);
            return null;
         }
      }
      else
      {
         return null;
      }
   }
}
