package tk

import "src.elv.sh/pkg/wcwidth"

// The number of lines the listing mode keeps between the current selected item
// and the top and bottom edges of the window, unless the available height is
// too small or if the selected item is near the top or bottom of the list.
var respectDistance = 2

// Determines the index of the first item to show in vertical mode.
//
// This function does not return the full window, but just the first item to
// show, and how many initial lines to crop. The window determined by this
// algorithm has the following properties:
//
//   - It always includes the selected item.
//
//   - The combined height of all the entries in the window is equal to
//     min(height, combined height of all entries).
//
//   - There are at least respectDistance rows above the first row of the selected
//     item, as well as that many rows below the last row of the selected item,
//     unless the height is too small.
//
//   - Among all values satisfying the above conditions, the value of first is
//     the one closest to lastFirst.
func getVerticalWindow(state ListBoxState, height int) (first, crop int) {
	items, selected, lastFirst := state.Items, state.Selected, state.First
	n := items.Len()
	if selected < 0 {
		selected = 0
	} else if selected >= n {
		selected = n - 1
	}
	selectedHeight := items.Show(selected).CountLines()

	if height <= selectedHeight {
		// The height is not big enough (or just big enough) to fit the selected
		// item. Fit as much as the selected item as we can.
		return selected, 0
	}

	// Determine the minimum amount of space required for the downward direction.
	budget := height - selectedHeight
	var needDown int
	if budget >= 2*respectDistance {
		// If we can afford maintaining the respect distance on both sides, then
		// the minimum amount of space required is the respect distance.
		needDown = respectDistance
	} else {
		// Otherwise we split the available space by half. The downward (no pun
		// intended) rounding here is an arbitrary choice.
		needDown = budget / 2
	}
	// Calculate how much of the budget the downward direction can use. This is
	// used to 1) potentially shrink needDown 2) decide how much to expand
	// upward later.
	useDown := 0
	for i := selected + 1; i < n; i++ {
		useDown += items.Show(i).CountLines()
		if useDown >= budget {
			break
		}
	}
	if needDown > useDown {
		// We reached the last item without using all of needDown. That means we
		// don't need so much in the downward direction.
		needDown = useDown
	}

	// The maximum amount of space we can use in the upward direction is the
	// entire budget minus the minimum amount of space we need in the downward
	// direction.
	budgetUp := budget - needDown

	useUp := 0
	// Extend upwards until any of the following becomes true:
	//
	// * We have exhausted budgetUp;
	//
	// * We have reached item 0;
	//
	// * We have reached or passed lastFirst, satisfied the upward respect
	//   distance, and will be able to use up the entire budget when expanding
	//   downwards later.
	for i := selected - 1; i >= 0; i-- {
		useUp += items.Show(i).CountLines()
		if useUp >= budgetUp {
			return i, useUp - budgetUp
		}
		if i <= lastFirst && useUp >= respectDistance && useUp+useDown >= budget {
			return i, 0
		}
	}
	return 0, 0
}

// Determines the window to show in horizontal. Returns the first item to show
// and the amount of height required.
func getHorizontalWindow(state ListBoxState, padding, width, height int) (int, int) {
	items := state.Items
	n := items.Len()
	// Lower bound of number of items that can fit in a row.
	perRow := (width + listBoxColGap) / (maxWidth(items, padding, 0, n) + listBoxColGap)
	if perRow == 0 {
		// We trim items that are too wide, so there is at least one item per row.
		perRow = 1
	}
	if height*perRow >= n {
		// All items can fit.
		return 0, (n + perRow - 1) / perRow
	}
	// Reduce the amount of available height by one because the last row will be
	// reserved for the scrollbar.
	height--
	selected, lastFirst := state.Selected, state.First
	// Start with the column containing the selected item, move left until
	// either the width is exhausted, or lastFirst has been reached.
	first := selected / height * height
	usedWidth := maxWidth(items, padding, first, first+height)
	for ; first > lastFirst; first -= height {
		usedWidth += maxWidth(items, padding, first-height, first) + listBoxColGap
		if usedWidth > width {
			break
		}
	}
	return first, height
}

func maxWidth(items Items, padding, low, high int) int {
	n := items.Len()
	width := 0
	for i := low; i < high && i < n; i++ {
		w := 0
		for _, seg := range items.Show(i) {
			w += wcwidth.Of(seg.Text)
		}
		if width < w {
			width = w
		}
	}
	return width + 2*padding
}
