// 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/.

//! Helper macros for implementing the FFI API for metric types.

/// Get a metric object by ID from the corresponding map, then
/// execute the provided closure with it.
///
/// # Arguments
///
/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps`
///            (or `factory::__jog_metric_maps`)
///            as generated by glean_parser.
/// * `$id`  - The ID of the metric to get.
/// * `$m`   - The identifier to use for the retrieved metric.
///            The expression `$f` can use this identifier.
/// * `$f`   - The expression to execute with the retrieved metric `$m`.
macro_rules! with_metric {
    (BOOLEAN_MAP, $id:ident, $m:ident, $f:expr) => {
        maybe_labeled_with_metric!(BOOLEAN_MAP, $id, $m, $f)
    };
    (COUNTER_MAP, $id:ident, $m:ident, $f:expr) => {
        maybe_labeled_with_metric!(COUNTER_MAP, $id, $m, $f)
    };
    (CUSTOM_DISTRIBUTION_MAP, $id:ident, $m:ident, $f:expr) => {
        maybe_labeled_with_metric!(CUSTOM_DISTRIBUTION_MAP, $id, $m, $f)
    };
    (MEMORY_DISTRIBUTION_MAP, $id:ident, $m:ident, $f:expr) => {
        maybe_labeled_with_metric!(MEMORY_DISTRIBUTION_MAP, $id, $m, $f)
    };
    (QUANTITY_MAP, $id:ident, $m:ident, $f:expr) => {
        maybe_labeled_with_metric!(QUANTITY_MAP, $id, $m, $f)
    };
    (STRING_MAP, $id:ident, $m:ident, $f:expr) => {
        maybe_labeled_with_metric!(STRING_MAP, $id, $m, $f)
    };
    (TIMING_DISTRIBUTION_MAP, $id:ident, $m:ident, $f:expr) => {
        maybe_labeled_with_metric!(TIMING_DISTRIBUTION_MAP, $id, $m, $f)
    };
    (DUAL_COUNTER_MAP, $id:ident, $m:ident, $f: expr) => {
        just_labeled_with_metric!(DUAL_COUNTER_MAP, $id, $m, $f)
    };
    ($map:ident, $id:ident, $m:ident, $f:expr) => {
        just_with_metric!($map, $id, $m, $f)
    };
}

/// Get a dynamically-registered metric object by id from the corresponding map,
/// then execute the provided closure with it.
///
/// Assumes `$id` is for a dynamic non-submetric metric.
/// Will panic if it isn't.
///
/// # Arguments
///
/// * `$map` - The name of the hash map within `factory::__jog_metric_maps`
///            as generated by glean_parser.
/// * `$id`  - The ID of the metric to get.
/// * `$m`   - The identifier to use for the retrieved metric.
///            The expression `$f` can use this identifier.
/// * `$f`   - The expression to execute with the retrieved metric `$m`.
macro_rules! just_with_jog_metric {
    ($map:ident, $id:ident, $m:ident, $f:expr) => {{
        let map = $crate::factory::__jog_metric_maps::$map
            .read()
            .expect("Read lock for dynamic metric map was poisoned");
        match map.get(&$id.into()) {
            Some($m) => $f,
            None => panic!("No (dynamic) metric for id {}", $id),
        }
    }};
}

/// Get a metric object by id from the corresponding map, then
/// execute the provided closure with it.
///
/// Ignores the possibility that the $id might be for a labeled submetric.
///
/// # Arguments
///
/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps`
///            (or `factory::__jog_metric_maps`)
///            as generated by glean_parser.
/// * `$id`  - The ID of the metric to get.
/// * `$m`   - The identifier to use for the retrieved metric.
///            The expression `$f` can use this identifier.
/// * `$f`   - The expression to execute with the retrieved metric `$m`.
macro_rules! just_with_metric {
    ($map:ident, $id:ident, $m:ident, $f:expr) => {
        if $id & (1 << $crate::factory::DYNAMIC_METRIC_BIT) > 0 {
            just_with_jog_metric!($map, $id, $m, $f)
        } else {
            match $crate::metrics::__glean_metric_maps::$map.get(&$id.into()) {
                Some($m) => $f,
                None => panic!("No metric for id {}", $id),
            }
        }
    };
}

/// Get a metric object by id from the corresponding map, then
/// execute the provided closure with it.
///
/// Requires that the provided $map be of a type that can be labeled, since it
/// assumes the presence of a same-named map in
/// `metrics::_glean_metrics_map::submetric_maps`.
///
/// # Arguments
///
/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps`
///            and `metrics::__glean_metric_maps::submetric_maps` as generated
///            by glean_parser.
/// * `$id`  - The ID of the metric to get.
/// * `$m`   - The identifier to use for the retrieved metric.
///            The expression `$f` can use this identifier.
/// * `$f`   - The expression to execute with the retrieved metric `$m`.
macro_rules! maybe_labeled_with_metric {
    ($map:ident, $id:ident, $m:ident, $f:expr) => {
        if $id & (1 << $crate::metrics::__glean_metric_maps::submetric_maps::SUBMETRIC_BIT) > 0 {
            just_labeled_with_metric!($map, $id, $m, $f)
        } else {
            just_with_metric!($map, $id, $m, $f)
        }
    };
}

/// Get a submetric object instance by id from the corresponding map,
/// then execute the providing closure with it.
///
/// # Arguments
///
/// * `$map` - The name of the hash map within
///            `metrics::__glean_metric_maps::submetric_maps` as generated
///            by glean_parser.
/// * `$id`  - The ID of the metric to get.
///            Definitely has `submetric_maps::SUBMETRIC_BIT` set.
/// * `$m`   - The identifier to use for the retrieved metric.
///            The expression `$f` can use this identifier.
/// * `$f`   - The expression to execute with the retrieved metric `$m`.
macro_rules! just_labeled_with_metric {
    ($map:ident, $id:ident, $m:ident, $f:expr) => {{
        let map = $crate::metrics::__glean_metric_maps::submetric_maps::$map
            .read()
            .expect("Read lock for labeled metric map was poisoned");
        match map.get(&$id.into()) {
            Some($m) => $f,
            None => panic!("No submetric for id {}", $id),
        }
    }};
}

/// Test whether a value is stored for the given metric.
///
/// # Arguments
///
/// * `$metric`  - The metric to test.
/// * `$storage` - the storage name to look into.
macro_rules! test_has {
    ($metric:ident, $storage:ident) => {{
        let storage = if $storage.is_empty() {
            None
        } else {
            Some($storage.to_utf8().into_owned())
        };
        $metric.test_get_value(storage).is_some()
    }};
}

/// Get the currently stored value for the given metric.
///
/// # Arguments
///
/// * `$metric`  - The metric to test.
/// * `$storage` - the storage name to look into.
macro_rules! test_get {
    ($metric:ident, $storage:ident) => {{
        let storage = if $storage.is_empty() {
            None
        } else {
            Some($storage.to_utf8().into_owned())
        };
        $metric.test_get_value(storage).unwrap()
    }};
}

/// Check the provided metric in the provided storage for errors.
/// On finding one, return an error string.
///
/// # Arguments
///
/// * `$metric`  - The metric to test.
macro_rules! test_get_errors {
    ($metric:path) => {{
        let error_types = [
            glean::ErrorType::InvalidValue,
            glean::ErrorType::InvalidLabel,
            glean::ErrorType::InvalidState,
            glean::ErrorType::InvalidOverflow,
        ];
        let mut error_str = None;
        // Only `events` use this trait right now
        // Ignoring the unused import simplifies the macro right now.
        // This might change in the future.
        #[allow(unused_imports)]
        use crate::private::TestGetNumErrors;

        for &error_type in error_types.iter() {
            let num_errors = $metric.test_get_num_recorded_errors(error_type);
            if num_errors > 0 {
                error_str = Some(format!(
                    "Metric had {} error(s) of type {}!",
                    num_errors,
                    error_type.as_str()
                ));
                break;
            }
        }
        error_str
    }};
}

/// Check the provided method on the metric map in the provided storage for errors.
/// On finding one, return an error string.
///
/// # Arguments
///
/// * `$metric_map`  - The metric map to use.
/// * `$method`      - The `labeled_<type>_test_get_num_recorded_errors` method to use.
/// * `$metric_id`   - The metric id to find in the metric map.
macro_rules! test_get_errors_with_method {
    ($metric_map:ident, $method:ident, $metric_id:path) => {{
        let error_types = [
            glean::ErrorType::InvalidValue,
            glean::ErrorType::InvalidLabel,
            glean::ErrorType::InvalidState,
            glean::ErrorType::InvalidOverflow,
        ];
        let mut error_str = None;
        for &error_type in error_types.iter() {
            let num_errors = $metric_map::$method($metric_id, error_type);
            if num_errors > 0 {
                error_str = Some(format!(
                    "Metric had {} error(s) of type {}!",
                    num_errors,
                    error_type.as_str()
                ));
                break;
            }
        }
        error_str
    }};
}
