/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.hbase.namespace;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.hadoop.hbase.MetaTableAccessor;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.master.MasterServices;
import org.apache.hadoop.hbase.master.TableNamespaceManager;
import org.apache.hadoop.hbase.quotas.QuotaExceededException;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * NamespaceStateManager manages state (in terms of quota) of all the namespaces. It contains a
 * cache which is updated based on the hooks in the NamespaceAuditor class.
 */
@InterfaceAudience.Private
class NamespaceStateManager {

  private static final Logger LOG = LoggerFactory.getLogger(NamespaceStateManager.class);

  private final ConcurrentMap<String, NamespaceTableAndRegionInfo> nsStateCache;
  private final MasterServices master;
  private volatile boolean initialized = false;

  public NamespaceStateManager(MasterServices masterServices) {
    nsStateCache = new ConcurrentHashMap<>();
    master = masterServices;
  }

  /**
   * Starts the NamespaceStateManager. The boot strap of cache is done in the post master start hook
   * of the NamespaceAuditor class.
   * @throws IOException Signals that an I/O exception has occurred.
   */
  public void start() throws IOException {
    LOG.info("Namespace State Manager started.");
    initialize();
  }

  /**
   * Gets an instance of NamespaceTableAndRegionInfo associated with namespace.
   * @param name The name of the namespace
   * @return An instance of NamespaceTableAndRegionInfo.
   */
  public NamespaceTableAndRegionInfo getState(String name) {
    return nsStateCache.get(name);
  }

  /**
   * Check if adding a region violates namespace quota, if not update namespace cache.
   * @return true, if region can be added to table.
   * @throws IOException Signals that an I/O exception has occurred.
   */
  synchronized boolean checkAndUpdateNamespaceRegionCount(TableName name, byte[] regionName,
    int incr) throws IOException {
    if (name.isSystemTable()) {
      return true;
    }
    String namespace = name.getNamespaceAsString();
    NamespaceDescriptor nspdesc = getNamespaceDescriptor(namespace);
    if (nspdesc != null) {
      NamespaceTableAndRegionInfo currentStatus;
      currentStatus = getState(namespace);
      int regionCount = currentStatus.getRegionCount();
      long maxRegionCount = TableNamespaceManager.getMaxRegions(nspdesc);
      if (incr > 0 && regionCount >= maxRegionCount) {
        LOG.warn(
          "The region {} cannot be created. The region count  will exceed quota on the namespace. "
            + "This may be transient, please retry later if there are any ongoing split"
            + " operations in the namespace.",
          Bytes.toStringBinary(regionName));
        return false;
      }
      NamespaceTableAndRegionInfo nsInfo = nsStateCache.get(namespace);
      if (nsInfo != null) {
        nsInfo.incRegionCountForTable(name, incr);
      } else {
        LOG.warn("Namespace state found null for namespace : {}", namespace);
      }
    }
    return true;
  }

  /**
   * Check and update region count for an existing table. To handle scenarios like restore snapshot
   * @param name name of the table for region count needs to be checked and updated
   * @param incr count of regions
   * @throws QuotaExceededException if quota exceeds for the number of regions allowed in a
   *                                namespace
   * @throws IOException            Signals that an I/O exception has occurred.
   */
  synchronized void checkAndUpdateNamespaceRegionCount(TableName name, int incr)
    throws IOException {
    if (name.isSystemTable()) {
      return;
    }
    String namespace = name.getNamespaceAsString();
    NamespaceDescriptor nspdesc = getNamespaceDescriptor(namespace);
    if (nspdesc != null) {
      NamespaceTableAndRegionInfo currentStatus = getState(namespace);
      int regionCountOfTable = currentStatus.getRegionCountOfTable(name);
      if (
        (currentStatus.getRegionCount() - regionCountOfTable + incr)
            > TableNamespaceManager.getMaxRegions(nspdesc)
      ) {
        throw new QuotaExceededException("The table " + name.getNameAsString()
          + " region count cannot be updated as it would exceed maximum number "
          + "of regions allowed in the namespace.  The total number of regions permitted is "
          + TableNamespaceManager.getMaxRegions(nspdesc));
      }
      currentStatus.removeTable(name);
      currentStatus.addTable(name, incr);
    }
  }

  private NamespaceDescriptor getNamespaceDescriptor(String namespaceAsString) {
    try {
      return this.master.getClusterSchema().getNamespace(namespaceAsString);
    } catch (IOException e) {
      LOG.error("Error while fetching namespace descriptor for namespace : {}", namespaceAsString);
      return null;
    }
  }

  synchronized void checkAndUpdateNamespaceTableCount(TableName table, int numRegions)
    throws IOException {
    if (table.isSystemTable()) {
      return;
    }
    String namespace = table.getNamespaceAsString();
    NamespaceDescriptor nspdesc = getNamespaceDescriptor(namespace);
    if (nspdesc != null) {
      NamespaceTableAndRegionInfo currentStatus;
      currentStatus = getState(nspdesc.getName());
      if ((currentStatus.getTables().size()) >= TableNamespaceManager.getMaxTables(nspdesc)) {
        throw new QuotaExceededException("The table " + table.getNameAsString()
          + " cannot be created as it would exceed maximum number of tables allowed "
          + " in the namespace.  The total number of tables permitted is "
          + TableNamespaceManager.getMaxTables(nspdesc));
      }
      if (
        (currentStatus.getRegionCount() + numRegions) > TableNamespaceManager.getMaxRegions(nspdesc)
      ) {
        throw new QuotaExceededException(
          "The table " + table.getNameAsString() + " is not allowed to have " + numRegions
            + " regions. The total number of regions permitted is only "
            + TableNamespaceManager.getMaxRegions(nspdesc) + ", while current region count is "
            + currentStatus.getRegionCount()
            + ". This may be transient, please retry later if there are any"
            + " ongoing split operations in the namespace.");
      }
    } else {
      throw new IOException(
        "Namespace Descriptor found null for " + namespace + " This is unexpected.");
    }
    addTable(table, numRegions);
  }

  NamespaceTableAndRegionInfo addNamespace(String namespace) {
    if (!nsStateCache.containsKey(namespace)) {
      NamespaceTableAndRegionInfo a1 = new NamespaceTableAndRegionInfo(namespace);
      nsStateCache.put(namespace, a1);
    }
    return nsStateCache.get(namespace);
  }

  /**
   * Delete the namespace state.
   * @param namespace the name of the namespace to delete
   */
  void deleteNamespace(String namespace) {
    this.nsStateCache.remove(namespace);
  }

  private void addTable(TableName tableName, int regionCount) throws IOException {
    assert !tableName.isSystemTable() : "Tracking of system tables is not supported";
    NamespaceTableAndRegionInfo info = nsStateCache.get(tableName.getNamespaceAsString());
    if (info != null) {
      info.addTable(tableName, regionCount);
    } else {
      throw new IOException("Bad state : Namespace quota information not found for namespace : "
        + tableName.getNamespaceAsString());
    }
  }

  synchronized void removeTable(TableName tableName) {
    if (tableName.isSystemTable()) {
      return;
    }
    NamespaceTableAndRegionInfo info = nsStateCache.get(tableName.getNamespaceAsString());
    if (info != null) {
      info.removeTable(tableName);
    }
  }

  /**
   * Initialize namespace state cache by scanning meta table.
   */
  private void initialize() throws IOException {
    List<NamespaceDescriptor> namespaces = this.master.getClusterSchema().getNamespaces();
    for (NamespaceDescriptor namespace : namespaces) {
      addNamespace(namespace.getName());
      List<TableName> tables = this.master.listTableNamesByNamespace(namespace.getName());
      for (TableName table : tables) {
        if (table.isSystemTable()) {
          continue;
        }
        List<RegionInfo> regions =
          MetaTableAccessor.getTableRegions(this.master.getConnection(), table, true);
        addTable(table, regions.size());
      }
    }
    LOG.info("Finished updating state of {} namespaces.", nsStateCache.size());
    initialized = true;
  }

  boolean isInitialized() {
    return initialized;
  }

  public synchronized void removeRegionFromTable(RegionInfo hri) throws IOException {
    String namespace = hri.getTable().getNamespaceAsString();
    NamespaceTableAndRegionInfo nsInfo = nsStateCache.get(namespace);
    if (nsInfo != null) {
      nsInfo.decrementRegionCountForTable(hri.getTable(), 1);
    } else {
      throw new IOException("Namespace state found null for namespace : " + namespace);
    }
  }
}
