package main

import (
	"fmt"
	"strconv"
	"strings"
	"time"
)

const (
	cReset  = "\x1b[0m"
	cBold   = "\x1b[1m"
	cDim    = "\x1b[2m"
	cRed    = "\x1b[31m"
	cGreen  = "\x1b[32m"
	cYellow = "\x1b[33m"
	cCyan   = "\x1b[36m"
	cGrey   = "\x1b[90m"
)

func paint(s, code string) string {
	if s == "" {
		return s
	}
	return code + s + cReset
}

func stripANSI(s string) string {
	var b strings.Builder
	for i := 0; i < len(s); {
		if s[i] == 0x1b {
			for i < len(s) && s[i] != 'm' {
				i++
			}
			if i < len(s) {
				i++
			}
			continue
		}
		b.WriteByte(s[i])
		i++
	}
	return b.String()
}

func visWidth(s string) int {
	return len([]rune(stripANSI(s)))
}

func pad(s string, w int, right bool) string {
	gap := w - visWidth(s)
	if gap < 0 {
		gap = 0
	}
	if right {
		return strings.Repeat(" ", gap) + s
	}
	return s + strings.Repeat(" ", gap)
}

func commas(v uint64) string {
	s := fmt.Sprintf("%d", v)
	if len(s) <= 3 {
		return s
	}
	var parts []string
	for len(s) > 3 {
		parts = append([]string{s[len(s)-3:]}, parts...)
		s = s[:len(s)-3]
	}
	return strings.Join(append([]string{s}, parts...), ",")
}

// The magnitude letter goes with the figure so the unit can stay a fixed word
// on the label. Below a thousand no letter is left dangling, since a trailing
// space would push the figure off centre.
func scaleSI(v float64) string {
	// %.2f rounds 999.995 and up to a fourth digit, so the magnitude rolls over
	// where the rounding does rather than at the bare thousand.
	for _, mag := range []string{"", "k", "M", "G", "T"} {
		if v < 999.995 {
			if mag == "" {
				return fmt.Sprintf("%.2f", v)
			}
			return fmt.Sprintf("%.2f %s", v, mag)
		}
		v /= 1000
	}
	return fmt.Sprintf("%.2f P", v)
}

// The integer counterpart, for whole things counted rather than a rate
// measured. Below a thousand the figure is the count itself, since two decimals
// on a quantity that cannot have them read as precision that is not there.
func scaleCount(v uint64) string {
	if v < 1000 {
		return strconv.FormatUint(v, 10)
	}
	return scaleSI(float64(v))
}

// The same shape for time, whose magnitudes are sixties and twenty-fours.
func scaleTime(d time.Duration) string {
	switch {
	case d < time.Minute:
		return fmt.Sprintf("%.2f s", d.Seconds())
	case d < time.Hour:
		return fmt.Sprintf("%.2f m", d.Minutes())
	case d < 24*time.Hour:
		return fmt.Sprintf("%.2f h", d.Hours())
	}
	return fmt.Sprintf("%.2f d", d.Hours()/24)
}

type colSpec struct {
	group string
	title string
	width int
	right bool
}

type streamTable struct {
	cols        []colSpec
	sinceHeader int
	headerEvery int
}

// A group change is drawn as a vertical break, so the two halves of the row
// read apart without a second header line naming them.
func (t *streamTable) join(cells []string, brk string) string {
	var b strings.Builder
	for i, c := range cells {
		if i > 0 {
			if t.cols[i].group != t.cols[i-1].group {
				b.WriteString(brk)
			} else {
				b.WriteByte(' ')
			}
		}
		b.WriteString(c)
	}
	return b.String()
}

func (t *streamTable) headerLines() []string {
	titles := make([]string, len(t.cols))
	rules := make([]string, len(t.cols))
	for i, c := range t.cols {
		titles[i] = paint(pad(c.title, c.width, c.right), cBold)
		rules[i] = strings.Repeat("─", c.width)
	}
	return []string{
		t.join(titles, paint(" │ ", cGrey)),
		paint(t.join(rules, "─┼─"), cGrey),
	}
}

func (t *streamTable) width() int {
	w := 0
	for i, c := range t.cols {
		w += c.width
		if i > 0 {
			w++
			if t.cols[i].group != t.cols[i-1].group {
				w += 2
			}
		}
	}
	return w
}

// A labelled horizontal rule spanning the table, for marking a break in the
// stream of rows.
func (t *streamTable) rule(label string) string {
	const lead = 3
	text := " " + label + " "
	trail := t.width() - lead - visWidth(text)
	if trail < 0 {
		trail = 0
	}
	return paint(strings.Repeat("─", lead), cGrey) +
		paint(text, cDim) +
		paint(strings.Repeat("─", trail), cGrey)
}

func (t *streamTable) emit(cells []string) []string {
	var out []string
	if t.sinceHeader == 0 || (t.headerEvery > 0 && t.sinceHeader >= t.headerEvery) {
		out = append(out, t.headerLines()...)
		t.sinceHeader = 0
	}
	var padded []string
	for i, c := range t.cols {
		v := ""
		if i < len(cells) {
			v = cells[i]
		}
		padded = append(padded, pad(v, c.width, c.right))
	}
	t.sinceHeader++
	return append(out, t.join(padded, paint(" │ ", cGrey)))
}

func renderBox(title string, headers []string, rights []bool, rows [][]string) string {
	n := len(headers)
	widths := make([]int, n)
	for i, h := range headers {
		widths[i] = visWidth(h)
	}
	for _, r := range rows {
		for i := 0; i < n && i < len(r); i++ {
			if w := visWidth(r[i]); w > widths[i] {
				widths[i] = w
			}
		}
	}

	line := func(l, m, r string) string {
		var parts []string
		for _, w := range widths {
			parts = append(parts, strings.Repeat("─", w+2))
		}
		return paint(l+strings.Join(parts, m)+r, cGrey)
	}
	rowText := func(cells []string, bold bool) string {
		var parts []string
		for i := 0; i < n; i++ {
			v := ""
			if i < len(cells) {
				v = cells[i]
			}
			if bold {
				v = paint(v, cBold)
			}
			parts = append(parts, " "+pad(v, widths[i], rights[i])+" ")
		}
		bar := paint("│", cGrey)
		return bar + strings.Join(parts, bar) + bar
	}

	var b strings.Builder
	writeln := func(s string) {
		b.WriteString(s)
		b.WriteByte('\n')
	}
	if title != "" {
		writeln(paint(title, cBold+cCyan))
	}
	writeln(line("┌", "┬", "┐"))
	writeln(rowText(headers, true))
	writeln(line("├", "┼", "┤"))
	for _, r := range rows {
		writeln(rowText(r, false))
	}
	b.WriteString(line("└", "┴", "┘"))
	return b.String()
}

// Whether rather than how many, matching the panel's top chips: over a window
// this short a count changes faster than it can be read.
func flagCell(v uint64) string {
	if v == 0 {
		return paint("ok", cGreen)
	}
	return paint("ERR", cRed)
}

// Exact rather than scaled: scaled, one lost frame and a thousand both read as
// 1.00, separated only by a letter.
func statusCell(v uint64) string {
	s := commas(v)
	if v == 0 {
		return paint(s, cGreen)
	}
	return paint(s, cRed)
}

// Per-interval rates jitter by a couple of percent at line rate, so green has
// to cover that. Yellow means a real shortfall, red means badly off.
const (
	rateGreenFrac  = 0.95
	rateYellowFrac = 0.80
)

func rateCell(bits float64, target float64) string {
	s := scaleSI(bits)
	switch {
	case bits >= target*rateGreenFrac:
		return paint(s, cGreen)
	case bits >= target*rateYellowFrac:
		return paint(s, cYellow)
	default:
		return paint(s, cRed)
	}
}
