package timing

import (
	"fmt"
	"sync"
	"time"

	"github.com/cactus/go-statsd-client/statsd"
	"golang.org/x/net/context"
)

// The key type is unexported to prevent collisions with context keys defined in other packages
type key int

const xactKey key = 0
const xactSampleRate = 1

func XactContext(ctx context.Context, xact *Xact) context.Context {
	return context.WithValue(ctx, xactKey, xact)
}

func XactFromContext(ctx context.Context) (*Xact, bool) {
	xact, ok := ctx.Value(xactKey).(*Xact)
	return xact, ok
}

type Xact struct {
	names            []string
	Stats            statsd.StatSender
	StatsdSampleRate float32

	lock    sync.Mutex
	timings map[string]time.Duration
	timer
}

func (xact *Xact) End(stat string) time.Duration {
	statsdSampleRate := xact.StatsdSampleRate
	if statsdSampleRate == 0 {
		statsdSampleRate = 1
	}

	if xact.pending() {
		xact.timer.End()
		if xact.Stats != nil {
			xact.lock.Lock()
			defer xact.lock.Unlock()
			for _, name := range xact.names {
				xact.Stats.TimingDuration(fmt.Sprintf("%s.%s.total", name, stat), xact.duration(), statsdSampleRate)
				if xact.timings != nil {
					for group, duration := range xact.timings {
						xact.Stats.TimingDuration(fmt.Sprintf("%s.%s.%s", name, stat, group), duration, statsdSampleRate)
					}
				}
			}
		}

	}
	return xact.duration()
}

func (xact *Xact) Sub(group string) *SubXact {
	return &SubXact{
		parent: xact,
		group:  group,
	}
}

func (xact *Xact) AddName(name string) {
	xact.lock.Lock()
	defer xact.lock.Unlock()
	xact.names = append(xact.names, name)
}

func (xact *Xact) Names() []string {
	xact.lock.Lock()
	defer xact.lock.Unlock()

	namesCopy := make([]string, len(xact.names))
	copy(namesCopy, xact.names)
	return namesCopy
}

func (xact *Xact) addTiming(group string, d time.Duration) {
	xact.lock.Lock()
	defer xact.lock.Unlock()
	if xact.timings == nil {
		xact.timings = make(map[string]time.Duration)
	}
	old := xact.timings[group]
	xact.timings[group] = old + d
}

type SubXact struct {
	parent *Xact
	group  string
	timer
}

func (sub *SubXact) End() time.Duration {
	if sub.pending() {
		sub.timer.End()
		sub.parent.addTiming(sub.group, sub.duration())
	}
	return sub.duration()
}

type timer struct {
	start time.Time
	end   time.Time
}

func (t *timer) Start() time.Time {
	if !t.pending() {
		t.start = time.Now()
	}
	return t.start
}

func (t *timer) End() time.Duration {
	if t.pending() {
		t.end = time.Now()
	}
	return t.duration()
}

func (t *timer) pending() bool {
	return !t.start.IsZero() && t.end.IsZero()
}

func (t *timer) duration() time.Duration {
	if !t.end.IsZero() {
		return t.end.Sub(t.start)
	} else {
		return 0
	}
}
