// Copyright 2016 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 benchfmt provides readers and writers for the Go benchmark format.
//
// The format is documented at https://golang.org/design/14313-benchmark-format
//
// This package only parses file configuration lines, not benchmark
// result lines. Parsing the result lines is left to the caller.
//
// Deprecated: See the golang.org/x/perf/benchfmt package, which
// implements readers and writers for the full Go benchmark format.
// It is also higher performance.
package benchfmt

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"sort"
	"strconv"
	"strings"
	"unicode"
)

// Reader reads benchmark results from an io.Reader.
// Use Next to advance through the results.
//
//	br := benchfmt.NewReader(r)
//	for br.Next() {
//	  res := br.Result()
//	  ...
//	}
//	err = br.Err() // get any error encountered during iteration
//	...
type Reader struct {
	s      *bufio.Scanner
	labels Labels
	// permLabels are permanent labels read from the start of the
	// file or provided by AddLabels. They cannot be overridden.
	permLabels Labels
	lineNum    int
	// cached from last call to newResult, to save on allocations
	lastName       string
	lastNameLabels Labels
	// cached from the last call to Next
	result *Result
	err    error
}

// NewReader creates a BenchmarkReader that reads from r.
func NewReader(r io.Reader) *Reader {
	return &Reader{
		s:      bufio.NewScanner(r),
		labels: make(Labels),
	}
}

// AddLabels adds additional labels as if they had been read from the header of a file.
// It must be called before the first call to r.Next.
func (r *Reader) AddLabels(labels Labels) {
	r.permLabels = labels.Copy()
	for k, v := range labels {
		r.labels[k] = v
	}
}

// Result represents a single line from a benchmark file.
// All information about that line is self-contained in the Result.
// A Result is immutable once created.
type Result struct {
	// Labels is the set of persistent labels that apply to the result.
	// Labels must not be modified.
	Labels Labels
	// NameLabels is the set of ephemeral labels that were parsed
	// from the benchmark name/line.
	// NameLabels must not be modified.
	NameLabels Labels
	// LineNum is the line number on which the result was found
	LineNum int
	// Content is the verbatim input line of the benchmark file, beginning with the string "Benchmark".
	Content string
}

// SameLabels reports whether r and b have the same labels.
func (r *Result) SameLabels(b *Result) bool {
	return r.Labels.Equal(b.Labels) && r.NameLabels.Equal(b.NameLabels)
}

// Labels is a set of key-value strings.
type Labels map[string]string

// String returns the labels formatted as a comma-separated
// list enclosed in braces.
func (l Labels) String() string {
	var out bytes.Buffer
	out.WriteString("{")
	for k, v := range l {
		fmt.Fprintf(&out, "%q: %q, ", k, v)
	}
	if out.Len() > 1 {
		// Remove extra ", "
		out.Truncate(out.Len() - 2)
	}
	out.WriteString("}")
	return out.String()
}

// Keys returns a sorted list of the keys in l.
func (l Labels) Keys() []string {
	var out []string
	for k := range l {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

// Equal reports whether l and b have the same keys and values.
func (l Labels) Equal(b Labels) bool {
	if len(l) != len(b) {
		return false
	}
	for k := range l {
		if l[k] != b[k] {
			return false
		}
	}
	return true
}

// A Printer prints a sequence of benchmark results.
type Printer struct {
	w      io.Writer
	labels Labels
}

// NewPrinter constructs a BenchmarkPrinter writing to w.
func NewPrinter(w io.Writer) *Printer {
	return &Printer{w: w}
}

// Print writes the lines necessary to recreate r.
func (p *Printer) Print(r *Result) error {
	var keys []string
	// Print removed keys first.
	for k := range p.labels {
		if r.Labels[k] == "" {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)
	for _, k := range keys {
		if _, err := fmt.Fprintf(p.w, "%s:\n", k); err != nil {
			return err
		}
	}
	// Then print new or changed keys.
	keys = keys[:0]
	for k, v := range r.Labels {
		if v != "" && p.labels[k] != v {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)
	for _, k := range keys {
		if _, err := fmt.Fprintf(p.w, "%s: %s\n", k, r.Labels[k]); err != nil {
			return err
		}
	}
	// Finally print the actual line itself.
	if _, err := fmt.Fprintf(p.w, "%s\n", r.Content); err != nil {
		return err
	}
	p.labels = r.Labels
	return nil
}

// parseNameLabels extracts extra labels from a benchmark name and sets them in labels.
func parseNameLabels(name string, labels Labels) {
	dash := strings.LastIndex(name, "-")
	if dash >= 0 {
		// Accept -N as an alias for /gomaxprocs=N
		_, err := strconv.Atoi(name[dash+1:])
		if err == nil {
			labels["gomaxprocs"] = name[dash+1:]
			name = name[:dash]
		}
	}
	parts := strings.Split(name, "/")
	labels["name"] = parts[0]
	for i, sub := range parts[1:] {
		equals := strings.Index(sub, "=")
		var key string
		if equals >= 0 {
			key, sub = sub[:equals], sub[equals+1:]
		} else {
			key = fmt.Sprintf("sub%d", i+1)
		}
		labels[key] = sub
	}
}

// newResult parses a line and returns a Result object for the line.
func (r *Reader) newResult(labels Labels, lineNum int, name, content string) *Result {
	res := &Result{
		Labels:  labels,
		LineNum: lineNum,
		Content: content,
	}
	if r.lastName != name {
		r.lastName = name
		r.lastNameLabels = make(Labels)
		parseNameLabels(name, r.lastNameLabels)
	}
	res.NameLabels = r.lastNameLabels
	return res
}

// Copy returns a new copy of the labels map, to protect against
// future modifications to labels.
func (l Labels) Copy() Labels {
	new := make(Labels)
	for k, v := range l {
		new[k] = v
	}
	return new
}

// TODO(quentin): How to represent and efficiently group multiple lines?

// Next returns the next benchmark result from the file. If there are
// no further results, it returns nil, io.EOF.
func (r *Reader) Next() bool {
	if r.err != nil {
		return false
	}
	copied := false
	havePerm := r.permLabels != nil
	for r.s.Scan() {
		r.lineNum++
		line := r.s.Text()
		if key, value, ok := parseKeyValueLine(line); ok {
			if _, ok := r.permLabels[key]; ok {
				continue
			}
			if !copied {
				copied = true
				r.labels = r.labels.Copy()
			}
			// TODO(quentin): Spec says empty value is valid, but
			// we need a way to cancel previous labels, so we'll
			// treat an empty value as a removal.
			if value == "" {
				delete(r.labels, key)
			} else {
				r.labels[key] = value
			}
			continue
		}
		// Blank line delimits the header. If we find anything else, the file must not have a header.
		if !havePerm {
			if line == "" {
				r.permLabels = r.labels.Copy()
			} else {
				r.permLabels = Labels{}
			}
		}
		if fullName, ok := parseBenchmarkLine(line); ok {
			r.result = r.newResult(r.labels, r.lineNum, fullName, line)
			return true
		}
	}
	if err := r.s.Err(); err != nil {
		r.err = err
		return false
	}
	r.err = io.EOF
	return false
}

// Result returns the most recent result generated by a call to Next.
func (r *Reader) Result() *Result {
	return r.result
}

// Err returns the error state of the reader.
func (r *Reader) Err() error {
	if r.err == io.EOF {
		return nil
	}
	return r.err
}

// parseKeyValueLine attempts to parse line as a key: value pair. ok
// indicates whether the line could be parsed.
func parseKeyValueLine(line string) (key, val string, ok bool) {
	for i, c := range line {
		if i == 0 && !unicode.IsLower(c) {
			return
		}
		if unicode.IsSpace(c) || unicode.IsUpper(c) {
			return
		}
		if i > 0 && c == ':' {
			key = line[:i]
			val = line[i+1:]
			break
		}
	}
	if key == "" {
		return
	}
	if val == "" {
		ok = true
		return
	}
	for len(val) > 0 && (val[0] == ' ' || val[0] == '\t') {
		val = val[1:]
		ok = true
	}
	return
}

// parseBenchmarkLine attempts to parse line as a benchmark result. If
// successful, fullName is the name of the benchmark with the
// "Benchmark" prefix stripped, and ok is true.
func parseBenchmarkLine(line string) (fullName string, ok bool) {
	space := strings.IndexFunc(line, unicode.IsSpace)
	if space < 0 {
		return
	}
	name := line[:space]
	if !strings.HasPrefix(name, "Benchmark") {
		return
	}
	return name[len("Benchmark"):], true
}
