/*
 * 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 alluxio.Constants;
import alluxio.annotation.PublicApi;
import alluxio.grpc.PMode;

import com.google.common.base.Preconditions;

import java.util.HashSet;
import java.util.Set;
import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;

/**
 * POSIX style file/directory access mode.
 */
@PublicApi
@NotThreadSafe
public final class Mode {
  private Bits mOwnerBits;
  private Bits mGroupBits;
  private Bits mOtherBits;

  /**
   * Gets the default mode.
   *
   * @return the default {@link Mode}
   */
  public static Mode defaults() {
    return new Mode(Constants.DEFAULT_FILE_SYSTEM_MODE);
  }

  /**
   * Creates the "no access" mode.
   *
   * @return the none {@link Mode}
   */
  public static Mode createNoAccess() {
    return new Mode();
  }

  /**
   * Creates the "full access" mode.
   *
   * @return the none {@link Mode}
   */
  public static Mode createFullAccess() {
    return new Mode(Bits.ALL, Bits.ALL, Bits.ALL);
  }

  /**
   * Default constructor for {@link Mode}. Default constructor is required for equality testing
   * and JSON deserialization.
   */
  private Mode() {
    mOwnerBits = Bits.NONE;
    mGroupBits = Bits.NONE;
    mOtherBits = Bits.NONE;
  }

  /**
   * Constructs an instance of {@link Mode} with the given {@link Bits}.
   *
   * @param ownerBits the owner {@link Bits}
   * @param groupBits the group {@link Bits}
   * @param otherBits the other {@link Bits}
   */
  public Mode(Bits ownerBits, Bits groupBits, Bits otherBits) {
    set(ownerBits, groupBits, otherBits);
  }

  /**
   * Constructs an instance of {@link Mode} with the given mode.
   *
   * @param mode the digital representation of a {@link Mode}
   * @see #toShort()
   */
  public Mode(short mode) {
    fromShort(mode);
  }

  /**
   * Copy constructor.
   *
   * @param mode another {@link Mode}
   */
  public Mode(Mode mode) {
    set(mode.mOwnerBits, mode.mGroupBits, mode.mOtherBits);
  }

  /**
   * @param mode1 first mode of the and operation
   * @param mode2 second mode of the and operation
   * @return the AND result of the two Modes
   */
  public static Mode and(Mode mode1, Mode mode2) {
    Bits u = mode1.mOwnerBits.and(mode2.mOwnerBits);
    Bits g = mode1.mGroupBits.and(mode2.mGroupBits);
    Bits o = mode1.mOtherBits.and(mode2.mOtherBits);
    return new Mode(u, g, o);
  }

  /**
   * @return the owner {@link Bits}
   */
  public Bits getOwnerBits() {
    return mOwnerBits;
  }

  /**
   * @param mode the digital representation of a {@link Mode}
   * @return the owner {@link Bits}
   */
  public static Bits extractOwnerBits(short mode) {
    return Bits.values()[(mode >>> 6) & 7];
  }

  /**
   * Sets owner bits.
   *
   * @param bits the owner bits to set
   */
  public void setOwnerBits(Bits bits) {
    mOwnerBits = bits;
  }

  /**
   * @return the group {@link Bits}
   */
  public Bits getGroupBits() {
    return mGroupBits;
  }

  /**
   * @param mode the digital representation of a {@link Mode}
   * @return the group {@link Bits}
   */
  public static Bits extractGroupBits(short mode) {
    return Bits.values()[(mode >>> 3) & 7];
  }

  /**
   * Sets group bits.
   *
   * @param bits the group bits to set
   */
  public void setGroupBits(Bits bits) {
    mGroupBits = bits;
  }

  /**
   * @return the other {@link Bits}
   */
  public Bits getOtherBits() {
    return mOtherBits;
  }

  /**
   * @param mode the digital representation of a {@link Mode}
   * @return the other {@link Bits}
   */
  public static Bits extractOtherBits(short mode) {
    return Bits.values()[mode & 7];
  }

  /**
   * Sets other bits.
   *
   * @param bits the other bits to set
   */
  public void setOtherBits(Bits bits) {
    mOtherBits = bits;
  }

  private void set(Bits u, Bits g, Bits o) {
    mOwnerBits = u;
    mGroupBits = g;
    mOtherBits = o;
  }

  /**
   * Sets {@link Mode} bits using a digital representation.
   *
   * @param n the digital representation of a {@link Mode}
   */
  public void fromShort(short n) {
    Bits[] v = Bits.values();
    set(v[(n >>> 6) & 7], v[(n >>> 3) & 7], v[n & 7]);
  }

  /**
   * Encodes the object as a short.
   *
   * @return the digital representation of this {@link Mode}
   */
  public short toShort() {
    int s = (mOwnerBits.ordinal() << 6) | (mGroupBits.ordinal() << 3) | mOtherBits.ordinal();
    return (short) s;
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Mode) {
      Mode that = (Mode) obj;
      return mOwnerBits == that.mOwnerBits && mGroupBits == that.mGroupBits
          && mOtherBits == that.mOtherBits;
    }
    return false;
  }

  @Override
  public int hashCode() {
    return toShort();
  }

  @Override
  public String toString() {
    return mOwnerBits.toString() + mGroupBits.toString() + mOtherBits.toString();
  }

  /**
   * Creates {@link Mode} from proto {@link PMode}.
   *
   * @param pMode proto mode
   * @return created mode
   */
  public static Mode fromProto(PMode pMode) {
    Bits ownerBits = pMode.hasOwnerBits() ? Bits.valueOf(pMode.getOwnerBits().name()) : Bits.NONE;
    Bits groupBits = pMode.hasGroupBits() ? Bits.valueOf(pMode.getGroupBits().name()) : Bits.NONE;
    Bits otherBits = pMode.hasOtherBits() ? Bits.valueOf(pMode.getOtherBits().name()) : Bits.NONE;
    return new Mode(ownerBits, groupBits, otherBits);
  }

  /**
   * @return proto representation of this mode instance
   */
  public PMode toProto() {
    return PMode.newBuilder()
        .setOwnerBits(alluxio.grpc.Bits.valueOf(mOwnerBits.name()))
        .setGroupBits(alluxio.grpc.Bits.valueOf(mGroupBits.name()))
        .setOtherBits(alluxio.grpc.Bits.valueOf(mOtherBits.name()))
        .build();
  }

  /**
   * Mode bits.
   */
  @PublicApi
  @ThreadSafe
  public enum Bits {
    NONE(Constants.MODE_BITS_NONE),
    EXECUTE(Constants.MODE_BITS_EXECUTE),
    WRITE(Constants.MODE_BITS_WRITE),
    WRITE_EXECUTE(Constants.MODE_BITS_WRITE_EXECUTE),
    READ(Constants.MODE_BITS_READ),
    READ_EXECUTE(Constants.MODE_BITS_READ_EXECUTE),
    READ_WRITE(Constants.MODE_BITS_READ_WRITE),
    ALL(Constants.MODE_BITS_ALL),
    ;

    /** String representation of the bits. */
    private final String mString;

    /** Retain reference to the values array. */
    private static final Bits[] SVALS = values();

    Bits(String s) {
      mString = s;
    }

    /**
     * Creates a {@link Bits} from a short.
     *
     * @param bits {@link Bits} in short
     * @return the {@link Bits} created
     */
    public static Bits fromShort(short bits) {
      return SVALS[bits];
    }

    /**
     * @param string the string representation
     * @return the {@link Bits} instance
     */
    public static Bits fromString(String string) {
      switch (string) {
        case Constants.MODE_BITS_NONE:
          return NONE;
        case Constants.MODE_BITS_EXECUTE:
          return EXECUTE;
        case Constants.MODE_BITS_WRITE:
          return WRITE;
        case Constants.MODE_BITS_WRITE_EXECUTE:
          return WRITE_EXECUTE;
        case Constants.MODE_BITS_READ:
          return READ;
        case Constants.MODE_BITS_READ_EXECUTE:
          return READ_EXECUTE;
        case Constants.MODE_BITS_READ_WRITE:
          return READ_WRITE;
        case Constants.MODE_BITS_ALL:
          return ALL;
        default:
          throw new IllegalArgumentException("Invalid mode string: " + string);
      }
    }

    /**
     * @param protoBits the proto bits
     * @return created instance from proto representation
     */
    public static Bits fromProto(alluxio.grpc.Bits protoBits) {
      return Bits.valueOf(protoBits.name());
    }

    /**
     * @return the proto representation of Bits
     */
    public alluxio.grpc.Bits toProto() {
      return alluxio.grpc.Bits.valueOf(name());
    }

    @Override
    public String toString() {
      return mString;
    }

    /**
     * Checks whether these bits imply the given bits.
     *
     * @param that mode bits
     * @return true when these bits imply the given bits
     */
    public boolean imply(Bits that) {
      if (that != null) {
        return (ordinal() & that.ordinal()) == that.ordinal();
      }
      return false;
    }

    /**
     * @param that mode bits
     * @return the intersection of thes bits and the given bits
     */
    public Bits and(Bits that) {
      Preconditions.checkNotNull(that, "that");
      return SVALS[ordinal() & that.ordinal()];
    }

    /**
     * @param that mode bits
     * @return the union of thes bits and the given bits
     */
    public Bits or(Bits that) {
      Preconditions.checkNotNull(that, "that");
      return SVALS[ordinal() | that.ordinal()];
    }

    /**
     * @return the complement of these bits
     */
    public Bits not() {
      return SVALS[7 - ordinal()];
    }

    /**
     * @return the set of {@link AclAction}s implied by this mode
     */
    public Set<AclAction> toAclActionSet() {
      Set<AclAction> actions = new HashSet<>();
      if (imply(READ)) {
        actions.add(AclAction.READ);
      }
      if (imply(WRITE)) {
        actions.add(AclAction.WRITE);
      }
      if (imply(EXECUTE)) {
        actions.add(AclAction.EXECUTE);
      }
      return actions;
    }

    /**
     * @return the set of {@link AclAction}s implied by this mode
     */
    public AclActions toAclActions() {
      AclActions actions = new AclActions();
      if (imply(READ)) {
        actions.add(AclAction.READ);
      }
      if (imply(WRITE)) {
        actions.add(AclAction.WRITE);
      }
      if (imply(EXECUTE)) {
        actions.add(AclAction.EXECUTE);
      }
      return actions;
    }
  }
}
