package main

import (
	"bufio"
	"bytes"
	"errors"
	"flag"
	"fmt"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"golang.org/x/tools/benchmark/parse"
)

var noPassthrough = flag.Bool("no-passthrough", false, "Don't print non-benchmark lines")

type BenchOutputGroup struct {
	Lines []*parse.Benchmark
	// Columns which are in use
	Measured int
}

type Table struct {
	MaxLengths []int
	Cells      [][]string
}

func (g *BenchOutputGroup) String() string {
	if len(g.Lines) == 0 {
		return ""
	}
	columnNames := []string{"benchmark", "iter", "time/iter"}
	if (g.Measured & parse.MBPerS) > 0 {
		columnNames = append(columnNames, "throughput")
	}
	if (g.Measured & parse.AllocedBytesPerOp) > 0 {
		columnNames = append(columnNames, "bytes alloc")
	}
	if (g.Measured & parse.AllocsPerOp) > 0 {
		columnNames = append(columnNames, "allocs")
	}
	table := &Table{Cells: [][]string{columnNames}}

	var underlines []string
	for _, name := range columnNames {
		underlines = append(underlines, strings.Repeat("-", len(name)))
	}
	table.Cells = append(table.Cells, underlines)
	timeFormatFunc := g.TimeFormatFunc()

	for _, line := range g.Lines {
		row := []string{line.Name, FormatIterations(line.N), timeFormatFunc(line.NsPerOp)}
		if (g.Measured & parse.MBPerS) > 0 {
			row = append(row, FormatMegaBytesPerSecond(line))
		}
		if (g.Measured & parse.AllocedBytesPerOp) > 0 {
			row = append(row, FormatBytesAllocPerOp(line))
		}
		if (g.Measured & parse.AllocsPerOp) > 0 {
			row = append(row, FormatAllocsPerOp(line))
		}
		table.Cells = append(table.Cells, row)
	}
	for i := range columnNames {
		maxLength := 0
		for _, row := range table.Cells {
			if len(row[i]) > maxLength {
				maxLength = len(row[i])
			}
		}
		table.MaxLengths = append(table.MaxLengths, maxLength)
	}
	var buf bytes.Buffer
	for _, row := range table.Cells {
		for i, cell := range row {
			var format string
			switch i {
			case 0:
				format = "%%-%ds   "
			case len(row) - 1:
				format = "%%%ds"
			default:
				format = "%%%ds   "
			}
			fmt.Fprintf(&buf, fmt.Sprintf(format, table.MaxLengths[i]), cell)
		}
		fmt.Fprint(&buf, "\n")
	}
	return buf.String()
}

func FormatIterations(iter int) string {
	return strconv.FormatInt(int64(iter), 10)
}

func (g *BenchOutputGroup) TimeFormatFunc() func(float64) string {
	// Find the smallest time
	smallest := g.Lines[0].NsPerOp
	for _, line := range g.Lines[1:] {
		if line.NsPerOp < smallest {
			smallest = line.NsPerOp
		}
	}
	switch {
	case smallest < float64(10000*time.Nanosecond):
		return func(ns float64) string {
			return fmt.Sprintf("%.2f ns/op", ns)
		}
	case smallest < float64(time.Millisecond):
		return func(ns float64) string {
			return fmt.Sprintf("%.2f μs/op", ns/1000)
		}
	case smallest < float64(10*time.Second):
		return func(ns float64) string {
			return fmt.Sprintf("%.2f ms/op", (ns / 1e6))
		}
	default:
		return func(ns float64) string {
			return fmt.Sprintf("%.2f s/op", ns/1e9)
		}
	}
}

func FormatMegaBytesPerSecond(l *parse.Benchmark) string {
	if (l.Measured & parse.MBPerS) == 0 {
		return ""
	}
	return fmt.Sprintf("%.2f MB/s", l.MBPerS)
}

func FormatBytesAllocPerOp(l *parse.Benchmark) string {
	if (l.Measured & parse.AllocedBytesPerOp) == 0 {
		return ""
	}
	return fmt.Sprintf("%d B/op", l.AllocedBytesPerOp)
}

func FormatAllocsPerOp(l *parse.Benchmark) string {
	if (l.Measured & parse.AllocsPerOp) == 0 {
		return ""
	}
	return fmt.Sprintf("%d allocs/op", l.AllocsPerOp)
}

func (g *BenchOutputGroup) AddLine(line *parse.Benchmark) {
	g.Lines = append(g.Lines, line)
	g.Measured |= line.Measured
}

var (
	benchLineMatcher = regexp.MustCompile(`^Benchmark.*\t.*\d+`)
	okLineMatcher    = regexp.MustCompile(`^ok\s`)
	errNotBenchLine  = errors.New("not a bench line")
)

func ParseLine(line string) (*parse.Benchmark, error) {
	if !benchLineMatcher.MatchString(line) {
		return nil, errNotBenchLine
	}
	fields := strings.Split(line, "\t")
	if len(fields) < 3 {
		return nil, errNotBenchLine
	}

	return parse.ParseLine(line)
}

func main() {
	flag.Parse()
	currentBenchmark := &BenchOutputGroup{}
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		text := scanner.Text()
		line, err := ParseLine(text)
		switch err {
		case errNotBenchLine:
			if okLineMatcher.MatchString(text) {
				fmt.Print(currentBenchmark)
				currentBenchmark = &BenchOutputGroup{}
			}
			if !*noPassthrough {
				fmt.Println(text)
			}
		case nil:
			currentBenchmark.AddLine(line)
		default:
			fmt.Fprintln(os.Stderr, "prettybench unrecognized line:")
			fmt.Println(text)
		}
	}
	if err := scanner.Err(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}
