package ui

import (
	"fmt"
	"strings"

	"github.com/muesli/reflow/ansi"
)

// helpEntry is a entry in a help menu containing values for a keystroke and
// it's associated action.
type helpEntry struct{ key, val string }

// helpColumn is a group of helpEntries which will be rendered into a column.
type helpColumn []helpEntry

// newHelpColumn creates a help column from pairs of string arguments
// representing keys and values. If the arguments are not even (and therein
// not every key has a matching value) the function will panic.
func newHelpColumn(pairs ...string) (h helpColumn) {
	if len(pairs)%2 != 0 {
		panic("help text group must have an even number of items")
	}

	for i := 0; i < len(pairs); i = i + 2 {
		h = append(h, helpEntry{key: pairs[i], val: pairs[i+1]})
	}

	return
}

// render returns styled and formatted rows from keys and values.
func (h helpColumn) render(height int) (rows []string) {
	keyWidth, valWidth := h.maxWidths()

	for i := 0; i < height; i++ {
		var (
			b    = strings.Builder{}
			k, v string
		)
		if i < len(h) {
			k = h[i].key
			v = h[i].val

			switch k {
			case "s":
				k = greenFg(k)
				v = semiDimGreenFg(v)
			default:
				k = grayFg(k)
				v = midGrayFg(v)
			}
		}
		b.WriteString(k)
		b.WriteString(strings.Repeat(" ", keyWidth-ansi.PrintableRuneWidth(k))) // pad keys
		b.WriteString("  ")                                                     // gap
		b.WriteString(v)
		b.WriteString(strings.Repeat(" ", valWidth-ansi.PrintableRuneWidth(v))) // pad vals
		rows = append(rows, b.String())
	}

	return
}

// maxWidths returns the widest key and values in the column, respectively.
func (h helpColumn) maxWidths() (maxKey int, maxVal int) {
	for _, v := range h {
		kw := ansi.PrintableRuneWidth(v.key)
		vw := ansi.PrintableRuneWidth(v.val)
		if kw > maxKey {
			maxKey = kw
		}
		if vw > maxVal {
			maxVal = vw
		}
	}

	return
}

// helpView returns either the mini or full help view depending on the state of
// the model, as well as the total height of the help view.
func (m stashModel) helpView() (string, int) {
	numDocs := len(m.getVisibleMarkdowns())

	// Help for when we're filtering
	if m.filterState == filtering {
		var h []string

		switch numDocs {
		case 0:
			h = []string{"enter/esc", "cancel"}
		case 1:
			h = []string{"enter", "open", "esc", "cancel"}
		default:
			h = []string{"enter", "confirm", "esc", "cancel", "ctrl+j/ctrl+k ↑/↓", "choose"}
		}

		return m.renderHelp(h)
	}

	var (
		navHelp       []string
		filterHelp    []string
		selectionHelp []string
		editHelp      []string
		sectionHelp   []string
		appHelp       []string
	)

	if numDocs > 0 && m.showFullHelp {
		navHelp = []string{"enter", "open", "j/k ↑/↓", "choose"}
	}

	if len(m.sections) > 1 {
		if m.showFullHelp {
			navHelp = append(navHelp, "tab/shift+tab", "section")
		} else {
			navHelp = append(navHelp, "tab", "section")
		}
	}

	if m.paginator().TotalPages > 1 {
		navHelp = append(navHelp, "h/l ←/→", "page")
	}

	// If we're browsing a filtered set
	if m.filterApplied() {
		filterHelp = []string{"/", "edit search", "esc", "clear filter"}
	} else {
		filterHelp = []string{"/", "find"}
	}

	// If there are errors
	if m.err != nil {
		appHelp = append(appHelp, "!", "errors")
	}

	appHelp = append(appHelp, "r", "refresh")

	if numDocs > 0 {
		appHelp = append(appHelp, "e", "edit")
	}

	appHelp = append(appHelp, "q", "quit")

	// Detailed help
	if m.showFullHelp {
		if m.filterState != filtering {
			appHelp = append(appHelp, "?", "close help")
		}
		return m.renderHelp(navHelp, filterHelp, append(selectionHelp, editHelp...), sectionHelp, appHelp)
	}

	// Mini help
	if m.filterState != filtering {
		appHelp = append(appHelp, "?", "more")
	}
	return m.renderHelp(navHelp, filterHelp, selectionHelp, editHelp, sectionHelp, appHelp)
}

const minHelpViewHeight = 5

// renderHelp returns the rendered help view and associated line height for
// the given groups of help items.
func (m stashModel) renderHelp(groups ...[]string) (string, int) {
	if m.showFullHelp {
		str := m.fullHelpView(groups...)
		numLines := strings.Count(str, "\n") + 1
		return str, max(numLines, minHelpViewHeight)
	}
	return m.miniHelpView(concatStringSlices(groups...)...), 1
}

// Builds the help view from various sections pieces, truncating it if the view
// would otherwise wrap to two lines. Help view entries should come in as pairs,
// with the first being the key and the second being the help text.
func (m stashModel) miniHelpView(entries ...string) string {
	if len(entries) == 0 {
		return ""
	}

	var (
		truncationChar  = subtleStyle.Render("…")
		truncationWidth = ansi.PrintableRuneWidth(truncationChar)
	)

	var (
		next       string
		leftGutter = "  "
		maxWidth   = m.common.width -
			stashViewHorizontalPadding -
			truncationWidth -
			ansi.PrintableRuneWidth(leftGutter)
		s = leftGutter
	)

	for i := 0; i < len(entries); i = i + 2 {
		k := entries[i]
		v := entries[i+1]

		k = grayFg(k)
		v = midGrayFg(v)

		next = fmt.Sprintf("%s %s", k, v)

		if i < len(entries)-2 {
			next += dividerDot.String()
		}

		// Only this (and the following) help text items if we have the
		// horizontal space
		if ansi.PrintableRuneWidth(s)+ansi.PrintableRuneWidth(next) >= maxWidth {
			s += truncationChar
			break
		}

		s += next
	}
	return s
}

func (m stashModel) fullHelpView(groups ...[]string) string {
	var tallestCol int
	columns := make([]helpColumn, 0, len(groups))
	renderedCols := make([][]string, 0, len(groups)) // final rows grouped by column

	// Get key/value pairs
	for _, g := range groups {
		if len(g) == 0 {
			continue // ignore empty columns
		}

		columns = append(columns, newHelpColumn(g...))
	}

	// Find the tallest column
	for _, c := range columns {
		if len(c) > tallestCol {
			tallestCol = len(c)
		}
	}

	// Build columns
	for _, c := range columns {
		renderedCols = append(renderedCols, c.render(tallestCol))
	}

	// Merge columns
	return mergeColumns(renderedCols...)
}

// Merge columns together to build the help view.
func mergeColumns(cols ...[]string) string {
	const minimumHeight = 3

	// Find the tallest column
	var tallestCol int
	for _, v := range cols {
		n := len(v)
		if n > tallestCol {
			tallestCol = n
		}
	}

	// Make sure the tallest column meets the minimum height
	if tallestCol < minimumHeight {
		tallestCol = minimumHeight
	}

	b := strings.Builder{}
	for i := 0; i < tallestCol; i++ {
		for j, col := range cols {
			if i >= len(col) {
				continue // skip if we're past the length of this column
			}
			if j == 0 {
				b.WriteString("  ") // gutter
			} else if j > 0 {
				b.WriteString("    ") // gap
			}
			b.WriteString(col[i])
		}
		if i < tallestCol-1 {
			b.WriteRune('\n')
		}
	}

	return b.String()
}

func concatStringSlices(s ...[]string) (agg []string) {
	for _, v := range s {
		agg = append(agg, v...)
	}
	return
}
