// Copyright 2017 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 benchstat

import (
	"bytes"
	"fmt"
	"io"
	"strconv"
	"strings"

	"golang.org/x/perf/internal/stats"
	"golang.org/x/perf/storage/benchfmt"
)

// A Collection is a collection of benchmark results.
type Collection struct {
	// Configs, Groups, and Units give the set of configs,
	// groups, and units from the keys in Stats in an order
	// meant to match the order the benchmarks were read in.
	Configs, Groups, Units []string

	// Benchmarks gives the set of benchmarks from the keys in
	// Stats by group in an order meant to match the order
	// benchmarks were read in.
	Benchmarks map[string][]string

	// Metrics holds the accumulated metrics for each key.
	Metrics map[Key]*Metrics

	// DeltaTest is the test to use to decide if a change is significant.
	// If nil, it defaults to UTest.
	DeltaTest DeltaTest

	// Alpha is the p-value cutoff to report a change as significant.
	// If zero, it defaults to 0.05.
	Alpha float64

	// AddGeoMean specifies whether to add a line to the table
	// showing the geometric mean of all the benchmark results.
	AddGeoMean bool

	// SplitBy specifies the labels to split results by.
	// By default, results will only be split by full name.
	SplitBy []string

	// Order specifies the row display order for this table.
	// If Order is nil, the table rows are printed in order of
	// first appearance in the input.
	Order Order
}

// A Key identifies one metric (e.g., "ns/op", "B/op") from one
// benchmark (function name sans "Benchmark" prefix) and optional
// group in one configuration (input file name).
type Key struct {
	Config, Group, Benchmark, Unit string
}

// A Metrics holds the measurements of a single metric
// (for example, ns/op or MB/s)
// for all runs of a particular benchmark.
type Metrics struct {
	Unit    string    // unit being measured
	Values  []float64 // measured values
	RValues []float64 // Values with outliers removed
	Min     float64   // min of RValues
	Mean    float64   // mean of RValues
	Max     float64   // max of RValues
}

// FormatMean formats m.Mean using scaler.
func (m *Metrics) FormatMean(scaler Scaler) string {
	var s string
	if scaler != nil {
		s = scaler(m.Mean)
	} else {
		s = fmt.Sprint(m.Mean)
	}
	return s
}

// FormatDiff computes and formats the percent variation of max and min compared to mean.
// If b.Mean or b.Max is zero, FormatDiff returns an empty string.
func (m *Metrics) FormatDiff() string {
	if m.Mean == 0 || m.Max == 0 {
		return ""
	}
	diff := 1 - m.Min/m.Mean
	if d := m.Max/m.Mean - 1; d > diff {
		diff = d
	}
	return fmt.Sprintf("%.0f%%", diff*100.0)
}

// Format returns a textual formatting of "Mean ±Diff" using scaler.
func (m *Metrics) Format(scaler Scaler) string {
	if m.Unit == "" {
		return ""
	}
	mean := m.FormatMean(scaler)
	diff := m.FormatDiff()
	if diff == "" {
		return mean + "     "
	}
	return fmt.Sprintf("%s ±%3s", mean, diff)
}

// computeStats updates the derived statistics in m from the raw
// samples in m.Values.
func (m *Metrics) computeStats() {
	// Discard outliers.
	values := stats.Sample{Xs: m.Values}
	q1, q3 := values.Percentile(0.25), values.Percentile(0.75)
	lo, hi := q1-1.5*(q3-q1), q3+1.5*(q3-q1)
	for _, value := range m.Values {
		if lo <= value && value <= hi {
			m.RValues = append(m.RValues, value)
		}
	}

	// Compute statistics of remaining data.
	m.Min, m.Max = stats.Bounds(m.RValues)
	m.Mean = stats.Mean(m.RValues)
}

// addMetrics returns the metrics with the given key from c,
// creating a new one if needed.
func (c *Collection) addMetrics(key Key) *Metrics {
	if c.Metrics == nil {
		c.Metrics = make(map[Key]*Metrics)
	}
	if stat, ok := c.Metrics[key]; ok {
		return stat
	}

	addString := func(strings *[]string, add string) {
		for _, s := range *strings {
			if s == add {
				return
			}
		}
		*strings = append(*strings, add)
	}
	addString(&c.Configs, key.Config)
	addString(&c.Groups, key.Group)
	if c.Benchmarks == nil {
		c.Benchmarks = make(map[string][]string)
	}
	benchmarks := c.Benchmarks[key.Group]
	addString(&benchmarks, key.Benchmark)
	c.Benchmarks[key.Group] = benchmarks
	addString(&c.Units, key.Unit)
	m := &Metrics{Unit: key.Unit}
	c.Metrics[key] = m
	return m
}

// AddConfig adds the benchmark results in the formatted data
// to the named configuration.
// If the input is large, AddFile should be preferred,
// since it avoids the need to read a copy of the entire raw input
// into memory.
func (c *Collection) AddConfig(config string, data []byte) {
	err := c.AddFile(config, bytes.NewReader(data))
	if err != nil {
		// bytes.Reader never returns errors
		panic(err)
	}
}

// AddFile adds the benchmark results in the formatted data
// (read from the reader r) to the named configuration.
func (c *Collection) AddFile(config string, r io.Reader) error {
	c.Configs = append(c.Configs, config)
	key := Key{Config: config}
	br := benchfmt.NewReader(r)
	for br.Next() {
		c.addResult(key, br.Result())
	}
	return br.Err()
}

// AddResults adds the benchmark results to the named configuration.
func (c *Collection) AddResults(config string, results []*benchfmt.Result) {
	c.Configs = append(c.Configs, config)
	key := Key{Config: config}
	for _, r := range results {
		c.addResult(key, r)
	}
}

func (c *Collection) addResult(key Key, r *benchfmt.Result) {
	f := strings.Fields(r.Content)
	if len(f) < 4 {
		return
	}
	name := f[0]
	if !strings.HasPrefix(name, "Benchmark") {
		return
	}
	name = strings.TrimPrefix(name, "Benchmark")
	n, _ := strconv.Atoi(f[1])
	if n == 0 {
		return
	}
	key.Group = c.makeGroup(r)
	key.Benchmark = name
	for i := 2; i+2 <= len(f); i += 2 {
		val, err := strconv.ParseFloat(f[i], 64)
		if err != nil {
			continue
		}
		key.Unit = f[i+1]
		m := c.addMetrics(key)
		m.Values = append(m.Values, val)
	}
}

func (c *Collection) makeGroup(r *benchfmt.Result) string {
	var out string
	for _, s := range c.SplitBy {
		v := r.NameLabels[s]
		if v == "" {
			v = r.Labels[s]
		}
		if v != "" {
			if out != "" {
				out = out + " "
			}
			out += fmt.Sprintf("%s:%s", s, v)
		}
	}
	return out
}
