// Package stats defines a lightweight interface for collecting statistics. It
// doesn't provide an implementation, just the shared interface.
package stats

// Client provides methods to collection statistics.
type Client interface {
	// BumpAvg bumps the average for the given key.
	BumpAvg(key string, val float64)

	// BumpSum bumps the sum for the given key.
	BumpSum(key string, val float64)

	// BumpHistogram bumps the histogram for the given key.
	BumpHistogram(key string, val float64)

	// BumpTime is a special version of BumpHistogram which is specialized for
	// timers. Calling it starts the timer, and it returns a value on which End()
	// can be called to indicate finishing the timer. A convenient way of
	// recording the duration of a function is calling it like such at the top of
	// the function:
	//
	//     defer s.BumpTime("my.function").End()
	BumpTime(key string) interface {
		End()
	}
}

// PrefixClient adds multiple keys for the same value, with each prefix
// added to the key and calls the underlying client.
func PrefixClient(prefixes []string, client Client) Client {
	return &prefixClient{
		Prefixes: prefixes,
		Client:   client,
	}
}

type prefixClient struct {
	Prefixes []string
	Client   Client
}

func (p *prefixClient) BumpAvg(key string, val float64) {
	for _, prefix := range p.Prefixes {
		p.Client.BumpAvg(prefix+key, val)
	}
}

func (p *prefixClient) BumpSum(key string, val float64) {
	for _, prefix := range p.Prefixes {
		p.Client.BumpSum(prefix+key, val)
	}
}

func (p *prefixClient) BumpHistogram(key string, val float64) {
	for _, prefix := range p.Prefixes {
		p.Client.BumpHistogram(prefix+key, val)
	}
}

func (p *prefixClient) BumpTime(key string) interface {
	End()
} {
	var m multiEnder
	for _, prefix := range p.Prefixes {
		m = append(m, p.Client.BumpTime(prefix+key))
	}
	return m
}

// multiEnder combines many enders together.
type multiEnder []interface {
	End()
}

func (m multiEnder) End() {
	for _, e := range m {
		e.End()
	}
}

// HookClient is useful for testing. It provides optional hooks for each
// expected method in the interface, which if provided will be called. If a
// hook is not provided, it will be ignored.
type HookClient struct {
	BumpAvgHook       func(key string, val float64)
	BumpSumHook       func(key string, val float64)
	BumpHistogramHook func(key string, val float64)
	BumpTimeHook      func(key string) interface {
		End()
	}
}

// BumpAvg will call BumpAvgHook if defined.
func (c *HookClient) BumpAvg(key string, val float64) {
	if c.BumpAvgHook != nil {
		c.BumpAvgHook(key, val)
	}
}

// BumpSum will call BumpSumHook if defined.
func (c *HookClient) BumpSum(key string, val float64) {
	if c.BumpSumHook != nil {
		c.BumpSumHook(key, val)
	}
}

// BumpHistogram will call BumpHistogramHook if defined.
func (c *HookClient) BumpHistogram(key string, val float64) {
	if c.BumpHistogramHook != nil {
		c.BumpHistogramHook(key, val)
	}
}

// BumpTime will call BumpTimeHook if defined.
func (c *HookClient) BumpTime(key string) interface {
	End()
} {
	if c.BumpTimeHook != nil {
		return c.BumpTimeHook(key)
	}
	return NoOpEnd
}

type noOpEnd struct{}

func (n noOpEnd) End() {}

// NoOpEnd provides a dummy value for use in tests as valid return value for
// BumpTime().
var NoOpEnd = noOpEnd{}

// BumpAvg calls BumpAvg on the Client if it isn't nil. This is useful when a
// component has an optional stats.Client.
func BumpAvg(c Client, key string, val float64) {
	if c != nil {
		c.BumpAvg(key, val)
	}
}

// BumpSum calls BumpSum on the Client if it isn't nil. This is useful when a
// component has an optional stats.Client.
func BumpSum(c Client, key string, val float64) {
	if c != nil {
		c.BumpSum(key, val)
	}
}

// BumpHistogram calls BumpHistogram on the Client if it isn't nil. This is
// useful when a component has an optional stats.Client.
func BumpHistogram(c Client, key string, val float64) {
	if c != nil {
		c.BumpHistogram(key, val)
	}
}

// BumpTime calls BumpTime on the Client if it isn't nil. If the Client is nil
// it still returns a valid return value which will be a no-op. This is useful
// when a component has an optional stats.Client.
func BumpTime(c Client, key string) interface {
	End()
} {
	if c != nil {
		return c.BumpTime(key)
	}
	return NoOpEnd
}