//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2023
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "td/db/binlog/Binlog.h"
#include "td/db/binlog/ConcurrentBinlog.h"
#include "td/db/BinlogKeyValue.h"
#include "td/db/DbKey.h"
#include "td/db/SeqKeyValue.h"
#include "td/db/SqliteConnectionSafe.h"
#include "td/db/SqliteDb.h"
#include "td/db/SqliteKeyValueAsync.h"
#include "td/db/SqliteKeyValueSafe.h"

#include "td/actor/actor.h"
#include "td/actor/ConcurrentScheduler.h"

#include "td/utils/benchmark.h"
#include "td/utils/common.h"
#include "td/utils/format.h"
#include "td/utils/logging.h"
#include "td/utils/SliceBuilder.h"
#include "td/utils/Status.h"
#include "td/utils/StringBuilder.h"

#include <memory>

template <class KeyValueT>
class TdKvBench final : public td::Benchmark {
  td::unique_ptr<td::ConcurrentScheduler> scheduler_;
  td::string name_;

 public:
  explicit TdKvBench(td::string name) {
    name_ = std::move(name);
  }

  td::string get_description() const final {
    return name_;
  }

  class Main final : public td::Actor {
   public:
    explicit Main(int n) : n_(n) {
    }

   private:
    void loop() final {
      KeyValueT::destroy("test_tddb").ignore();

      class Worker final : public Actor {
       public:
        Worker(int n, td::string db_name) : n_(n) {
          kv_.init(db_name).ensure();
        }

       private:
        void loop() final {
          for (int i = 0; i < n_; i++) {
            kv_.set(td::to_string(i % 10), td::to_string(i));
          }
          td::Scheduler::instance()->finish();
        }
        int n_;
        KeyValueT kv_;
      };
      td::create_actor_on_scheduler<Worker>("Worker", 0, n_, "test_tddb").release();
    }
    int n_;
  };

  void start_up_n(int n) final {
    scheduler_ = td::make_unique<td::ConcurrentScheduler>(1, 0);
    scheduler_->create_actor_unsafe<Main>(1, "Main", n).release();
  }

  void run(int n) final {
    scheduler_->start();
    while (scheduler_->run_main(10)) {
      // empty
    }
    scheduler_->finish();
  }

  void tear_down() final {
    scheduler_.reset();
  }
};

template <bool is_encrypted = false>
class SqliteKVBench final : public td::Benchmark {
  td::SqliteDb db;
  td::string get_description() const final {
    return PSTRING() << "SqliteKV " << td::tag("is_encrypted", is_encrypted);
  }
  void start_up() final {
    td::string path = "testdb.sqlite";
    td::SqliteDb::destroy(path).ignore();
    if (is_encrypted) {
      db = td::SqliteDb::change_key(path, true, td::DbKey::password("cucumber"), td::DbKey::empty()).move_as_ok();
    } else {
      db = td::SqliteDb::open_with_key(path, true, td::DbKey::empty()).move_as_ok();
    }
    db.exec("PRAGMA encoding=\"UTF-8\"").ensure();
    db.exec("PRAGMA synchronous=NORMAL").ensure();
    db.exec("PRAGMA journal_mode=WAL").ensure();
    db.exec("PRAGMA temp_store=MEMORY").ensure();
    db.exec("DROP TABLE IF EXISTS KV").ensure();
    db.exec("CREATE TABLE IF NOT EXISTS KV (k BLOB PRIMARY KEY, v BLOB)").ensure();
  }
  void run(int n) final {
    auto stmt = db.get_statement("REPLACE INTO KV (k, v) VALUES(?1, ?2)").move_as_ok();
    db.exec("BEGIN TRANSACTION").ensure();
    for (int i = 0; i < n; i++) {
      auto key = td::to_string(i % 10);
      auto value = td::to_string(i);
      stmt.bind_blob(1, key).ensure();
      stmt.bind_blob(2, value).ensure();
      stmt.step().ensure();
      CHECK(!stmt.can_step());
      stmt.reset();

      if (i % 10 == 0) {
        db.exec("COMMIT TRANSACTION").ensure();
        db.exec("BEGIN TRANSACTION").ensure();
      }
    }
    db.exec("COMMIT TRANSACTION").ensure();
  }
};

static td::Status init_db(td::SqliteDb &db) {
  TRY_STATUS(db.exec("PRAGMA encoding=\"UTF-8\""));
  TRY_STATUS(db.exec("PRAGMA journal_mode=WAL"));

  TRY_STATUS(db.exec("PRAGMA synchronous=NORMAL"));
  TRY_STATUS(db.exec("PRAGMA temp_store=MEMORY"));
  // TRY_STATUS(db.exec("PRAGMA secure_delete=1"));

  return td::Status::OK();
}

class SqliteKeyValueAsyncBench final : public td::Benchmark {
 public:
  td::string get_description() const final {
    return "SqliteKeyValueAsync";
  }
  void start_up() final {
    do_start_up().ensure();
    scheduler_->start();
  }
  void run(int n) final {
    auto guard = scheduler_->get_main_guard();

    for (int i = 0; i < n; i++) {
      auto key = td::to_string(i % 10);
      auto value = td::to_string(i);
      sqlite_kv_async_->set(key, value, td::Auto());
    }
  }
  void tear_down() final {
    scheduler_->run_main(0.1);
    {
      auto guard = scheduler_->get_main_guard();
      sqlite_kv_async_.reset();
      sqlite_kv_safe_.reset();
      sql_connection_->close_and_destroy();
    }

    scheduler_->finish();
    scheduler_.reset();
  }

 private:
  td::unique_ptr<td::ConcurrentScheduler> scheduler_;
  std::shared_ptr<td::SqliteConnectionSafe> sql_connection_;
  std::shared_ptr<td::SqliteKeyValueSafe> sqlite_kv_safe_;
  td::unique_ptr<td::SqliteKeyValueAsyncInterface> sqlite_kv_async_;

  td::Status do_start_up() {
    scheduler_ = td::make_unique<td::ConcurrentScheduler>(1, 0);

    auto guard = scheduler_->get_main_guard();

    td::string sql_db_name = "testdb.sqlite";
    td::SqliteDb::destroy(sql_db_name).ignore();
    td::SqliteDb::open_with_key(sql_db_name, true, td::DbKey::empty()).move_as_ok();

    sql_connection_ = std::make_shared<td::SqliteConnectionSafe>(sql_db_name, td::DbKey::empty());
    auto &db = sql_connection_->get();
    TRY_STATUS(init_db(db));

    sqlite_kv_safe_ = std::make_shared<td::SqliteKeyValueSafe>("common", sql_connection_);
    sqlite_kv_async_ = create_sqlite_key_value_async(sqlite_kv_safe_, 0);

    return td::Status::OK();
  }
};

class SeqKvBench final : public td::Benchmark {
  td::string get_description() const final {
    return "SeqKvBench";
  }

  td::SeqKeyValue kv;
  void run(int n) final {
    for (int i = 0; i < n; i++) {
      kv.set(PSLICE() << i % 10, PSLICE() << i);
    }
  }
};

template <bool is_encrypted = false>
class BinlogKeyValueBench final : public td::Benchmark {
  td::string get_description() const final {
    return PSTRING() << "BinlogKeyValue " << td::tag("is_encrypted", is_encrypted);
  }

  td::BinlogKeyValue<td::Binlog> kv;
  void start_up() final {
    td::SqliteDb::destroy("test_binlog").ignore();
    kv.init("test_binlog", is_encrypted ? td::DbKey::password("cucumber") : td::DbKey::empty()).ensure();
  }
  void run(int n) final {
    for (int i = 0; i < n; i++) {
      kv.set(td::to_string(i % 10), td::to_string(i));
    }
  }
};

int main() {
  SET_VERBOSITY_LEVEL(VERBOSITY_NAME(WARNING));
  bench(TdKvBench<td::BinlogKeyValue<td::Binlog>>("BinlogKeyValue<Binlog>"));
  bench(TdKvBench<td::BinlogKeyValue<td::ConcurrentBinlog>>("BinlogKeyValue<ConcurrentBinlog>"));

  bench(BinlogKeyValueBench<true>());
  bench(BinlogKeyValueBench<false>());
  bench(SqliteKVBench<false>());
  bench(SqliteKVBench<true>());
  bench(SqliteKeyValueAsyncBench());
  bench(SeqKvBench());
}