/**
 * 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.impl;

import org.apache.commons.lang3.StringUtils;
import org.exoplatform.commons.utils.ClassLoading;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.management.annotations.ManagedBy;
import org.exoplatform.services.cache.*;
import org.exoplatform.services.cache.invalidation.AsyncInvalidationExoCache;
import org.exoplatform.services.cache.invalidation.InvalidationExoCache;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.*;

/**
 * Created by The eXo Platform SAS. Author : Tuan Nguyen
 * tuan08@users.sourceforge.net Sat, Sep 13, 2003 @ Time: 1:12:22 PM
 */
@SuppressWarnings("deprecation")
@ManagedBy(CacheServiceManaged.class)
public class CacheServiceImpl implements CacheService
{
   /**
    * Logger.
    */
   private static final Log LOG = ExoLogger.getLogger("exo.kernel.component.cache.CacheServiceImpl");

   private final ExoCacheFactory DEFAULT_FACTORY = new SimpleExoCacheFactory();

   private final HashMap<String, ExoCacheConfig> configs_ = new HashMap<String, ExoCacheConfig>();

   private final ConcurrentHashMap<String, FutureExoCacheCreationTask> cacheMap_ =
      new ConcurrentHashMap<String, FutureExoCacheCreationTask>();
   
   private final ExoCacheConfig defaultConfig_;

   private final LoggingCacheListener loggingListener_;

   private final ExoCacheFactory factory_;

   CacheServiceManaged managed;
   
   public CacheServiceImpl(InitParams params) throws Exception
   {
      this(params, null);
   }

   public CacheServiceImpl(InitParams params, ExoCacheFactory factory) throws Exception
   {
      List<ExoCacheConfig> configs = params.getObjectParamValues(ExoCacheConfig.class);
      for (ExoCacheConfig config : configs)
      {
         configs_.put(config.getName(), config);
      }
      defaultConfig_ = configs_.get("default");
      loggingListener_ = new LoggingCacheListener();
      factory_ = factory == null ? DEFAULT_FACTORY : factory;
   }

   public void addExoCacheConfig(ComponentPlugin plugin)
   {
      addExoCacheConfig((ExoCacheConfigPlugin)plugin);
   }

   public void addExoCacheConfig(ExoCacheConfigPlugin plugin)
   {
      List<ExoCacheConfig> configs = plugin.getConfigs();
      for (ExoCacheConfig config : configs)
      {
         configs_.put(config.getName(), config);
      }
   }

   @SuppressWarnings("unchecked")
   public <K extends Serializable, V> ExoCache<K, V> getCacheInstance(final String region)
   {
      if (region == null)
      {
         throw new IllegalArgumentException("region cannot be null");
      }
      if (region.length() == 0)
      {
         throw new IllegalArgumentException("region cannot be empty");
      }
      FutureExoCacheCreationTask creationTask = cacheMap_.get(region);
      if (creationTask == null)
      {
         Callable<ExoCache<? extends Serializable,?>> task = new Callable<ExoCache<? extends Serializable,?>>()
         {
            public ExoCache<? extends Serializable, ?> call() throws Exception
            {
               return createCacheInstance(region);
            }
         };
         creationTask = new FutureExoCacheCreationTask(task);
         FutureExoCacheCreationTask existingTask = cacheMap_.putIfAbsent(region, creationTask);
         if (existingTask != null)
         {
            creationTask = existingTask;
         }
         else
         {
            creationTask.run();
         }
      }
      try
      {
         return (ExoCache<K, V>)creationTask.get();
      }
      catch (CancellationException e)
      {
         cacheMap_.remove(region, creationTask);
      }
      catch (InterruptedException e)
      {
         Thread.currentThread().interrupt();
      }
      catch (ExecutionException e)
      {
         LOG.error("Could not create the cache for the region '" + region + "'", e.getCause());
      }
      return null;
   }

   @SuppressWarnings({"rawtypes", "unchecked"})
   private ExoCache<? extends Serializable, ?> createCacheInstance(String region) throws Exception
   {
      ExoCacheConfig config = configs_.get(region);
      if (config == null)
         config = defaultConfig_;

      // Ensure the configuration integrity
      final ExoCacheConfig safeConfig = config.clone();
      // Set the region as name 
      safeConfig.setName(region);
      
      ExoCache simple = null;
      if (factory_ != DEFAULT_FACTORY && !safeConfig.isInvalidated())
      {
         // The implementation exists and the config is not a sub class of ExoCacheConfig
         // we assume that we expect to use the default cache factory
         try
         {
            // We check if the given implementation is a known class
            Class<?> implClass = null;
            if(safeConfig.getClass().isAssignableFrom(ExoCacheConfig.class) && safeConfig.getImplementation() != null)
            {
               implClass = ClassLoading.loadClass(safeConfig.getImplementation(), this);
            }
            else if (safeConfig.getCacheMode().isLocal() && defaultConfig_!= null &&
                    StringUtils.isNotBlank(defaultConfig_.getImplementation()) )
            {
               implClass = ClassLoading.loadClass(defaultConfig_.getImplementation() , this);
               if(implClass != null)
               {
                  safeConfig.setImplementation(defaultConfig_.getImplementation());
               }
            }
            // Implementation is an existing class
            if (implClass != null && ExoCache.class.isAssignableFrom(implClass))
            {
               // The implementation is a sub class of eXo Cache so we use the default factory
               simple = DEFAULT_FACTORY.createCache(safeConfig);               
            }
         }
         catch (ClassNotFoundException e)
         {
            if (LOG.isTraceEnabled())
            {
               LOG.trace("An exception occurred: " + e.getMessage());
            }
         }
      }
      if (simple == null)
      {
         // We use the configured cache factory
         simple = factory_.createCache(safeConfig);
      }
      
      if (managed != null)
      {
         if(! (safeConfig.getCacheMode() == CacheMode.ASYNCINVALIDATION))
         {
            managed.registerCache(simple);
         }
      }
      // If the flag avoid value replication (sync invalidation) or async invalidation is enabled and the
      // cache is replicated we wrap the eXo cache instance into an InvalidationExoCache
      // to enable the invalidation

      if(safeConfig.isInvalidated() && safeConfig.getCacheMode().isSync())
      {
         return safeConfig.getImplementation() != null ?
                 getCustomInstance(safeConfig.getImplementation(), safeConfig) : new InvalidationExoCache(simple);
      }
      else if (safeConfig.isInvalidated() && !safeConfig.getCacheMode().isSync())
      {
         ExoCache exoCache = safeConfig.getImplementation() != null ?
                 getCustomInstance(safeConfig.getImplementation(), safeConfig) :new AsyncInvalidationExoCache(simple);
         if (managed != null)
         {
            managed.registerCache(exoCache);
         }
         return exoCache;
      }
      return simple;
   }

   public Collection<ExoCache<? extends Serializable, ?>> getAllCacheInstances()
   {
      Collection<ExoCache<? extends Serializable, ?>> caches = 
         new ArrayList<ExoCache<? extends Serializable,?>>(cacheMap_.size());
      for (FutureTask<ExoCache<? extends Serializable,?>> task : cacheMap_.values())
      {
         ExoCache<? extends Serializable, ?> cache = null;
         try
         {
            cache = task.get();
         }
         catch (CancellationException e)
         {
            if (LOG.isTraceEnabled())
            {
               LOG.trace("An exception occurred: " + e.getMessage());
            }
         }
         catch (InterruptedException e)
         {
            if (LOG.isTraceEnabled())
            {
               LOG.trace("An exception occurred: " + e.getMessage());
            }
         }
         catch (ExecutionException e)
         {
            if (LOG.isTraceEnabled())
            {
               LOG.trace("An exception occurred: " + e.getMessage());
            }
         }
         if (cache != null)
         {
            caches.add(cache);            
         }
      }
      return caches;
   }

   /**
    * Default implementation of an {@link org.exoplatform.services.cache.ExoCacheFactory}
    */
   private class SimpleExoCacheFactory implements ExoCacheFactory
   {

      /**
       * {@inheritDoc}
       */
      @SuppressWarnings({"rawtypes", "unchecked"})
      public ExoCache createCache(ExoCacheConfig config) throws ExoCacheInitException
      {
         final ExoCache simple = createCacheInstance(config);
         simple.setName(config.getName());
         simple.setLabel(config.getLabel());
         simple.setMaxSize(config.getMaxSize());
         simple.setLiveTime(config.getLiveTime());
         simple.setLogEnabled(config.isLogEnabled());
         if (simple.isLogEnabled())
         {
            simple.addCacheListener(loggingListener_);
         }
         return simple;
      }

      /**
       * Create a new instance of ExoCache according to the given configuration
       * @param config the ExoCache configuration
       * @return a new instance of ExoCache
       * @throws ExoCacheInitException if any exception happens while initializing the cache
       */
      @SuppressWarnings("rawtypes")
      private ExoCache createCacheInstance(ExoCacheConfig config) throws ExoCacheInitException
      {
         if (config.getImplementation() == null)
         {
            // No implementation has been defined
            return new SimpleExoCache();
         }
         else
         {
            // An implementation has been defined
            try
            {
               final Class<?> clazz = ClassLoading.loadClass(config.getImplementation(), this);
               return (ExoCache)clazz.newInstance();
            }
            catch (ExceptionInInitializerError e)
            {
               throw new ExoCacheInitException("Cannot create instance of ExoCache of type "
                  + config.getImplementation(), e);
            }
            catch (SecurityException e)
            {
               throw new ExoCacheInitException("Cannot create instance of ExoCache of type "
                  + config.getImplementation(), e);
            }
            catch (ClassNotFoundException e)
            {
               throw new ExoCacheInitException("Cannot create instance of ExoCache of type "
                  + config.getImplementation(), e);
            }
            catch (InstantiationException e)
            {
               throw new ExoCacheInitException("Cannot create instance of ExoCache of type "
                  + config.getImplementation(), e);
            }
            catch (IllegalAccessException e)
            {
               throw new ExoCacheInitException("Cannot create instance of ExoCache of type "
                  + config.getImplementation(), e);
            }
         }
      }
   }
   
   /**
    * This class is used to reduce the contention when the cache is already created
    */
   private static class FutureExoCacheCreationTask extends FutureTask<ExoCache<? extends Serializable, ?>>
   {

      private volatile ExoCache<? extends Serializable, ?> cache;
      
      /**
       * @param callable
       */
      public FutureExoCacheCreationTask(Callable<ExoCache<? extends Serializable, ?>> callable)
      {
         super(callable);
      }

      @Override
      public ExoCache<? extends Serializable, ?> get() throws InterruptedException, ExecutionException
      {
         if (cache != null)
         {
            return cache;
         }
         return cache = super.get();
      }
   }

   /**
    *  Creates a new instance of the custom exo Cache implementation
    */
   private ExoCache<? extends Serializable, ?> getCustomInstance(String clazz, ExoCacheConfig config) throws Exception
   {
      // We check if the given implementation is a known class
      Class<?> implClass = ClassLoading.loadClass(clazz, this);
      if (implClass != null && ExoCache.class.isAssignableFrom(implClass))
      {
         return (ExoCache<? extends Serializable, ?>)
                 implClass.getConstructor(ExoCacheConfig.class).newInstance(config);
      }
      return null;
   }
}
