// 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 std::sync::{Arc, RwLock};

use inherent::inherent;

use glean_core::metrics::{MetricType, TimeUnit};
use glean_core::ErrorType;

use crate::dispatcher;

// We need to wrap the glean-core type: otherwise if we try to implement
// the trait for the metric in `glean_core::metrics` we hit error[E0117]:
// only traits defined in the current crate can be implemented for arbitrary
// types.

/// Developer-facing API for recording timespan metrics.
///
/// Instances of this class type are automatically generated by the parsers
/// at build time, allowing developers to record values that were previously
/// registered in the metrics.yaml file.
#[derive(Clone)]
pub struct TimespanMetric(pub(crate) Arc<RwLock<glean_core::metrics::TimespanMetric>>);

impl TimespanMetric {
    /// The public constructor used by automatically generated metrics.
    pub fn new(meta: glean_core::CommonMetricData, time_unit: TimeUnit) -> Self {
        let timespan = glean_core::metrics::TimespanMetric::new(meta, time_unit);
        Self(Arc::new(RwLock::new(timespan)))
    }
}

#[inherent(pub)]
impl glean_core::traits::Timespan for TimespanMetric {
    fn start(&self) {
        let start_time = time::precise_time_ns();

        let metric = Arc::clone(&self.0);
        dispatcher::launch(move || {
            crate::with_glean(|glean| {
                let mut lock = metric
                    .write()
                    .expect("Lock poisoned for timespan metric on start.");
                lock.set_start(glean, start_time)
            })
        });
    }

    fn stop(&self) {
        let stop_time = time::precise_time_ns();

        let metric = Arc::clone(&self.0);
        dispatcher::launch(move || {
            crate::with_glean(|glean| {
                let mut lock = metric
                    .write()
                    .expect("Lock poisoned for timespan metric on stop.");
                lock.set_stop(glean, stop_time)
            })
        });
    }

    fn cancel(&self) {
        let metric = Arc::clone(&self.0);
        dispatcher::launch(move || {
            let mut lock = metric
                .write()
                .expect("Lock poisoned for timespan metric on cancel.");
            lock.cancel()
        });
    }

    fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<u64> {
        crate::block_on_dispatcher();

        crate::with_glean(|glean| {
            // Note: The order of operations is important here to avoid potential deadlocks because
            // of `lock-order-inversion`.
            // `with_glean` takes a lock on the global Glean object,
            // then we take a lock on the metric itself here.
            //
            // Other parts do it in the same order, see for example `start`.
            let metric = self
                .0
                .read()
                .expect("Lock poisoned for timespan metric on test_get_value.");

            let queried_ping_name = ping_name
                .into()
                .unwrap_or_else(|| &metric.meta().send_in_pings[0]);
            metric.test_get_value(glean, queried_ping_name)
        })
    }

    fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>(
        &self,
        error: ErrorType,
        ping_name: S,
    ) -> i32 {
        crate::block_on_dispatcher();

        let metric = self
            .0
            .read()
            .expect("Lock poisoned for timespan metric on test_get_value.");

        crate::with_glean_mut(|glean| {
            glean_core::test_get_num_recorded_errors(&glean, metric.meta(), error, ping_name.into())
                .unwrap_or(0)
        })
    }
}

#[cfg(test)]
mod test {
    use std::{thread, time::Duration};

    use super::*;
    use crate::common_test::{lock_test, new_glean};
    use crate::CommonMetricData;

    #[test]
    fn timespan_convenient_api() {
        let _lock = lock_test();
        let _t = new_glean(None, true);

        let metric: TimespanMetric = TimespanMetric::new(
            CommonMetricData {
                name: "timespan".into(),
                category: "test".into(),
                send_in_pings: vec!["test1".into()],
                ..Default::default()
            },
            TimeUnit::Millisecond,
        );

        // Canceling doesn't store data.
        metric.start();
        metric.cancel();
        assert!(metric.test_get_value(None).is_none());

        // Starting and stopping measures time.
        metric.start();
        thread::sleep(Duration::from_millis(10));
        metric.stop();
        assert!(10 <= metric.test_get_value(None).unwrap());

        // No errors
        assert_eq!(
            metric.test_get_num_recorded_errors(ErrorType::InvalidState, None),
            0
        );

        // Stopping without starting is an error
        metric.stop();
        assert_eq!(
            metric.test_get_num_recorded_errors(ErrorType::InvalidState, None),
            1
        )
    }
}
