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")
}
