package statsd

import (
	"fmt"
	"io"
	"math/rand"
	"net"
	"strings"
	"time"
)

type Stats interface {
	Incr(bucket string)
	IncrBy(bucket string, step int)
	IncrUnsampled(bucket string)
	IncrByUnsampled(bucket string, step int)
	Timing(bucket string, d time.Duration)
	TimingUnsampled(bucket string, d time.Duration)
	IntegerTiming(bucket string, i int64)
	IntegerTimingUnsampled(bucket string, i int64)
	GaugeChange(bucket string, step int)
	GaugeSet(bucket string, value uint)
	GaugeChangeUnsampled(bucket string, step int)
	GaugeSetUnsampled(bucket string, value uint)
	Unique(bucket string, value string)
}

type noStats struct{}

func (*noStats) Incr(bucket string)                             {}
func (*noStats) IncrBy(bucket string, step int)                 {}
func (*noStats) IncrUnsampled(bucket string)                    {}
func (*noStats) IncrByUnsampled(bucket string, step int)        {}
func (*noStats) Timing(bucket string, d time.Duration)          {}
func (*noStats) TimingUnsampled(bucket string, d time.Duration) {}
func (*noStats) IntegerTiming(bucket string, i int64)           {}
func (*noStats) IntegerTimingUnsampled(bucket string, i int64)  {}
func (*noStats) GaugeChange(bucket string, step int)            {}
func (*noStats) GaugeSet(bucket string, value uint)             {}
func (*noStats) GaugeChangeUnsampled(bucket string, step int)   {}
func (*noStats) GaugeSetUnsampled(bucket string, value uint)    {}
func (*noStats) Unique(bucket string, value string)             {}

var _ Stats = (*noStats)(nil)

type StatsConfig struct {
	Rate   float32
	Prefix string
}

type stats struct {
	c StatsConfig
	w io.Writer
}

var _ Stats = (*stats)(nil)

var noopStats = &noStats{}

// This allows for simple initialization of Stats
func Noop() Stats {
	return noopStats
}

// Dial takes the same parameters as net.Dial, ie. a transport protocol
// (typically "udp") and an endpoint. It returns a new Stats structure,
// ready to use.
//
// Note that g2s currently performs no management on the connection it creates.
func Dial(proto, endpoint string, conf StatsConfig) (Stats, error) {
	c, err := net.DialTimeout(proto, endpoint, 2*time.Second)
	if err != nil {
		return nil, err
	}
	return New(c, conf), nil
}

// This is a version of Dial that never fails. It will return a Noop version
// of Stats structure if there is an error during initialization.
func SafeDial(proto, endpoint string, conf StatsConfig) Stats {
	c, err := Dial(proto, endpoint, conf)
	if err != nil {
		return noopStats
	}
	return c
}

// This is a version of Dial that always succeeds. It will panic
// when the connection to statsd server can't be established
func MustDial(proto, endpoint string, conf StatsConfig) Stats {
	c, err := Dial(proto, endpoint, conf)
	if err != nil {
		panic(fmt.Sprintf("could not dial statsd at %#v, %#v: %v", proto, endpoint, err))
	}
	return c
}

// New constructs a Stats structure which will write statsd-protocol messages
// into the given io.Writer. New is intended to be used by consumers who want
// nonstandard behavior: for example, they may pass an io.Writer which performs
// buffering and aggregation of statsd-protocol messages.
//
// Note that statsd provides no synchronization. If you pass an io.Writer which
// is not goroutine-safe, for example a bytes.Buffer, you must make sure you
// synchronize your calls to the Stats methods.
func New(w io.Writer, conf StatsConfig) Stats {
	if conf.Prefix != "" && !strings.HasSuffix(conf.Prefix, ".") {
		conf.Prefix = conf.Prefix + "."
	}
	if conf.Rate > 1.0 {
		conf.Rate = 1.0
	}

	return &stats{
		c: conf,
		w: w,
	}
}

// shouldSend returns a randomly sampled value, where r (0..1) is
// probability of returning true
func shouldSend(r float32) bool {
	return rand.Float32() < r
}

// sends the message to statsd server
func (s *stats) message(bucket string, value string, suffix string) {
	rateStr := ""
	if s.c.Rate < 1.0 {
		rateStr = fmt.Sprintf("|@%f", s.c.Rate)
	}

	message := fmt.Sprintf("%s%s:%s|%s%s", s.c.Prefix, bucket, value, suffix, rateStr)
	s.w.Write([]byte(message))
}

func (s *stats) messageNoRate(bucket string, value string, suffix string) {
	message := fmt.Sprintf("%s%s:%s|%s", s.c.Prefix, bucket, value, suffix)
	s.w.Write([]byte(message))
}

// rolls the dice and then maybe sends the message
func (s *stats) maybeMessage(bucket string, value string, suffix string) {
	if shouldSend(s.c.Rate) {
		s.message(bucket, value, suffix)
	}
}

func (s *stats) Incr(bucket string) {
	s.IncrBy(bucket, 1)
}

func (s *stats) IncrUnsampled(bucket string) {
	s.IncrByUnsampled(bucket, 1)
}

func (s *stats) IncrBy(bucket string, step int) {
	s.maybeMessage(bucket, fmt.Sprintf("%d", step), "c")
}

func (s *stats) IncrByUnsampled(bucket string, step int) {
	s.messageNoRate(bucket, fmt.Sprintf("%d", step), "c")
}

func (s *stats) Timing(bucket string, d time.Duration) {
	s.IntegerTiming(bucket, int64(d/time.Millisecond))
}

func (s *stats) TimingUnsampled(bucket string, d time.Duration) {
	s.IntegerTimingUnsampled(bucket, int64(d/time.Millisecond))
}

func (s *stats) IntegerTiming(bucket string, i int64) {
	s.maybeMessage(bucket, fmt.Sprintf("%d", i), "ms")
}

func (s *stats) IntegerTimingUnsampled(bucket string, i int64) {
	s.messageNoRate(bucket, fmt.Sprintf("%d", i), "ms")
}

func (s *stats) GaugeChange(bucket string, value int) {
	s.maybeMessage(bucket, fmt.Sprintf("%+d", value), "g")
}

func (s *stats) GaugeSet(bucket string, value uint) {
	s.maybeMessage(bucket, fmt.Sprintf("%d", value), "g")
}

func (s *stats) GaugeChangeUnsampled(bucket string, value int) {
	s.messageNoRate(bucket, fmt.Sprintf("%+d", value), "g")
}

func (s *stats) GaugeSetUnsampled(bucket string, value uint) {
	s.messageNoRate(bucket, fmt.Sprintf("%d", value), "g")
}

func (s *stats) Unique(bucket string, value string) {
	s.maybeMessage(bucket, value, "s")
}
