package telemetry

import (
	"math"
)

// bucketFactor is the configured bucket width.
// This should not be changed since it needs to match datastores that also use SEH1 histograms.
var bucketFactor = float64(math.Log(1 + 0.1))

// SEH1 is a sample aggregator that produces a sparse exponential histogram using an epsilon of 0.1.
type SEH1 struct {
	// Histogram is a sparse exponential histogram representing the samples that were aggregated into this Datum.
	// It can be used to estimate percentiles.
	// Since SEH1 does not work for the value zero and negative values, these are counted using SEH1ZeroBucket.
	Histogram map[int32]int32

	// ZeroBucket is the bucket for counting zero values.
	ZeroBucket int
}

// NewSEH1 returns a new sparse exponential histogram
func NewSEH1() *SEH1 {
	seh1 := &SEH1{
		Histogram: make(map[int32]int32),
	}
	return seh1
}

// Include adds a sample to the histogram.
func (histogram *SEH1) Include(sample *Sample) {
	if sample.Value > 0 {
		bucket := computeBucket(sample.Value)
		histogram.Histogram[bucket]++
	} else {
		histogram.ZeroBucket++
	}
}

// Union updates this instance to include the aggregated data from another SEH1 instance.
// TODO: is this still required?
func (histogram *SEH1) Union(other *SEH1) {
	otherSEH1 := other
	if otherSEH1 != nil {
		histogram.ZeroBucket += otherSEH1.ZeroBucket
		for bucket, value := range otherSEH1.Histogram {
			histogram.Histogram[bucket] += value
		}
	}
}

// ApproximateOriginalValue will give the estimated orignal value for items falling
// into the given bucketIndex
func (histogram *SEH1) ApproximateOriginalValue(bucketIndex int32) float64 {
	// compute originally --> y = math.Log(x) / bucketFactor
	// so... y*bucketFactor = math.Log(x)
	// so... math.Pow(e, y*bucketFactor) = x
	// Then add 0.5 to the bucket to make the value be in the center so that the
	// margin of error will average out and to avoid off by one errors
	// when converting bucket to value and back to bucket.
	return math.Pow(math.E, (float64(bucketIndex)+0.5)*bucketFactor)
}

// helper to compute the bucket for a sample value
func computeBucket(v float64) int32 {
	return int32(math.Floor(math.Log(v) / bucketFactor))
}
