// Copyright 2014 The Prometheus Authors
// Copyright 2019 TiKV Project Authors. Licensed under Apache-2.0.

use crate::atomic64::{Atomic, Number};
use crate::desc::{Desc, Describer};
use crate::errors::{Error, Result};
use crate::proto::{Counter, Gauge, LabelPair, Metric, MetricFamily, MetricType};

use std::time::{SystemTime, UNIX_EPOCH};

/// `ValueType` is an enumeration of metric types that represent a simple value
/// for [`Counter`] and [`Gauge`].
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ValueType {
    Counter,
    Gauge,
}

impl ValueType {
    /// `metric_type` returns the corresponding proto metric type.
    pub fn metric_type(self) -> MetricType {
        match self {
            ValueType::Counter => MetricType::COUNTER,
            ValueType::Gauge => MetricType::GAUGE,
        }
    }
}

/// A generic metric for [`Counter`] and [`Gauge`].
/// Its effective type is determined by `ValueType`. This is a low-level
/// building block used by the library to back the implementations of
/// [`Counter`] and [`Gauge`].
#[derive(Debug)]
pub struct Value<P: Atomic> {
    pub desc: Desc,
    pub val: P,
    pub val_type: ValueType,
    pub label_pairs: Vec<LabelPair>,
}

impl<P: Atomic> Value<P> {
    pub fn new<D: Describer>(
        describer: &D,
        val_type: ValueType,
        val: P::T,
        label_values: &[&str],
    ) -> Result<Self> {
        let desc = describer.describe()?;
        if desc.variable_labels.len() != label_values.len() {
            return Err(Error::InconsistentCardinality {
                expect: desc.variable_labels.len(),
                got: label_values.len(),
            });
        }

        let label_pairs = make_label_pairs(&desc, label_values);

        Ok(Self {
            desc,
            val: P::new(val),
            val_type,
            label_pairs,
        })
    }

    #[inline]
    pub fn get(&self) -> P::T {
        self.val.get()
    }

    #[inline]
    pub fn set(&self, val: P::T) {
        self.val.set(val);
    }

    #[inline]
    pub fn inc_by(&self, val: P::T) {
        self.val.inc_by(val);
    }

    #[inline]
    pub fn inc(&self) {
        self.inc_by(P::T::from_i64(1));
    }

    #[inline]
    pub fn dec(&self) {
        self.dec_by(P::T::from_i64(1));
    }

    #[inline]
    pub fn dec_by(&self, val: P::T) {
        self.val.dec_by(val)
    }

    pub fn metric(&self) -> Metric {
        let mut m = Metric::default();
        m.set_label(from_vec!(self.label_pairs.clone()));
        m.set_timestamp_ms(
            (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() * 1000) as i64
        );

        let val = self.get();
        match self.val_type {
            ValueType::Counter => {
                let mut counter = Counter::default();
                counter.set_value(val.into_f64());
                m.set_counter(counter);
            }
            ValueType::Gauge => {
                let mut gauge = Gauge::default();
                gauge.set_value(val.into_f64());
                m.set_gauge(gauge);
            }
        }

        m
    }

    pub fn collect(&self) -> MetricFamily {
        let mut m = MetricFamily::default();
        m.set_name(self.desc.fq_name.clone());
        m.set_help(self.desc.help.clone());
        m.set_field_type(self.val_type.metric_type());
        m.set_metric(from_vec!(vec![self.metric()]));
        m
    }
}

pub fn make_label_pairs(desc: &Desc, label_values: &[&str]) -> Vec<LabelPair> {
    let total_len = desc.variable_labels.len() + desc.const_label_pairs.len();
    if total_len == 0 {
        return vec![];
    }

    if desc.variable_labels.is_empty() {
        return desc.const_label_pairs.clone();
    }

    let mut label_pairs = Vec::with_capacity(total_len);
    for (i, n) in desc.variable_labels.iter().enumerate() {
        let mut label_pair = LabelPair::default();
        label_pair.set_name(n.clone());
        label_pair.set_value(label_values[i].to_owned());
        label_pairs.push(label_pair);
    }

    for label_pair in &desc.const_label_pairs {
        label_pairs.push(label_pair.clone());
    }
    label_pairs.sort();
    label_pairs
}
