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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Durability;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.client.RegionInfoBuilder;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
import org.apache.hadoop.hbase.io.hfile.BlockCache;
import org.apache.hadoop.hbase.io.hfile.BlockCacheFactory;
import org.apache.hadoop.hbase.io.hfile.HFile;
import org.apache.hadoop.hbase.testclassification.RegionServerTests;
import org.apache.hadoop.hbase.testclassification.SmallTests;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManagerTestHelper;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Tag(RegionServerTests.TAG)
@Tag(SmallTests.TAG)
public class TestBlocksRead {

  private static final Logger LOG = LoggerFactory.getLogger(TestBlocksRead.class);
  private String testName;

  @BeforeEach
  public void setTestName(TestInfo testInfo) {
    this.testName = testInfo.getTestMethod().get().getName();
  }

  static final BloomType[] BLOOM_TYPE =
    new BloomType[] { BloomType.ROWCOL, BloomType.ROW, BloomType.NONE };

  HRegion region = null;
  private static HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
  private final String DIR = TEST_UTIL.getDataTestDir("TestBlocksRead").toString();
  private Configuration conf = TEST_UTIL.getConfiguration();

  @BeforeAll
  public static void setUp() throws Exception {
    // disable compactions in this test.
    TEST_UTIL.getConfiguration().setInt("hbase.hstore.compactionThreshold", 10000);
  }

  @AfterAll
  public static void tearDown() throws Exception {
    EnvironmentEdgeManagerTestHelper.reset();
  }

  /**
   * Callers must afterward call {@link HBaseTestingUtility#closeRegionAndWAL(HRegion)}
   * @return created and initialized region.
   */
  private HRegion initHRegion(byte[] tableName, String callingMethod, Configuration conf,
    String family) throws IOException {
    return initHRegion(tableName, callingMethod, conf, family, null);
  }

  /**
   * Callers must afterward call {@link HBaseTestingUtility#closeRegionAndWAL(HRegion)}
   */
  private HRegion initHRegion(byte[] tableName, String callingMethod, Configuration conf,
    String family, BlockCache blockCache) throws IOException {
    TableDescriptorBuilder builder =
      TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));
    for (int i = 0; i < BLOOM_TYPE.length; i++) {
      BloomType bloomType = BLOOM_TYPE[i];
      builder.setColumnFamily(
        ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(family + "_" + bloomType))
          .setBlocksize(1).setBloomFilterType(bloomType).build());
    }
    RegionInfo info = RegionInfoBuilder.newBuilder(TableName.valueOf(tableName)).build();
    Path path = new Path(DIR + callingMethod);
    if (blockCache != null) {
      return HBaseTestingUtility.createRegionAndWAL(info, path, conf, builder.build(), blockCache);
    } else {
      return HBaseTestingUtility.createRegionAndWAL(info, path, conf, builder.build());
    }
  }

  private void putData(String family, String row, String col, long version) throws IOException {
    for (int i = 0; i < BLOOM_TYPE.length; i++) {
      putData(Bytes.toBytes(family + "_" + BLOOM_TYPE[i]), row, col, version, version);
    }
  }

  // generates a value to put for a row/col/version.
  private static byte[] genValue(String row, String col, long version) {
    return Bytes.toBytes("Value:" + row + "#" + col + "#" + version);
  }

  private void putData(byte[] cf, String row, String col, long versionStart, long versionEnd)
    throws IOException {
    byte[] columnBytes = Bytes.toBytes(col);
    Put put = new Put(Bytes.toBytes(row));
    put.setDurability(Durability.SKIP_WAL);

    for (long version = versionStart; version <= versionEnd; version++) {
      put.addColumn(cf, columnBytes, version, genValue(row, col, version));
    }
    region.put(put);
  }

  private Cell[] getData(String family, String row, List<String> columns, int expBlocks)
    throws IOException {
    return getData(family, row, columns, expBlocks, expBlocks, expBlocks);
  }

  private Cell[] getData(String family, String row, List<String> columns, int expBlocksRowCol,
    int expBlocksRow, int expBlocksNone) throws IOException {
    int[] expBlocks = new int[] { expBlocksRowCol, expBlocksRow, expBlocksNone };
    Cell[] kvs = null;

    for (int i = 0; i < BLOOM_TYPE.length; i++) {
      BloomType bloomType = BLOOM_TYPE[i];
      byte[] cf = Bytes.toBytes(family + "_" + bloomType);
      long blocksStart = getBlkAccessCount(cf);
      Get get = new Get(Bytes.toBytes(row));

      for (String column : columns) {
        get.addColumn(cf, Bytes.toBytes(column));
      }

      kvs = region.get(get).rawCells();
      long blocksEnd = getBlkAccessCount(cf);
      if (expBlocks[i] != -1) {
        assertEquals(expBlocks[i], blocksEnd - blocksStart,
          "Blocks Read Check for Bloom: " + bloomType);
      }
      System.out.println("Blocks Read for Bloom: " + bloomType + " = " + (blocksEnd - blocksStart)
        + "Expected = " + expBlocks[i]);
    }
    return kvs;
  }

  private Cell[] getData(String family, String row, String column, int expBlocks)
    throws IOException {
    return getData(family, row, Arrays.asList(column), expBlocks, expBlocks, expBlocks);
  }

  private Cell[] getData(String family, String row, String column, int expBlocksRowCol,
    int expBlocksRow, int expBlocksNone) throws IOException {
    return getData(family, row, Arrays.asList(column), expBlocksRowCol, expBlocksRow,
      expBlocksNone);
  }

  private void deleteFamily(String family, String row, long version) throws IOException {
    Delete del = new Delete(Bytes.toBytes(row));
    del.addFamily(Bytes.toBytes(family + "_ROWCOL"), version);
    del.addFamily(Bytes.toBytes(family + "_ROW"), version);
    del.addFamily(Bytes.toBytes(family + "_NONE"), version);
    region.delete(del);
  }

  private static void verifyData(Cell kv, String expectedRow, String expectedCol,
    long expectedVersion) {
    assertTrue(CellUtil.matchingRows(kv, Bytes.toBytes(expectedRow)), "RowCheck");
    assertTrue(CellUtil.matchingQualifier(kv, Bytes.toBytes(expectedCol)), "ColumnCheck");
    assertEquals(expectedVersion, kv.getTimestamp(), "TSCheck");
    assertTrue(CellUtil.matchingValue(kv, genValue(expectedRow, expectedCol, expectedVersion)),
      "ValueCheck");
  }

  private static long getBlkAccessCount(byte[] cf) {
    return HFile.DATABLOCK_READ_COUNT.sum();
  }

  /**
   * Test # of blocks read for some simple seek cases.
   */
  @Test
  public void testBlocksRead() throws Exception {
    byte[] TABLE = Bytes.toBytes("testBlocksRead");
    String FAMILY = "cf1";
    Cell[] kvs;
    this.region = initHRegion(TABLE, testName, conf, FAMILY);

    try {
      putData(FAMILY, "row", "col1", 1);
      putData(FAMILY, "row", "col2", 2);
      putData(FAMILY, "row", "col3", 3);
      putData(FAMILY, "row", "col4", 4);
      putData(FAMILY, "row", "col5", 5);
      putData(FAMILY, "row", "col6", 6);
      putData(FAMILY, "row", "col7", 7);
      region.flush(true);

      // Expected block reads: 1
      // The top block has the KV we are
      // interested. So only 1 seek is needed.
      kvs = getData(FAMILY, "row", "col1", 1);
      assertEquals(1, kvs.length);
      verifyData(kvs[0], "row", "col1", 1);

      // Expected block reads: 2
      // The top block and next block has the KVs we are
      // interested. So only 2 seek is needed.
      kvs = getData(FAMILY, "row", Arrays.asList("col1", "col2"), 2);
      assertEquals(2, kvs.length);
      verifyData(kvs[0], "row", "col1", 1);
      verifyData(kvs[1], "row", "col2", 2);

      // Expected block reads: 3
      // The first 2 seeks is to find out col2. [HBASE-4443]
      // One additional seek for col3
      // So 3 seeks are needed.
      kvs = getData(FAMILY, "row", Arrays.asList("col2", "col3"), 2);
      assertEquals(2, kvs.length);
      verifyData(kvs[0], "row", "col2", 2);
      verifyData(kvs[1], "row", "col3", 3);

      // Expected block reads: 1. [HBASE-4443]&[HBASE-7845]
      kvs = getData(FAMILY, "row", Arrays.asList("col5"), 1);
      assertEquals(1, kvs.length);
      verifyData(kvs[0], "row", "col5", 5);
    } finally {
      HBaseTestingUtility.closeRegionAndWAL(this.region);
      this.region = null;
    }
  }

  /**
   * Test # of blocks read (targeted at some of the cases Lazy Seek optimizes).
   */
  @Test
  public void testLazySeekBlocksRead() throws Exception {
    byte[] TABLE = Bytes.toBytes("testLazySeekBlocksRead");
    String FAMILY = "cf1";
    Cell[] kvs;
    this.region = initHRegion(TABLE, testName, conf, FAMILY);

    try {
      // File 1
      putData(FAMILY, "row", "col1", 1);
      putData(FAMILY, "row", "col2", 2);
      region.flush(true);

      // File 2
      putData(FAMILY, "row", "col1", 3);
      putData(FAMILY, "row", "col2", 4);
      region.flush(true);

      // Expected blocks read: 1.
      // File 2's top block is also the KV we are
      // interested. So only 1 seek is needed.
      kvs = getData(FAMILY, "row", Arrays.asList("col1"), 1);
      assertEquals(1, kvs.length);
      verifyData(kvs[0], "row", "col1", 3);

      // Expected blocks read: 2
      // File 2's top block has the "col1" KV we are
      // interested. We also need "col2" which is in a block
      // of its own. So, we need that block as well.
      kvs = getData(FAMILY, "row", Arrays.asList("col1", "col2"), 2);
      assertEquals(2, kvs.length);
      verifyData(kvs[0], "row", "col1", 3);
      verifyData(kvs[1], "row", "col2", 4);

      // File 3: Add another column
      putData(FAMILY, "row", "col3", 5);
      region.flush(true);

      // Expected blocks read: 1
      // File 3's top block has the "col3" KV we are
      // interested. So only 1 seek is needed.
      kvs = getData(FAMILY, "row", "col3", 1);
      assertEquals(1, kvs.length);
      verifyData(kvs[0], "row", "col3", 5);

      // Get a column from older file.
      // For ROWCOL Bloom filter: Expected blocks read: 1.
      // For ROW Bloom filter: Expected blocks read: 2.
      // For NONE Bloom filter: Expected blocks read: 2.
      kvs = getData(FAMILY, "row", Arrays.asList("col1"), 1, 2, 2);
      assertEquals(1, kvs.length);
      verifyData(kvs[0], "row", "col1", 3);

      // File 4: Delete the entire row.
      deleteFamily(FAMILY, "row", 6);
      region.flush(true);

      // For ROWCOL Bloom filter: Expected blocks read: 2.
      // For ROW Bloom filter: Expected blocks read: 3.
      // For NONE Bloom filter: Expected blocks read: 3.
      kvs = getData(FAMILY, "row", "col1", 2, 3, 3);
      assertEquals(0, kvs.length);
      kvs = getData(FAMILY, "row", "col2", 2, 3, 3);
      assertEquals(0, kvs.length);
      kvs = getData(FAMILY, "row", "col3", 2);
      assertEquals(0, kvs.length);
      kvs = getData(FAMILY, "row", Arrays.asList("col1", "col2", "col3"), 4);
      assertEquals(0, kvs.length);

      // File 5: Delete
      deleteFamily(FAMILY, "row", 10);
      region.flush(true);

      // File 6: some more puts, but with timestamps older than the
      // previous delete.
      putData(FAMILY, "row", "col1", 7);
      putData(FAMILY, "row", "col2", 8);
      putData(FAMILY, "row", "col3", 9);
      region.flush(true);

      // Baseline expected blocks read: 6. [HBASE-4532]
      kvs = getData(FAMILY, "row", Arrays.asList("col1", "col2", "col3"), 6, 7, 7);
      assertEquals(0, kvs.length);

      // File 7: Put back new data
      putData(FAMILY, "row", "col1", 11);
      putData(FAMILY, "row", "col2", 12);
      putData(FAMILY, "row", "col3", 13);
      region.flush(true);

      // Expected blocks read: 8. [HBASE-4585, HBASE-13109]
      kvs = getData(FAMILY, "row", Arrays.asList("col1", "col2", "col3"), 8, 9, 9);
      assertEquals(3, kvs.length);
      verifyData(kvs[0], "row", "col1", 11);
      verifyData(kvs[1], "row", "col2", 12);
      verifyData(kvs[2], "row", "col3", 13);
    } finally {
      HBaseTestingUtility.closeRegionAndWAL(this.region);
      this.region = null;
    }
  }

  /**
   * Test # of blocks read to ensure disabling cache-fill on Scan works.
   */
  @Test
  public void testBlocksStoredWhenCachingDisabled() throws Exception {
    byte[] TABLE = Bytes.toBytes("testBlocksReadWhenCachingDisabled");
    String FAMILY = "cf1";

    BlockCache blockCache = BlockCacheFactory.createBlockCache(conf);
    this.region = initHRegion(TABLE, testName, conf, FAMILY, blockCache);

    try {
      putData(FAMILY, "row", "col1", 1);
      putData(FAMILY, "row", "col2", 2);
      region.flush(true);

      // Execute a scan with caching turned off
      // Expected blocks stored: 0
      long blocksStart = blockCache.getBlockCount();
      Scan scan = new Scan();
      scan.setCacheBlocks(false);
      RegionScanner rs = region.getScanner(scan);
      List<Cell> result = new ArrayList<>(2);
      rs.next(result);
      assertEquals(2 * BLOOM_TYPE.length, result.size());
      rs.close();
      long blocksEnd = blockCache.getBlockCount();

      assertEquals(blocksStart, blocksEnd);

      // Execute with caching turned on
      // Expected blocks stored: 2
      blocksStart = blocksEnd;
      scan.setCacheBlocks(true);
      rs = region.getScanner(scan);
      result = new ArrayList<>(2);
      rs.next(result);
      assertEquals(2 * BLOOM_TYPE.length, result.size());
      rs.close();
      blocksEnd = blockCache.getBlockCount();

      assertEquals(2 * BLOOM_TYPE.length, blocksEnd - blocksStart);
    } finally {
      HBaseTestingUtility.closeRegionAndWAL(this.region);
      this.region = null;
    }
  }

  @Test
  public void testLazySeekBlocksReadWithDelete() throws Exception {
    byte[] TABLE = Bytes.toBytes("testLazySeekBlocksReadWithDelete");
    String FAMILY = "cf1";
    Cell[] kvs;
    this.region = initHRegion(TABLE, testName, conf, FAMILY);
    try {
      deleteFamily(FAMILY, "row", 200);
      for (int i = 0; i < 100; i++) {
        putData(FAMILY, "row", "col" + i, i);
      }
      putData(FAMILY, "row", "col99", 201);
      region.flush(true);

      kvs = getData(FAMILY, "row", Arrays.asList("col0"), 2);
      assertEquals(0, kvs.length);

      kvs = getData(FAMILY, "row", Arrays.asList("col99"), 2);
      assertEquals(1, kvs.length);
      verifyData(kvs[0], "row", "col99", 201);
    } finally {
      HBaseTestingUtility.closeRegionAndWAL(this.region);
      this.region = null;
    }
  }

}
