/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/glean/bindings/jog/JOG.h"

#include <locale>

#include "mozilla/AppShutdown.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/glean/bindings/jog/jog_ffi_generated.h"
#include "mozilla/Logging.h"
#include "mozilla/IntegerPrintfMacros.h"
#include "mozilla/StaticPrefs_telemetry.h"
#include "mozilla/AppShutdown.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsThreadUtils.h"
#include "nsTHashMap.h"
#include "nsTHashSet.h"
#ifdef ANDROID
#  include "nsContentUtils.h"
#  include "nsIChannel.h"
#  include "nsIInputStream.h"
#  include "nsNetUtil.h"
#endif  // ANDROID

namespace mozilla::glean {

static LazyLogModule sJOGLog("jog");

// Storage
// Thread Safety: Only used on the main thread.
StaticAutoPtr<nsTHashSet<nsCString>> gCategories;
StaticAutoPtr<nsTHashMap<nsCString, uint32_t>> gMetrics;
StaticAutoPtr<nsTHashMap<uint32_t, nsCString>> gMetricNames;
StaticAutoPtr<nsTHashMap<nsCString, uint32_t>> gPings;

// static
bool JOG::HasCategory(const nsACString& aCategoryName) {
  MOZ_ASSERT(NS_IsMainThread());

  return gCategories && gCategories->Contains(aCategoryName);
}

static Maybe<bool> sFoundAndLoadedJogfile;

// static
bool JOG::EnsureRuntimeMetricsRegistered(bool aForce) {
  MOZ_ASSERT(NS_IsMainThread());

  if (sFoundAndLoadedJogfile) {
    return sFoundAndLoadedJogfile.value();
  }
  sFoundAndLoadedJogfile = Some(false);

  MOZ_LOG(sJOGLog, LogLevel::Debug,
          ("Determining whether there's JOG for you."));

  if (!mozilla::StaticPrefs::telemetry_fog_artifact_build()) {
    // Supporting Artifact Builds is a developer-only thing.
    // We're on the main thread here.
    // Let's not spend any more time than we need to.
    MOZ_LOG(sJOGLog, LogLevel::Debug,
            ("!telemetry.fog.artifact_build. No JOG for you."));
    return false;
  }

#ifdef ANDROID
  // On Android, there is no GreD.
  // What there is instead is the assets/ folder.
  // Unfortunately, its contents are not necessarily "files" and might still
  // be inside the AAR, so we have to use URI to get to it.
  nsCOMPtr<nsIURI> uri;
  if (NS_WARN_IF(NS_FAILED(NS_NewURI(
          getter_AddRefs(uri), "resource://android/assets/jogfile.json"))) ||
      NS_WARN_IF(!uri)) {
    MOZ_LOG(sJOGLog, LogLevel::Debug, ("Unable to create URI to jogfile."));
    return false;
  }
  nsCOMPtr<nsIChannel> channel;
  nsresult rv = NS_NewChannel(
      getter_AddRefs(channel), uri, nsContentUtils::GetSystemPrincipal(),
      nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
      nsIContentPolicy::TYPE_OTHER, nullptr, /* nsICookieJarSettings */
      nullptr,                               /* aPerformanceStorage */
      nullptr,                               /* aLoadGroup */
      nullptr,                               /* aCallbacks */
      nsIRequest::LOAD_NORMAL);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    MOZ_LOG(sJOGLog, LogLevel::Debug,
            ("Unable to create channel for jogfile."));
    return false;
  }
  nsCOMPtr<nsIInputStream> input;
  if (NS_WARN_IF(NS_FAILED(channel->Open(getter_AddRefs(input))))) {
    MOZ_LOG(sJOGLog, LogLevel::Debug, ("Unable to open stream for jogfile."));
    return false;
  }
  nsCString contents;
  if (NS_WARN_IF(NS_FAILED(NS_ReadInputStreamToString(input, contents, -1)))) {
    MOZ_LOG(sJOGLog, LogLevel::Debug, ("Unable to read jogfile to string."));
    return false;
  }
  sFoundAndLoadedJogfile = Some(jog::jog_load_jogfile_str(&contents));

#else
  // The metrics we need to process were placed in GreD in jogfile.json
  // That file was generated by
  // toolkit/components/glean/build_scripts/glean_parser_ext/jog.py
  nsCOMPtr<nsIFile> jogfile;
  if (NS_WARN_IF(NS_FAILED(
          NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(jogfile))))) {
    return false;
  }
  if (NS_WARN_IF(NS_FAILED(jogfile->Append(u"jogfile.json"_ns)))) {
    return false;
  }
  bool jogfileExists = false;
  if (NS_WARN_IF(NS_FAILED(jogfile->Exists(&jogfileExists))) ||
      !jogfileExists) {
    return false;
  }

  // We _could_ register everything here in C++ land,
  // but let's use Rust because (among other reasons) it's more fun.
  nsAutoString jogfileString;
  if (NS_WARN_IF(NS_FAILED(jogfile->GetPath(jogfileString)))) {
    return false;
  }
  sFoundAndLoadedJogfile = Some(jog::jog_load_jogfile(&jogfileString));

#endif

  MOZ_LOG(sJOGLog, LogLevel::Debug,
          ("%s", sFoundAndLoadedJogfile.value()
                     ? "Found and loaded jogfile. Yes! JOG for you!"
                     : "Couldn't find and load jogfile. No JOG for you."));
  return sFoundAndLoadedJogfile.value();
}

// static
bool JOG::AreRuntimeMetricsComprehensive() {
  MOZ_ASSERT(NS_IsMainThread());
  return sFoundAndLoadedJogfile && sFoundAndLoadedJogfile.value();
}

// static
void JOG::GetCategoryNames(nsTArray<nsString>& aNames) {
  MOZ_ASSERT(NS_IsMainThread());
  if (!gCategories) {
    return;
  }
  for (const auto& category : *gCategories) {
    aNames.EmplaceBack(NS_ConvertUTF8toUTF16(category));
  }
}

// static
Maybe<uint32_t> JOG::GetMetric(const nsACString& aMetricName) {
  MOZ_ASSERT(NS_IsMainThread());
  return !gMetrics ? Nothing() : gMetrics->MaybeGet(aMetricName);
}

// static
Maybe<nsCString> JOG::GetMetricName(uint32_t aMetricId) {
  MOZ_ASSERT(NS_IsMainThread());
  return !gMetricNames ? Nothing() : gMetricNames->MaybeGet(aMetricId);
}

// static
void JOG::GetMetricNames(const nsACString& aCategoryName,
                         nsTArray<nsString>& aNames) {
  MOZ_ASSERT(NS_IsMainThread());
  if (!gMetricNames) {
    return;
  }
  for (const auto& identifier : gMetricNames->Values()) {
    if (StringBeginsWith(identifier, aCategoryName) &&
        identifier.CharAt(aCategoryName.Length()) == '.') {
      const char* metricName = &identifier.Data()[aCategoryName.Length() + 1];
      aNames.AppendElement()->AssignASCII(metricName);
    }
  }
}

// static
Maybe<uint32_t> JOG::GetPing(const nsACString& aPingName) {
  MOZ_ASSERT(NS_IsMainThread());
  return !gPings ? Nothing() : gPings->MaybeGet(aPingName);
}

// static
void JOG::GetPingNames(nsTArray<nsString>& aNames) {
  MOZ_ASSERT(NS_IsMainThread());
  if (!gPings) {
    return;
  }
  for (const auto& ping : gPings->Keys()) {
    aNames.EmplaceBack(NS_ConvertUTF8toUTF16(ping));
  }
}

}  // namespace mozilla::glean

// static
nsCString dottedSnakeToCamel(const nsACString& aSnake) {
  nsCString camel;
  bool first = true;
  for (const nsACString& segment : aSnake.Split('_')) {
    for (const nsACString& part : segment.Split('.')) {
      if (first) {
        first = false;
        camel.Append(part);
      } else if (part.Length()) {
        char lower = part.CharAt(0);
        if ('a' <= lower && lower <= 'z') {
          camel.Append(
              std::toupper(lower, std::locale()));  // append the Capital.
          camel.Append(part.BeginReading() + 1,
                       part.Length() - 1);  // append the rest.
        } else {
          // Not gonna try to capitalize anything outside a->z.
          camel.Append(part);
        }
      }
    }
  }
  return camel;
}

// static
nsCString kebabToCamel(const nsACString& aKebab) {
  nsCString camel;
  bool first = true;
  for (const nsACString& segment : aKebab.Split('-')) {
    if (first) {
      first = false;
      camel.Append(segment);
    } else if (segment.Length()) {
      char lower = segment.CharAt(0);
      if ('a' <= lower && lower <= 'z') {
        camel.Append(
            std::toupper(lower, std::locale()));  // append the Capital.
        camel.Append(segment.BeginReading() + 1,
                     segment.Length() - 1);  // append the rest.
      } else {
        // Not gonna try to capitalize anything outside a->z.
        camel.Append(segment);
      }
    }
  }
  return camel;
}

using mozilla::AppShutdown;
using mozilla::ShutdownPhase;
using mozilla::glean::gCategories;
using mozilla::glean::gMetricNames;
using mozilla::glean::gMetrics;
using mozilla::glean::gPings;

extern "C" NS_EXPORT void JOG_RegisterMetric(
    const nsACString& aCategory, const nsACString& aName,
    uint32_t aMetric,  // includes type.
    uint32_t aMetricId) {
  MOZ_ASSERT(NS_IsMainThread());

  if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
    return;
  }

  MOZ_LOG(mozilla::glean::sJOGLog, mozilla::LogLevel::Verbose,
          ("Registering metric %s.%s id %" PRIu32 " id+type %" PRIu32 "",
           PromiseFlatCString(aCategory).get(), PromiseFlatCString(aName).get(),
           aMetricId, aMetric));

  // aCategory is dotted.snake_case. aName is snake_case.
  auto categoryCamel = dottedSnakeToCamel(aCategory);
  auto nameCamel = dottedSnakeToCamel(aName);

  // Register the category
  if (!gCategories) {
    gCategories = new nsTHashSet<nsCString>();
    RunOnShutdown([&] { gCategories = nullptr; },
                  ShutdownPhase::XPCOMWillShutdown);
  }
  gCategories->Insert(categoryCamel);

  // Register the metric
  if (!gMetrics) {
    gMetrics = new nsTHashMap<nsCString, uint32_t>();
    RunOnShutdown([&] { gMetrics = nullptr; },
                  ShutdownPhase::XPCOMWillShutdown);
  }
  gMetrics->InsertOrUpdate(categoryCamel + "."_ns + nameCamel, aMetric);

  // Register the metric name (for GIFFT)
  if (!gMetricNames) {
    gMetricNames = new nsTHashMap<uint32_t, nsCString>();
    RunOnShutdown([&] { gMetricNames = nullptr; },
                  ShutdownPhase::XPCOMWillShutdown);
  }
  gMetricNames->InsertOrUpdate(aMetricId, categoryCamel + "."_ns + nameCamel);
}

extern "C" void jog_test_clear_registered_metrics_and_pings();

extern "C" NS_EXPORT void JOG_MaybeReload() {
  MOZ_ASSERT(NS_IsMainThread());

  // Don't clear JOG's registries if there's no jogfile.
  // There might be registered pings and metrics in here that won't know they
  // need to re-register.
  if (!mozilla::glean::sFoundAndLoadedJogfile.valueOr(false)) {
    return;
  }

  gCategories = nullptr;
  gMetricNames = nullptr;
  gMetrics = nullptr;
  gPings = nullptr;
  jog_test_clear_registered_metrics_and_pings();
  mozilla::glean::sFoundAndLoadedJogfile = mozilla::Nothing();
  mozilla::glean::JOG::EnsureRuntimeMetricsRegistered();
}

extern "C" NS_EXPORT void JOG_RegisterPing(const nsACString& aPingName,
                                           uint32_t aPingId) {
  MOZ_ASSERT(NS_IsMainThread());

  MOZ_LOG(mozilla::glean::sJOGLog, mozilla::LogLevel::Verbose,
          ("Registering ping %s id %" PRIu32 "",
           PromiseFlatCString(aPingName).get(), aPingId));

  if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
    return;
  }

  // aPingName is kebab-case. JS expects camelCase.
  auto pingCamel = kebabToCamel(aPingName);

  // Register the ping
  if (!gPings) {
    gPings = new nsTHashMap<nsCString, uint32_t>();
    RunOnShutdown([&] { gPings = nullptr; }, ShutdownPhase::XPCOMWillShutdown);
  }
  gPings->InsertOrUpdate(pingCamel, aPingId);
}
