package graphite

import (
	"fmt"
	"math"
	"strings"

	"github.com/valyala/histogram"
)

var aggrStateFuncs = map[string]func(int) aggrState{
	"average":  newAggrStateAvg,
	"avg":      newAggrStateAvg,
	"avg_zero": newAggrStateAvgZero,
	"median":   newAggrStateMedian,
	"sum":      newAggrStateSum,
	"total":    newAggrStateSum,
	"min":      newAggrStateMin,
	"max":      newAggrStateMax,
	"diff":     newAggrStateDiff,
	"pow":      newAggrStatePow,
	"stddev":   newAggrStateStddev,
	"count":    newAggrStateCount,
	"range":    newAggrStateRange,
	"rangeOf":  newAggrStateRange,
	"multiply": newAggrStateMultiply,
	"first":    newAggrStateFirst,
	"last":     newAggrStateLast,
	"current":  newAggrStateLast,
}

type aggrState interface {
	Update(values []float64)
	Finalize(xFilesFactor float64) []float64
}

func newAggrState(pointsLen int, funcName string) (aggrState, error) {
	s := strings.TrimSuffix(funcName, "Series")
	asf := aggrStateFuncs[s]
	if asf == nil {
		return nil, fmt.Errorf("unsupported aggregate function %q", funcName)
	}
	return asf(pointsLen), nil
}

type aggrStateAvg struct {
	pointsLen   int
	sums        []float64
	counts      []int
	seriesTotal int
}

func newAggrStateAvg(pointsLen int) aggrState {
	return &aggrStateAvg{
		pointsLen: pointsLen,
		sums:      make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateAvg) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	sums := as.sums
	counts := as.counts
	for i, v := range values {
		if !math.IsNaN(v) {
			sums[i] += v
			counts[i]++
		}
	}
	as.seriesTotal++
}

func (as *aggrStateAvg) Finalize(xFilesFactor float64) []float64 {
	sums := as.sums
	counts := as.counts
	values := make([]float64, as.pointsLen)
	xff := int(xFilesFactor * float64(as.seriesTotal))
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = sums[i] / float64(count)
		}
		values[i] = v
	}
	return values
}

type aggrStateAvgZero struct {
	pointsLen   int
	sums        []float64
	seriesTotal int
}

func newAggrStateAvgZero(pointsLen int) aggrState {
	return &aggrStateAvgZero{
		pointsLen: pointsLen,
		sums:      make([]float64, pointsLen),
	}
}

func (as *aggrStateAvgZero) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	sums := as.sums
	for i, v := range values {
		if !math.IsNaN(v) {
			sums[i] += v
		}
	}
	as.seriesTotal++
}

func (as *aggrStateAvgZero) Finalize(_ float64) []float64 {
	sums := as.sums
	values := make([]float64, as.pointsLen)
	count := float64(as.seriesTotal)
	for i, sum := range sums {
		v := nan
		if count > 0 {
			v = sum / count
		}
		values[i] = v
	}
	return values
}

func newAggrStateMedian(pointsLen int) aggrState {
	return newAggrStatePercentile(pointsLen, 50)
}

type aggrStatePercentile struct {
	phi         float64
	pointsLen   int
	hs          []*histogram.Fast
	counts      []int
	seriesTotal int
}

func newAggrStatePercentile(pointsLen int, n float64) aggrState {
	hs := make([]*histogram.Fast, pointsLen)
	for i := range pointsLen {
		hs[i] = histogram.NewFast()
	}
	return &aggrStatePercentile{
		phi:       n / 100,
		pointsLen: pointsLen,
		hs:        hs,
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStatePercentile) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	hs := as.hs
	counts := as.counts
	for i, v := range values {
		if !math.IsNaN(v) {
			hs[i].Update(v)
			counts[i]++
		}
	}
	as.seriesTotal++
}

func (as *aggrStatePercentile) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	hs := as.hs
	for i, count := range as.counts {
		v := nan
		if count > 0 && count >= xff {
			v = hs[i].Quantile(as.phi)
		}
		values[i] = v
	}
	return values
}

type aggrStateSum struct {
	pointsLen   int
	sums        []float64
	counts      []int
	seriesTotal int
}

func newAggrStateSum(pointsLen int) aggrState {
	return &aggrStateSum{
		pointsLen: pointsLen,
		sums:      make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateSum) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	sums := as.sums
	counts := as.counts
	for i, v := range values {
		if !math.IsNaN(v) {
			sums[i] += v
			counts[i]++
		}
	}
	as.seriesTotal++
}

func (as *aggrStateSum) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	sums := as.sums
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = sums[i]
		}
		values[i] = v
	}
	return values
}

type aggrStateMin struct {
	pointsLen   int
	mins        []float64
	counts      []int
	seriesTotal int
}

func newAggrStateMin(pointsLen int) aggrState {
	return &aggrStateMin{
		pointsLen: pointsLen,
		mins:      make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateMin) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	mins := as.mins
	counts := as.counts
	for i, v := range values {
		if math.IsNaN(v) {
			continue
		}
		counts[i]++
		if counts[i] == 1 {
			mins[i] = v
		} else if v < mins[i] {
			mins[i] = v
		}
	}
	as.seriesTotal++
}

func (as *aggrStateMin) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	mins := as.mins
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = mins[i]
		}
		values[i] = v
	}
	return values
}

type aggrStateMax struct {
	pointsLen   int
	maxs        []float64
	counts      []int
	seriesTotal int
}

func newAggrStateMax(pointsLen int) aggrState {
	return &aggrStateMax{
		pointsLen: pointsLen,
		maxs:      make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateMax) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	maxs := as.maxs
	counts := as.counts
	for i, v := range values {
		if math.IsNaN(v) {
			continue
		}
		counts[i]++
		if counts[i] == 1 {
			maxs[i] = v
		} else if v > maxs[i] {
			maxs[i] = v
		}
	}
	as.seriesTotal++
}

func (as *aggrStateMax) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	maxs := as.maxs
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = maxs[i]
		}
		values[i] = v
	}
	return values
}

type aggrStateDiff struct {
	pointsLen   int
	vs          []float64
	counts      []int
	seriesTotal int
}

func newAggrStateDiff(pointsLen int) aggrState {
	return &aggrStateDiff{
		pointsLen: pointsLen,
		vs:        make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateDiff) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	vs := as.vs
	counts := as.counts
	for i, v := range values {
		if !math.IsNaN(v) {
			if counts[i] == 0 {
				vs[i] = v
			} else {
				vs[i] -= v
			}
			counts[i]++
		}
	}
	as.seriesTotal++
}

func (as *aggrStateDiff) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	vs := as.vs
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = vs[i]
		}
		values[i] = v
	}
	return values
}

type aggrStatePow struct {
	pointsLen   int
	vs          []float64
	counts      []int
	seriesTotal int
}

func newAggrStatePow(pointsLen int) aggrState {
	return &aggrStatePow{
		pointsLen: pointsLen,
		vs:        make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStatePow) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	vs := as.vs
	counts := as.counts
	for i, v := range values {
		if !math.IsNaN(v) {
			if counts[i] == 0 {
				vs[i] = v
			} else {
				vs[i] = math.Pow(vs[i], v)
			}
			counts[i]++
		}
	}
	as.seriesTotal++
}

func (as *aggrStatePow) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	vs := as.vs
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = vs[i]
		}
		values[i] = v
	}
	return values
}

type aggrStateStddev struct {
	pointsLen   int
	means       []float64
	m2s         []float64
	counts      []int
	seriesTotal int
}

func newAggrStateStddev(pointsLen int) aggrState {
	return &aggrStateStddev{
		pointsLen: pointsLen,
		means:     make([]float64, pointsLen),
		m2s:       make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateStddev) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	means := as.means
	m2s := as.m2s
	counts := as.counts
	for i, v := range values {
		if math.IsNaN(v) {
			continue
		}
		// See https://en.m.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
		count := counts[i]
		mean := means[i]
		count++
		delta := v - mean
		mean += delta / float64(count)
		delta2 := v - mean
		means[i] = mean
		m2s[i] += delta * delta2
		counts[i] = count
	}
	as.seriesTotal++
}

func (as *aggrStateStddev) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	m2s := as.m2s
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = math.Sqrt(m2s[i] / float64(count))
		}
		values[i] = v
	}
	return values
}

type aggrStateCount struct {
	pointsLen   int
	counts      []int
	seriesTotal int
}

func newAggrStateCount(pointsLen int) aggrState {
	return &aggrStateCount{
		pointsLen: pointsLen,
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateCount) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	counts := as.counts
	for i, v := range values {
		if !math.IsNaN(v) {
			counts[i]++
		}
	}
	as.seriesTotal++
}

func (as *aggrStateCount) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = float64(count)
		}
		values[i] = v
	}
	return values
}

type aggrStateRange struct {
	pointsLen   int
	mins        []float64
	maxs        []float64
	counts      []int
	seriesTotal int
}

func newAggrStateRange(pointsLen int) aggrState {
	return &aggrStateRange{
		pointsLen: pointsLen,
		mins:      make([]float64, pointsLen),
		maxs:      make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateRange) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	mins := as.mins
	maxs := as.maxs
	counts := as.counts
	for i, v := range values {
		if math.IsNaN(v) {
			continue
		}
		counts[i]++
		if counts[i] == 1 {
			mins[i] = v
			maxs[i] = v
		} else if v < mins[i] {
			mins[i] = v
		} else if v > maxs[i] {
			maxs[i] = v
		}
	}
	as.seriesTotal++
}

func (as *aggrStateRange) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	mins := as.mins
	maxs := as.maxs
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = maxs[i] - mins[i]
		}
		values[i] = v
	}
	return values
}

type aggrStateMultiply struct {
	pointsLen   int
	ms          []float64
	counts      []int
	seriesTotal int
}

func newAggrStateMultiply(pointsLen int) aggrState {
	return &aggrStateMultiply{
		pointsLen: pointsLen,
		ms:        make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateMultiply) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	ms := as.ms
	counts := as.counts
	for i, v := range values {
		if math.IsNaN(v) {
			continue
		}
		counts[i]++
		if counts[i] == 1 {
			ms[i] = v
		} else {
			ms[i] *= v
		}
	}
	as.seriesTotal++
}

func (as *aggrStateMultiply) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	ms := as.ms
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = ms[i]
		}
		values[i] = v
	}
	return values
}

type aggrStateFirst struct {
	pointsLen   int
	vs          []float64
	counts      []int
	seriesTotal int
}

func newAggrStateFirst(pointsLen int) aggrState {
	return &aggrStateFirst{
		pointsLen: pointsLen,
		vs:        make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateFirst) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	vs := as.vs
	counts := as.counts
	for i, v := range values {
		if math.IsNaN(v) {
			continue
		}
		counts[i]++
		if counts[i] == 1 {
			vs[i] = v
		}
	}
	as.seriesTotal++
}

func (as *aggrStateFirst) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	vs := as.vs
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = vs[i]
		}
		values[i] = v
	}
	return values
}

type aggrStateLast struct {
	pointsLen   int
	vs          []float64
	counts      []int
	seriesTotal int
}

func newAggrStateLast(pointsLen int) aggrState {
	return &aggrStateLast{
		pointsLen: pointsLen,
		vs:        make([]float64, pointsLen),
		counts:    make([]int, pointsLen),
	}
}

func (as *aggrStateLast) Update(values []float64) {
	if len(values) != as.pointsLen {
		panic(fmt.Errorf("BUG: unexpected number of points in values; got %d; want %d", len(values), as.pointsLen))
	}
	vs := as.vs
	counts := as.counts
	for i, v := range values {
		if math.IsNaN(v) {
			continue
		}
		vs[i] = v
		counts[i]++
	}
	as.seriesTotal++
}

func (as *aggrStateLast) Finalize(xFilesFactor float64) []float64 {
	xff := int(xFilesFactor * float64(as.seriesTotal))
	values := make([]float64, as.pointsLen)
	vs := as.vs
	counts := as.counts
	for i, count := range counts {
		v := nan
		if count > 0 && count >= xff {
			v = vs[i]
		}
		values[i] = v
	}
	return values
}
