package poller

import (
	"expvar"
	"runtime"
	"time"

	logging "code.justin.tv/amzn/TwitchLogging"
	"code.justin.tv/amzn/TwitchTelemetry"
)

// NewGoStatsPollingCollector returns a PollingCollector that scrapes Golang runtime statistics.
// The metrics generated are as follows:
// * FreesPerSec: number of heap objects freed since last fetch, standardized per second
// * MallocsPerSec: number of heap objects allocated since last fetch, standardized per second
// * NumGCPerSec: number of GC cycles completed since last fetch, standardized per second
// * PauseTotalSecPerSec: unitless ratio of the number of seconds spent paused on GC since last fetch to the seconds since last fetch
// * TotalAllocPerSec: total bytes allocated for heap objects since last fetch, standardized per second
// * HeapObjects: the current number of allocated heap objects
// * HeapAlloc: the current total bytes across all heap objects
//
// For information about where these values come from, and what they mean,
// see: https://golang.org/pkg/runtime/#MemStats
func NewGoStatsPollingCollector(interval time.Duration, sb *telemetry.SampleBuilder, obs telemetry.SampleObserver, l logging.Logger) *PollingCollector {
	gspj := &GoStatsPollingJob{
		lastTimestamp: time.Now(),
	}
	return NewPollingCollector(gspj, interval, sb, obs, l)
}

// GoStatsPollingJob is a PollingJob that collects Golang expvar data,
// and holds state so subsequent Fetch's return diffs from the fetch before.
//
// Note that the metrics implemented here are a first pass that demonstrate the idea.
// More metrics can be added relatively easily by extending this implementation
type GoStatsPollingJob struct {
	lastTimestamp    time.Time
	lastFrees        uint64
	lastMallocs      uint64
	lastNumGC        uint32
	lastPauseTotalNs uint64
	lastTotalAlloc   uint64
}

// Fetch retrieves the latest golang statistics, diffing against the last
// polled data where necessary (cumulative counters) and returns the data
func (gspj *GoStatsPollingJob) Fetch() ([]*telemetry.Sample, error) {

	// Fetch current runtime stats
	// adapted from: http://blog.ralch.com/tutorial/golang-metrics-with-expvar/
	memstatsFunc := expvar.Get("memstats").(expvar.Func)
	memstats := memstatsFunc().(runtime.MemStats)

	// Create an array of samples to return
	samples := make([]*telemetry.Sample, 8)

	// We divide numbers by secInterval to standardize per second (so the values aren't dependent on the period)
	timeNow := time.Now()
	secInterval := timeNow.Sub(gspj.lastTimestamp).Seconds()

	// Find the things we care about and perform necessary computation
	// FreesPerSec
	samples[0] = gspj.formatSample("FreesPerSec", float64(memstats.Frees-gspj.lastFrees)/secInterval)
	gspj.lastFrees = memstats.Frees

	// MallocsPerSec
	samples[1] = gspj.formatSample("MallocsPerSec", float64(memstats.Mallocs-gspj.lastMallocs)/secInterval)
	gspj.lastMallocs = memstats.Mallocs

	// NumGCPerSec
	samples[2] = gspj.formatSample("NumGCPerSec", float64(memstats.NumGC-gspj.lastNumGC)/secInterval)
	gspj.lastNumGC = memstats.NumGC

	// PauseTotalSecPerSec
	// Unlike other cases, we take float64(memstats.PauseTotalNs-gspj.lastPauseTotalNs)/gspj.secInterval and divide by
	// time.Second.Nanoseconds() so our unitless metric is a s/s ratio instead of Ns/s ratio
	pauseTotalNsPerSec := float64(memstats.PauseTotalNs-gspj.lastPauseTotalNs) / secInterval
	samples[3] = gspj.formatSample("PauseTotalSecPerSec", pauseTotalNsPerSec/float64(time.Second.Nanoseconds()))
	gspj.lastPauseTotalNs = memstats.PauseTotalNs

	// TotalAllocPerSec
	samples[4] = gspj.formatSample("TotalAllocPerSec", float64(memstats.TotalAlloc-gspj.lastTotalAlloc)/secInterval)
	gspj.lastTotalAlloc = memstats.TotalAlloc

	// HeapObjects
	samples[5] = gspj.formatSample("HeapObjects", float64(memstats.HeapObjects))

	// HeapAlloc
	samples[6] = gspj.formatSample("HeapAlloc", float64(memstats.HeapAlloc))

	// NumGoroutines
	samples[7] = gspj.formatSample("NumGoroutines", float64(runtime.NumGoroutine()))

	// Update the last synced time
	gspj.lastTimestamp = timeNow

	return samples, nil
}

func (gspj *GoStatsPollingJob) formatSample(name string, value float64) *telemetry.Sample {
	return &telemetry.Sample{
		MetricID: *telemetry.NewMetricID(name), // let the polling collector harness add base dimensions
		Value:    value,                        // just include a name, value, and unit
		Unit:     telemetry.UnitCount,          // ... the PollingCollector will add dimensions, rollups, and a timestamp
	}
}
