/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.hdfs.server.namenode;

import java.io.IOException;

import javax.annotation.Nonnull;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.fs.permission.FsAction;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.hdfs.protocol.CacheDirective;
import org.apache.hadoop.hdfs.protocol.CachePoolEntry;
import org.apache.hadoop.hdfs.protocol.CachePoolInfo;
import org.apache.hadoop.hdfs.protocol.CachePoolStats;
import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.util.IntrusiveCollection;

import com.google.common.base.Preconditions;

/**
 * A CachePool describes a set of cache resources being managed by the NameNode.
 * User caching requests are billed to the cache pool specified in the request.
 *
 * This is an internal class, only used on the NameNode.  For identifying or
 * describing a cache pool to clients, please use CachePoolInfo.
 * 
 * CachePools must be accessed under the FSNamesystem lock.
 */
@InterfaceAudience.Private
public final class CachePool {
  @Nonnull
  private final String poolName;

  @Nonnull
  private String ownerName;

  @Nonnull
  private String groupName;
  
  /**
   * Cache pool permissions.
   * 
   * READ permission means that you can list the cache directives in this pool.
   * WRITE permission means that you can add, remove, or modify cache directives
   *       in this pool.
   * EXECUTE permission is unused.
   */
  @Nonnull
  private FsPermission mode;

  /**
   * Maximum number of bytes that can be cached in this pool.
   */
  private long limit;

  /**
   * Maximum duration that a CacheDirective in this pool remains valid,
   * in milliseconds.
   */
  private long maxRelativeExpiryMs;

  private long bytesNeeded;
  private long bytesCached;
  private long filesNeeded;
  private long filesCached;

  public final static class DirectiveList
      extends IntrusiveCollection<CacheDirective> {
    private final CachePool cachePool;

    private DirectiveList(CachePool cachePool) {
      this.cachePool = cachePool;
    }

    public CachePool getCachePool() {
      return cachePool;
    }
  }

  @Nonnull
  private final DirectiveList directiveList = new DirectiveList(this);

  /**
   * Create a new cache pool based on a CachePoolInfo object and the defaults.
   * We will fill in information that was not supplied according to the
   * defaults.
   */
  static CachePool createFromInfoAndDefaults(CachePoolInfo info)
      throws IOException {
    UserGroupInformation ugi = null;
    String ownerName = info.getOwnerName();
    if (ownerName == null) {
      ugi = NameNode.getRemoteUser();
      ownerName = ugi.getShortUserName();
    }
    String groupName = info.getGroupName();
    if (groupName == null) {
      if (ugi == null) {
        ugi = NameNode.getRemoteUser();
      }
      groupName = ugi.getPrimaryGroupName();
    }
    FsPermission mode = (info.getMode() == null) ? 
        FsPermission.getCachePoolDefault() : info.getMode();
    long limit = info.getLimit() == null ?
        CachePoolInfo.DEFAULT_LIMIT : info.getLimit();
    long maxRelativeExpiry = info.getMaxRelativeExpiryMs() == null ?
        CachePoolInfo.DEFAULT_MAX_RELATIVE_EXPIRY :
        info.getMaxRelativeExpiryMs();
    return new CachePool(info.getPoolName(),
        ownerName, groupName, mode, limit, maxRelativeExpiry);
  }

  /**
   * Create a new cache pool based on a CachePoolInfo object.
   * No fields in the CachePoolInfo can be blank.
   */
  static CachePool createFromInfo(CachePoolInfo info) {
    return new CachePool(info.getPoolName(),
        info.getOwnerName(), info.getGroupName(),
        info.getMode(), info.getLimit(), info.getMaxRelativeExpiryMs());
  }

  CachePool(String poolName, String ownerName, String groupName,
      FsPermission mode, long limit, long maxRelativeExpiry) {
    Preconditions.checkNotNull(poolName);
    Preconditions.checkNotNull(ownerName);
    Preconditions.checkNotNull(groupName);
    Preconditions.checkNotNull(mode);
    this.poolName = poolName;
    this.ownerName = ownerName;
    this.groupName = groupName;
    this.mode = new FsPermission(mode);
    this.limit = limit;
    this.maxRelativeExpiryMs = maxRelativeExpiry;
  }

  public String getPoolName() {
    return poolName;
  }

  public String getOwnerName() {
    return ownerName;
  }

  public CachePool setOwnerName(String ownerName) {
    this.ownerName = ownerName;
    return this;
  }

  public String getGroupName() {
    return groupName;
  }

  public CachePool setGroupName(String groupName) {
    this.groupName = groupName;
    return this;
  }

  public FsPermission getMode() {
    return mode;
  }

  public CachePool setMode(FsPermission mode) {
    this.mode = new FsPermission(mode);
    return this;
  }

  public long getLimit() {
    return limit;
  }

  public CachePool setLimit(long bytes) {
    this.limit = bytes;
    return this;
  }

  public long getMaxRelativeExpiryMs() {
    return maxRelativeExpiryMs;
  }

  public CachePool setMaxRelativeExpiryMs(long expiry) {
    this.maxRelativeExpiryMs = expiry;
    return this;
  }

  /**
   * Get either full or partial information about this CachePool.
   *
   * @param fullInfo
   *          If true, only the name will be returned (i.e., what you 
   *          would get if you didn't have read permission for this pool.)
   * @return
   *          Cache pool information.
   */
  CachePoolInfo getInfo(boolean fullInfo) {
    CachePoolInfo info = new CachePoolInfo(poolName);
    if (!fullInfo) {
      return info;
    }
    return info.setOwnerName(ownerName).
        setGroupName(groupName).
        setMode(new FsPermission(mode)).
        setLimit(limit).
        setMaxRelativeExpiryMs(maxRelativeExpiryMs);
  }

  /**
   * Resets statistics related to this CachePool
   */
  public void resetStatistics() {
    bytesNeeded = 0;
    bytesCached = 0;
    filesNeeded = 0;
    filesCached = 0;
  }

  public void addBytesNeeded(long bytes) {
    bytesNeeded += bytes;
  }

  public void addBytesCached(long bytes) {
    bytesCached += bytes;
  }

  public void addFilesNeeded(long files) {
    filesNeeded += files;
  }

  public void addFilesCached(long files) {
    filesCached += files;
  }

  public long getBytesNeeded() {
    return bytesNeeded;
  }

  public long getBytesCached() {
    return bytesCached;
  }

  public long getBytesOverlimit() {
    return Math.max(bytesNeeded-limit, 0);
  }

  public long getFilesNeeded() {
    return filesNeeded;
  }

  public long getFilesCached() {
    return filesCached;
  }

  /**
   * Get statistics about this CachePool.
   *
   * @return   Cache pool statistics.
   */
  private CachePoolStats getStats() {
    return new CachePoolStats.Builder().
        setBytesNeeded(bytesNeeded).
        setBytesCached(bytesCached).
        setBytesOverlimit(getBytesOverlimit()).
        setFilesNeeded(filesNeeded).
        setFilesCached(filesCached).
        build();
  }

  /**
   * Returns a CachePoolInfo describing this CachePool based on the permissions
   * of the calling user. Unprivileged users will see only minimal descriptive
   * information about the pool.
   * 
   * @param pc Permission checker to be used to validate the user's permissions,
   *          or null
   * @return CachePoolEntry describing this CachePool
   */
  public CachePoolEntry getEntry(FSPermissionChecker pc) {
    boolean hasPermission = true;
    if (pc != null) {
      try {
        pc.checkPermission(this, FsAction.READ);
      } catch (AccessControlException e) {
        hasPermission = false;
      }
    }
    return new CachePoolEntry(getInfo(hasPermission), 
        hasPermission ? getStats() : new CachePoolStats.Builder().build());
  }

  public String toString() {
    return new StringBuilder().
        append("{ ").append("poolName:").append(poolName).
        append(", ownerName:").append(ownerName).
        append(", groupName:").append(groupName).
        append(", mode:").append(mode).
        append(", limit:").append(limit).
        append(", maxRelativeExpiryMs:").append(maxRelativeExpiryMs).
        append(" }").toString();
  }

  public DirectiveList getDirectiveList() {
    return directiveList;
  }
}