using rapidjson::Document;
using rapidjson::StringBuffer;
using rapidjson::Writer;
using tonic::ToDart;

namespace blink {

static const char* kHandleKey = "handle";
static const char* kRepresentationKey = "representation";
static const char* kNameKey = "name";
static const char* kClassNameKey = "class_name";
static const char* kLibraryPathKey = "library_path";
static const char* kCacheName = "flutter_callback_cache.json";
std::mutex DartCallbackCache::mutex_;
std::string DartCallbackCache::cache_path_;
std::map<int64_t, DartCallbackRepresentation> DartCallbackCache::cache_;

void DartCallbackCache::SetCachePath(const std::string& path) {
  cache_path_ = fml::paths::JoinPaths({path, kCacheName});
}

Dart_Handle DartCallbackCache::GetCallback(int64_t handle) {
  std::lock_guard<std::mutex> lock(mutex_);
  auto iterator = cache_.find(handle);
  if (iterator != cache_.end()) {
    DartCallbackRepresentation cb = iterator->second;
    return LookupDartClosure(cb.name, cb.class_name, cb.library_path);
  }
  return Dart_Null();
}

int64_t DartCallbackCache::GetCallbackHandle(const std::string& name,
                                             const std::string& class_name,
                                             const std::string& library_path) {
  std::lock_guard<std::mutex> lock(mutex_);
  std::hash<std::string> hasher;
  int64_t hash = hasher(name);
  hash += hasher(class_name);
  hash += hasher(library_path);

  if (cache_.find(hash) == cache_.end()) {
    cache_[hash] = {name, class_name, library_path};
    SaveCacheToDisk();
  }
  return hash;
}

std::unique_ptr<DartCallbackRepresentation>
DartCallbackCache::GetCallbackInformation(int64_t handle) {
  std::lock_guard<std::mutex> lock(mutex_);
  auto iterator = cache_.find(handle);
  if (iterator != cache_.end()) {
    return std::make_unique<DartCallbackRepresentation>(iterator->second);
  }
  return nullptr;
}

void DartCallbackCache::SaveCacheToDisk() {
  // Cache JSON format
  // [
  //   {
  //      "hash": 42,
  //      "representation": {
  //          "name": "...",
  //          "class_name": "...",
  //          "library_path": "..."
  //      }
  //   },
  //   {
  //   ...
  //   }
  // ]
  StringBuffer s;
  Writer<StringBuffer> writer(s);
  writer.StartArray();
  for (auto iterator = cache_.begin(); iterator != cache_.end(); ++iterator) {
    int64_t hash = iterator->first;
    DartCallbackRepresentation cb = iterator->second;
    writer.StartObject();
    writer.Key(kHandleKey);
    writer.Int64(hash);
    writer.Key(kRepresentationKey);
    writer.StartObject();
    writer.Key(kNameKey);
    writer.String(cb.name.c_str());
    writer.Key(kClassNameKey);
    writer.String(cb.class_name.c_str());
    writer.Key(kLibraryPathKey);
    writer.String(cb.library_path.c_str());
    writer.EndObject();
    writer.EndObject();
  }
  writer.EndArray();

  std::ofstream output(cache_path_);
  output << s.GetString();
  output.close();
}

void DartCallbackCache::LoadCacheFromDisk() {
  std::lock_guard<std::mutex> lock(mutex_);

  // Don't reload the cache if it's already populated.
  if (!cache_.empty()) {
    return;
  }
  std::ifstream input(cache_path_);
  if (!input) {
    return;
  }
  std::string cache_contents{std::istreambuf_iterator<char>(input),
                             std::istreambuf_iterator<char>()};
  Document d;
  d.Parse(cache_contents.c_str());
  if (d.HasParseError() || !d.IsArray()) {
    FML_LOG(WARNING) << "Could not parse callback cache, aborting restore";
    // TODO(bkonyi): log and bail (delete cache?)
    return;
  }
  const auto entries = d.GetArray();
  for (auto it = entries.begin(); it != entries.end(); ++it) {
    const auto root_obj = it->GetObject();
    const auto representation = root_obj[kRepresentationKey].GetObject();

    const int64_t hash = root_obj[kHandleKey].GetInt64();
    DartCallbackRepresentation cb;
    cb.name = representation[kNameKey].GetString();
    cb.class_name = representation[kClassNameKey].GetString();
    cb.library_path = representation[kLibraryPathKey].GetString();
    cache_[hash] = cb;
  }
}

Dart_Handle DartCallbackCache::LookupDartClosure(
    const std::string& name,
    const std::string& class_name,
    const std::string& library_path) {
  Dart_Handle closure_name = ToDart(name);
  Dart_Handle library_name =
      library_path.empty() ? Dart_Null() : ToDart(library_path);
  Dart_Handle cls_name = class_name.empty() ? Dart_Null() : ToDart(class_name);
  DART_CHECK_VALID(closure_name);
  DART_CHECK_VALID(library_name);
  DART_CHECK_VALID(cls_name);

  Dart_Handle library;
  if (library_name == Dart_Null()) {
    library = Dart_RootLibrary();
  } else {
    library = Dart_LookupLibrary(library_name);
  }
  DART_CHECK_VALID(library);

  Dart_Handle closure;
  if (Dart_IsNull(cls_name)) {
    closure = Dart_GetField(library, closure_name);
  } else {
    Dart_Handle cls = Dart_GetClass(library, cls_name);
    DART_CHECK_VALID(cls);
    if (Dart_IsNull(cls)) {
      closure = Dart_Null();
    } else {
      closure = Dart_GetStaticMethodClosure(library, cls, closure_name);
    }
  }
  DART_CHECK_VALID(closure);
  return closure;
}

}  // namespace blink
