/*
 * 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.Objects;
import com.google.common.collect.ImmutableList;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.concurrent.NotThreadSafe;

/**
 * Stores the extended ACL entries.
 */
@NotThreadSafe
public class ExtendedACLEntries {
  protected Map<String, AclActions> mNamedUserActions;
  protected Map<String, AclActions> mNamedGroupActions;
  protected AclActions mMaskActions;

  /**
   * Creates an empty extended ACL.
   */
  public ExtendedACLEntries() {
    mNamedUserActions = new TreeMap<>();
    mNamedGroupActions = new TreeMap<>();
    mMaskActions = new AclActions();
  }

  /**
   * Creates a copy.
   *
   * @param other the object to copy
   */
  public ExtendedACLEntries(ExtendedACLEntries other) {
    mNamedUserActions = new TreeMap<>(other.mNamedUserActions);
    mNamedGroupActions = new TreeMap<>(other.mNamedGroupActions);
    mMaskActions = new AclActions(other.mMaskActions);
  }

  /**
   * Returns a list of {@link AclEntry} which represent this ACL instance. The mask will only be
   * included if extended ACL entries exist.
   *
   * @return an immutable list of ACL entries
   */
  public List<AclEntry> getEntries() {
    ImmutableList.Builder<AclEntry> builder = new ImmutableList.Builder<>();
    for (Map.Entry<String, AclActions> kv : mNamedUserActions.entrySet()) {
      builder.add(new AclEntry.Builder()
          .setType(AclEntryType.NAMED_USER)
          .setSubject(kv.getKey())
          .setActions(kv.getValue())
          .build());
    }
    for (Map.Entry<String, AclActions> kv : mNamedGroupActions.entrySet()) {
      builder.add(new AclEntry.Builder()
          .setType(AclEntryType.NAMED_GROUP)
          .setSubject(kv.getKey())
          .setActions(kv.getValue())
          .build());
    }
    if (hasExtended()) {
      // The mask is only relevant if the ACL contains extended entries.
      builder.add(new AclEntry.Builder()
          .setType(AclEntryType.MASK)
          .setActions(mMaskActions)
          .build());
    }
    return builder.build();
  }

  /**
   * @return named user actions
   */
  public Map<String, AclActions> getNamedUserActions() {
    return mNamedUserActions;
  }

  /**
   * @return named group actions
   */
  public Map<String, AclActions> getNamedGroupActions() {
    return mNamedGroupActions;
  }

  /**
   * @return true if has extended ACL (named users, named groups)
   */
  public boolean hasExtended() {
    return !mNamedUserActions.isEmpty() || !mNamedGroupActions.isEmpty();
  }

  /**
   * Removes the specified entry. A base entry is not allowed to be removed.
   *
   * @param entry the entry to be removed
   */
  public void removeEntry(AclEntry entry) {
    switch (entry.getType()) {
      case NAMED_USER:
        mNamedUserActions.remove(entry.getSubject());
        return;
      case NAMED_GROUP:
        mNamedGroupActions.remove(entry.getSubject());
        return;
      case MASK:
        if (hasExtended()) {
          throw new IllegalStateException(
              "Deleting the mask for extended ACLs is not allowed. entry: " + entry);
        } else {
          mMaskActions = new AclActions();
        }
        return;
      case OWNING_USER:  // fall through
      case OWNING_GROUP: // fall through
      case OTHER:
        throw new IllegalStateException(
            "Deleting base entry is not allowed. entry: " + entry);
      default:
        throw new IllegalStateException("Unknown ACL entry type: " + entry.getType());
    }
  }

  /**
   * Sets an entry into the access control list.
   * If an entry with the same type and subject already exists, overwrites the existing entry;
   * Otherwise, adds this new entry.
   *
   * @param entry the entry to be added or updated
   */
  public void setEntry(AclEntry entry) {
    // TODO(cc): when setting non-mask entries, the mask should be dynamically updated too.
    switch (entry.getType()) {
      case NAMED_USER:
        mNamedUserActions.put(entry.getSubject(), entry.getActions());
        return;
      case NAMED_GROUP:
        mNamedGroupActions.put(entry.getSubject(), entry.getActions());
        return;
      case MASK:
        mMaskActions = entry.getActions();
        return;
      case OWNING_USER:  // fall through
      case OWNING_GROUP: // fall through
      case OTHER:
        throw new IllegalStateException(
            "Deleting base entry is not allowed. entry: " + entry);
      default:
        throw new IllegalStateException("Unknown ACL entry type: " + entry.getType());
    }
  }

  /**
   * @param user the user to look up
   * @return the actions for the user, or null if the user is not found
   */
  public AclActions getNamedUser(String user) {
    return mNamedUserActions.get(user);
  }

  /**
   * @param group the group to look up
   * @return the actions for the group, or null if the group is not found
   */
  public AclActions getNamedGroup(String group) {
    return mNamedGroupActions.get(group);
  }

  /**
   * @return the mask actions
   */
  public AclActions getMask() {
    return mMaskActions;
  }

  /**
   * Update the mask to be the union of owning group entry, named user entry and named group entry.
   * @param groupActions the group entry to be integrated into the mask
   */
  public void updateMask(AclActions groupActions) {
    AclActions result = new AclActions(groupActions);

    for (Map.Entry<String, AclActions> kv : mNamedUserActions.entrySet()) {
      AclActions userAction = kv.getValue();
      result.merge(userAction);

      for (AclAction action : AclAction.values()) {
        if (result.contains(action) || userAction.contains(action)) {
          result.add(action);
        }
      }
    }

    for (Map.Entry<String, AclActions> kv : mNamedGroupActions.entrySet()) {
      AclActions userAction = kv.getValue();
      result.merge(userAction);

      for (AclAction action : AclAction.values()) {
        if (result.contains(action) || userAction.contains(action)) {
          result.add(action);
        }
      }
    }

    mMaskActions = result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof ExtendedACLEntries)) {
      return false;
    }
    ExtendedACLEntries that = (ExtendedACLEntries) o;
    return mNamedUserActions.equals(that.mNamedUserActions)
        && mNamedGroupActions.equals(that.mNamedGroupActions)
        && mMaskActions.equals(that.mMaskActions);
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(mNamedUserActions, mNamedGroupActions, mMaskActions);
  }
}
