// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package metric

import (
	"fmt"
	"sort"
	"time"

	"golang.org/x/tools/internal/event/keys"
	"golang.org/x/tools/internal/event/label"
)

// Data represents a single point in the time series of a metric.
// This provides the common interface to all metrics no matter their data
// format.
// To get the actual values for the metric you must type assert to a concrete
// metric type.
type Data interface {
	// Handle returns the metric handle this data is for.
	//TODO: rethink the concept of metric handles
	Handle() string
	// Groups reports the rows that currently exist for this metric.
	Groups() [][]label.Label
}

// Int64Data is a concrete implementation of Data for int64 scalar metrics.
type Int64Data struct {
	// Info holds the original construction information.
	Info *Scalar
	// IsGauge is true for metrics that track values, rather than increasing over time.
	IsGauge bool
	// Rows holds the per group values for the metric.
	Rows []int64
	// EndTime is the last time this metric was updated.
	EndTime time.Time

	groups [][]label.Label
	key    *keys.Int64
}

// Float64Data is a concrete implementation of Data for float64 scalar metrics.
type Float64Data struct {
	// Info holds the original construction information.
	Info *Scalar
	// IsGauge is true for metrics that track values, rather than increasing over time.
	IsGauge bool
	// Rows holds the per group values for the metric.
	Rows []float64
	// EndTime is the last time this metric was updated.
	EndTime time.Time

	groups [][]label.Label
	key    *keys.Float64
}

// HistogramInt64Data is a concrete implementation of Data for int64 histogram metrics.
type HistogramInt64Data struct {
	// Info holds the original construction information.
	Info *HistogramInt64
	// Rows holds the per group values for the metric.
	Rows []*HistogramInt64Row
	// EndTime is the last time this metric was updated.
	EndTime time.Time

	groups [][]label.Label
	key    *keys.Int64
}

// HistogramInt64Row holds the values for a single row of a HistogramInt64Data.
type HistogramInt64Row struct {
	// Values is the counts per bucket.
	Values []int64
	// Count is the total count.
	Count int64
	// Sum is the sum of all the values recorded.
	Sum int64
	// Min is the smallest recorded value.
	Min int64
	// Max is the largest recorded value.
	Max int64
}

// HistogramFloat64Data is a concrete implementation of Data for float64 histogram metrics.
type HistogramFloat64Data struct {
	// Info holds the original construction information.
	Info *HistogramFloat64
	// Rows holds the per group values for the metric.
	Rows []*HistogramFloat64Row
	// EndTime is the last time this metric was updated.
	EndTime time.Time

	groups [][]label.Label
	key    *keys.Float64
}

// HistogramFloat64Row holds the values for a single row of a HistogramFloat64Data.
type HistogramFloat64Row struct {
	// Values is the counts per bucket.
	Values []int64
	// Count is the total count.
	Count int64
	// Sum is the sum of all the values recorded.
	Sum float64
	// Min is the smallest recorded value.
	Min float64
	// Max is the largest recorded value.
	Max float64
}

func labelListEqual(a, b []label.Label) bool {
	//TODO: make this more efficient
	return fmt.Sprint(a) == fmt.Sprint(b)
}

func labelListLess(a, b []label.Label) bool {
	//TODO: make this more efficient
	return fmt.Sprint(a) < fmt.Sprint(b)
}

func getGroup(lm label.Map, g *[][]label.Label, keys []label.Key) (int, bool) {
	group := make([]label.Label, len(keys))
	for i, key := range keys {
		l := lm.Find(key)
		if l.Valid() {
			group[i] = l
		}
	}
	old := *g
	index := sort.Search(len(old), func(i int) bool {
		return !labelListLess(old[i], group)
	})
	if index < len(old) && labelListEqual(group, old[index]) {
		// not a new group
		return index, false
	}
	*g = make([][]label.Label, len(old)+1)
	copy(*g, old[:index])
	copy((*g)[index+1:], old[index:])
	(*g)[index] = group
	return index, true
}

func (data *Int64Data) Handle() string          { return data.Info.Name }
func (data *Int64Data) Groups() [][]label.Label { return data.groups }

func (data *Int64Data) modify(at time.Time, lm label.Map, f func(v int64) int64) Data {
	index, insert := getGroup(lm, &data.groups, data.Info.Keys)
	old := data.Rows
	if insert {
		data.Rows = make([]int64, len(old)+1)
		copy(data.Rows, old[:index])
		copy(data.Rows[index+1:], old[index:])
	} else {
		data.Rows = make([]int64, len(old))
		copy(data.Rows, old)
	}
	data.Rows[index] = f(data.Rows[index])
	data.EndTime = at
	frozen := *data
	return &frozen
}

func (data *Int64Data) count(at time.Time, lm label.Map, l label.Label) Data {
	return data.modify(at, lm, func(v int64) int64 {
		return v + 1
	})
}

func (data *Int64Data) sum(at time.Time, lm label.Map, l label.Label) Data {
	return data.modify(at, lm, func(v int64) int64 {
		return v + data.key.From(l)
	})
}

func (data *Int64Data) latest(at time.Time, lm label.Map, l label.Label) Data {
	return data.modify(at, lm, func(v int64) int64 {
		return data.key.From(l)
	})
}

func (data *Float64Data) Handle() string          { return data.Info.Name }
func (data *Float64Data) Groups() [][]label.Label { return data.groups }

func (data *Float64Data) modify(at time.Time, lm label.Map, f func(v float64) float64) Data {
	index, insert := getGroup(lm, &data.groups, data.Info.Keys)
	old := data.Rows
	if insert {
		data.Rows = make([]float64, len(old)+1)
		copy(data.Rows, old[:index])
		copy(data.Rows[index+1:], old[index:])
	} else {
		data.Rows = make([]float64, len(old))
		copy(data.Rows, old)
	}
	data.Rows[index] = f(data.Rows[index])
	data.EndTime = at
	frozen := *data
	return &frozen
}

func (data *Float64Data) sum(at time.Time, lm label.Map, l label.Label) Data {
	return data.modify(at, lm, func(v float64) float64 {
		return v + data.key.From(l)
	})
}

func (data *Float64Data) latest(at time.Time, lm label.Map, l label.Label) Data {
	return data.modify(at, lm, func(v float64) float64 {
		return data.key.From(l)
	})
}

func (data *HistogramInt64Data) Handle() string          { return data.Info.Name }
func (data *HistogramInt64Data) Groups() [][]label.Label { return data.groups }

func (data *HistogramInt64Data) modify(at time.Time, lm label.Map, f func(v *HistogramInt64Row)) Data {
	index, insert := getGroup(lm, &data.groups, data.Info.Keys)
	old := data.Rows
	var v HistogramInt64Row
	if insert {
		data.Rows = make([]*HistogramInt64Row, len(old)+1)
		copy(data.Rows, old[:index])
		copy(data.Rows[index+1:], old[index:])
	} else {
		data.Rows = make([]*HistogramInt64Row, len(old))
		copy(data.Rows, old)
		v = *data.Rows[index]
	}
	oldValues := v.Values
	v.Values = make([]int64, len(data.Info.Buckets))
	copy(v.Values, oldValues)
	f(&v)
	data.Rows[index] = &v
	data.EndTime = at
	frozen := *data
	return &frozen
}

func (data *HistogramInt64Data) record(at time.Time, lm label.Map, l label.Label) Data {
	return data.modify(at, lm, func(v *HistogramInt64Row) {
		value := data.key.From(l)
		v.Sum += value
		if v.Min > value || v.Count == 0 {
			v.Min = value
		}
		if v.Max < value || v.Count == 0 {
			v.Max = value
		}
		v.Count++
		for i, b := range data.Info.Buckets {
			if value <= b {
				v.Values[i]++
			}
		}
	})
}

func (data *HistogramFloat64Data) Handle() string          { return data.Info.Name }
func (data *HistogramFloat64Data) Groups() [][]label.Label { return data.groups }

func (data *HistogramFloat64Data) modify(at time.Time, lm label.Map, f func(v *HistogramFloat64Row)) Data {
	index, insert := getGroup(lm, &data.groups, data.Info.Keys)
	old := data.Rows
	var v HistogramFloat64Row
	if insert {
		data.Rows = make([]*HistogramFloat64Row, len(old)+1)
		copy(data.Rows, old[:index])
		copy(data.Rows[index+1:], old[index:])
	} else {
		data.Rows = make([]*HistogramFloat64Row, len(old))
		copy(data.Rows, old)
		v = *data.Rows[index]
	}
	oldValues := v.Values
	v.Values = make([]int64, len(data.Info.Buckets))
	copy(v.Values, oldValues)
	f(&v)
	data.Rows[index] = &v
	data.EndTime = at
	frozen := *data
	return &frozen
}

func (data *HistogramFloat64Data) record(at time.Time, lm label.Map, l label.Label) Data {
	return data.modify(at, lm, func(v *HistogramFloat64Row) {
		value := data.key.From(l)
		v.Sum += value
		if v.Min > value || v.Count == 0 {
			v.Min = value
		}
		if v.Max < value || v.Count == 0 {
			v.Max = value
		}
		v.Count++
		for i, b := range data.Info.Buckets {
			if value <= b {
				v.Values[i]++
			}
		}
	})
}
