// 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 https://mozilla.org/MPL/2.0/.

use inherent::inherent;
use std::collections::HashMap;
use std::convert::TryInto;
use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Arc, RwLock,
};
use std::time::{Duration, Instant};
#[cfg(feature = "with_gecko")]
use thin_vec::ThinVec;

use super::{BaseMetricId, ChildMetricMeta, CommonMetricData, MetricId, MetricNamer, TimeUnit};
use glean::{DistributionData, ErrorType, MetricIdentifier, TimerId};

use crate::ipc::{need_ipc, with_ipc_payload};
use glean::traits::TimingDistribution;

#[cfg(feature = "with_gecko")]
use super::MetricMetadataGetter;

#[cfg(feature = "with_gecko")]
use super::profiler_utils::{truncate_vector_for_marker, TelemetryProfilerCategory};

#[cfg(feature = "with_gecko")]
use std::marker::PhantomData;

#[cfg(feature = "with_gecko")]
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub(crate) enum TDMPayload {
    Duration(std::time::Duration),
    Sample(i64),
    Samples(Vec<i64>),
    SamplesNS(Vec<u64>),
}

#[cfg(feature = "with_gecko")]
impl TDMPayload {
    pub fn from_samples_signed(samples: &Vec<i64>) -> TDMPayload {
        TDMPayload::Samples(truncate_vector_for_marker(samples))
    }
    pub fn from_samples_unsigned(samples: &Vec<u64>) -> TDMPayload {
        TDMPayload::SamplesNS(truncate_vector_for_marker(samples))
    }
}

#[cfg(feature = "with_gecko")]
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub(crate) struct TimingDistributionMetricMarker<MetricT> {
    id: MetricId,
    label: Option<String>,
    timer_id: Option<u64>,
    value: Option<TDMPayload>,
    _phantom: PhantomData<MetricT>,
}

#[cfg(feature = "with_gecko")]
impl<MetricT: 'static> TimingDistributionMetricMarker<MetricT> {
    pub fn new(
        id: MetricId,
        label: Option<String>,
        timer_id: Option<u64>,
        value: Option<TDMPayload>,
    ) -> TimingDistributionMetricMarker<MetricT> {
        TimingDistributionMetricMarker {
            id,
            label,
            timer_id,
            value,
            _phantom: PhantomData,
        }
    }
}

#[cfg(feature = "with_gecko")]
impl<MetricT: MetricMetadataGetter + MetricNamer + 'static> gecko_profiler::ProfilerMarker
    for TimingDistributionMetricMarker<MetricT>
{
    fn marker_type_name() -> &'static str {
        "TimingDist"
    }

    fn marker_type_display() -> gecko_profiler::MarkerSchema {
        use gecko_profiler::schema::*;
        let mut schema = MarkerSchema::new(&[Location::MarkerChart, Location::MarkerTable]);
        schema.set_tooltip_label(
            "{marker.data.cat}.{marker.data.id} {marker.data.label} {marker.data.duration}{marker.data.sample}",
        );
        schema.set_table_label("{marker.data.cat}.{marker.data.id} {marker.data.label}: {marker.data.duration}{marker.data.sample}{marker.data.samples}");
        schema.set_chart_label("{marker.data.cat}.{marker.data.id} {marker.data.label}");
        schema.add_key_label_format("cat", "Category", Format::UniqueString);
        schema.add_key_label_format("id", "Metric", Format::UniqueString);
        schema.add_key_label_format("label", "Label", Format::UniqueString);
        schema.add_key_label_format("timer_id", "TimerId", Format::Integer);
        schema.add_key_label_format("duration", "Duration", Format::String);
        schema.add_key_label_format("sample", "Sample", Format::String);
        schema.add_key_label_format("samples", "Samples", Format::String);
        schema
    }

    fn stream_json_marker_data(&self, json_writer: &mut gecko_profiler::JSONWriter) {
        crate::private::profiler_utils::stream_identifiers_by_id::<MetricT>(
            &self.id.into(),
            json_writer,
        );

        if let Some(id) = &self.timer_id {
            // We don't care about exactly what the timer id is - so just
            // perform a bitwise cast, as that provides a 1:1 mapping.
            json_writer.int_property("timer_id", *id as i64);
        };

        match &self.value {
            Some(p) => {
                match p {
                    TDMPayload::Duration(d) => {
                        // Durations do not have a `Display` implementation,
                        // however for the profiler, the debug formatting
                        // should be more than sufficient.
                        let s = format!("{:?}", d);
                        json_writer.string_property("duration", s.as_str());
                    }
                    TDMPayload::Sample(s) => {
                        let s = format!("{}", s);
                        json_writer.string_property("sample", s.as_str());
                    }
                    TDMPayload::Samples(s) => {
                        let s = format!(
                            "[{}]",
                            s.iter()
                                .map(|v| v.to_string())
                                .collect::<Vec<_>>()
                                .join(",")
                        );
                        json_writer.string_property("samples", s.as_str());
                    }
                    TDMPayload::SamplesNS(s) => {
                        let s = format!(
                            "(ns) [{}]",
                            s.iter()
                                .map(|v| v.to_string())
                                .collect::<Vec<_>>()
                                .join(",")
                        );
                        json_writer.string_property("samples", s.as_str());
                    }
                };
            }
            None => {}
        };
    }
}

/// A timing distribution metric.
///
/// Timing distributions are used to accumulate and store time measurements for analyzing distributions of the timing data.
pub enum TimingDistributionMetric {
    Parent {
        /// The metric's ID. Used for testing, GIFFT, and profiler markers.
        /// Timing distribution metrics can be labeled, so we may have either
        /// a metric ID or sub-metric ID.
        id: MetricId,
        gifft_time_unit: TimeUnit,
        inner: Arc<glean::private::TimingDistributionMetric>,
    },
    Child(TimingDistributionMetricIpc),
}
#[derive(Debug)]
pub struct TimingDistributionMetricIpc {
    meta: ChildMetricMeta,
    #[allow(unused)]
    gifft_time_unit: TimeUnit,
    next_timer_id: AtomicUsize,
    instants: RwLock<HashMap<u64, Instant>>,
}

impl TimingDistributionMetric {
    /// Create a new timing distribution metric, _child process only_.
    pub(crate) fn new_child(meta: ChildMetricMeta, time_unit: TimeUnit) -> Self {
        debug_assert!(need_ipc());
        TimingDistributionMetric::Child(TimingDistributionMetricIpc {
            meta,
            gifft_time_unit: time_unit,
            next_timer_id: AtomicUsize::new(0),
            instants: RwLock::new(HashMap::new()),
        })
    }

    /// Create a new timing distribution metric.
    pub fn new(id: BaseMetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self {
        if need_ipc() {
            Self::new_child(
                ChildMetricMeta::from_common_metric_data(id, meta),
                time_unit,
            )
        } else {
            let inner = glean::private::TimingDistributionMetric::new(meta, time_unit);
            TimingDistributionMetric::Parent {
                id: id.into(),
                gifft_time_unit: time_unit,
                inner: Arc::new(inner),
            }
        }
    }

    #[cfg(test)]
    pub(crate) fn child_metric(&self) -> Self {
        match self {
            TimingDistributionMetric::Parent {
                id,
                gifft_time_unit,
                inner,
            } => TimingDistributionMetric::Child(TimingDistributionMetricIpc {
                // SAFETY: We can unwrap here, as this code is only run in
                // the context of a test. If this code is used elsewhere,
                // the `unwrap` should be replaced with proper error
                // handling of the `None` case.
                meta: ChildMetricMeta::from_metric_identifier(
                    id.base_metric_id().unwrap(),
                    inner.as_ref(),
                ),
                gifft_time_unit: *gifft_time_unit,
                next_timer_id: AtomicUsize::new(0),
                instants: RwLock::new(HashMap::new()),
            }),
            TimingDistributionMetric::Child(_) => {
                panic!("Can't get a child metric from a child metric")
            }
        }
    }

    /// Performs the core portions of a start() call, but no frippery like GIFFT.
    pub(crate) fn inner_start(&self) -> TimerId {
        match self {
            TimingDistributionMetric::Parent { inner, .. } => inner.start(),
            TimingDistributionMetric::Child(c) => {
                // There is no glean-core on this process to give us a TimerId,
                // so we'll have to make our own and do our own bookkeeping.
                let id = c
                    .next_timer_id
                    .fetch_add(1, Ordering::SeqCst)
                    .try_into()
                    .unwrap();
                let mut map = c
                    .instants
                    .write()
                    .expect("lock of instants map was poisoned");
                if let Some(_v) = map.insert(id, Instant::now()) {
                    // TODO: report an error and find a different TimerId.
                }
                id.into()
            }
        }
    }

    /// Performs the core portions of a stop_and_accumulate() call, but no frippery like GIFFT.
    pub(crate) fn inner_stop_and_accumulate(&self, id: TimerId) {
        match self {
            TimingDistributionMetric::Parent { inner, .. } => inner.stop_and_accumulate(id),
            TimingDistributionMetric::Child(c) => {
                if let Some(sample) = self.child_stop(id) {
                    with_ipc_payload(move |payload| {
                        if let Some(v) = payload.timing_samples.get_mut(&c.meta.id) {
                            v.push(sample);
                        } else {
                            payload.timing_samples.insert(c.meta.id, vec![sample]);
                        }
                    });
                } else {
                    // TODO: report an error (timer id for stop wasn't started).
                }
            }
        }
    }

    /// Stops the provided TimerId, but instead of accumulating the sample, returns it. No GIFFT neither.
    pub(crate) fn child_stop(&self, id: TimerId) -> Option<u64> {
        match self {
            TimingDistributionMetric::Parent { .. } => {
                panic!("Can't child_stop a parent-process timing_distribution")
            }
            TimingDistributionMetric::Child(c) => {
                let mut map = c
                    .instants
                    .write()
                    .expect("Write lock must've been poisoned.");
                if let Some(start) = map.remove(&id.id) {
                    let now = Instant::now();
                    let sample = now
                        .checked_duration_since(start)
                        .map(|s| s.as_nanos().try_into());
                    match sample {
                        Some(Ok(sample)) => Some(sample),
                        Some(Err(_)) => {
                            log::warn!("Elapsed time larger than fits into 64-bytes. Saturating at u64::MAX.");
                            Some(u64::MAX)
                        }
                        None => {
                            log::warn!("Time went backwards. Not recording.");
                            // TODO: report an error (timer id for stop was started, but time went backwards).
                            None
                        }
                    }
                } else {
                    // TODO: report an error (timer id for stop was never started).
                    None
                }
            }
        }
    }

    /// Cancels the provided TimerId without notifying GIFFT.
    pub(crate) fn inner_cancel(&self, id: TimerId) {
        match self {
            TimingDistributionMetric::Parent { inner, .. } => inner.cancel(id),
            TimingDistributionMetric::Child(c) => {
                let mut map = c
                    .instants
                    .write()
                    .expect("Write lock must've been poisoned.");
                if map.remove(&id.id).is_none() {
                    // TODO: report an error (cancelled a non-started id).
                }
            }
        }
    }

    /// Accumulates the raw duration without notifying GIFFT.
    pub(crate) fn inner_accumulate_raw_duration(&self, duration: Duration) {
        let sample = duration.as_nanos().try_into().unwrap_or_else(|_| {
            // TODO: Instrument this error
            log::warn!(
                "Elapsed nanoseconds larger than fits into 64-bytes. Saturating at u64::MAX."
            );
            u64::MAX
        });
        match self {
            TimingDistributionMetric::Parent { inner, .. } => {
                inner.accumulate_raw_duration(duration)
            }
            TimingDistributionMetric::Child(c) => {
                with_ipc_payload(move |payload| {
                    if let Some(v) = payload.timing_samples.get_mut(&c.meta.id) {
                        v.push(sample);
                    } else {
                        payload.timing_samples.insert(c.meta.id, vec![sample]);
                    }
                });
            }
        }
    }

    /// Accumulates the samples without notifying GIFFT.
    pub(crate) fn inner_accumulate_samples(&self, samples: Vec<i64>) {
        match self {
            #[allow(unused)]
            TimingDistributionMetric::Parent {
                id: id @ MetricId::Id(_),
                inner,
                ..
            } => {
                // Only record a marker if we are accumulating samples for an
                // actual metric. Sub-metrics will record their own markers
                // in their own variant of `accumulate_samples`, so if we
                // also record a marker, there will be duplicates.
                #[cfg(feature = "with_gecko")]
                gecko_profiler::lazy_add_marker!(
                    "TimingDistribution::accumulate",
                    TelemetryProfilerCategory,
                    TimingDistributionMetricMarker::<TimingDistributionMetric>::new(
                        *id,
                        None,
                        None,
                        Some(TDMPayload::from_samples_signed(&samples)),
                    )
                );
                inner.accumulate_samples(samples)
            }
            TimingDistributionMetric::Parent { inner, .. } => inner.accumulate_samples(samples),
            TimingDistributionMetric::Child(_c) => {
                // TODO: Instrument this error
                log::error!("Can't record samples for a timing distribution from a child metric");
            }
        }
    }

    /// Accumulates the single sample without notifying GIFFT.
    pub(crate) fn inner_accumulate_single_sample(&self, sample: i64) {
        match self {
            #[allow(unused)]
            TimingDistributionMetric::Parent {
                id: id @ MetricId::Id(_),
                inner,
                ..
            } => {
                // Only record metric markers, not sub-metric markers. See
                // `inner_accumulate_samples` for details of why.
                #[cfg(feature = "with_gecko")]
                gecko_profiler::lazy_add_marker!(
                    "TimingDistribution::accumulate",
                    TelemetryProfilerCategory,
                    TimingDistributionMetricMarker::<TimingDistributionMetric>::new(
                        *id,
                        None,
                        None,
                        Some(TDMPayload::Sample(sample)),
                    )
                );
                inner.accumulate_single_sample(sample)
            }
            TimingDistributionMetric::Parent { inner, .. } => {
                inner.accumulate_single_sample(sample)
            }
            TimingDistributionMetric::Child(_c) => {
                // TODO: Instrument this error
                log::error!("Can't record samples for a timing distribution from a child metric");
            }
        }
    }
}

#[inherent]
impl TimingDistribution for TimingDistributionMetric {
    /// Starts tracking time for the provided metric.
    ///
    /// This records an error if it’s already tracking time (i.e.
    /// [`start`](TimingDistribution::start) was already called with no corresponding
    /// [`stop_and_accumulate`](TimingDistribution::stop_and_accumulate)): in that case the
    /// original start time will be preserved.
    ///
    /// # Returns
    ///
    /// A unique [`TimerId`] for the new timer.
    pub fn start(&self) -> TimerId {
        let timer_id = self.inner_start();
        #[cfg(feature = "with_gecko")]
        {
            let metric_id: BaseMetricId = match self {
                TimingDistributionMetric::Parent { id, .. } => id
                    .base_metric_id()
                    .expect("Cannot perform GIFFT calls without a metric id."),
                TimingDistributionMetric::Child(c) => c.meta.id,
            };
            extern "C" {
                fn GIFFT_TimingDistributionStart(metric_id: u32, timer_id: u64);
            }
            // SAFETY: using only primitives, no return value.
            unsafe {
                GIFFT_TimingDistributionStart(metric_id.0, timer_id.id);
            }
            // NOTE: we would like to record interval markers, either separate
            // markers with start/end, or a single marker with both start/end.
            // This is currently not possible, as the profiler incorrectly
            // matches separate start/end markers in the frontend, and we do
            // not have sufficient information to emit one marker when we stop
            // or cancel a timer.
            // This is being tracked in the following two bugs:
            // - Profiler, Bug 1929070,
            // - Glean, Bug 1931369,
            // While these bugs are being solved, we record instant markers so
            // that we still have *some* information.
            gecko_profiler::lazy_add_marker!(
                "TimingDistribution::start",
                TelemetryProfilerCategory,
                gecko_profiler::MarkerOptions::default()
                    .with_timing(gecko_profiler::MarkerTiming::instant_now()),
                TimingDistributionMetricMarker::<TimingDistributionMetric>::new(
                    metric_id.into(),
                    None,
                    Some(timer_id.id),
                    None
                )
            );
        }
        timer_id.into()
    }

    /// Stops tracking time for the provided metric and associated timer id.
    ///
    /// Adds a count to the corresponding bucket in the timing distribution.
    /// This will record an error if no [`start`](TimingDistribution::start) was
    /// called.
    ///
    /// # Arguments
    ///
    /// * `id` - The [`TimerId`] to associate with this timing. This allows
    ///   for concurrent timing of events associated with different ids to the
    ///   same timespan metric.
    pub fn stop_and_accumulate(&self, id: TimerId) {
        self.inner_stop_and_accumulate(id);
        #[cfg(feature = "with_gecko")]
        {
            let (metric_id, gifft_time_unit) = match self {
                TimingDistributionMetric::Parent {
                    id,
                    gifft_time_unit,
                    ..
                } => (
                    id.base_metric_id()
                        .expect("Cannot perform GIFFT calls without a metric id."),
                    gifft_time_unit,
                ),
                TimingDistributionMetric::Child(c) => (c.meta.id, &c.gifft_time_unit),
            };
            extern "C" {
                fn GIFFT_TimingDistributionStopAndAccumulate(
                    metric_id: u32,
                    timer_id: u64,
                    unit: i32,
                );
            }
            // SAFETY: using only primitives, no return value.
            unsafe {
                GIFFT_TimingDistributionStopAndAccumulate(
                    metric_id.0,
                    id.id,
                    *gifft_time_unit as i32,
                );
            }
            // See note on TimingDistribution::start
            gecko_profiler::lazy_add_marker!(
                "TimingDistribution::stop",
                TelemetryProfilerCategory,
                gecko_profiler::MarkerOptions::default()
                    .with_timing(gecko_profiler::MarkerTiming::instant_now()),
                TimingDistributionMetricMarker::<TimingDistributionMetric>::new(
                    metric_id.into(),
                    None,
                    Some(id.id),
                    None
                )
            );
        }
    }

    /// Aborts a previous [`start`](TimingDistribution::start) call. No
    /// error is recorded if no [`start`](TimingDistribution::start) was
    /// called.
    ///
    /// # Arguments
    ///
    /// * `id` - The [`TimerId`] to associate with this timing. This allows
    ///   for concurrent timing of events associated with different ids to the
    ///   same timing distribution metric.
    pub fn cancel(&self, id: TimerId) {
        self.inner_cancel(id);
        #[cfg(feature = "with_gecko")]
        {
            let metric_id: BaseMetricId = match self {
                TimingDistributionMetric::Parent { id, .. } => id
                    .base_metric_id()
                    .expect("Cannot perform GIFFT calls without a metric id."),
                TimingDistributionMetric::Child(c) => c.meta.id,
            };
            extern "C" {
                fn GIFFT_TimingDistributionCancel(metric_id: u32, timer_id: u64);
            }
            // SAFETY: using only primitives, no return value.
            unsafe {
                GIFFT_TimingDistributionCancel(metric_id.0, id.id);
            }
            // See note on TimingDistribution::start
            gecko_profiler::lazy_add_marker!(
                "TimingDistribution::cancel",
                TelemetryProfilerCategory,
                gecko_profiler::MarkerOptions::default()
                    .with_timing(gecko_profiler::MarkerTiming::instant_now()),
                TimingDistributionMetricMarker::<TimingDistributionMetric>::new(
                    metric_id.into(),
                    None,
                    Some(id.id),
                    None
                )
            );
        }
    }

    /// Accumulates the provided signed samples in the metric.
    ///
    /// This is required so that the platform-specific code can provide us with
    /// 64 bit signed integers if no `u64` comparable type is available. This
    /// will take care of filtering and reporting errors for any provided negative
    /// sample.
    ///
    /// Please note that this assumes that the provided samples are already in
    /// the "unit" declared by the instance of the metric type (e.g. if the
    /// instance this method was called on is using [`crate::TimeUnit::Second`], then
    /// `samples` are assumed to be in that unit).
    ///
    /// # Arguments
    ///
    /// * `samples` - The vector holding the samples to be recorded by the metric.
    ///
    /// ## Notes
    ///
    /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`]
    /// for each of them. Reports an [`ErrorType::InvalidOverflow`] error for samples that
    /// are longer than `MAX_SAMPLE_TIME`.
    pub fn accumulate_samples(&self, samples: Vec<i64>) {
        #[cfg(feature = "with_gecko")]
        {
            let gifft_samples = samples
                .iter()
                .filter(|&&i| i >= 0)
                .map(|&i| {
                    i.try_into().unwrap_or_else(|_| {
                        // TODO: Instrument this error
                        log::warn!(
                            "Samples larger than fits into 32-bytes. Saturating at u32::MAX."
                        );
                        u32::MAX
                    })
                })
                .collect();
            let metric_id = match self {
                TimingDistributionMetric::Parent { id, .. } => id
                    .base_metric_id()
                    .expect("Cannot perform GIFFT calls without a metric id."),
                TimingDistributionMetric::Child(c) => c.meta.id,
            };
            extern "C" {
                fn GIFFT_TimingDistributionAccumulateRawSamples(
                    metric_id: u32,
                    samples: &ThinVec<u32>,
                );
            }
            // SAFETY: No one uses the ThinVec after lending it to C++, no return value.
            unsafe {
                GIFFT_TimingDistributionAccumulateRawSamples(metric_id.0, &gifft_samples);
            }
        }
        self.inner_accumulate_samples(samples);
    }

    /// Accumulates the provided samples in the metric.
    ///
    /// # Arguments
    ///
    /// * `samples` - A list of samples recorded by the metric.
    ///               Samples must be in nanoseconds.
    /// ## Notes
    ///
    /// Reports an [`ErrorType::InvalidOverflow`] error for samples that
    /// are longer than `MAX_SAMPLE_TIME`.
    pub(crate) fn accumulate_raw_samples_nanos(&self, samples: Vec<u64>) {
        match self {
            #[allow(unused)]
            TimingDistributionMetric::Parent {
                id: id @ MetricId::Id(_),
                inner,
                ..
            } => {
                // Only record metric markers, not sub-metric markers. See
                // `inner_accumulate_samples` for details of why.
                #[cfg(feature = "with_gecko")]
                gecko_profiler::lazy_add_marker!(
                    "TimingDistribution::accumulate",
                    TelemetryProfilerCategory,
                    TimingDistributionMetricMarker::<TimingDistributionMetric>::new(
                        *id,
                        None,
                        None,
                        Some(TDMPayload::from_samples_unsigned(&samples)),
                    )
                );
                inner.accumulate_raw_samples_nanos(samples)
            }
            TimingDistributionMetric::Parent { inner, .. } => {
                inner.accumulate_raw_samples_nanos(samples)
            }
            TimingDistributionMetric::Child(_c) => {
                // TODO: Instrument this error
                log::error!("Can't record samples for a timing distribution from a child metric");
            }
        }
    }

    pub fn accumulate_single_sample(&self, sample: i64) {
        self.inner_accumulate_single_sample(sample);
        #[cfg(feature = "with_gecko")]
        {
            let metric_id = match self {
                TimingDistributionMetric::Parent { id, .. } => id
                    .base_metric_id()
                    .expect("Cannot perform GIFFT calls without a metric id."),
                TimingDistributionMetric::Child(c) => c.meta.id,
            };
            let sample = sample.try_into().unwrap_or_else(|_| {
                // TODO: Instrument this error
                log::warn!("Sample larger than fits into 32-bytes. Saturating at u32::MAX.");
                u32::MAX
            });
            extern "C" {
                fn GIFFT_TimingDistributionAccumulateRawSample(metric_id: u32, sample: u32);
            }
            // SAFETY: using only primitives, no return value.
            unsafe {
                GIFFT_TimingDistributionAccumulateRawSample(metric_id.0, sample);
            }
        }
    }

    /// Accumulates a time duration sample for the provided metric.
    ///
    /// Adds a count to the corresponding bucket in the timing distribution.
    /// Saturates at u64::MAX nanoseconds.
    ///
    /// Prefer start() and stop_and_accumulate() where possible.
    ///
    /// Users of this API are responsible for ensuring the timing source used
    /// to calculate the duration is monotonic and consistent across platforms.
    ///
    /// # Arguments
    ///
    /// * `duration` - The [`Duration`] of the accumulated sample.
    pub fn accumulate_raw_duration(&self, duration: Duration) {
        self.inner_accumulate_raw_duration(duration);
        #[cfg(feature = "with_gecko")]
        {
            let (metric_id, gifft_time_unit) = match self {
                TimingDistributionMetric::Parent {
                    id,
                    gifft_time_unit,
                    ..
                } => (
                    id.base_metric_id()
                        .expect("Cannot perform GIFFT calls without a metric id."),
                    gifft_time_unit,
                ),
                TimingDistributionMetric::Child(c) => (c.meta.id, &c.gifft_time_unit),
            };
            let sample = gifft_time_unit.duration_convert(duration);
            let sample = sample.try_into().unwrap_or_else(|_| {
                // TODO: Instrument this error
                log::warn!(
                    "Elapsed duration larger than fits into 32-bytes. Saturating at u32::MAX."
                );
                u32::MAX
            });
            extern "C" {
                fn GIFFT_TimingDistributionAccumulateRawSample(metric_id: u32, sample: u32);
            }
            // SAFETY: using only primitives, no return value.
            unsafe {
                GIFFT_TimingDistributionAccumulateRawSample(metric_id.0, sample);
            }

            gecko_profiler::lazy_add_marker!(
                "TimingDistribution::accumulate",
                TelemetryProfilerCategory,
                TimingDistributionMetricMarker::<TimingDistributionMetric>::new(
                    metric_id.into(),
                    None,
                    None,
                    Some(TDMPayload::Duration(duration)),
                )
            );
        }
    }

    /// **Exported for test purposes.**
    ///
    /// Gets the number of recorded errors for the given error type.
    ///
    /// # Arguments
    ///
    /// * `error` - The type of error
    /// * `ping_name` - represents the optional name of the ping to retrieve the
    ///   metric for. Defaults to the first value in `send_in_pings`.
    ///
    /// # Returns
    ///
    /// The number of errors recorded.
    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
        match self {
            TimingDistributionMetric::Parent { inner, .. } => {
                inner.test_get_num_recorded_errors(error)
            }
            TimingDistributionMetric::Child(c) => panic!(
                "Cannot get number of recorded errors for {:?} in non-parent process!",
                c
            ),
        }
    }
}

#[inherent]
impl glean::TestGetValue for TimingDistributionMetric {
    type Output = DistributionData;

    /// **Exported for test purposes.**
    ///
    /// Gets the currently stored value of the metric.
    ///
    /// This doesn't clear the stored value.
    ///
    /// # Arguments
    ///
    /// * `ping_name` - represents the optional name of the ping to retrieve the
    ///   metric for. Defaults to the first value in `send_in_pings`.
    pub fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> {
        match self {
            TimingDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
            TimingDistributionMetric::Child(c) => {
                panic!("Cannot get test value for {:?} in non-parent process!", c)
            }
        }
    }
}

impl MetricNamer for TimingDistributionMetric {
    fn get_metadata(&self) -> super::MetricMetadata {
        crate::private::MetricMetadata::from_triple(match self {
            TimingDistributionMetric::Parent { inner, .. } => inner.get_identifiers(),
            TimingDistributionMetric::Child(tdmi) => tdmi.meta.get_identifiers(),
        })
    }
}

#[cfg(test)]
mod test {
    use crate::{common_test::*, ipc, metrics};

    #[test]
    fn smoke_test_timing_distribution() {
        let _lock = lock_test();

        let metric = &metrics::test_only_ipc::a_timing_dist;

        let id = metric.start();
        // Stopping right away might not give us data, if the underlying clock source is not precise
        // enough.
        // So let's cancel and make sure nothing blows up.
        metric.cancel(id);

        // We can't inspect the values yet.
        assert!(metric
            .test_get_value(Some("test-ping".to_string()))
            .is_none());
    }

    #[test]
    fn timing_distribution_child() {
        let _lock = lock_test();

        let parent_metric = &metrics::test_only_ipc::a_timing_dist;
        let id = parent_metric.start();
        std::thread::sleep(std::time::Duration::from_millis(10));
        parent_metric.stop_and_accumulate(id);

        {
            let child_metric = parent_metric.child_metric();

            // scope for need_ipc RAII
            let _raii = ipc::test_set_need_ipc(true);

            let id = child_metric.start();
            let id2 = child_metric.start();
            assert_ne!(id, id2);
            std::thread::sleep(std::time::Duration::from_millis(10));
            child_metric.stop_and_accumulate(id);

            child_metric.cancel(id2);
        }

        let buf = ipc::take_buf().unwrap();
        assert!(buf.len() > 0);
        assert!(ipc::replay_from_buf(&buf).is_ok());

        let data = parent_metric
            .test_get_value(Some("test-ping".to_string()))
            .expect("should have some data");

        // No guarantees from timers means no guarantees on buckets.
        // But we can guarantee it's only two samples.
        assert_eq!(
            2,
            data.values.values().fold(0, |acc, count| acc + count),
            "record 2 values, one parent, one child measurement"
        );
        assert!(0 < data.sum, "record some time");
    }
}
