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

import static org.apache.hadoop.hbase.TableName.META_TABLE_NAME;
import static org.apache.hadoop.hbase.exceptions.ClientExceptionsUtil.findException;
import static org.apache.hadoop.hbase.exceptions.ClientExceptionsUtil.isMetaClearingException;

import org.apache.hadoop.hbase.shaded.io.netty.util.HashedWheelTimer;
import org.apache.hadoop.hbase.shaded.io.netty.util.Timeout;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hbase.HRegionLocation;
import org.apache.hadoop.hbase.TableName;
import org.apache.yetus.audience.InterfaceAudience;
import org.apache.hadoop.hbase.exceptions.RegionMovedException;
import org.apache.hadoop.hbase.exceptions.TimeoutIOException;
import org.apache.hadoop.hbase.util.Bytes;

/**
 * The asynchronous region locator.
 */
@InterfaceAudience.Private
class AsyncRegionLocator {

  private static final Log LOG = LogFactory.getLog(AsyncRegionLocator.class);

  private final HashedWheelTimer retryTimer;

  private final AsyncMetaRegionLocator metaRegionLocator;

  private final AsyncNonMetaRegionLocator nonMetaRegionLocator;

  AsyncRegionLocator(AsyncConnectionImpl conn, HashedWheelTimer retryTimer) {
    this.metaRegionLocator = new AsyncMetaRegionLocator(conn.registry);
    this.nonMetaRegionLocator = new AsyncNonMetaRegionLocator(conn);
    this.retryTimer = retryTimer;
  }

  private CompletableFuture<HRegionLocation> withTimeout(CompletableFuture<HRegionLocation> future,
      long timeoutNs, Supplier<String> timeoutMsg) {
    if (future.isDone() || timeoutNs <= 0) {
      return future;
    }
    Timeout timeoutTask = retryTimer.newTimeout(t -> {
      if (future.isDone()) {
        return;
      }
      future.completeExceptionally(new TimeoutIOException(timeoutMsg.get()));
    }, timeoutNs, TimeUnit.NANOSECONDS);
    return future.whenComplete((loc, error) -> {
      if (error != null && error.getClass() != TimeoutIOException.class) {
        // cancel timeout task if we are not completed by it.
        timeoutTask.cancel();
      }
    });
  }

  CompletableFuture<HRegionLocation> getRegionLocation(TableName tableName, byte[] row,
      RegionLocateType type, boolean reload, long timeoutNs) {
    // meta region can not be split right now so we always call the same method.
    // Change it later if the meta table can have more than one regions.
    CompletableFuture<HRegionLocation> future =
        tableName.equals(META_TABLE_NAME) ? metaRegionLocator.getRegionLocation(reload)
            : nonMetaRegionLocator.getRegionLocation(tableName, row, type, reload);
    return withTimeout(future, timeoutNs,
      () -> "Timeout(" + TimeUnit.NANOSECONDS.toMillis(timeoutNs) +
          "ms) waiting for region location for " + tableName + ", row='" +
          Bytes.toStringBinary(row) + "'");
  }

  CompletableFuture<HRegionLocation> getRegionLocation(TableName tableName, byte[] row,
      RegionLocateType type, long timeoutNs) {
    return getRegionLocation(tableName, row, type, false, timeoutNs);
  }

  static boolean canUpdate(HRegionLocation loc, HRegionLocation oldLoc) {
    // Do not need to update if no such location, or the location is newer, or the location is not
    // same with us
    return oldLoc != null && oldLoc.getSeqNum() <= loc.getSeqNum() &&
        oldLoc.getServerName().equals(loc.getServerName());
  }

  static void updateCachedLocation(HRegionLocation loc, Throwable exception,
      Function<HRegionLocation, HRegionLocation> cachedLocationSupplier,
      Consumer<HRegionLocation> addToCache, Consumer<HRegionLocation> removeFromCache) {
    HRegionLocation oldLoc = cachedLocationSupplier.apply(loc);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Try updating " + loc + ", the old value is " + oldLoc, exception);
    }
    if (!canUpdate(loc, oldLoc)) {
      return;
    }
    Throwable cause = findException(exception);
    if (LOG.isDebugEnabled()) {
      LOG.debug("The actual exception when updating " + loc, cause);
    }
    if (cause == null || !isMetaClearingException(cause)) {
      if (LOG.isDebugEnabled()) {
        LOG.debug(
          "Will not update " + loc + " because the exception is null or not the one we care about");
      }
      return;
    }
    if (cause instanceof RegionMovedException) {
      RegionMovedException rme = (RegionMovedException) cause;
      HRegionLocation newLoc =
          new HRegionLocation(loc.getRegionInfo(), rme.getServerName(), rme.getLocationSeqNum());
      if (LOG.isDebugEnabled()) {
        LOG.debug(
          "Try updating " + loc + " with the new location " + newLoc + " constructed by " + rme);
      }
      addToCache.accept(newLoc);
    } else {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Try removing " + loc + " from cache");
      }
      removeFromCache.accept(loc);
    }
  }

  void updateCachedLocation(HRegionLocation loc, Throwable exception) {
    if (loc.getRegionInfo().isMetaTable()) {
      metaRegionLocator.updateCachedLocation(loc, exception);
    } else {
      nonMetaRegionLocator.updateCachedLocation(loc, exception);
    }
  }

  void clearCache(TableName tableName) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Clear meta cache for " + tableName);
    }
    if (tableName.equals(META_TABLE_NAME)) {
      metaRegionLocator.clearCache();
    } else {
      nonMetaRegionLocator.clearCache(tableName);
    }
  }
}
