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

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

/**
 * This class defines "Scope" of a cache quota. For Presto caching, scope is
 * hierarchical with different levels including:
 * <ul>
 *   <li>GLOBAL (".") for global scope;</li>
 *   <li>SCHEMA (e.g., "schema1") for an entire schema;</li>
 *   <li>TABLE (e.g., "schema2.table1") for a given table;</li>
 *   <li>PARTITION (e.g., "schema3.table2.partition1") for a given partition</li>
 * </ul>
 */
@ThreadSafe
public class CacheScope {
  private static final String GLOBAL_ID = ".";
  public static final String SEPARATOR = ".";
  public static final CacheScope GLOBAL = new CacheScope(GLOBAL_ID, 1, Level.GLOBAL);

  /**
   * Level of a scope.
   */
  public enum Level {
    GLOBAL("\\."),
    SCHEMA("\\w+"),
    TABLE("\\w+\\.\\w+"),
    PARTITION("\\w+\\.\\w+.\\w+");

    private final Pattern mPattern;

    /**
     * @param re regexp of the scope
     */
    Level(String re) {
      mPattern = Pattern.compile(re);
    }

    /**
     * @return parent level
     */
    @Nullable
    public Level parent() {
      if (ordinal() > 0) {
        return values()[ordinal() - 1];
      } else {
        return null;
      }
    }

    /**
     * @param input the input property key string
     * @return whether the input string matches this template
     */
    public boolean matches(String input) {
      Matcher matcher = mPattern.matcher(input);
      return matcher.matches();
    }
  }

  private final String mId;
  private final int mLength;
  private final Level mLevel;

  /**
   * @param id id of the scope
   * @return an instance of this scope converted from a string
   */
  public static CacheScope create(String id) {
    Preconditions.checkArgument(id != null && id.length() > 0,
        "scope id can not be null or empty string");
    if (GLOBAL_ID.equals(id)) {
      return GLOBAL;
    } else if (Level.SCHEMA.matches(id)) {
      return new CacheScope(id, id.length(), Level.SCHEMA);
    } else if (Level.TABLE.matches(id)) {
      return new CacheScope(id, id.length(), Level.TABLE);
    } else if (Level.PARTITION.matches(id)) {
      return new CacheScope(id, id.length(), Level.PARTITION);
    }
    throw new IllegalArgumentException("Failed to parse cache scope: " + id);
  }

  private CacheScope(String id, int len, Level level) {
    mId = id;
    mLength = len;
    mLevel = level;
  }

  /**
   * @return parent of this scope
   */
  @Nullable
  public CacheScope parent() {
    int r = mId.lastIndexOf(SEPARATOR, mLength - 1);
    if (r < 0) {
      return GLOBAL;
    } else if (r == 0) {
      return null;
    }
    return new CacheScope(mId, r, mLevel.parent());
  }

  /**
   * @return the level of this scope
   */
  public Level level() {
    return mLevel;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    CacheScope cacheScope = (CacheScope) o;
    if (mLength != cacheScope.mLength) {
      return false;
    }
    if (Objects.equal(mId, cacheScope.mId)) {
      return true;
    }
    return Objects.equal(
          mId.substring(0, mLength), cacheScope.mId.substring(0, cacheScope.mLength));
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(mId.substring(0, mLength), mLength);
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("id", mId.substring(0, mLength))
        .toString();
  }
}
