package perf

import (
	"bytes"
	"expvar"
	"fmt"
	"math"
	"sync"
	"time"

	"github.com/anacrolix/missinggo"
)

var (
	em = missinggo.NewExpvarIndentMap("perfBuckets")
	mu sync.RWMutex
)

type Timer struct {
	started time.Time
}

func NewTimer() Timer {
	return Timer{time.Now()}
}

// The exponent is the upper bound of the duration in seconds.
func bucketExponent(d time.Duration) int {
	e := -9
	for d != 0 {
		d /= 10
		e++
	}
	return e
}

type buckets struct {
	mu      sync.Mutex
	buckets []int64
}

func (me *buckets) Add(t time.Duration) {
	e := bucketExponent(t)
	me.mu.Lock()
	for e+9 >= len(me.buckets) {
		me.buckets = append(me.buckets, 0)
	}
	me.buckets[e+9]++
	me.mu.Unlock()
}

func humanExponent(e int) string {
	if e == -9 {
		return "<1ns"
	}
	return ">" + time.Duration(math.Pow10(e-1)).String()
}

func (me *buckets) String() string {
	var b bytes.Buffer
	fmt.Fprintf(&b, "{")
	first := true
	me.mu.Lock()
	for i, count := range me.buckets {
		if first {
			if count == 0 {
				continue
			}
			first = false
		} else {
			fmt.Fprintf(&b, ", ")
		}
		fmt.Fprintf(&b, "%q: %d", humanExponent(i), count)
	}
	me.mu.Unlock()
	fmt.Fprintf(&b, "}")
	return b.String()
}

var _ expvar.Var = &buckets{}

func (t *Timer) Stop(desc string) time.Duration {
	d := time.Since(t.started)
	t.addDuration(desc, d)
	return d
}

func (t *Timer) addDuration(desc string, d time.Duration) {
	mu.RLock()
	_m := em.Get(desc)
	mu.RUnlock()
	if _m == nil {
		mu.Lock()
		_m = em.Get(desc)
		if _m == nil {
			_m = new(buckets)
			em.Set(desc, _m)
		}
		mu.Unlock()
	}
	m := _m.(*buckets)
	m.Add(d)
}
