//  Copyright (c) 2011-present, Facebook, Inc.  All rights reserved.
//  This source code is licensed under both the GPLv2 (found in the
//  COPYING file in the root directory) and Apache 2.0 License
//  (found in the LICENSE.Apache file in the root directory).
#ifndef ROCKSDB_LITE

#include "rocksdb/utilities/json_document.h"

#ifndef __STDC_FORMAT_MACROS
#define __STDC_FORMAT_MACROS
#endif

#include <assert.h>
#include <inttypes.h>
#include <string.h>

#include <functional>
#include <limits>
#include <map>
#include <memory>
#include <string>
#include <vector>


#include "third-party/fbson/FbsonDocument.h"
#include "third-party/fbson/FbsonJsonParser.h"
#include "third-party/fbson/FbsonUtil.h"
#include "util/coding.h"

using std::placeholders::_1;

namespace {

size_t ObjectNumElem(const fbson::ObjectVal& objectVal) {
  size_t size = 0;
  for (auto keyValuePair : objectVal) {
    (void)keyValuePair;
    ++size;
  }
  return size;
}

template <typename Func>
void InitJSONDocument(std::unique_ptr<char[]>* data,
                      fbson::FbsonValue** value,
                      Func f) {
  // TODO(stash): maybe add function to FbsonDocument to avoid creating array?
  fbson::FbsonWriter writer;
  bool res __attribute__((unused)) = writer.writeStartArray();
  assert(res);
  uint32_t bytesWritten __attribute__((unused)) = f(writer);
  assert(bytesWritten != 0);
  res = writer.writeEndArray();
  assert(res);
  char* buf = new char[writer.getOutput()->getSize()];
  memcpy(buf, writer.getOutput()->getBuffer(), writer.getOutput()->getSize());

  *value = ((fbson::FbsonDocument *)buf)->getValue();
  assert((*value)->isArray());
  assert(((fbson::ArrayVal*)*value)->numElem() == 1);
  *value = ((fbson::ArrayVal*)*value)->get(0);
  data->reset(buf);
}

void InitString(std::unique_ptr<char[]>* data,
                fbson::FbsonValue** value,
                const std::string& s) {
  InitJSONDocument(data, value, std::bind(
      [](fbson::FbsonWriter& writer, const std::string& str) -> uint32_t {
        bool res __attribute__((unused)) = writer.writeStartString();
        assert(res);
        auto bytesWritten = writer.writeString(str.c_str(),
                            static_cast<uint32_t>(str.length()));
        res = writer.writeEndString();
        assert(res);
        // If the string is empty, then bytesWritten == 0, and assert in
        // InitJsonDocument will fail.
        return bytesWritten + static_cast<uint32_t>(str.empty());
      },
  _1, s));
}

bool IsNumeric(fbson::FbsonValue* value) {
  return value->isInt8() || value->isInt16() ||
         value->isInt32() ||  value->isInt64();
}

int64_t GetInt64ValFromFbsonNumericType(fbson::FbsonValue* value) {
  switch (value->type()) {
    case fbson::FbsonType::T_Int8:
      return reinterpret_cast<fbson::Int8Val*>(value)->val();
    case fbson::FbsonType::T_Int16:
      return reinterpret_cast<fbson::Int16Val*>(value)->val();
    case fbson::FbsonType::T_Int32:
      return reinterpret_cast<fbson::Int32Val*>(value)->val();
    case fbson::FbsonType::T_Int64:
      return reinterpret_cast<fbson::Int64Val*>(value)->val();
    default:
      assert(false);
  }
  return 0;
}

bool IsComparable(fbson::FbsonValue* left, fbson::FbsonValue* right) {
  if (left->type() == right->type()) {
    return true;
  }
  if (IsNumeric(left) && IsNumeric(right)) {
    return true;
  }
  return false;
}

void CreateArray(std::unique_ptr<char[]>* data, fbson::FbsonValue** value) {
  fbson::FbsonWriter writer;
  bool res __attribute__((unused)) = writer.writeStartArray();
  assert(res);
  res = writer.writeEndArray();
  assert(res);
  data->reset(new char[writer.getOutput()->getSize()]);
  memcpy(data->get(),
         writer.getOutput()->getBuffer(),
         writer.getOutput()->getSize());
  *value = reinterpret_cast<fbson::FbsonDocument*>(data->get())->getValue();
}

void CreateObject(std::unique_ptr<char[]>* data, fbson::FbsonValue** value) {
  fbson::FbsonWriter writer;
  bool res __attribute__((unused)) = writer.writeStartObject();
  assert(res);
  res = writer.writeEndObject();
  assert(res);
  data->reset(new char[writer.getOutput()->getSize()]);
  memcpy(data->get(),
         writer.getOutput()->getBuffer(),
         writer.getOutput()->getSize());
  *value = reinterpret_cast<fbson::FbsonDocument*>(data->get())->getValue();
}

}  // namespace

namespace rocksdb {


// TODO(stash): find smth easier
JSONDocument::JSONDocument() {
  InitJSONDocument(&data_,
                   &value_,
                   std::bind(&fbson::FbsonWriter::writeNull, _1));
}

JSONDocument::JSONDocument(bool b) {
  InitJSONDocument(&data_,
                   &value_,
                   std::bind(&fbson::FbsonWriter::writeBool, _1, b));
}

JSONDocument::JSONDocument(double d) {
  InitJSONDocument(&data_,
                   &value_,
                   std::bind(&fbson::FbsonWriter::writeDouble, _1, d));
}

JSONDocument::JSONDocument(int8_t i) {
  InitJSONDocument(&data_,
                   &value_,
                   std::bind(&fbson::FbsonWriter::writeInt8, _1, i));
}

JSONDocument::JSONDocument(int16_t i) {
  InitJSONDocument(&data_,
                   &value_,
                   std::bind(&fbson::FbsonWriter::writeInt16, _1, i));
}

JSONDocument::JSONDocument(int32_t i) {
  InitJSONDocument(&data_,
                   &value_,
                   std::bind(&fbson::FbsonWriter::writeInt32, _1, i));
}

JSONDocument::JSONDocument(int64_t i) {
  InitJSONDocument(&data_,
                   &value_,
                   std::bind(&fbson::FbsonWriter::writeInt64, _1, i));
}

JSONDocument::JSONDocument(const std::string& s) {
  InitString(&data_, &value_, s);
}

JSONDocument::JSONDocument(const char* s) : JSONDocument(std::string(s)) {
}

void JSONDocument::InitFromValue(const fbson::FbsonValue* val) {
  data_.reset(new char[val->numPackedBytes()]);
  memcpy(data_.get(), val, val->numPackedBytes());
  value_ = reinterpret_cast<fbson::FbsonValue*>(data_.get());
}

// Private constructor
JSONDocument::JSONDocument(fbson::FbsonValue* val, bool makeCopy) {
  if (makeCopy) {
    InitFromValue(val);
  } else {
    value_ = val;
  }
}

JSONDocument::JSONDocument(Type _type) {
  // TODO(icanadi) make all of this better by using templates
  switch (_type) {
    case kNull:
      InitJSONDocument(&data_, &value_,
                       std::bind(&fbson::FbsonWriter::writeNull, _1));
      break;
    case kObject:
      CreateObject(&data_, &value_);
      break;
    case kBool:
      InitJSONDocument(&data_, &value_,
                       std::bind(&fbson::FbsonWriter::writeBool, _1, false));
      break;
    case kDouble:
      InitJSONDocument(&data_, &value_,
                       std::bind(&fbson::FbsonWriter::writeDouble, _1, 0.));
      break;
    case kArray:
      CreateArray(&data_, &value_);
      break;
    case kInt64:
      InitJSONDocument(&data_, &value_,
                       std::bind(&fbson::FbsonWriter::writeInt64, _1, 0));
      break;
    case kString:
      InitString(&data_, &value_, "");
      break;
    default:
      assert(false);
  }
}

JSONDocument::JSONDocument(const JSONDocument& jsonDocument) {
  if (jsonDocument.IsOwner()) {
    InitFromValue(jsonDocument.value_);
  } else {
    value_ = jsonDocument.value_;
  }
}

JSONDocument::JSONDocument(JSONDocument&& jsonDocument) {
  value_ = jsonDocument.value_;
  data_.swap(jsonDocument.data_);
}

JSONDocument& JSONDocument::operator=(JSONDocument jsonDocument) {
  value_ = jsonDocument.value_;
  data_.swap(jsonDocument.data_);
  return *this;
}

JSONDocument::Type JSONDocument::type() const {
  switch (value_->type()) {
    case fbson::FbsonType::T_Null:
      return JSONDocument::kNull;

    case fbson::FbsonType::T_True:
    case fbson::FbsonType::T_False:
      return JSONDocument::kBool;

    case fbson::FbsonType::T_Int8:
    case fbson::FbsonType::T_Int16:
    case fbson::FbsonType::T_Int32:
    case fbson::FbsonType::T_Int64:
      return JSONDocument::kInt64;

    case fbson::FbsonType::T_Double:
      return JSONDocument::kDouble;

    case fbson::FbsonType::T_String:
      return JSONDocument::kString;

    case fbson::FbsonType::T_Object:
      return JSONDocument::kObject;

    case fbson::FbsonType::T_Array:
      return JSONDocument::kArray;

    case fbson::FbsonType::T_Binary:
    default:
      assert(false);
  }
  return JSONDocument::kNull;
}

bool JSONDocument::Contains(const std::string& key) const {
  assert(IsObject());
  auto objectVal = reinterpret_cast<fbson::ObjectVal*>(value_);
  return objectVal->find(key.c_str()) != nullptr;
}

JSONDocument JSONDocument::operator[](const std::string& key) const {
  assert(IsObject());
  auto objectVal = reinterpret_cast<fbson::ObjectVal*>(value_);
  auto foundValue = objectVal->find(key.c_str());
  assert(foundValue != nullptr);
  // No need to save paths in const objects
  JSONDocument ans(foundValue, false);
  return ans;
}

size_t JSONDocument::Count() const {
  assert(IsObject() || IsArray());
  if (IsObject()) {
    // TODO(stash): add to fbson?
    const fbson::ObjectVal& objectVal =
          *reinterpret_cast<fbson::ObjectVal*>(value_);
    return ObjectNumElem(objectVal);
  } else if (IsArray()) {
    auto arrayVal = reinterpret_cast<fbson::ArrayVal*>(value_);
    return arrayVal->numElem();
  }
  assert(false);
  return 0;
}

JSONDocument JSONDocument::operator[](size_t i) const {
  assert(IsArray());
  auto arrayVal = reinterpret_cast<fbson::ArrayVal*>(value_);
  auto foundValue = arrayVal->get(static_cast<int>(i));
  JSONDocument ans(foundValue, false);
  return ans;
}

bool JSONDocument::IsNull() const {
  return value_->isNull();
}

bool JSONDocument::IsArray() const {
  return value_->isArray();
}

bool JSONDocument::IsBool() const {
  return value_->isTrue() || value_->isFalse();
}

bool JSONDocument::IsDouble() const {
  return value_->isDouble();
}

bool JSONDocument::IsInt64() const {
  return value_->isInt8() || value_->isInt16() ||
         value_->isInt32() || value_->isInt64();
}

bool JSONDocument::IsObject() const {
  return value_->isObject();
}

bool JSONDocument::IsString() const {
  return value_->isString();
}

bool JSONDocument::GetBool() const {
  assert(IsBool());
  return value_->isTrue();
}

double JSONDocument::GetDouble() const {
  assert(IsDouble());
  return ((fbson::DoubleVal*)value_)->val();
}

int64_t JSONDocument::GetInt64() const {
  assert(IsInt64());
  return GetInt64ValFromFbsonNumericType(value_);
}

std::string JSONDocument::GetString() const {
  assert(IsString());
  fbson::StringVal* stringVal = (fbson::StringVal*)value_;
  return std::string(stringVal->getBlob(), stringVal->getBlobLen());
}

namespace {

// FbsonValue can be int8, int16, int32, int64
bool CompareNumeric(fbson::FbsonValue* left, fbson::FbsonValue* right) {
  assert(IsNumeric(left) && IsNumeric(right));
  return GetInt64ValFromFbsonNumericType(left) ==
         GetInt64ValFromFbsonNumericType(right);
}

bool CompareSimpleTypes(fbson::FbsonValue* left, fbson::FbsonValue* right) {
  if (IsNumeric(left)) {
    return CompareNumeric(left, right);
  }
  if (left->numPackedBytes() != right->numPackedBytes()) {
    return false;
  }
  return memcmp(left, right, left->numPackedBytes()) == 0;
}

bool CompareFbsonValue(fbson::FbsonValue* left, fbson::FbsonValue* right) {
  if (!IsComparable(left, right)) {
    return false;
  }

  switch (left->type()) {
    case fbson::FbsonType::T_True:
    case fbson::FbsonType::T_False:
    case fbson::FbsonType::T_Null:
      return true;
    case fbson::FbsonType::T_Int8:
    case fbson::FbsonType::T_Int16:
    case fbson::FbsonType::T_Int32:
    case fbson::FbsonType::T_Int64:
      return CompareNumeric(left, right);
    case fbson::FbsonType::T_String:
    case fbson::FbsonType::T_Double:
      return CompareSimpleTypes(left, right);
    case fbson::FbsonType::T_Object:
    {
      auto leftObject = reinterpret_cast<fbson::ObjectVal*>(left);
      auto rightObject = reinterpret_cast<fbson::ObjectVal*>(right);
      if (ObjectNumElem(*leftObject) != ObjectNumElem(*rightObject)) {
        return false;
      }
      for (auto && keyValue : *leftObject) {
        std::string str(keyValue.getKeyStr(), keyValue.klen());
        if (rightObject->find(str.c_str()) == nullptr) {
          return false;
        }
        if (!CompareFbsonValue(keyValue.value(),
                               rightObject->find(str.c_str()))) {
          return false;
        }
      }
      return true;
    }
    case fbson::FbsonType::T_Array:
    {
      auto leftArr = reinterpret_cast<fbson::ArrayVal*>(left);
      auto rightArr = reinterpret_cast<fbson::ArrayVal*>(right);
      if (leftArr->numElem() != rightArr->numElem()) {
        return false;
      }
      for (int i = 0; i < static_cast<int>(leftArr->numElem()); ++i) {
        if (!CompareFbsonValue(leftArr->get(i), rightArr->get(i))) {
          return false;
        }
      }
      return true;
    }
    default:
      assert(false);
  }
  return false;
}

}  // namespace

bool JSONDocument::operator==(const JSONDocument& rhs) const {
  return CompareFbsonValue(value_, rhs.value_);
}

bool JSONDocument::operator!=(const JSONDocument& rhs) const {
  return !(*this == rhs);
}

JSONDocument JSONDocument::Copy() const {
  return JSONDocument(value_, true);
}

bool JSONDocument::IsOwner() const {
  return data_.get() != nullptr;
}

std::string JSONDocument::DebugString() const {
  fbson::FbsonToJson fbsonToJson;
  return fbsonToJson.json(value_);
}

JSONDocument::ItemsIteratorGenerator JSONDocument::Items() const {
  assert(IsObject());
  return ItemsIteratorGenerator(*(reinterpret_cast<fbson::ObjectVal*>(value_)));
}

// TODO(icanadi) (perf) allocate objects with arena
JSONDocument* JSONDocument::ParseJSON(const char* json) {
  fbson::FbsonJsonParser parser;
  if (!parser.parse(json)) {
    return nullptr;
  }

  auto fbsonVal = fbson::FbsonDocument::createValue(
                    parser.getWriter().getOutput()->getBuffer(),
              static_cast<uint32_t>(parser.getWriter().getOutput()->getSize()));

  if (fbsonVal == nullptr) {
    return nullptr;
  }

  return new JSONDocument(fbsonVal, true);
}

void JSONDocument::Serialize(std::string* dst) const {
  // first byte is reserved for header
  // currently, header is only version number. that will help us provide
  // backwards compatility. we might also store more information here if
  // necessary
  dst->push_back(kSerializationFormatVersion);
  dst->push_back(FBSON_VER);
  dst->append(reinterpret_cast<char*>(value_), value_->numPackedBytes());
}

const char JSONDocument::kSerializationFormatVersion = 2;

JSONDocument* JSONDocument::Deserialize(const Slice& src) {
  Slice input(src);
  if (src.size() == 0) {
    return nullptr;
  }
  char header = input[0];
  if (header == 1) {
    assert(false);
  }
  input.remove_prefix(1);
  auto value = fbson::FbsonDocument::createValue(input.data(),
                static_cast<uint32_t>(input.size()));
  if (value == nullptr) {
    return nullptr;
  }

  return new JSONDocument(value, true);
}

class JSONDocument::const_item_iterator::Impl {
 public:
  typedef fbson::ObjectVal::const_iterator It;

  explicit Impl(It it) : it_(it) {}

  const char* getKeyStr() const {
    return it_->getKeyStr();
  }

  uint8_t klen() const {
    return it_->klen();
  }

  It& operator++() {
    return ++it_;
  }

  bool operator!=(const Impl& other) {
    return it_ != other.it_;
  }

  fbson::FbsonValue* value() const {
    return it_->value();
  }

 private:
  It it_;
};

JSONDocument::const_item_iterator::const_item_iterator(Impl* impl)
: it_(impl) {}

JSONDocument::const_item_iterator::const_item_iterator(const_item_iterator&& a)
: it_(std::move(a.it_)) {}

JSONDocument::const_item_iterator&
  JSONDocument::const_item_iterator::operator++() {
  ++(*it_);
  return *this;
}

bool JSONDocument::const_item_iterator::operator!=(
                                  const const_item_iterator& other) {
  return *it_ != *(other.it_);
}

JSONDocument::const_item_iterator::~const_item_iterator() {
}

JSONDocument::const_item_iterator::value_type
  JSONDocument::const_item_iterator::operator*() {
  return JSONDocument::const_item_iterator::value_type(std::string(it_->getKeyStr(), it_->klen()),
    JSONDocument(it_->value(), false));
}

JSONDocument::ItemsIteratorGenerator::ItemsIteratorGenerator(
                                      const fbson::ObjectVal& object)
  : object_(object) {}

JSONDocument::const_item_iterator
      JSONDocument::ItemsIteratorGenerator::begin() const {
  return const_item_iterator(new const_item_iterator::Impl(object_.begin()));
}

JSONDocument::const_item_iterator
      JSONDocument::ItemsIteratorGenerator::end() const {
  return const_item_iterator(new const_item_iterator::Impl(object_.end()));
}

}  // namespace rocksdb
#endif  // ROCKSDB_LITE
