/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.security.group;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/**
 * Class to map user to groups. It maintains a cache for user to groups mapping. The underlying
 * implementation depends on {@link GroupMappingService}.
 */
public class CachedGroupMapping implements GroupMappingService {
  /** The underlying implementation of GroupMappingService. */
  private final GroupMappingService mService;
  /** Whether the cache is enabled. Set timeout to non-positive value to disable cache. */
  private final boolean mCacheEnabled;
  /** A loading cache for user to groups mapping, refresh periodically. */
  private LoadingCache<String, List<String>> mCache;
  /** Max size of the cache. */
  private static final long MAXSIZE = 10000;

  /** Create an executor service that provide ListenableFuture instances. */
  private final ThreadFactory mThreadFactory = new ThreadFactoryBuilder()
      .setNameFormat("UserGroupMappingCachePool-%d").setDaemon(true).build();
  private final ExecutorService mParentExecutor = Executors.newSingleThreadExecutor(mThreadFactory);
  private final ListeningExecutorService mExecutorService =
      MoreExecutors.listeningDecorator(mParentExecutor);

  /**
   * Constructor with specified {@link GroupMappingService}. Initializes the cache if enabled.
   *
   * @param service group mapping service
   * @param groupMappingCacheTimeoutMs The timeout in millseconds before we should reload the cache
   */
  public CachedGroupMapping(GroupMappingService service, long groupMappingCacheTimeoutMs) {
    mService = service;
    mCacheEnabled = groupMappingCacheTimeoutMs > 0;
    if (mCacheEnabled) {
      mCache = CacheBuilder.newBuilder()
          // the maximum number of entries the cache may contain.
          .maximumSize(MAXSIZE)
          // active entries are eligible for automatic refresh once the specified time duration has
          // elapsed after the entry was last modified.
          .refreshAfterWrite(groupMappingCacheTimeoutMs, TimeUnit.MILLISECONDS)
          // each entry should be automatically removed from the cache once the specified time
          // duration has elapsed after the entry was last modified.
          .expireAfterWrite(10 * groupMappingCacheTimeoutMs, TimeUnit.MILLISECONDS)
          .build(new GroupMappingCacheLoader());
    }
  }

  /**
   * Class to reload cache from underlying user group mapping service implementation.
   */
  private class GroupMappingCacheLoader extends CacheLoader<String, List<String>> {
    /**
     * Constructs a new {@link GroupMappingCacheLoader}.
     */
    public GroupMappingCacheLoader() {}

    @Override
    public List<String> load(String user) throws IOException {
      return mService.getGroups(user);
    }

    @Override
    public ListenableFuture<List<String>> reload(final String user, List<String> oldValue)
        throws IOException {
      // Load values asynchronously.
      ListenableFuture<List<String>> listenableFuture = mExecutorService.submit(
          () -> load(user)
      );
      return listenableFuture;
    }
  }

  @Override
  public List<String> getGroups(String user) throws IOException {
    if (!mCacheEnabled) {
      return mService.getGroups(user);
    }
    try {
      return mCache.get(user);
    } catch (ExecutionException e) {
      throw new IOException(e);
    }
  }
}
