/* 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/TimingDistribution.h"

#include "mozilla/AppShutdown.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/ResultVariant.h"
#include "mozilla/dom/GleanMetricsBinding.h"
#include "mozilla/dom/ToJSValue.h"
#include "mozilla/glean/bindings/HistogramGIFFTMap.h"
#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
#include "mozilla/glean/fog_ffi_generated.h"
#include "nsJSUtils.h"
#include "nsPrintfCString.h"
#include "nsString.h"
#include "js/PropertyAndElement.h"  // JS_DefineProperty
#include "GIFFTFwd.h"

using mozilla::TimeDuration;
using mozilla::TimeStamp;

namespace mozilla::glean {

using MetricId = uint32_t;  // Same type as in api/src/private/mod.rs
struct MetricTimerTuple {
  MetricId mMetricId;
  TimerId mTimerId;
};
class MetricTimerTupleHashKey : public PLDHashEntryHdr {
 public:
  using KeyType = const MetricTimerTuple&;
  using KeyTypePointer = const MetricTimerTuple*;

  explicit MetricTimerTupleHashKey(KeyTypePointer aKey) : mValue(*aKey) {}
  MetricTimerTupleHashKey(MetricTimerTupleHashKey&& aOther)
      : PLDHashEntryHdr(std::move(aOther)), mValue(aOther.mValue) {}
  ~MetricTimerTupleHashKey() = default;

  KeyType GetKey() const { return mValue; }
  bool KeyEquals(KeyTypePointer aKey) const {
    return aKey->mMetricId == mValue.mMetricId &&
           aKey->mTimerId == mValue.mTimerId;
  }

  static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
  static PLDHashNumber HashKey(KeyTypePointer aKey) {
    // Chosen because this is how nsIntegralHashKey does it.
    return HashGeneric(aKey->mMetricId, aKey->mTimerId);
  }
  enum { ALLOW_MEMMOVE = true };
  static_assert(std::is_trivially_copyable_v<MetricTimerTuple>);

 private:
  const MetricTimerTuple mValue;
};

using TimerToStampMutex =
    StaticDataMutex<UniquePtr<nsTHashMap<MetricTimerTupleHashKey, TimeStamp>>>;
static Maybe<TimerToStampMutex::AutoLock> GetTimerIdToStartsLock() {
  static TimerToStampMutex sTimerIdToStarts("sTimerIdToStarts");
  auto lock = sTimerIdToStarts.Lock();
  // GIFFT will work up to the end of AppShutdownTelemetry.
  if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
    return Nothing();
  }
  if (!*lock) {
    *lock = MakeUnique<nsTHashMap<MetricTimerTupleHashKey, TimeStamp>>();
    RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] {
      if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
        auto lock = sTimerIdToStarts.Lock();
        *lock = nullptr;  // deletes, see UniquePtr.h
        return;
      }
      RunOnShutdown(
          [&] {
            auto lock = sTimerIdToStarts.Lock();
            *lock = nullptr;  // deletes, see UniquePtr.h
          },
          ShutdownPhase::XPCOMWillShutdown);
    });
    // Both getting the main thread and dispatching to it can fail.
    // In that event we leak. Grab a pointer so we have something to NS_RELEASE
    // in that case.
    nsIRunnable* temp = cleanupFn.get();
    nsCOMPtr<nsIThread> mainThread;
    if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) ||
        NS_FAILED(mainThread->Dispatch(cleanupFn.forget(),
                                       nsIThread::DISPATCH_NORMAL))) {
      // Failed to dispatch cleanup routine.
      // First, un-leak the runnable (but only if we actually attempted
      // dispatch)
      if (!cleanupFn) {
        NS_RELEASE(temp);
      }
      // Next, cleanup immediately, and allow metrics to try again later.
      *lock = nullptr;
      return Nothing();
    }
  }
  return Some(std::move(lock));
}

struct MetricLabelTimerTuple {
  MetricId mMetricId;
  nsCString mLabel;
  TimerId mTimerId;
};
class MetricLabelTimerTupleHashKey : public PLDHashEntryHdr {
 public:
  using KeyType = const MetricLabelTimerTuple&;
  using KeyTypePointer = const MetricLabelTimerTuple*;

  explicit MetricLabelTimerTupleHashKey(KeyTypePointer aKey) : mValue(*aKey) {}
  MetricLabelTimerTupleHashKey(MetricLabelTimerTupleHashKey&& aOther)
      : PLDHashEntryHdr(std::move(aOther)), mValue(aOther.mValue) {}
  ~MetricLabelTimerTupleHashKey() = default;

  KeyType GetKey() const { return mValue; }
  bool KeyEquals(KeyTypePointer aKey) const {
    return aKey->mMetricId == mValue.mMetricId &&
           aKey->mLabel == mValue.mLabel && aKey->mTimerId == mValue.mTimerId;
  }

  static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
  static PLDHashNumber HashKey(KeyTypePointer aKey) {
    return HashGeneric(aKey->mMetricId, HashString(aKey->mLabel),
                       aKey->mTimerId);
  }
  // Permitted to memmove nsCString even though it's not trivially copyable.
  enum { ALLOW_MEMMOVE = true };

 private:
  const MetricLabelTimerTuple mValue;
};

using LabelTimerToStampMutex = StaticDataMutex<
    UniquePtr<nsTHashMap<MetricLabelTimerTupleHashKey, TimeStamp>>>;
static Maybe<LabelTimerToStampMutex::AutoLock> GetLabelTimerIdToStartsLock() {
  static LabelTimerToStampMutex sLabelTimerIdToStarts("sLabelTimerIdToStarts");
  auto lock = sLabelTimerIdToStarts.Lock();
  // GIFFT will work up to the end of AppShutdownTelemetry.
  if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
    return Nothing();
  }
  if (!*lock) {
    *lock = MakeUnique<nsTHashMap<MetricLabelTimerTupleHashKey, TimeStamp>>();
    RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] {
      if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
        auto lock = sLabelTimerIdToStarts.Lock();
        *lock = nullptr;  // deletes, see UniquePtr.h
        return;
      }
      RunOnShutdown(
          [&] {
            auto lock = sLabelTimerIdToStarts.Lock();
            *lock = nullptr;  // deletes, see UniquePtr.h
          },
          ShutdownPhase::XPCOMWillShutdown);
    });
    // Both getting the main thread and dispatching to it can fail.
    // In that event we leak. Grab a pointer so we have something to NS_RELEASE
    // in that case.
    nsIRunnable* temp = cleanupFn.get();
    nsCOMPtr<nsIThread> mainThread;
    if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) ||
        NS_FAILED(mainThread->Dispatch(cleanupFn.forget(),
                                       nsIThread::DISPATCH_NORMAL))) {
      // Failed to dispatch cleanup routine.
      // First, un-leak the runnable (but only if we actually attempted
      // dispatch)
      if (!cleanupFn) {
        NS_RELEASE(temp);
      }
      // Next, cleanup immediately, and allow metrics to try again later.
      *lock = nullptr;
      return Nothing();
    }
  }
  return Some(std::move(lock));
}

}  // namespace mozilla::glean

using mozilla::glean::TimerId;

// Called from within FOG's Rust impl.
extern "C" NS_EXPORT void GIFFT_TimingDistributionStart(uint32_t aMetricId,
                                                        TimerId aTimerId) {
  auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
  if (mirrorId) {
    mozilla::glean::GetTimerIdToStartsLock().apply([&](const auto& lock) {
      auto tuple = mozilla::glean::MetricTimerTuple{aMetricId, aTimerId};
      // It should be all but impossible for anyone to have already inserted
      // this timer for this metric given the monotonicity of timer ids.
      (void)NS_WARN_IF(lock.ref()->Remove(tuple));
      lock.ref()->InsertOrUpdate(tuple, mozilla::TimeStamp::Now());
    });
  }
}

// Called from within FOG's Rust impl.
extern "C" NS_EXPORT void GIFFT_TimingDistributionStopAndAccumulate(
    uint32_t aMetricId, TimerId aTimerId, int32_t aUnit) {
  auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
  if (mirrorId) {
    mozilla::glean::GetTimerIdToStartsLock().apply([&](const auto& lock) {
      auto tuple = mozilla::glean::MetricTimerTuple{aMetricId, aTimerId};
      auto optStart = lock.ref()->Extract(tuple);
      // The timer might not be in the map to be removed if it's already been
      // cancelled or stop_and_accumulate'd.
      if (!NS_WARN_IF(!optStart)) {
        TimeDuration duration = TimeStamp::Now() - optStart.extract();
        // Values are from Glean's `TimeUnit`
        switch (aUnit) {
          case 0:  // Nanos
            TelemetryHistogram::Accumulate(mirrorId.extract(),
                                           duration.ToMicroseconds() * 1000);
            break;
          case 1:  // Micros
            TelemetryHistogram::Accumulate(mirrorId.extract(),
                                           duration.ToMicroseconds());
            break;
          case 2:  // Millis
            TelemetryHistogram::Accumulate(mirrorId.extract(),
                                           duration.ToMilliseconds());
            break;
          case 3:  // Seconds
            TelemetryHistogram::Accumulate(mirrorId.extract(),
                                           duration.ToSeconds());
            break;
          case 4:  // Minutes
            TelemetryHistogram::Accumulate(mirrorId.extract(),
                                           duration.ToSeconds() / 60);
            break;
          case 5:  // Hours
            TelemetryHistogram::Accumulate(mirrorId.extract(),
                                           duration.ToSeconds() / 60 / 60);
            break;
          case 6:  // Days
            TelemetryHistogram::Accumulate(mirrorId.extract(),
                                           duration.ToSeconds() / 60 / 60 / 24);
            break;
          default:
            MOZ_ASSERT_UNREACHABLE("Invalid/Unsupported time unit");
            return;
        }
      }
    });
  }
}

// Called from within FOG's Rust impl.
extern "C" NS_EXPORT void GIFFT_TimingDistributionAccumulateRawSample(
    uint32_t aMetricId, uint32_t aSample) {
  auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
  if (mirrorId) {
    TelemetryHistogram::Accumulate(mirrorId.extract(), aSample);
  }
}

// Called from within FOG's Rust impl.
extern "C" NS_EXPORT void GIFFT_TimingDistributionAccumulateRawSamples(
    uint32_t aMetricId, const nsTArray<uint32_t>& aSamples) {
  auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
  if (mirrorId) {
    TelemetryHistogram::Accumulate(mirrorId.extract(), aSamples);
  }
}

// Called from within FOG's Rust impl.
extern "C" NS_EXPORT void GIFFT_TimingDistributionCancel(uint32_t aMetricId,
                                                         TimerId aTimerId) {
  auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
  if (mirrorId) {
    mozilla::glean::GetTimerIdToStartsLock().apply([&](const auto& lock) {
      // The timer might not be in the map to be removed if it's already been
      // cancelled or stop_and_accumulate'd.
      auto tuple = mozilla::glean::MetricTimerTuple{aMetricId, aTimerId};
      (void)NS_WARN_IF(!lock.ref()->Remove(tuple));
    });
  }
}

// Called from within FOG's Rust impl.
extern "C" NS_EXPORT void GIFFT_LabeledTimingDistributionStart(
    uint32_t aMetricId, const nsACString& aLabel, TimerId aTimerId) {
  auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
  if (mirrorId) {
    mozilla::glean::GetLabelTimerIdToStartsLock().apply([&](const auto& lock) {
      auto tuple = mozilla::glean::MetricLabelTimerTuple{
          aMetricId, PromiseFlatCString(aLabel), aTimerId};
      lock.ref()->InsertOrUpdate(tuple, mozilla::TimeStamp::Now());
    });
  }
}

// Called from within FOG's Rust impl.
extern "C" NS_EXPORT void GIFFT_LabeledTimingDistributionStopAndAccumulate(
    uint32_t aMetricId, const nsACString& aLabel, TimerId aTimerId) {
  auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
  if (mirrorId) {
    mozilla::glean::GetLabelTimerIdToStartsLock().apply([&](const auto& lock) {
      auto tuple = mozilla::glean::MetricLabelTimerTuple{
          aMetricId, PromiseFlatCString(aLabel), aTimerId};
      auto optStart = lock.ref()->Extract(tuple);
      // The timer might not be in the map to be removed if it's already been
      // cancelled or stop_and_accumulate'd.
      if (!NS_WARN_IF(!optStart)) {
        TelemetryHistogram::Accumulate(
            mirrorId.extract(), PromiseFlatCString(aLabel),
            static_cast<uint32_t>(
                (mozilla::TimeStamp::Now() - optStart.extract())
                    .ToMilliseconds()));
      }
    });
  }
}

// Called from within FOG's Rust impl.
extern "C" NS_EXPORT void GIFFT_LabeledTimingDistributionAccumulateRawMillis(
    uint32_t aMetricId, const nsACString& aLabel, uint32_t aMS) {
  auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
  if (mirrorId) {
    TelemetryHistogram::Accumulate(mirrorId.extract(),
                                   PromiseFlatCString(aLabel), aMS);
  }
}

// Called from within FOG's Rust impl.
extern "C" NS_EXPORT void GIFFT_LabeledTimingDistributionCancel(
    uint32_t aMetricId, const nsACString& aLabel, TimerId aTimerId) {
  auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
  if (mirrorId) {
    mozilla::glean::GetLabelTimerIdToStartsLock().apply([&](const auto& lock) {
      // The timer might not be in the map to be removed if it's already been
      // cancelled or stop_and_accumulate'd.
      auto tuple = mozilla::glean::MetricLabelTimerTuple{
          aMetricId, PromiseFlatCString(aLabel), aTimerId};
      (void)NS_WARN_IF(!lock.ref()->Remove(tuple));
    });
  }
}

namespace mozilla::glean {

namespace impl {

TimerId TimingDistributionStandalone::Start() const {
  return fog_timing_distribution_start(mId);
}

void TimingDistributionStandalone::StopAndAccumulate(
    const TimerId&& aId) const {
  fog_timing_distribution_stop_and_accumulate(mId, aId);
}

// Intentionally not exposed to JS for lack of use case and a time duration
// type.
void TimingDistributionStandalone::AccumulateRawDuration(
    const TimeDuration& aDuration) const {
  // `* 1000.0` is an acceptable overflow risk as durations are unlikely to be
  // on the order of (-)10^282 years.
  double durationNs = aDuration.ToMicroseconds() * 1000.0;
  double roundedDurationNs = std::round(durationNs);
  if (MOZ_UNLIKELY(
          roundedDurationNs <
              static_cast<double>(std::numeric_limits<uint64_t>::min()) ||
          roundedDurationNs >
              static_cast<double>(std::numeric_limits<uint64_t>::max()))) {
    // TODO(bug 1691073): Instrument this error.
    return;
  }
  fog_timing_distribution_accumulate_raw_nanos(
      mId, static_cast<uint64_t>(roundedDurationNs));
}

void TimingDistributionStandalone::Cancel(const TimerId&& aId) const {
  fog_timing_distribution_cancel(mId, aId);
}

Result<Maybe<DistributionData>, nsCString>
TimingDistributionMetric::TestGetValue(const nsACString& aPingName) const {
  nsCString err;
  if (fog_timing_distribution_test_get_error(mId, &err)) {
    return Err(err);
  }
  if (!fog_timing_distribution_test_has_value(mId, &aPingName)) {
    return Maybe<DistributionData>();
  }
  nsTArray<uint64_t> buckets;
  nsTArray<uint64_t> counts;
  uint64_t sum;
  uint64_t count;
  fog_timing_distribution_test_get_value(mId, &aPingName, &sum, &count,
                                         &buckets, &counts);
  return Some(DistributionData(buckets, counts, sum, count));
}

TimingDistributionStandalone::AutoTimer TimingDistributionStandalone::Measure()
    const {
  return AutoTimer(mId, this->Start());
}

void TimingDistributionStandalone::AutoTimer::Cancel() {
  fog_timing_distribution_cancel(mMetricId, std::move(mTimerId));
  mTimerId = 0;
}

TimingDistributionStandalone::AutoTimer::~AutoTimer() {
  if (mTimerId) {
    fog_timing_distribution_stop_and_accumulate(mMetricId, std::move(mTimerId));
  }
}

}  // namespace impl

/* virtual */
JSObject* GleanTimingDistribution::WrapObject(
    JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
  return dom::GleanTimingDistribution_Binding::Wrap(aCx, this, aGivenProto);
}

uint64_t GleanTimingDistribution::Start() { return mTimingDist.Start(); }

void GleanTimingDistribution::StopAndAccumulate(uint64_t aId) {
  mTimingDist.StopAndAccumulate(std::move(aId));
}

void GleanTimingDistribution::Cancel(uint64_t aId) {
  mTimingDist.Cancel(std::move(aId));
}

void GleanTimingDistribution::AccumulateSamples(
    const nsTArray<int64_t>& aSamples) {
  impl::fog_timing_distribution_accumulate_samples(mTimingDist.mId, &aSamples);
}

void GleanTimingDistribution::AccumulateSingleSample(int64_t aSample) {
  impl::fog_timing_distribution_accumulate_single_sample(mTimingDist.mId,
                                                         aSample);
}

void GleanTimingDistribution::TestGetValue(
    const nsACString& aPingName,
    dom::Nullable<dom::GleanDistributionData>& aRetval, ErrorResult& aRv) {
  auto result = mTimingDist.TestGetValue(aPingName);
  if (result.isErr()) {
    aRv.ThrowDataError(result.unwrapErr());
    return;
  }
  auto optresult = result.unwrap();
  if (optresult.isNothing()) {
    return;
  }

  dom::GleanDistributionData ret;
  ret.mSum = optresult.ref().sum;
  ret.mCount = optresult.ref().count;
  auto& data = optresult.ref().values;
  for (const auto& entry : data) {
    dom::binding_detail::RecordEntry<nsCString, uint64_t> bucket;
    bucket.mKey = nsPrintfCString("%" PRIu64, entry.GetKey());
    bucket.mValue = entry.GetData();
    ret.mValues.Entries().EmplaceBack(std::move(bucket));
  }
  aRetval.SetValue(std::move(ret));
}

void GleanTimingDistribution::TestAccumulateRawMillis(uint64_t aSample) {
  mTimingDist.AccumulateRawDuration(TimeDuration::FromMilliseconds(aSample));
}

}  // namespace mozilla::glean
