// 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.

#include <algorithm>
#include <cstdint>
#include <limits>
#include <numeric>
#include <random>
#include <string>
#include <vector>

#include "benchmark/benchmark.h"

#include "arrow/builder.h"
#include "arrow/memory_pool.h"
#include "arrow/testing/gtest_util.h"
#include "arrow/util/bit_util.h"
#include "arrow/util/decimal.h"
#include "arrow/util/string_view.h"

namespace arrow {

using ValueType = int64_t;
using VectorType = std::vector<ValueType>;
constexpr int64_t kNumberOfElements = 256 * 512;

static VectorType AlmostU8CompressibleVector() {
  VectorType data(kNumberOfElements, 64);

  // Insert an element late in the game that does not fit in the 8bit
  // representation. This forces AdaptiveIntBuilder's to resize.
  data[kNumberOfElements - 2] = 1L << 13;

  return data;
}

constexpr int64_t kRounds = 256;
static VectorType kData = AlmostU8CompressibleVector();
constexpr int64_t kBytesProcessPerRound = kNumberOfElements * sizeof(ValueType);
constexpr int64_t kBytesProcessed = kRounds * kBytesProcessPerRound;

static const char* kBinaryString = "12345678";
static arrow::util::string_view kBinaryView(kBinaryString);

static void BuildIntArrayNoNulls(benchmark::State& state) {  // NOLINT non-const reference
  for (auto _ : state) {
    Int64Builder builder;

    for (int i = 0; i < kRounds; i++) {
      ABORT_NOT_OK(builder.AppendValues(kData.data(), kData.size(), nullptr));
    }

    std::shared_ptr<Array> out;
    ABORT_NOT_OK(builder.Finish(&out));
  }

  state.SetBytesProcessed(state.iterations() * kBytesProcessed);
}

static void BuildAdaptiveIntNoNulls(
    benchmark::State& state) {  // NOLINT non-const reference
  for (auto _ : state) {
    AdaptiveIntBuilder builder;

    for (int i = 0; i < kRounds; i++) {
      ABORT_NOT_OK(builder.AppendValues(kData.data(), kData.size(), nullptr));
    }

    std::shared_ptr<Array> out;
    ABORT_NOT_OK(builder.Finish(&out));
  }

  state.SetBytesProcessed(state.iterations() * kBytesProcessed);
}

static void BuildAdaptiveIntNoNullsScalarAppend(
    benchmark::State& state) {  // NOLINT non-const reference
  for (auto _ : state) {
    AdaptiveIntBuilder builder;

    for (int i = 0; i < kRounds; i++) {
      for (size_t j = 0; j < kData.size(); j++) {
        ABORT_NOT_OK(builder.Append(kData[i]))
      }
    }

    std::shared_ptr<Array> out;
    ABORT_NOT_OK(builder.Finish(&out));
  }

  state.SetBytesProcessed(state.iterations() * kBytesProcessed);
}

static void BuildBooleanArrayNoNulls(
    benchmark::State& state) {  // NOLINT non-const reference

  size_t n_bytes = kBytesProcessPerRound;
  const uint8_t* data = reinterpret_cast<const uint8_t*>(kData.data());

  for (auto _ : state) {
    BooleanBuilder builder;

    for (int i = 0; i < kRounds; i++) {
      ABORT_NOT_OK(builder.AppendValues(data, n_bytes));
    }

    std::shared_ptr<Array> out;
    ABORT_NOT_OK(builder.Finish(&out));
  }

  state.SetBytesProcessed(state.iterations() * kBytesProcessed);
}

static void BuildBinaryArray(benchmark::State& state) {  // NOLINT non-const reference
  for (auto _ : state) {
    BinaryBuilder builder;

    for (int64_t i = 0; i < kRounds * kNumberOfElements; i++) {
      ABORT_NOT_OK(builder.Append(kBinaryView));
    }

    std::shared_ptr<Array> out;
    ABORT_NOT_OK(builder.Finish(&out));
  }

  state.SetBytesProcessed(state.iterations() * kBytesProcessed);
}

static void BuildChunkedBinaryArray(
    benchmark::State& state) {  // NOLINT non-const reference
  // 1MB chunks
  const int32_t kChunkSize = 1 << 20;

  for (auto _ : state) {
    internal::ChunkedBinaryBuilder builder(kChunkSize);

    for (int64_t i = 0; i < kRounds * kNumberOfElements; i++) {
      ABORT_NOT_OK(builder.Append(kBinaryView));
    }

    ArrayVector out;
    ABORT_NOT_OK(builder.Finish(&out));
  }

  state.SetBytesProcessed(state.iterations() * kBytesProcessed);
}

static void BuildFixedSizeBinaryArray(
    benchmark::State& state) {  // NOLINT non-const reference
  auto type = fixed_size_binary(static_cast<int32_t>(kBinaryView.size()));

  for (auto _ : state) {
    FixedSizeBinaryBuilder builder(type);

    for (int64_t i = 0; i < kRounds * kNumberOfElements; i++) {
      ABORT_NOT_OK(builder.Append(kBinaryView));
    }

    std::shared_ptr<Array> out;
    ABORT_NOT_OK(builder.Finish(&out));
  }

  state.SetBytesProcessed(state.iterations() * kBytesProcessed);
}

static void BuildDecimalArray(benchmark::State& state) {  // NOLINT non-const reference
  auto type = decimal(10, 5);
  Decimal128 value;
  int32_t precision = 0;
  int32_t scale = 0;
  ABORT_NOT_OK(Decimal128::FromString("1234.1234", &value, &precision, &scale));
  for (auto _ : state) {
    Decimal128Builder builder(type);

    for (int64_t i = 0; i < kRounds * kNumberOfElements; i++) {
      ABORT_NOT_OK(builder.Append(value));
    }

    std::shared_ptr<Array> out;
    ABORT_NOT_OK(builder.Finish(&out));
  }

  state.SetBytesProcessed(state.iterations() * kRounds * kNumberOfElements * 16);
}

// ----------------------------------------------------------------------
// DictionaryBuilder benchmarks

size_t kDistinctElements = kNumberOfElements / 100;

// Testing with different distributions of integer values helps stress
// the hash table's robustness.

// Make a vector out of `n_distinct` sequential int values
template <class Integer = ValueType>
static std::vector<Integer> MakeSequentialIntDictFodder() {
  std::default_random_engine gen(42);
  std::vector<Integer> values(kNumberOfElements);
  {
    std::uniform_int_distribution<Integer> values_dist(0, kDistinctElements - 1);
    std::generate(values.begin(), values.end(), [&]() { return values_dist(gen); });
  }
  return values;
}

// Make a vector out of `n_distinct` int values with potentially colliding hash
// entries as only their highest bits differ.
template <class Integer = ValueType>
static std::vector<Integer> MakeSimilarIntDictFodder() {
  std::default_random_engine gen(42);
  std::vector<Integer> values(kNumberOfElements);
  {
    std::uniform_int_distribution<Integer> values_dist(0, kDistinctElements - 1);
    auto max_int = std::numeric_limits<Integer>::max();
    auto multiplier =
        static_cast<Integer>(BitUtil::NextPower2(max_int / kDistinctElements / 2));
    std::generate(values.begin(), values.end(),
                  [&]() { return multiplier * values_dist(gen); });
  }
  return values;
}

// Make a vector out of `n_distinct` random int values
template <class Integer = ValueType>
static std::vector<Integer> MakeRandomIntDictFodder() {
  std::default_random_engine gen(42);
  std::vector<Integer> values_dict(kDistinctElements);
  std::vector<Integer> values(kNumberOfElements);

  {
    std::uniform_int_distribution<Integer> values_dist(
        0, std::numeric_limits<Integer>::max());
    std::generate(values_dict.begin(), values_dict.end(),
                  [&]() { return static_cast<Integer>(values_dist(gen)); });
  }
  {
    std::uniform_int_distribution<int32_t> indices_dist(
        0, static_cast<int32_t>(kDistinctElements - 1));
    std::generate(values.begin(), values.end(),
                  [&]() { return values_dict[indices_dist(gen)]; });
  }
  return values;
}

// Make a vector out of `kDistinctElements` string values
static std::vector<std::string> MakeStringDictFodder() {
  std::default_random_engine gen(42);
  std::vector<std::string> values_dict(kDistinctElements);
  std::vector<std::string> values(kNumberOfElements);

  {
    auto it = values_dict.begin();
    // Add empty string
    *it++ = "";
    // Add a few similar strings
    *it++ = "abc";
    *it++ = "abcdef";
    *it++ = "abcfgh";
    // Add random strings
    std::uniform_int_distribution<int32_t> length_dist(2, 20);
    std::independent_bits_engine<std::default_random_engine, 8, uint16_t> bytes_gen(42);

    std::generate(it, values_dict.end(), [&] {
      auto length = length_dist(gen);
      std::string s(length, 'X');
      for (int32_t i = 0; i < length; ++i) {
        s[i] = static_cast<char>(bytes_gen());
      }
      return s;
    });
  }
  {
    std::uniform_int_distribution<int32_t> indices_dist(
        0, static_cast<int32_t>(kDistinctElements - 1));
    std::generate(values.begin(), values.end(),
                  [&] { return values_dict[indices_dist(gen)]; });
  }
  return values;
}

template <class DictionaryBuilderType, class Scalar>
static void BenchmarkDictionaryArray(
    benchmark::State& state,  // NOLINT non-const reference
    const std::vector<Scalar>& fodder, size_t fodder_nbytes = 0) {
  for (auto _ : state) {
    DictionaryBuilderType builder(default_memory_pool());

    for (int64_t i = 0; i < kRounds; i++) {
      for (const auto& value : fodder) {
        ABORT_NOT_OK(builder.Append(value));
      }
    }

    std::shared_ptr<Array> out;
    ABORT_NOT_OK(builder.Finish(&out));
  }

  if (fodder_nbytes == 0) {
    fodder_nbytes = fodder.size() * sizeof(Scalar);
  }
  state.SetBytesProcessed(state.iterations() * fodder_nbytes * kRounds);
}

static void BuildInt64DictionaryArrayRandom(
    benchmark::State& state) {  // NOLINT non-const reference
  const auto fodder = MakeRandomIntDictFodder();
  BenchmarkDictionaryArray<DictionaryBuilder<Int64Type>>(state, fodder);
}

static void BuildInt64DictionaryArraySequential(
    benchmark::State& state) {  // NOLINT non-const reference
  const auto fodder = MakeSequentialIntDictFodder();
  BenchmarkDictionaryArray<DictionaryBuilder<Int64Type>>(state, fodder);
}

static void BuildInt64DictionaryArraySimilar(
    benchmark::State& state) {  // NOLINT non-const reference
  const auto fodder = MakeSimilarIntDictFodder();
  BenchmarkDictionaryArray<DictionaryBuilder<Int64Type>>(state, fodder);
}

static void BuildStringDictionaryArray(
    benchmark::State& state) {  // NOLINT non-const reference
  const auto fodder = MakeStringDictFodder();
  auto fodder_nbytes =
      std::accumulate(fodder.begin(), fodder.end(), 0ULL,
                      [&](size_t acc, const std::string& s) { return acc + s.size(); });
  BenchmarkDictionaryArray<BinaryDictionaryBuilder>(state, fodder, fodder_nbytes);
}

static void ArrayDataConstructDestruct(
    benchmark::State& state) {  // NOLINT non-const reference
  std::vector<std::shared_ptr<ArrayData>> arrays;

  const int kNumArrays = 1000;
  auto InitArrays = [&]() {
    for (int i = 0; i < kNumArrays; ++i) {
      arrays.emplace_back(new ArrayData);
    }
  };

  for (auto _ : state) {
    InitArrays();
    arrays.clear();
  }
}

// ----------------------------------------------------------------------
// BufferBuilder benchmarks

static void BenchmarkBufferBuilder(
    const std::string& datum,
    benchmark::State& state) {  // NOLINT non-const reference
  const void* raw_data = datum.data();
  int64_t raw_nbytes = static_cast<int64_t>(datum.size());
  // Write approx. 256 MB to BufferBuilder
  int64_t num_raw_values = (1 << 28) / raw_nbytes;
  for (auto _ : state) {
    BufferBuilder builder;
    std::shared_ptr<Buffer> buf;
    for (int64_t i = 0; i < num_raw_values; ++i) {
      ABORT_NOT_OK(builder.Append(raw_data, raw_nbytes));
    }
    ABORT_NOT_OK(builder.Finish(&buf));
  }
  state.SetBytesProcessed(int64_t(state.iterations()) * num_raw_values * raw_nbytes);
}

static void BufferBuilderTinyWrites(
    benchmark::State& state) {  // NOLINT non-const reference
  // A 8-byte datum
  return BenchmarkBufferBuilder("abdefghi", state);
}

static void BufferBuilderSmallWrites(
    benchmark::State& state) {  // NOLINT non-const reference
  // A 700-byte datum
  std::string datum;
  for (int i = 0; i < 100; ++i) {
    datum += "abcdefg";
  }
  return BenchmarkBufferBuilder(datum, state);
}

static void BufferBuilderLargeWrites(
    benchmark::State& state) {  // NOLINT non-const reference
  // A 1.5MB datum
  std::string datum(1500000, 'x');
  return BenchmarkBufferBuilder(datum, state);
}

BENCHMARK(BufferBuilderTinyWrites)->UseRealTime();
BENCHMARK(BufferBuilderSmallWrites)->UseRealTime();
BENCHMARK(BufferBuilderLargeWrites)->UseRealTime();

// ----------------------------------------------------------------------
// Benchmark declarations
//

#ifdef ARROW_WITH_BENCHMARKS_REFERENCE

// This benchmarks acts as a reference to the native std::vector
// implementation. It appends kRounds chunks into a vector.
static void ReferenceBuildVectorNoNulls(
    benchmark::State& state) {  // NOLINT non-const reference
  for (auto _ : state) {
    std::vector<int64_t> builder;

    for (int i = 0; i < kRounds; i++) {
      builder.insert(builder.end(), kData.cbegin(), kData.cend());
    }
  }

  state.SetBytesProcessed(state.iterations() * kBytesProcessed);
}

BENCHMARK(ReferenceBuildVectorNoNulls);

#endif

BENCHMARK(BuildBooleanArrayNoNulls);

BENCHMARK(BuildIntArrayNoNulls);
BENCHMARK(BuildAdaptiveIntNoNulls);
BENCHMARK(BuildAdaptiveIntNoNullsScalarAppend);

BENCHMARK(BuildBinaryArray);
BENCHMARK(BuildChunkedBinaryArray);
BENCHMARK(BuildFixedSizeBinaryArray);
BENCHMARK(BuildDecimalArray);

BENCHMARK(BuildInt64DictionaryArrayRandom);
BENCHMARK(BuildInt64DictionaryArraySequential);
BENCHMARK(BuildInt64DictionaryArraySimilar);
BENCHMARK(BuildStringDictionaryArray);

BENCHMARK(ArrayDataConstructDestruct);

}  // namespace arrow
