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

import alluxio.grpc.BlockStoreLocationProto;

import com.google.common.base.MoreObjects;

import java.util.Objects;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Where to store a block within a block store. It describes the block storage location in three
 * dimensions, tierAlias, dir index within the tier and the medium type of the storage location.
 * Currently, there is an assumption that the medium type and the tier alias are not set at the
 * same time.
 */
@ThreadSafe
public final class BlockStoreLocation {

  /** Special value to indicate any tier. */
  public static final String ANY_TIER = "";

  /** Special value to indicate any dir. */
  private static final int ANY_DIR = -1;

  /** Special value to indicate any medium type. */
  private static final String ANY_MEDIUM = "";

  private static final BlockStoreLocation ANY_TIER_LOCATION =
      new BlockStoreLocation(ANY_TIER, ANY_DIR, ANY_MEDIUM);

  /** Alias of the storage tier. */
  private final String mTierAlias;

  /** Index of the directory in its tier, 0 indexed. */
  private final int mDirIndex;

  /** Medium type of the storage directory. */
  private final String mMediumType;

  /**
   * Convenience method to return the block store location representing any dir in any tier.
   *
   * @return a BlockStoreLocation of any dir in any tier
   */
  public static BlockStoreLocation anyTier() {
    return ANY_TIER_LOCATION;
  }

  /**
   * Convenience method to return the block store location representing any dir in the tier.
   *
   * @param tierAlias The alias of the tier this returned block store location will represent
   * @return a BlockStoreLocation of any dir in the specified tier
   */
  public static BlockStoreLocation anyDirInTier(String tierAlias) {
    return new BlockStoreLocation(tierAlias, ANY_DIR, ANY_MEDIUM);
  }

  /**
   * Convenience method to return the block store location representing any dir in any tier
   * with specific medium.
   *
   * @param mediumType mediumType this returned block store location will represent
   * @return a BlockStoreLocation of any dir in any tier with a specific medium
   */
  public static BlockStoreLocation anyDirInAnyTierWithMedium(String mediumType) {
    return new BlockStoreLocation(ANY_TIER, ANY_DIR, mediumType);
  }

  /**
   * Creates a new instance of {@link BlockStoreLocation}.
   *
   * @param tierAlias the tier alias to use
   * @param dirIndex the directory index to use
   */
  public BlockStoreLocation(String tierAlias, int dirIndex) {
    mTierAlias = tierAlias;
    mDirIndex = dirIndex;
    mMediumType = ANY_MEDIUM;
  }

  /**
   * Creates a new instance of {@link BlockStoreLocation}.
   *
   * @param tierAlias the tier alias to use
   * @param dirIndex the directory index to use
   * @param mediumType the medium type to use
   */
  public BlockStoreLocation(String tierAlias, int dirIndex, String mediumType) {
    mTierAlias = tierAlias;
    mDirIndex = dirIndex;
    mMediumType = mediumType;
  }

  /**
   * Gets the storage tier alias of the location.
   *
   * @return the tier alias of the location
   */
  public String tierAlias() {
    return mTierAlias;
  }

  /**
   * Check whether the location has no restriction, i.e., {@link #ANY_TIER_LOCATION}.
   *
   * @return true if location is equal to {@link #ANY_TIER_LOCATION}
   */
  public boolean hasNoRestriction() {
    return this.equals(ANY_TIER_LOCATION);
  }

  /**
   * Check whether the location is {@link #ANY_TIER}.
   *
   * @return true if location is {@link #ANY_TIER}
   */
  public boolean isAnyTier() {
    return tierAlias().isEmpty();
  }

  /**
   * Gets the directory index of the location.
   *
   * @return the directory index of the location, {@link #ANY_DIR} for any directory
   */
  public int dir() {
    return mDirIndex;
  }

  /**
   * Check whether the location is {@link #ANY_DIR}.
   *
   * @return true if location is {@link #ANY_DIR}
   */
  public boolean isAnyDir() {
    return dir() == ANY_DIR;
  }

  /**
   * Check whether the location is {@link #ANY_DIR} within the specified tier.
   *
   * @return true if location is {@link #ANY_DIR} within the specified tier
   */
  public boolean isAnyDirWithTier() {
    return isAnyDir() && !isAnyTier();
  }

  /**
   * Gets the medium type of the location.
   *
   * @return the medium type of the location
   */
  public String mediumType() {
    return mMediumType;
  }

  /**
   * Check whether the location is {@link #ANY_MEDIUM}.
   *
   * @return true if location is {@link #ANY_MEDIUM}
   */
  public boolean isAnyMedium() {
    return mediumType().isEmpty();
  }

  /**
   * Returns whether this location belongs to the specific location.
   *
   * Location A belongs to B when tier and dir of A are all in the range of B respectively.
   *
   * @param location the target BlockStoreLocation
   * @return true when this BlockStoreLocation belongs to the target, otherwise false
   */
  public boolean belongsTo(BlockStoreLocation location) {
    boolean tierInRange =
        tierAlias().equals(location.tierAlias()) || location.tierAlias().equals(ANY_TIER);
    boolean dirInRange = (dir() == location.dir()) || (location.dir() == ANY_DIR);
    boolean mediumTypeInRange = (mediumType().equals(location.mediumType())
        || (location.mediumType().equals(ANY_MEDIUM)));
    return tierInRange && dirInRange && mediumTypeInRange;
  }

  /**
   * Converts the location to a human readable form.
   *
   * @return a human readable string representing the information of this block store location
   */
  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("TierAlias", (!mTierAlias.equals(ANY_TIER)) ? mTierAlias : "<Any>")
        .add("DirIndex", (mDirIndex != ANY_DIR) ? mDirIndex : "<Any>")
        .add("MediumType", (!mMediumType.equals(ANY_MEDIUM)) ? mMediumType : "<Any>")
        .toString();
  }

  /**
   * Compares to a specific object.
   *
   * @param o the object to compare
   * @return true if object is also {@link BlockStoreLocation} and represents the same tier and dir
   */
  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof BlockStoreLocation)) {
      return false;
    }
    BlockStoreLocation that = (BlockStoreLocation) o;
    return mTierAlias.equals(that.mTierAlias) && mDirIndex == that.mDirIndex
        && mMediumType.equals(that.mMediumType);
  }

  @Override
  public int hashCode() {
    return Objects.hash(mTierAlias, mDirIndex, mMediumType);
  }

  /**
   * Fill BlockStoreLocationproto with location information.
   *
   * @return a proto object
   */
  public BlockStoreLocationProto toProto() {
    return BlockStoreLocationProto.newBuilder().setTierAlias(tierAlias())
        .setMediumType(mediumType()).build();
  }
}
