// Copyright 2014 The Prometheus 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 prometheus

import (
	"encoding/json"
	"expvar"
)

// ExpvarCollector collects metrics from the expvar interface. It provides a
// quick way to expose numeric values that are already exported via expvar as
// Prometheus metrics. Note that the data models of expvar and Prometheus are
// fundamentally different, and that the ExpvarCollector is inherently
// slow. Thus, the ExpvarCollector is probably great for experiments and
// prototying, but you should seriously consider a more direct implementation of
// Prometheus metrics for monitoring production systems.
//
// Use NewExpvarCollector to create new instances.
type ExpvarCollector struct {
	exports map[string]*Desc
}

// NewExpvarCollector returns a newly allocated ExpvarCollector that still has
// to be registered with the Prometheus registry.
//
// The exports map has the following meaning:
//
// The keys in the map correspond to expvar keys, i.e. for every expvar key you
// want to export as Prometheus metric, you need an entry in the exports
// map. The descriptor mapped to each key describes how to export the expvar
// value. It defines the name and the help string of the Prometheus metric
// proxying the expvar value. The type will always be Untyped.
//
// For descriptors without variable labels, the expvar value must be a number or
// a bool. The number is then directly exported as the Prometheus sample
// value. (For a bool, 'false' translates to 0 and 'true' to 1). Expvar values
// that are not numbers or bools are silently ignored.
//
// If the descriptor has one variable label, the expvar value must be an expvar
// map. The keys in the expvar map become the various values of the one
// Prometheus label. The values in the expvar map must be numbers or bools again
// as above.
//
// For descriptors with more than one variable label, the expvar must be a
// nested expvar map, i.e. where the values of the topmost map are maps again
// etc. until a depth is reached that corresponds to the number of labels. The
// leaves of that structure must be numbers or bools as above to serve as the
// sample values.
//
// Anything that does not fit into the scheme above is silently ignored.
func NewExpvarCollector(exports map[string]*Desc) *ExpvarCollector {
	return &ExpvarCollector{
		exports: exports,
	}
}

// Describe implements Collector.
func (e *ExpvarCollector) Describe(ch chan<- *Desc) {
	for _, desc := range e.exports {
		ch <- desc
	}
}

// Collect implements Collector.
func (e *ExpvarCollector) Collect(ch chan<- Metric) {
	for name, desc := range e.exports {
		var m Metric
		expVar := expvar.Get(name)
		if expVar == nil {
			continue
		}
		var v interface{}
		labels := make([]string, len(desc.variableLabels))
		if err := json.Unmarshal([]byte(expVar.String()), &v); err != nil {
			ch <- NewInvalidMetric(desc, err)
			continue
		}
		var processValue func(v interface{}, i int)
		processValue = func(v interface{}, i int) {
			if i >= len(labels) {
				copiedLabels := append(make([]string, 0, len(labels)), labels...)
				switch v := v.(type) {
				case float64:
					m = MustNewConstMetric(desc, UntypedValue, v, copiedLabels...)
				case bool:
					if v {
						m = MustNewConstMetric(desc, UntypedValue, 1, copiedLabels...)
					} else {
						m = MustNewConstMetric(desc, UntypedValue, 0, copiedLabels...)
					}
				default:
					return
				}
				ch <- m
				return
			}
			vm, ok := v.(map[string]interface{})
			if !ok {
				return
			}
			for lv, val := range vm {
				labels[i] = lv
				processValue(val, i+1)
			}
		}
		processValue(v, 0)
	}
}
