// 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 (
	"encoding/csv"
	"fmt"
	"io"
	"path/filepath"
	"strings"
	"unicode/utf8"
)

// FormatText appends a fixed-width text formatting of the tables to w.
func FormatText(w io.Writer, tables []*Table) {
	var textTables [][]*textRow
	for _, t := range tables {
		textTables = append(textTables, toText(t))
	}

	var max []int
	for _, table := range textTables {
		for _, row := range table {
			if len(row.cols) == 1 {
				// Header row
				continue
			}
			for len(max) < len(row.cols) {
				max = append(max, 0)
			}
			for i, s := range row.cols {
				n := utf8.RuneCountInString(s)
				if max[i] < n {
					max[i] = n
				}
			}
		}
	}

	for i, table := range textTables {
		if i > 0 {
			fmt.Fprintf(w, "\n")
		}

		// headings
		row := table[0]
		for i, s := range row.cols {
			switch i {
			case 0:
				fmt.Fprintf(w, "%-*s", max[i], s)
			default:
				fmt.Fprintf(w, "  %-*s", max[i], s)
			case len(row.cols) - 1:
				fmt.Fprintf(w, "  %s\n", s)
			}
		}

		// data
		for _, row := range table[1:] {
			for i, s := range row.cols {
				switch {
				case len(row.cols) == 1:
					// Header row
					fmt.Fprint(w, s)
				case i == 0:
					fmt.Fprintf(w, "%-*s", max[i], s)
				default:
					if i == len(row.cols)-1 && len(s) > 0 && s[0] == '(' {
						// Left-align p value.
						fmt.Fprintf(w, "  %s", s)
						break
					}
					fmt.Fprintf(w, "  %*s", max[i], s)
				}
			}
			fmt.Fprintf(w, "\n")
		}
	}
}

func must(e error) {
	if e != nil {
		panic(e)
	}
}

// FormatCSV appends a CSV formatting of the tables to w.
// norange suppresses the range columns.
func FormatCSV(w io.Writer, tables []*Table, norange bool) {
	var textTables [][]*textRow
	for _, t := range tables {
		textTables = append(textTables, toCSV(t, norange))
	}

	for i, table := range textTables {
		if i > 0 {
			fmt.Fprintf(w, "\n")
		}
		csvw := csv.NewWriter(w)

		// headings
		row := table[0]
		must(csvw.Write(row.cols))

		// data
		for _, row := range table[1:] {
			must(csvw.Write(row.cols))
		}
		csvw.Flush()
	}
}

// trimCommonPathPrefix returns a string slice with common
// path-separator-terminated prefixes removed.  Empty strings
// are ignored and a singleton non-empty string is left unchanged
func trimCommonPathPrefix(ss []string) []string {
	commonPrefixLen := -1
	commonPrefix := ""
	trimCommonPrefix := false // true if more than one data row w/ non-empty title

	// begin finding common byte prefix (could end on partial rune)
	for _, s := range ss {
		if s == "" {
			continue
		}
		if commonPrefixLen == -1 {
			commonPrefix = s
			commonPrefixLen = len(s)
			continue
		}
		trimCommonPrefix = true // More than one not-empty
		if commonPrefixLen > len(s) {
			commonPrefixLen = len(s)
			commonPrefix = commonPrefix[0:commonPrefixLen]
		}
		for j := 0; j < commonPrefixLen; j++ {
			if commonPrefix[j] != s[j] {
				commonPrefixLen = j
				commonPrefix = commonPrefix[0:commonPrefixLen]
				break
			}
		}
	}
	if !trimCommonPrefix || commonPrefixLen == 0 {
		return ss
	}
	// trim to include last path separator.
	commonPrefixLen = 1 + strings.LastIndex(commonPrefix, string(filepath.Separator))
	if commonPrefixLen == 0 {
		// No separator found means commonPrefixLen = 1 + (-1) == 0
		return ss
	}

	rs := []string{}
	for _, s := range ss {
		if s == "" {
			rs = append(rs, "")
		} else {
			rs = append(rs, s[commonPrefixLen:])
		}
	}
	return rs
}

// A textRow is a row of printed text columns.
type textRow struct {
	cols []string
}

func newTextRow(cols ...string) *textRow {
	return &textRow{cols: cols}
}

// newTextRowDelta returns a labeled row of text, with "±" inserted after
// each member of "cols" unless norange is true.
func newTextRowDelta(norange bool, label string, cols ...string) *textRow {
	newcols := []string{}
	newcols = append(newcols, label)
	for _, s := range cols {
		newcols = append(newcols, s)
		if !norange {
			newcols = append(newcols, "±")
		}
	}
	return &textRow{cols: newcols}
}

func (r *textRow) add(col string) {
	r.cols = append(r.cols, col)
}

func (r *textRow) trim() {
	for len(r.cols) > 0 && r.cols[len(r.cols)-1] == "" {
		r.cols = r.cols[:len(r.cols)-1]
	}
}

// toText converts the Table to a textual grid of cells,
// which can then be printed in fixed-width output.
func toText(t *Table) []*textRow {
	var textRows []*textRow
	switch len(t.Configs) {
	case 1:
		textRows = append(textRows, newTextRow("name", t.Metric))
	case 2:
		textRows = append(textRows, newTextRow("name", "old "+t.Metric, "new "+t.Metric, "delta"))
	default:
		row := newTextRow("name \\ " + t.Metric)
		row.cols = append(row.cols, t.Configs...) // TODO Should this also trim common path prefix? (see toCSV)
		textRows = append(textRows, row)
	}

	var group string

	for _, row := range t.Rows {
		if row.Group != group {
			group = row.Group
			textRows = append(textRows, newTextRow(group))
		}
		text := newTextRow(row.Benchmark)
		for _, m := range row.Metrics {
			text.cols = append(text.cols, m.Format(row.Scaler))
		}
		if len(t.Configs) == 2 {
			delta := row.Delta
			if delta == "~" {
				delta = "~   "
			}
			text.cols = append(text.cols, delta)
			text.cols = append(text.cols, row.Note)
		}
		textRows = append(textRows, text)
	}
	for _, r := range textRows {
		r.trim()
	}
	return textRows
}

// toCSV converts the Table to a slice of rows containing CSV-separated data
// If norange is true, suppress the range information for each data item.
// If norange is false, insert a "±" in the appropriate columns of the header row.
func toCSV(t *Table, norange bool) []*textRow {
	var textRows []*textRow
	units := ""
	if len(t.Rows) > 0 && len(t.Rows[0].Metrics) > 0 {
		units = " (" + t.Rows[0].Metrics[0].Unit + ")"
	}
	switch len(t.Configs) {
	case 1:
		textRows = append(textRows, newTextRowDelta(norange, "name", t.Metric+units))
	case 2:
		textRows = append(textRows, newTextRowDelta(norange, "name", "old "+t.Metric+units, "new "+t.Metric+units, "delta"))
	default:
		rowname := "name \\ " + t.Metric
		row := newTextRowDelta(norange, rowname+units, trimCommonPathPrefix(t.Configs)...)
		textRows = append(textRows, row)
	}

	var group string

	for _, row := range t.Rows {
		if row.Group != group {
			group = row.Group
			textRows = append(textRows, newTextRow(group))
		}
		text := newTextRow(row.Benchmark)
		for _, m := range row.Metrics {
			mean := fmt.Sprintf("%.5E", m.Mean)
			diff := m.FormatDiff()
			if m.Unit == "" {
				mean = ""
				diff = ""
			}
			text.cols = append(text.cols, mean)
			if !norange {
				text.cols = append(text.cols, diff)
			}
		}
		if len(t.Configs) == 2 {
			delta := row.Delta
			text.cols = append(text.cols, delta)
			text.cols = append(text.cols, row.Note)
		}
		textRows = append(textRows, text)
	}
	for _, r := range textRows {
		r.trim()
	}
	return textRows
}
