// Copyright 2020 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 benchtab

import (
	"encoding/csv"
	"fmt"
	"io"
	"strings"

	"golang.org/x/perf/benchmath"
	"golang.org/x/perf/benchproc"
	"golang.org/x/perf/benchunit"
	"golang.org/x/perf/cmd/benchstat/internal/texttab"
)

// A Table summarizes and compares benchmark results in a 2D grid.
// Each cell summarizes a Sample of results with identical row and
// column Keys. Comparisons are done within each row between the
// Sample in the first column and the Samples in any remaining
// columns.
type Table struct {
	// Opts is the configuration options for this table.
	Opts TableOpts

	// Unit is the benchmark unit of all samples in this Table.
	Unit string

	// Assumption is the distributional assumption used for all
	// samples in this table.
	Assumption benchmath.Assumption

	// Rows and Cols give the sequence of row and column Keys
	// in this table. All row Keys have the same Projection and all
	// col Keys have the same Projection.
	Rows, Cols []benchproc.Key

	// Cells is the cells in the body of this table. Each key in
	// this map is a pair of some Key from Rows and some Key
	// from Cols. However, not all Pairs may be present in the
	// map.
	Cells map[TableKey]*TableCell

	// Summary is the final row of this table, which gives summary
	// information across all benchmarks in this table. It is
	// keyed by Cols.
	Summary map[benchproc.Key]*TableSummary

	// SummaryLabel is the label for the summary row.
	SummaryLabel string
}

// TableKey is a map key used to index a single cell in a Table.
type TableKey struct {
	Row, Col benchproc.Key
}

// TableCell is a single cell in a Table. It represents a sample of
// benchmark results with the same row and column Key.
type TableCell struct {
	// Sample is the set of benchmark results in this cell.
	Sample *benchmath.Sample

	// Summary is the summary of Sample, as computed by the
	// Table's distributional assumption.
	Summary benchmath.Summary

	// Baseline is the baseline cell used for comparisons with
	// this cell, or nil if there is no comparison. This is the
	// cell in the first column of this cell's row, if any.
	Baseline *TableCell

	// Comparison is the comparison with the Baseline cell, as
	// computed by the Table's distributional assumption. If
	// Baseline is nil, this value is meaningless.
	Comparison benchmath.Comparison
}

// TableSummary is a cell that summarizes a column of a Table.
// It appears in the last row of a table.
type TableSummary struct {
	// HasSummary indicates that Summary is valid.
	HasSummary bool
	// Summary summarizes all of the TableCell.Summary values in
	// this column.
	Summary float64

	// HasRatio indicates that Ratio is valid.
	HasRatio bool
	// Ratio summarizes all of the TableCell.Comparison values in
	// this column.
	Ratio float64

	// Warnings is a list of warnings for this summary cell.
	Warnings []error
}

// RowScaler returns a common scaler for the values in row.
func (t *Table) RowScaler(row benchproc.Key, unitClass benchunit.Class) benchunit.Scaler {
	// Collect the row summaries.
	var values []float64
	for _, col := range t.Cols {
		cell, ok := t.Cells[TableKey{row, col}]
		if ok {
			values = append(values, cell.Summary.Center)
		}
	}
	return benchunit.CommonScale(values, unitClass)
}

// ToText renders t to a textual representation, assuming a
// fixed-width font.
func (t *Table) ToText(w io.Writer, color bool) error {
	var o texttab.Table

	// Each logical column expands to centerCols columns, plus
	// deltaCols columns if there's a baseline.
	const labelCols = 1
	const centerCols = 3 // <center ±> <CI> <warnings>
	const deltaCols = 3  // <P%> <(p=0.PPP n=N)> <warnings>

	// startCol returns the index of the first centerCol of
	// logical column exp.
	startCol := func(exp int) int {
		if exp == 0 {
			return labelCols
		}
		// The width of experiment 0 is just centerCols. All
		// later experiments are centerCols+deltaCols.
		return labelCols + centerCols + (exp-1)*(centerCols+deltaCols)
	}

	var warningList []string
	warningSet := make(map[string]int)
	warn := func(msgs ...[]error) {
		var footnotes []string
		for _, msgs1 := range msgs {
			for _, msg := range msgs1 {
				s := msg.Error()
				i, ok := warningSet[s]
				if !ok {
					i = len(warningList)
					warningSet[s] = i
					warningList = append(warningList, s)
				}
				footnotes = append(footnotes, superscript(i+1))
			}
		}
		s := strings.Join(footnotes, " ")
		o.Cell(s)
	}

	// Construct the header.
	kt := benchproc.NewKeyHeader(t.Cols)
	rEdge := startCol(len(t.Cols) + 1)
	nodes := kt.Top
	for len(nodes) > 0 {
		// Process this level.
		var nextNodes []*benchproc.KeyHeaderNode
		o.Row()
		for _, node := range nodes {
			l := startCol(node.Start)
			r := startCol(node.Start + node.Len)
			// Configuration headers can span a lot of
			// columns, so we add a vertical rule to more
			// clearly delineate the columns they span. We
			// also add some space so that each logical
			// column in the rest of the table is better
			// separated.
			o.Col(l).Span(r-l, node.Value, texttab.Center, texttab.LeftMargin(" │ "))
			nextNodes = append(nextNodes, node.Children...)
		}
		// Add a vertical bar down the right side to match the other
		// separators.
		o.Col(rEdge).Cell("", texttab.LeftMargin(" │"))
		nodes = nextNodes
	}

	// Add the column labels row, set margins, and create stretch
	// columns.
	o.Row()
	for i := range t.Cols {
		l := startCol(i)
		o.Col(l)

		// Show the unit over the center column group, since
		// these are values in that unit.
		o.Span(centerCols, t.Unit, texttab.Center, texttab.LeftMargin(" │ "))

		if i > 0 {
			// All but the first column will have A/B
			// comparisons.
			//
			// Separate center and delta column groups by
			// 2 spaces.
			o.Span(deltaCols, "vs base", texttab.Left, texttab.LeftMargin("  "))
		}

		// Make all of the interior columns in this column
		// group shrink columns, leaving on the leftmost and
		// rightmost to stretch.
		for j := l + 1; j < o.CurCol(); j++ {
			o.SetShrink(j, true)
		}
	}
	o.Col(rEdge).Cell("", texttab.LeftMargin(" │"))

	// Emit measurements.
	unitClass := benchunit.ClassOf(t.Unit)
	for _, row := range t.Rows {
		o.Row()

		// TODO: Should I put each row key value in a
		// column? With the keys as headers?
		o.Cell(row.StringValues())

		// Get a common scalar across this row.
		scalar := t.RowScaler(row, unitClass)

		for exp, col := range t.Cols {
			cell, ok := t.Cells[TableKey{row, col}]
			if !ok {
				continue
			}

			o.Col(startCol(exp))
			o.Cell(scalar.Format(cell.Summary.Center), texttab.Right)
			// Put ± in the margin so 1) the ±s line up,
			// 2) the geomean value (which doesn't have ±)
			// aligns with the summary column, 3) we can
			// right align the range column.
			o.Cell(cell.Summary.PctRangeString(), texttab.Right, texttab.LeftMargin(" ± "))
			warn(cell.Sample.Warnings, cell.Summary.Warnings)
			if exp > 0 && cell.Baseline != nil {
				d := cell.Comparison.FormatDelta(cell.Baseline.Summary.Center, cell.Summary.Center)
				// TODO: Color the delta for whether
				// it's good or bad.
				o.Cell(d, texttab.Right)
				o.Cell("(" + cell.Comparison.String() + ")")
				warn(cell.Comparison.Warnings)
			}
		}
	}

	// Emit summary row.
	if len(t.Rows) > 1 {
		o.Row()
		o.Cell(t.SummaryLabel)
		for exp, col := range t.Cols {
			tsum, ok := t.Summary[col]
			if !ok {
				continue
			}

			if tsum.HasSummary {
				o.Col(startCol(exp))
				o.Cell(benchunit.Scale(tsum.Summary, unitClass), texttab.Right)
			}
			if exp > 0 {
				o.Col(startCol(exp) + centerCols)
				if tsum.HasRatio {
					o.Cell(fmt.Sprintf("%+.2f%%", (tsum.Ratio-1)*100), texttab.Right)
				} else {
					o.Cell("?")
				}
			}

			o.Col(startCol(exp+1) - 1)
			warn(tsum.Warnings)
		}
	}

	// Emit table.
	if err := o.Format(w); err != nil {
		return err
	}

	// Emit warnings.
	if len(warningList) > 0 {
		for i, msg := range warningList {
			if _, err := fmt.Fprintf(w, "%s %s\n", superscript(i+1), msg); err != nil {
				return err
			}
		}
	}

	return nil
}

var superDigits = []rune("⁰¹²³⁴⁵⁶⁷⁸⁹")

func superscript(i int) string {
	if i == 0 {
		return string(superDigits[0])
	}

	var buf [20]rune
	pos := len(buf)
	for i > 0 && pos > 0 {
		pos--
		buf[pos] = superDigits[i%10]
		i /= 10
	}
	return string(buf[pos:])
}

// ToCSV renders t to CSV format. Warnings are written in text format
// to the "warnings" Writer, and prefixed with spreadsheet-style cell
// references. These references assume the table begins on row
// "startRow".
func (t *Table) ToCSV(o *csv.Writer, startRow int, warnings io.Writer) (rowCount int) {
	const labelCols = 1
	const centerCols = 2 // <center> <CI>
	const deltaCols = 2  // <P%> <(p=0.PPP n=N)>
	startCol := func(exp int) int {
		if exp == 0 {
			// Baseline, so no delta.
			return labelCols
		}
		// Center and delta columns.
		l := labelCols + centerCols + (exp-1)*(centerCols+deltaCols)
		return l
	}
	row := make([]string, startCol(len(t.Cols)))
	row = row[:0]
	clearTo := func(col int) {
		for len(row) < col {
			row = append(row, "")
		}
	}
	emit := func() {
		o.Write(row)
		row = row[:0]
		rowCount++
	}
	warn := func(msgs []error) {
		// Construct a spreadsheet-style cell label.
		colName := make([]byte, 10)
		colNamePos := len(colName)
		for x := len(row); x > 0; {
			colNamePos--
			colName[colNamePos] = 'A' + byte(x%26)
			x /= 26
		}
		if colNamePos == len(colName) {
			colNamePos--
			colName[colNamePos] = 'A'
		}
		colName = colName[colNamePos:]
		// Print warnings.
		for _, msg := range msgs {
			fmt.Fprintf(warnings, "%s%d: %s\n", colName, startRow+rowCount, msg)
		}
	}

	// Emit column configurations header.
	colFields := t.Cols[0].Projection().FlattenedFields()
	for _, field := range colFields {
		for exp, key := range t.Cols {
			clearTo(startCol(exp))
			row = append(row, key.Get(field))
		}
		emit()
	}

	// Emit column headers.
	for exp := range t.Cols {
		clearTo(startCol(exp))
		row = append(row, t.Unit, "CI")
		if exp > 0 {
			row = append(row, "vs base", "P")
		}
	}
	emit()

	// Emit table.
	for _, rowKey := range t.Rows {
		row = append(row, rowKey.StringValues())
		for exp, colKey := range t.Cols {
			cell, ok := t.Cells[TableKey{rowKey, colKey}]
			if !ok {
				continue
			}

			clearTo(startCol(exp))
			warn(cell.Sample.Warnings)
			warn(cell.Summary.Warnings)
			row = append(row,
				fmt.Sprint(cell.Summary.Center),
				cell.Summary.PctRangeString(),
			)
			if exp > 0 && cell.Baseline != nil {
				warn(cell.Comparison.Warnings)
				row = append(row,
					cell.Comparison.FormatDelta(cell.Baseline.Summary.Center, cell.Summary.Center),
					cell.Comparison.String(),
				)
			}
		}
		emit()
	}

	// Emit summary row.
	row = append(row, t.SummaryLabel)
	for exp, key := range t.Cols {
		tsum, ok := t.Summary[key]
		if !ok {
			continue
		}

		clearTo(startCol(exp))
		warn(tsum.Warnings)
		if tsum.HasSummary {
			row = append(row, fmt.Sprint(tsum.Summary))
		}
		if exp > 0 {
			clearTo(startCol(exp) + centerCols)
			if tsum.HasRatio {
				row = append(row, fmt.Sprintf("%+.2f%%", (tsum.Ratio-1)*100))
			} else {
				row = append(row, "?")
			}
		}
	}
	emit()

	return
}
