#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include "cid.h"
#include <crypto/crypto_util.h>
#include <memory_tracker-inl.h>
#include <node_mutex.h>
#include <string_bytes.h>

namespace node {
namespace quic {

// ============================================================================
// CID

CID::CID() : ptr_(&cid_) {
  cid_.datalen = 0;
}

CID::CID(const ngtcp2_cid& cid) : CID(cid.data, cid.datalen) {}

CID::CID(const uint8_t* data, size_t len) : CID() {
  DCHECK_GE(len, kMinLength);
  DCHECK_LE(len, kMaxLength);
  ngtcp2_cid_init(&cid_, data, len);
}

CID::CID(const ngtcp2_cid* cid) : ptr_(cid) {
  CHECK_NOT_NULL(cid);
  DCHECK_GE(cid->datalen, kMinLength);
  DCHECK_LE(cid->datalen, kMaxLength);
}

CID::CID(const CID& other) : ptr_(&cid_) {
  CHECK_NOT_NULL(other.ptr_);
  ngtcp2_cid_init(&cid_, other.ptr_->data, other.ptr_->datalen);
}

bool CID::operator==(const CID& other) const noexcept {
  if (this == &other || (length() == 0 && other.length() == 0)) return true;
  if (length() != other.length()) return false;
  return memcmp(ptr_->data, other.ptr_->data, ptr_->datalen) == 0;
}

bool CID::operator!=(const CID& other) const noexcept {
  return !(*this == other);
}

CID::operator const uint8_t*() const {
  return ptr_->data;
}
CID::operator const ngtcp2_cid&() const {
  return *ptr_;
}
CID::operator const ngtcp2_cid*() const {
  return ptr_;
}
CID::operator bool() const {
  return ptr_->datalen >= kMinLength;
}

size_t CID::length() const {
  return ptr_->datalen;
}

std::string CID::ToString() const {
  char dest[kMaxLength * 2];
  size_t written =
      StringBytes::hex_encode(reinterpret_cast<const char*>(ptr_->data),
                              ptr_->datalen,
                              dest,
                              arraysize(dest));
  return std::string(dest, written);
}

CID CID::kInvalid{};

// ============================================================================
// CID::Hash

size_t CID::Hash::operator()(const CID& cid) const {
  size_t hash = 0;
  for (size_t n = 0; n < cid.length(); n++) {
    hash ^= std::hash<uint8_t>{}(cid.ptr_->data[n] + 0x9e3779b9 + (hash << 6) +
                                 (hash >> 2));
  }
  return hash;
}

// ============================================================================
// CID::Factory

namespace {
class RandomCIDFactory : public CID::Factory {
 public:
  RandomCIDFactory() = default;
  RandomCIDFactory(const RandomCIDFactory&) = delete;
  RandomCIDFactory(RandomCIDFactory&&) = delete;
  RandomCIDFactory& operator=(const RandomCIDFactory&) = delete;
  RandomCIDFactory& operator=(RandomCIDFactory&&) = delete;

  CID Generate(size_t length_hint) const override {
    DCHECK_GE(length_hint, CID::kMinLength);
    DCHECK_LE(length_hint, CID::kMaxLength);
    Mutex::ScopedLock lock(mutex_);
    maybe_refresh_pool(length_hint);
    auto start = pool_ + pos_;
    pos_ += length_hint;
    return CID(start, length_hint);
  }

  void GenerateInto(ngtcp2_cid* cid,
                    size_t length_hint = CID::kMaxLength) const override {
    DCHECK_GE(length_hint, CID::kMinLength);
    DCHECK_LE(length_hint, CID::kMaxLength);
    Mutex::ScopedLock lock(mutex_);
    maybe_refresh_pool(length_hint);
    auto start = pool_ + pos_;
    pos_ += length_hint;
    ngtcp2_cid_init(cid, start, length_hint);
  }

 private:
  void maybe_refresh_pool(size_t length_hint) const {
    // We generate a pool of random data kPoolSize in length
    // and pull our random CID from that. If we don't have
    // enough random random remaining in the pool to generate
    // a CID of the requested size, we regenerate the pool
    // and reset it to zero.
    if (pos_ + length_hint > kPoolSize) {
      CHECK(crypto::CSPRNG(pool_, kPoolSize).is_ok());
      pos_ = 0;
    }
  }

  static constexpr int kPoolSize = 4096;
  mutable int pos_ = kPoolSize;
  mutable uint8_t pool_[kPoolSize];
  mutable Mutex mutex_;
};
}  // namespace

const CID::Factory& CID::Factory::random() {
  static RandomCIDFactory instance;
  return instance;
}

}  // namespace quic
}  // namespace node
#endif  // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
