// 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)
    };
    (STRING_MAP, $id:ident, $m:ident, $f:expr) => {
        maybe_labeled_with_metric!(STRING_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 {
            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),
            }
        } else {
            just_with_metric!($map, $id, $m, $f)
        }
    };
}

/// 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())
        };
        $metric.test_get_value(storage.as_deref()).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())
        };
        $metric.test_get_value(storage.as_deref()).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;
        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
    }};
}

/// Get the submetric id for a given labeled metric and label.
///
/// # Arguments
///
/// * `$id`						- The id of the labeled metric.
/// * `$label`				- The (string) label of the submetric.
/// * `$labeled_map`	- The name of the labeled metric's map for retrieval (JOG only).
/// * `$labeled_get`	- The name of the labeled metric's get fn for retrieval.
/// * `$submetric_map`- The name of the submetrics' map for storage.
/// * `$metric_type`  - The submetric's type (needed for an internal closure).
macro_rules! labeled_submetric_get {
    ($id:ident, $label:ident, $labeled_map:ident, $labeled_get:ident, $submetric_map:ident, $metric_type:ty) => {{
        let tuple = ($id, $label.to_utf8().into());
        {
            let map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_METRICS_TO_IDS
                .read()
                .expect("read lock of submetric ids was poisoned");
            if let Some(submetric_id) = map.get(&tuple) {
                return *submetric_id;
            }
        }

        // Gotta actually create a new submetric with a new id.
        let submetric_id =
            $crate::metrics::__glean_metric_maps::submetric_maps::NEXT_LABELED_SUBMETRIC_ID
                .fetch_add(1, Ordering::SeqCst);
        {
            if $id & (1 << $crate::factory::DYNAMIC_METRIC_BIT) > 0 {
                just_with_jog_metric!($labeled_map, $id, metric, {
                    let submetric = metric.get(&tuple.1);
                    let mut map =
                        $crate::metrics::__glean_metric_maps::submetric_maps::$submetric_map
                            .write()
                            .expect("write lock of submetric map was poisoned");
                    map.insert(submetric_id.into(), submetric);
                });
            } else {
                let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::$submetric_map
                    .write()
                    .expect("write lock of submetric map was poisoned");
                map.insert(
                    submetric_id.into(),
                    $crate::metrics::__glean_metric_maps::$labeled_get($id, &tuple.1),
                );
            }
        }

        let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_METRICS_TO_IDS
            .write()
            .expect("write lock of submetric ids was poisoned");
        map.insert(tuple, submetric_id);
        submetric_id
    }};
}

/// Get the submetric id for a given labeled metric and label enum.
///
/// # Arguments
///
/// * `$id`						- The id of the labeled metric.
/// * `$label`				- The (enum) label of the submetric.
/// * `$labeled_get`	- The name of the labeled metric's get fn for retrieval.
/// * `$submetric_map`- The name of the submetrics' map for storage.
/// * `$metric_type`  - The submetric's type (needed for an internal closure).
macro_rules! labeled_submetric_enum_get {
    ($id:ident, $label_enum:ident, $labeled_get:ident, $submetric_map:ident, $metric_type:ty) => {{
        let tuple = ($id, $label_enum.into());
        // First: Have we seen this enum before? If so, give out the same submetric id.
        {
            let map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_ENUMS_TO_IDS
                .read()
                .expect("read lock of enum submetric ids was poisoned");
            if let Some(submetric_id) = map.get(&tuple) {
                return *submetric_id;
            }
        }

        // Alas, this is the first time we've needed to handle this metric with this enum.
        // Gotta actually create a new submetric with a new id.
        let submetric_id =
            $crate::metrics::__glean_metric_maps::submetric_maps::NEXT_LABELED_SUBMETRIC_ID
                .fetch_add(1, Ordering::SeqCst);
        {
            // What if the dynamic bit is set?
            // JOG only supports JS, and enum_get isn't (yet) supported in JS.
            assert_eq!(
                0,
                $id & (1 << $crate::factory::DYNAMIC_METRIC_BIT),
                "No enum_get support for JOG"
            );
            let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::$submetric_map
                .write()
                .expect("write lock of submetric map was poisoned");
            map.insert(
                submetric_id.into(),
                $crate::metrics::__glean_metric_maps::$labeled_get($id, tuple.1),
            );
        }

        // And now ensure we store the submetric so we need not create it on subsequent calls.
        let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_ENUMS_TO_IDS
            .write()
            .expect("write lock of submetric ids was poisoned");
        map.insert(tuple, submetric_id);
        submetric_id
    }};
}
