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

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;

import java.io.Serializable;
import java.util.BitSet;
import java.util.Set;
import javax.annotation.concurrent.NotThreadSafe;

/**
 * Permitted actions of an entry in {@link AccessControlList}.
 */
@NotThreadSafe
public final class AclActions implements Serializable {
  private static final long serialVersionUID = 4500558548535992938L;

  // TODO(ohboring): have a static default AclActions object, and then copy on write.
  /**
   * Initial bits of the actions bitset.
   * Since most of the time, there are at most 3 actions: read, write, and execute,
   * so it's set to 3.
   */
  private static final int ACTIONS_BITSET_INITIAL_BITS = 3;

  /** An index of the bitset equals an ordinal of an action in {@link AclAction}. */
  private BitSet mActions;

  /**
   * Creates a new instance where no action is permitted.
   */
  public AclActions() {
    mActions = new BitSet(ACTIONS_BITSET_INITIAL_BITS);
  }

  /**
   * Creates a copy of actions.
   *
   * @param actions the actions to be copied from
   */
  public AclActions(AclActions actions) {
    mActions = (BitSet) actions.mActions.clone();
  }

  /**
   * Creats a new instance with initial permitted actions.
   *
   * @param actions the initial setEntry of permitted actions which is copied from
   */
  public AclActions(Set<AclAction> actions) {
    mActions = new BitSet(ACTIONS_BITSET_INITIAL_BITS);
    for (AclAction action : actions) {
      mActions.set(action.ordinal());
    }
  }

  /**
   * @return an immutable copy of permitted actions
   */
  public Set<AclAction> getActions() {
    ImmutableSet.Builder<AclAction> builder = ImmutableSet.builder();
    for (int i = mActions.nextSetBit(0); i >= 0; i = mActions.nextSetBit(i + 1)) {
      builder.add(AclAction.ofOrdinal(i));
    }
    return builder.build();
  }

  /**
   * Updates permitted actions based on the mode bits.
   *
   * For example, if bits imply READ, then READ is added to permitted actions, otherwise,
   * READ is removed from permitted actions.
   * Same logic applies to WRITE and EXECUTE.
   *
   * @param bits the mode bits
   */
  public void updateByModeBits(Mode.Bits bits) {
    // Each index equals to corresponding AclAction's ordinal.
    // E.g. Mode.Bits.READ corresponds to AclAction.READ, the former has index 0 in indexedBits,
    // the later has ordinal 0 in AclAction.
    Mode.Bits[] indexedBits = new Mode.Bits[]{
        Mode.Bits.READ, Mode.Bits.WRITE, Mode.Bits.EXECUTE
    };
    for (int i = 0; i < 3; i++) {
      if (bits.imply(indexedBits[i])) {
        mActions.set(i);
      } else {
        mActions.clear(i);
      }
    }
  }

  /**
   * Adds a permitted action.
   *
   * @param action the permitted action
   */
  public void add(AclAction action) {
    mActions.set(action.ordinal());
  }

  /**
   * Merges the actions. (Or operation)
   *
   * @param actions the actions to be merged from
   */
  public void merge(AclActions actions) {
    mActions.or(actions.mActions);
  }

  /**
   * Mask the actions. (And operation)
   * @param actions the acl mask
   */
  public void mask(AclActions actions)  {
    mActions.and(actions.mActions);
  }

  /**
   * @param action the action to be checked
   * @return whether the action is contained in the permitted actions
   */
  public boolean contains(AclAction action) {
    return mActions.get(action.ordinal());
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof AclActions)) {
      return false;
    }
    AclActions that = (AclActions) o;
    return Objects.equal(mActions, that.mActions);
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(mActions);
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("actions", getActions())
        .toString();
  }

  /**
   * @return the string representation for the CLI
   */
  public String toCliString() {
    return toModeBits().toString();
  }

  /**
   * @return the representation of the permitted actions in the format of {@link Mode.Bits}
   */
  public Mode.Bits toModeBits() {
    Mode.Bits bits = Mode.Bits.NONE;
    if (contains(AclAction.READ)) {
      bits = bits.or(Mode.Bits.READ);
    }
    if (contains(AclAction.WRITE)) {
      bits = bits.or(Mode.Bits.WRITE);
    }
    if (contains(AclAction.EXECUTE)) {
      bits = bits.or(Mode.Bits.EXECUTE);
    }
    return bits;
  }
}
