/*
Copyright 2019 The Vitess Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package prometheusbackend

import (
	"strings"

	"github.com/prometheus/client_golang/prometheus"

	"git.nspix.com/golang/micro/stats"
)

type metricFuncCollector struct {
	// f returns the floating point value of the metric.
	f    func() float64
	desc *prometheus.Desc
	vt   prometheus.ValueType
}

func newMetricFuncCollector(v stats.Variable, name string, vt prometheus.ValueType, f func() float64) {
	collector := &metricFuncCollector{
		f: f,
		desc: prometheus.NewDesc(
			name,
			v.Help(),
			nil,
			nil),
		vt: vt}

	// Will panic if it fails
	prometheus.MustRegister(collector)
}

// Describe implements Collector.
func (mc *metricFuncCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- mc.desc
}

// Collect implements Collector.
func (mc *metricFuncCollector) Collect(ch chan<- prometheus.Metric) {
	metric, err := prometheus.NewConstMetric(mc.desc, mc.vt, float64(mc.f()))
	if err == nil {
		ch <- metric
	}
}

// countersWithSingleLabelCollector collects stats.CountersWithSingleLabel.
type countersWithSingleLabelCollector struct {
	counters *stats.CountersWithSingleLabel
	desc     *prometheus.Desc
	vt       prometheus.ValueType
}

func newCountersWithSingleLabelCollector(c *stats.CountersWithSingleLabel, name string, labelName string, vt prometheus.ValueType) {
	collector := &countersWithSingleLabelCollector{
		counters: c,
		desc: prometheus.NewDesc(
			name,
			c.Help(),
			[]string{labelName},
			nil),
		vt: vt}

	prometheus.MustRegister(collector)
}

// Describe implements Collector.
func (c *countersWithSingleLabelCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.desc
}

// Collect implements Collector.
func (c *countersWithSingleLabelCollector) Collect(ch chan<- prometheus.Metric) {
	for tag, val := range c.counters.Counts() {
		metric, err := prometheus.NewConstMetric(c.desc, c.vt, float64(val), tag)
		if err == nil {
			ch <- metric
		}
	}
}

// gaugesWithSingleLabelCollector collects stats.GaugesWithSingleLabel.
type gaugesWithSingleLabelCollector struct {
	gauges *stats.GaugesWithSingleLabel
	desc   *prometheus.Desc
	vt     prometheus.ValueType
}

func newGaugesWithSingleLabelCollector(g *stats.GaugesWithSingleLabel, name string, labelName string, vt prometheus.ValueType) {
	collector := &gaugesWithSingleLabelCollector{
		gauges: g,
		desc: prometheus.NewDesc(
			name,
			g.Help(),
			[]string{labelName},
			nil),
		vt: vt}

	prometheus.MustRegister(collector)
}

// Describe implements Collector.
func (g *gaugesWithSingleLabelCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- g.desc
}

// Collect implements Collector.
func (g *gaugesWithSingleLabelCollector) Collect(ch chan<- prometheus.Metric) {
	for tag, val := range g.gauges.Counts() {
		metric, err := prometheus.NewConstMetric(g.desc, g.vt, float64(val), tag)
		if err == nil {
			ch <- metric
		}
	}
}

type metricWithMultiLabelsCollector struct {
	cml  *stats.CountersWithMultiLabels
	desc *prometheus.Desc
}

func newMetricWithMultiLabelsCollector(cml *stats.CountersWithMultiLabels, name string) {
	c := &metricWithMultiLabelsCollector{
		cml: cml,
		desc: prometheus.NewDesc(
			name,
			cml.Help(),
			labelsToSnake(cml.Labels()),
			nil),
	}

	prometheus.MustRegister(c)
}

// Describe implements Collector.
func (c *metricWithMultiLabelsCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.desc
}

// Collect implements Collector.
func (c *metricWithMultiLabelsCollector) Collect(ch chan<- prometheus.Metric) {
	for lvs, val := range c.cml.Counts() {
		labelValues := strings.Split(lvs, ".")
		value := float64(val)
		metric, err := prometheus.NewConstMetric(c.desc, prometheus.CounterValue, value, labelValues...)
		if err == nil {
			ch <- metric
		}
	}
}

type gaugesWithMultiLabelsCollector struct {
	gml  *stats.GaugesWithMultiLabels
	desc *prometheus.Desc
}

func newGaugesWithMultiLabelsCollector(gml *stats.GaugesWithMultiLabels, name string) {
	c := &gaugesWithMultiLabelsCollector{
		gml: gml,
		desc: prometheus.NewDesc(
			name,
			gml.Help(),
			labelsToSnake(gml.Labels()),
			nil),
	}

	prometheus.MustRegister(c)
}

// Describe implements Collector.
func (c *gaugesWithMultiLabelsCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.desc
}

// Collect implements Collector.
func (c *gaugesWithMultiLabelsCollector) Collect(ch chan<- prometheus.Metric) {
	for lvs, val := range c.gml.Counts() {
		labelValues := strings.Split(lvs, ".")
		value := float64(val)
		metric, err := prometheus.NewConstMetric(c.desc, prometheus.GaugeValue, value, labelValues...)
		if err == nil {
			ch <- metric
		}
	}
}

type metricsFuncWithMultiLabelsCollector struct {
	cfml *stats.CountersFuncWithMultiLabels
	desc *prometheus.Desc
	vt   prometheus.ValueType
}

func newMetricsFuncWithMultiLabelsCollector(cfml *stats.CountersFuncWithMultiLabels, name string, vt prometheus.ValueType) {
	collector := &metricsFuncWithMultiLabelsCollector{
		cfml: cfml,
		desc: prometheus.NewDesc(
			name,
			cfml.Help(),
			labelsToSnake(cfml.Labels()),
			nil),
		vt: vt,
	}

	prometheus.MustRegister(collector)
}

// Describe implements Collector.
func (c *metricsFuncWithMultiLabelsCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.desc
}

// Collect implements Collector.
func (c *metricsFuncWithMultiLabelsCollector) Collect(ch chan<- prometheus.Metric) {
	for lvs, val := range c.cfml.Counts() {
		labelValues := strings.Split(lvs, ".")
		value := float64(val)
		metric, err := prometheus.NewConstMetric(c.desc, c.vt, value, labelValues...)
		if err == nil {
			ch <- metric
		}
	}
}

type timingsCollector struct {
	t       *stats.Timings
	cutoffs []float64
	desc    *prometheus.Desc
}

func newTimingsCollector(t *stats.Timings, name string) {
	cutoffs := make([]float64, len(t.Cutoffs()))
	for i, val := range t.Cutoffs() {
		cutoffs[i] = float64(val) / 1000000000
	}

	collector := &timingsCollector{
		t:       t,
		cutoffs: cutoffs,
		desc: prometheus.NewDesc(
			name,
			t.Help(),
			[]string{t.Label()},
			nil),
	}

	prometheus.MustRegister(collector)
}

// Describe implements Collector.
func (c *timingsCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.desc
}

// Collect implements Collector.
func (c *timingsCollector) Collect(ch chan<- prometheus.Metric) {
	for cat, his := range c.t.Histograms() {
		metric, err := prometheus.NewConstHistogram(c.desc,
			uint64(his.Count()),
			float64(his.Total())/1000000000,
			makeCumulativeBuckets(c.cutoffs,
				his.Buckets()), cat)
		if err == nil {
			ch <- metric
		}
	}
}

func makeCumulativeBuckets(cutoffs []float64, buckets []int64) map[float64]uint64 {
	output := make(map[float64]uint64)
	last := uint64(0)
	for i, key := range cutoffs {
		//TODO(zmagg): int64 => uint64 conversion. error if it overflows?
		output[key] = uint64(buckets[i]) + last
		last = output[key]
	}
	return output
}

type multiTimingsCollector struct {
	mt      *stats.MultiTimings
	cutoffs []float64
	desc    *prometheus.Desc
}

func newMultiTimingsCollector(mt *stats.MultiTimings, name string) {
	cutoffs := make([]float64, len(mt.Cutoffs()))
	for i, val := range mt.Cutoffs() {
		cutoffs[i] = float64(val) / 1000000000
	}

	collector := &multiTimingsCollector{
		mt:      mt,
		cutoffs: cutoffs,
		desc: prometheus.NewDesc(
			name,
			mt.Help(),
			labelsToSnake(mt.Labels()),
			nil),
	}

	prometheus.MustRegister(collector)
}

// Describe implements Collector.
func (c *multiTimingsCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.desc
}

// Collect implements Collector.
func (c *multiTimingsCollector) Collect(ch chan<- prometheus.Metric) {
	for cat, his := range c.mt.Timings.Histograms() {
		labelValues := strings.Split(cat, ".")
		metric, err := prometheus.NewConstHistogram(
			c.desc,
			uint64(his.Count()),
			float64(his.Total())/1000000000,
			makeCumulativeBuckets(c.cutoffs, his.Buckets()),
			labelValues...)
		if err == nil {
			ch <- metric
		}
	}
}

type histogramCollector struct {
	h       *stats.Histogram
	cutoffs []float64
	desc    *prometheus.Desc
}

func newHistogramCollector(h *stats.Histogram, name string) {
	cutoffs := make([]float64, len(h.Cutoffs()))
	for i, val := range h.Cutoffs() {
		cutoffs[i] = float64(val)
	}

	collector := &histogramCollector{
		h:       h,
		cutoffs: cutoffs,
		desc: prometheus.NewDesc(
			name,
			h.Help(),
			[]string{},
			nil),
	}

	prometheus.MustRegister(collector)
}

// Describe implements Collector.
func (c *histogramCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.desc
}

// Collect implements Collector.
func (c *histogramCollector) Collect(ch chan<- prometheus.Metric) {
	metric, err := prometheus.NewConstHistogram(c.desc,
		uint64(c.h.Count()),
		float64(c.h.Total()),
		makeCumulativeBuckets(c.cutoffs, c.h.Buckets()))
	if err == nil {
		ch <- metric
	}
}
