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

import static org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants.PHYSICAL_DATA_TABLE_NAME;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.regionserver.Region;
import org.apache.hadoop.hbase.regionserver.RegionScanner;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.compat.hbase.CompatScanMetrics;
import org.apache.phoenix.execute.TupleProjector;
import org.apache.phoenix.hbase.index.parallel.EarlyExitFailure;
import org.apache.phoenix.hbase.index.parallel.Task;
import org.apache.phoenix.hbase.index.parallel.TaskBatch;
import org.apache.phoenix.hbase.index.parallel.TaskRunner;
import org.apache.phoenix.hbase.index.table.HTableFactory;
import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
import org.apache.phoenix.hbase.index.write.IndexWriterUtils;
import org.apache.phoenix.index.IndexMaintainer;
import org.apache.phoenix.util.ClientUtil;
import org.apache.phoenix.util.EnvironmentEdgeManager;
import org.apache.phoenix.util.ScanUtil;
import org.apache.phoenix.util.ServerUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is an index table region scanner which scans index table rows locally and then extracts data
 * table row keys from them. Using the data table row keys, the data table rows are scanned using
 * the HBase client available to region servers.
 */
public class UncoveredGlobalIndexRegionScanner extends UncoveredIndexRegionScanner {
  private static final Logger LOGGER =
    LoggerFactory.getLogger(UncoveredGlobalIndexRegionScanner.class);
  public static final String NUM_CONCURRENT_INDEX_THREADS_CONF_KEY = "phoenix.index.threads.max";
  public static final String INDEX_ROW_COUNTS_PER_TASK_CONF_KEY =
    "phoenix.index.row.count.per.task";
  public static final int DEFAULT_INDEX_ROW_COUNTS_PER_TASK = 2048;

  protected byte[][] regionEndKeys;
  protected final Table dataHTable;
  protected final int rowCountPerTask;
  protected String exceptionMessage;
  protected final HTableFactory hTableFactory;
  private final boolean isScanMetricsEnabled;
  private List<DataTableScanMetricsWithScanTime> dataTableScanMetrics;

  // This relies on Hadoop Configuration to handle warning about deprecated configs and
  // to set the correct non-deprecated configs when an old one shows up.
  static {
    Configuration.addDeprecation("index.threads.max", NUM_CONCURRENT_INDEX_THREADS_CONF_KEY);
    Configuration.addDeprecation("index.row.count.per.task", INDEX_ROW_COUNTS_PER_TASK_CONF_KEY);
  }

  public UncoveredGlobalIndexRegionScanner(final RegionScanner innerScanner, final Region region,
    final Scan scan, final RegionCoprocessorEnvironment env, final Scan dataTableScan,
    final TupleProjector tupleProjector, final IndexMaintainer indexMaintainer,
    final byte[][] viewConstants, final ImmutableBytesWritable ptr, final long pageSizeMs,
    final long queryLimit, boolean isDistinct) throws IOException {
    super(innerScanner, region, scan, env, dataTableScan, tupleProjector, indexMaintainer,
      viewConstants, ptr, pageSizeMs, queryLimit, isDistinct);
    final Configuration config = env.getConfiguration();
    hTableFactory = IndexWriterUtils.getDefaultDelegateHTableFactory(env);
    rowCountPerTask =
      config.getInt(INDEX_ROW_COUNTS_PER_TASK_CONF_KEY, DEFAULT_INDEX_ROW_COUNTS_PER_TASK);
    byte[] dataTableName = scan.getAttribute(PHYSICAL_DATA_TABLE_NAME);
    dataHTable = hTableFactory.getTable(new ImmutableBytesPtr(dataTableName));
    regionEndKeys =
      hTableFactory.getConnection().getRegionLocator(dataHTable.getName()).getEndKeys();
    if (indexMaintainer.isUncovered()) {
      // Empty column should also be added to the data columns to join for uncovered
      // global indexes. This is required to verify the index row against the data table row and
      // repair it
      ScanUtil.addEmptyColumnToScan(dataTableScan, indexMaintainer.getDataEmptyKeyValueCF(),
        indexMaintainer.getEmptyKeyValueQualifierForDataTable());
    }
    isScanMetricsEnabled =
      scan.isScanMetricsEnabled() && CompatScanMetrics.supportsFineGrainedReadMetrics();
    if (isScanMetricsEnabled) {
      dataTableScanMetrics = new ArrayList<>();
    }
  }

  @Override
  public void close() throws IOException {
    innerScanner.close();
    hTableFactory.shutdown();
    if (dataHTable != null) {
      dataHTable.close();
    }
  }

  protected void scanDataRows(Collection<byte[]> dataRowKeys, long startTime) throws IOException {
    Scan dataScan = prepareDataTableScan(dataRowKeys);
    if (dataScan == null) {
      return;
    }
    dataScan.setScanMetricsEnabled(isScanMetricsEnabled);
    try (ResultScanner resultScanner = dataHTable.getScanner(dataScan)) {
      for (Result result = resultScanner.next(); (result != null); result = resultScanner.next()) {
        if (ScanUtil.isDummy(result)) {
          state = State.SCANNING_DATA_INTERRUPTED;
          break;
        }
        dataRows.put(new ImmutableBytesPtr(result.getRow()), result);
        if ((EnvironmentEdgeManager.currentTimeMillis() - startTime) >= pageSizeMs) {
          state = State.SCANNING_DATA_INTERRUPTED;
          break;
        }
      }
      if (state == State.SCANNING_DATA_INTERRUPTED) {
        LOGGER.info("One of the scan tasks in UncoveredGlobalIndexRegionScanner" + " for region "
          + region.getRegionInfo().getRegionNameAsString() + " could not complete on time (in "
          + pageSizeMs + " ms) and" + " will be resubmitted");
      }
      if (isScanMetricsEnabled) {
        ScanMetrics scanMetrics = resultScanner.getScanMetrics();
        long scanTimeInMs = EnvironmentEdgeManager.currentTimeMillis() - startTime;
        // Capture scan time to identify slowest parallel scan later as that's the one which slows
        // down the whole merge operation from data table.
        dataTableScanMetrics.add(buildDataTableScanMetrics(scanMetrics, scanTimeInMs));
      }
    } catch (Throwable t) {
      exceptionMessage = "scanDataRows fails for at least one task";
      ClientUtil.throwIOException(dataHTable.getName().toString(), t);
    }
  }

  private void addTasksForScanningDataTableRowsInParallel(TaskBatch<Boolean> tasks,
    final Set<byte[]> dataRowKeys, final long startTime) {
    tasks.add(new Task<Boolean>() {
      @Override
      public Boolean call() throws Exception {
        try {
          // in HBase 1.x we could check if the coproc environment was closed or aborted,
          // but in HBase 2.x the coproc environment can't check region server services
          if (Thread.currentThread().isInterrupted()) {
            exceptionMessage = "Pool closed, not retrieving data table rows for "
              + region.getRegionInfo().getRegionNameAsString();
            throw new IOException(exceptionMessage);
          }
          scanDataRows(dataRowKeys, startTime);
        } catch (Exception e) {
          throw e;
        }
        return Boolean.TRUE;
      }
    });
  }

  protected void submitTasks(TaskBatch<Boolean> tasks) throws IOException {
    Pair<List<Boolean>, List<Future<Boolean>>> resultsAndFutures = null;
    try {
      LOGGER.debug("Waiting on index tasks to complete...");
      TaskRunner pool = PhoenixRegionServerEndpoint.getUncoveredIndexThreadPool();
      if (pool == null) {
        throw new IOException(
          "PhoenixRegionServerEndpoint should be loaded to use Uncovered Indexes.");
      }
      resultsAndFutures = pool.submitUninterruptible(tasks);
    } catch (ExecutionException e) {
      throw new RuntimeException(
        "Should not fail on the results while using a WaitForCompletionTaskRunner", e);
    } catch (EarlyExitFailure e) {
      throw new RuntimeException("Stopped while waiting for batch, quitting!", e);
    }
    int index = 0;
    for (Boolean result : resultsAndFutures.getFirst()) {
      if (result == null) {
        Throwable cause =
          ServerUtil.getExceptionFromFailedFuture(resultsAndFutures.getSecond().get(index));
        // there was a failure
        throw new IOException(exceptionMessage == null ? "" : exceptionMessage, cause);
      }
      index++;
    }
  }

  @Override
  protected void scanDataTableRows(long startTime) throws IOException {
    if (indexToDataRowKeyMap.size() == 0) {
      state = State.READY;
      return;
    }
    TreeSet<byte[]> dataRowKeys = new TreeSet<>(Bytes.BYTES_COMPARATOR);
    for (byte[] dataRowKey : indexToDataRowKeyMap.values()) {
      dataRowKeys.add(dataRowKey);
    }
    List<Set<byte[]>> setList =
      IndexRepairRegionScanner.getPerTaskDataRowKeys(dataRowKeys, regionEndKeys, rowCountPerTask);
    int taskCount = setList.size();
    TaskBatch<Boolean> tasks = new TaskBatch<>(taskCount);
    for (int i = 0; i < taskCount; i++) {
      addTasksForScanningDataTableRowsInParallel(tasks, setList.get(i), startTime);
    }
    submitTasks(tasks);
    if (isScanMetricsEnabled) {
      DataTableScanMetricsWithScanTime dataTableScanMetricsForSlowestScan = null;
      for (DataTableScanMetricsWithScanTime dataTableScanMetrics : dataTableScanMetrics) {
        if (dataTableScanMetricsForSlowestScan == null) {
          dataTableScanMetricsForSlowestScan = dataTableScanMetrics;
        } else if (
          dataTableScanMetricsForSlowestScan.getScanTimeInMs()
              < dataTableScanMetrics.getScanTimeInMs()
        ) {
          dataTableScanMetricsForSlowestScan = dataTableScanMetrics;
        }
      }
      if (dataTableScanMetricsForSlowestScan != null) {
        dataTableScanMetricsForSlowestScan.populateThreadLocalServerSideScanMetrics();
      }
    }
    if (state == State.SCANNING_DATA_INTERRUPTED) {
      state = State.SCANNING_DATA;
    } else {
      state = State.READY;
    }
  }

  private static DataTableScanMetricsWithScanTime buildDataTableScanMetrics(ScanMetrics scanMetrics,
    long scanTimeInMs) {
    DataTableScanMetricsWithScanTime.Builder builder =
      new DataTableScanMetricsWithScanTime.Builder();
    builder.setScanTimeInMs(scanTimeInMs);
    DataTableScanMetrics.populateDataTableScanMetrics(scanMetrics, builder);
    return builder.build();
  }

  private static class DataTableScanMetricsWithScanTime extends DataTableScanMetrics {
    private final long scanTimeInMs;

    DataTableScanMetricsWithScanTime(long scanTimeInMs, long fsReadTimeInMs, long bytesReadFromFS,
      long bytesReadFromMemstore, long bytesReadFromBlockcache, long blockReadOps) {
      super(fsReadTimeInMs, bytesReadFromFS, bytesReadFromMemstore, bytesReadFromBlockcache,
        blockReadOps);
      this.scanTimeInMs = scanTimeInMs;
    }

    long getScanTimeInMs() {
      return scanTimeInMs;
    }

    private static class Builder extends DataTableScanMetrics.Builder {
      private long scanTimeInMs = 0;

      public Builder setScanTimeInMs(long scanTimeInMs) {
        this.scanTimeInMs = scanTimeInMs;
        return this;
      }

      @Override
      public DataTableScanMetricsWithScanTime build() {
        return new DataTableScanMetricsWithScanTime(scanTimeInMs, fsReadTimeInMs, bytesReadFromFS,
          bytesReadFromMemstore, bytesReadFromBlockcache, blockReadOps);
      }
    }
  }
}
