package huh

import (
	"cmp"
	"fmt"
	"io"
	"slices"
	"strings"
	"time"

	"charm.land/bubbles/v2/key"
	"charm.land/bubbles/v2/spinner"
	"charm.land/bubbles/v2/textinput"
	"charm.land/bubbles/v2/viewport"
	tea "charm.land/bubbletea/v2"
	"charm.land/huh/v2/internal/accessibility"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/exp/ordered"
)

// MultiSelect is a form multi-select field.
type MultiSelect[T comparable] struct {
	accessor Accessor[[]T]
	key      string
	id       int

	// customization
	title           Eval[string]
	description     Eval[string]
	options         Eval[[]Option[T]]
	filterable      bool
	filteredOptions []Option[T]
	limit           int

	// error handling
	validate func([]T) error
	err      error

	// state
	cursor    int
	focused   bool
	filtering bool
	filter    textinput.Model
	viewport  viewport.Model
	spinner   spinner.Model

	// options
	width     int
	height    int
	theme     Theme
	hasDarkBg bool
	keymap    MultiSelectKeyMap
}

// NewMultiSelect returns a new multi-select field.
func NewMultiSelect[T comparable]() *MultiSelect[T] {
	filter := textinput.New()
	filter.Prompt = "/"

	s := spinner.New(spinner.WithSpinner(spinner.Line))

	return &MultiSelect[T]{
		accessor:    &EmbeddedAccessor[[]T]{},
		validate:    func([]T) error { return nil },
		filtering:   false,
		filter:      filter,
		id:          nextID(),
		options:     Eval[[]Option[T]]{cache: make(map[uint64][]Option[T])},
		title:       Eval[string]{cache: make(map[uint64]string)},
		description: Eval[string]{cache: make(map[uint64]string)},
		spinner:     s,
		filterable:  true,
	}
}

// Value sets the value of the multi-select field.
func (m *MultiSelect[T]) Value(value *[]T) *MultiSelect[T] {
	return m.Accessor(NewPointerAccessor(value))
}

// Accessor sets the accessor of the input field.
func (m *MultiSelect[T]) Accessor(accessor Accessor[[]T]) *MultiSelect[T] {
	m.accessor = accessor
	for i, o := range m.options.val {
		if slices.Contains(m.accessor.Get(), o.Value) {
			m.options.val[i].selected = true
		}
	}
	return m
}

// Key sets the key of the select field which can be used to retrieve the value
// after submission.
func (m *MultiSelect[T]) Key(key string) *MultiSelect[T] {
	m.key = key
	return m
}

// Title sets the title of the multi-select field.
func (m *MultiSelect[T]) Title(title string) *MultiSelect[T] {
	m.title.val = title
	m.title.fn = nil
	return m
}

// TitleFunc sets the title func of the multi-select field.
func (m *MultiSelect[T]) TitleFunc(f func() string, bindings any) *MultiSelect[T] {
	m.title.fn = f
	m.title.bindings = bindings
	return m
}

// Description sets the description of the multi-select field.
func (m *MultiSelect[T]) Description(description string) *MultiSelect[T] {
	m.description.val = description
	return m
}

// DescriptionFunc sets the description func of the multi-select field.
func (m *MultiSelect[T]) DescriptionFunc(f func() string, bindings any) *MultiSelect[T] {
	m.description.fn = f
	m.description.bindings = bindings
	return m
}

// Options sets the options of the multi-select field.
func (m *MultiSelect[T]) Options(options ...Option[T]) *MultiSelect[T] {
	if len(options) <= 0 {
		return m
	}

	m.options.val = options
	m.filteredOptions = options
	m.selectOptions()
	m.updateViewportSize()
	return m
}

func (m *MultiSelect[T]) selectOptions() {
	// Set the cursor to the existing value or the last selected option.
	for i, o := range m.options.val {
		for _, v := range m.accessor.Get() {
			if o.Value == v {
				m.options.val[i].selected = true
			}
		}
	}

	for i, o := range m.options.val {
		if !o.selected {
			continue
		}
		m.cursor = i
		m.ensureCursorVisible()
		break
	}
}

// OptionsFunc sets the options func of the multi-select field.
func (m *MultiSelect[T]) OptionsFunc(f func() []Option[T], bindings any) *MultiSelect[T] {
	m.options.fn = f
	m.options.bindings = bindings
	m.filteredOptions = make([]Option[T], 0)
	// If there is no height set, we should attach a static height since these
	// options are possibly dynamic.
	if m.height <= 0 {
		m.height = defaultHeight
		m.updateViewportSize()
	}
	if m.width <= 0 {
		m.Width(20)
	}
	return m
}

// Filterable sets the multi-select field as filterable.
func (m *MultiSelect[T]) Filterable(filterable bool) *MultiSelect[T] {
	m.filterable = filterable
	return m
}

// Filtering sets the filtering state of the multi-select field.
func (m *MultiSelect[T]) Filtering(filtering bool) *MultiSelect[T] {
	m.filtering = filtering
	m.filter.Focus()
	return m
}

// Limit sets the limit of the multi-select field.
func (m *MultiSelect[T]) Limit(limit int) *MultiSelect[T] {
	m.limit = limit
	m.setSelectAllHelp()
	return m
}

// Width sets the width of the multi-select field.
func (m *MultiSelect[T]) Width(width int) *MultiSelect[T] {
	// What we really want to do is set the width of the viewport, but we
	// need a theme applied before we can calcualate its width.
	m.width = width
	m.updateViewportSize()
	return m
}

// Height sets the height of the multi-select field.
func (m *MultiSelect[T]) Height(height int) *MultiSelect[T] {
	// What we really want to do is set the height of the viewport, but we
	// need a theme applied before we can calcualate its height.
	m.height = height
	m.updateViewportSize()
	return m
}

// Validate sets the validation function of the multi-select field.
func (m *MultiSelect[T]) Validate(validate func([]T) error) *MultiSelect[T] {
	m.validate = validate
	return m
}

// Error returns the error of the multi-select field.
func (m *MultiSelect[T]) Error() error {
	return m.err
}

// Skip returns whether the multiselect should be skipped or should be blocking.
func (*MultiSelect[T]) Skip() bool {
	return false
}

// Zoom returns whether the multiselect should be zoomed.
func (*MultiSelect[T]) Zoom() bool {
	return false
}

// Focus focuses the multi-select field.
func (m *MultiSelect[T]) Focus() tea.Cmd {
	m.updateValue()
	m.focused = true
	return nil
}

// Blur blurs the multi-select field.
func (m *MultiSelect[T]) Blur() tea.Cmd {
	m.updateValue()
	m.focused = false
	return nil
}

// Hovered returns the value of the option under the cursor, and a bool
// indicating whether one was found. If there are no visible options, returns
// a zero-valued T and false.
func (m *MultiSelect[T]) Hovered() (T, bool) {
	if len(m.filteredOptions) == 0 || m.cursor >= len(m.filteredOptions) {
		var zero T
		return zero, false
	}
	return m.filteredOptions[m.cursor].Value, true
}

// KeyBinds returns the help message for the multi-select field.
func (m *MultiSelect[T]) KeyBinds() []key.Binding {
	m.setSelectAllHelp()
	binds := []key.Binding{
		m.keymap.Toggle,
		m.keymap.Up,
		m.keymap.Down,
	}
	if m.filterable {
		binds = append(
			binds,
			m.keymap.Filter,
			m.keymap.SetFilter,
			m.keymap.ClearFilter,
		)
	}
	binds = append(
		binds,
		m.keymap.Prev,
		m.keymap.Submit,
		m.keymap.Next,
		m.keymap.SelectAll,
		m.keymap.SelectNone,
	)
	return binds
}

// Init initializes the multi-select field.
func (m *MultiSelect[T]) Init() tea.Cmd {
	return nil
}

// Update updates the multi-select field.
func (m *MultiSelect[T]) Update(msg tea.Msg) (Model, tea.Cmd) {
	var cmds []tea.Cmd

	// Enforce height on the viewport during update as we need themes to
	// be applied before we can calculate the height.
	m.updateViewportSize()

	var cmd tea.Cmd
	if m.filtering {
		m.filter, cmd = m.filter.Update(msg)
		m.setSelectAllHelp()
		cmds = append(cmds, cmd)
	}

	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		m.hasDarkBg = msg.IsDark()
	case updateFieldMsg:
		var fieldCmds []tea.Cmd
		if ok, hash := m.title.shouldUpdate(); ok {
			m.title.bindingsHash = hash
			if !m.title.loadFromCache() {
				m.title.loading = true
				fieldCmds = append(fieldCmds, func() tea.Msg {
					return updateTitleMsg{id: m.id, title: m.title.fn(), hash: hash}
				})
			}
		}
		if ok, hash := m.description.shouldUpdate(); ok {
			m.description.bindingsHash = hash
			if !m.description.loadFromCache() {
				m.description.loading = true
				fieldCmds = append(fieldCmds, func() tea.Msg {
					return updateDescriptionMsg{id: m.id, description: m.description.fn(), hash: hash}
				})
			}
		}
		if ok, hash := m.options.shouldUpdate(); ok {
			m.options.bindingsHash = hash
			if m.options.loadFromCache() {
				m.filteredOptions = m.options.val
				m.updateValue()
				m.cursor = ordered.Clamp(m.cursor, 0, len(m.filteredOptions)-1)
			} else {
				m.options.loading = true
				m.options.loadingStart = time.Now()
				fieldCmds = append(fieldCmds, func() tea.Msg {
					return updateOptionsMsg[T]{id: m.id, options: m.options.fn(), hash: hash}
				}, m.spinner.Tick)
			}
		}

		return m, tea.Batch(fieldCmds...)

	case spinner.TickMsg:
		if !m.options.loading {
			break
		}
		m.spinner, cmd = m.spinner.Update(msg)
		return m, cmd

	case updateTitleMsg:
		if msg.id == m.id && msg.hash == m.title.bindingsHash {
			m.title.update(msg.title)
		}
	case updateDescriptionMsg:
		if msg.id == m.id && msg.hash == m.description.bindingsHash {
			m.description.update(msg.description)
		}
	case updateOptionsMsg[T]:
		if msg.id == m.id && msg.hash == m.options.bindingsHash {
			m.options.update(msg.options)
			m.selectOptions()
			// since we're updating the options, we need to reset the cursor.
			m.filteredOptions = m.options.val
			m.updateValue()
			m.cursor = ordered.Clamp(m.cursor, 0, len(m.filteredOptions)-1)
		}
	case tea.KeyPressMsg:
		m.err = nil
		switch {
		case key.Matches(msg, m.keymap.Filter):
			m.setFilter(true)
			return m, m.filter.Focus()
		case key.Matches(msg, m.keymap.SetFilter):
			if len(m.filteredOptions) <= 0 {
				m.filter.SetValue("")
				m.filteredOptions = m.options.val
			}
			m.setFilter(false)
		case key.Matches(msg, m.keymap.ClearFilter):
			m.filter.SetValue("")
			m.filteredOptions = m.options.val
			m.setFilter(false)
		case key.Matches(msg, m.keymap.Up):
			//nolint:godox
			// FIXME: should use keys in keymap
			if m.filtering && msg.String() == "k" {
				break
			}

			m.cursor = max(m.cursor-1, 0)
			m.ensureCursorVisible()
		case key.Matches(msg, m.keymap.Down):
			//nolint:godox
			// FIXME: should use keys in keymap
			if m.filtering && msg.String() == "j" {
				break
			}

			m.cursor = min(m.cursor+1, len(m.filteredOptions)-1)
			m.ensureCursorVisible()
		case key.Matches(msg, m.keymap.GotoTop):
			if m.filtering {
				break
			}
			m.cursor = 0
			m.viewport.GotoTop()
		case key.Matches(msg, m.keymap.GotoBottom):
			if m.filtering {
				break
			}
			m.cursor = len(m.filteredOptions) - 1
			m.viewport.GotoBottom()
		case key.Matches(msg, m.keymap.HalfPageUp):
			m.cursor = max(m.cursor-m.viewport.Height()/2, 0)
			m.ensureCursorVisible()
		case key.Matches(msg, m.keymap.HalfPageDown):
			m.cursor = min(m.cursor+m.viewport.Height()/2, len(m.filteredOptions)-1)
			m.ensureCursorVisible()
		case key.Matches(msg, m.keymap.Toggle) && !m.filtering:
			for i, option := range m.options.val {
				if option.Key == m.filteredOptions[m.cursor].Key {
					if !m.options.val[m.cursor].selected && m.limit > 0 && m.numSelected() >= m.limit {
						break
					}
					selected := m.options.val[i].selected
					m.options.val[i].selected = !selected
					m.filteredOptions[m.cursor].selected = !selected
				}
			}
			m.setSelectAllHelp()
			m.updateValue()
		case key.Matches(msg, m.keymap.SelectAll, m.keymap.SelectNone) && m.limit <= 0:
			selected := false

			for _, option := range m.filteredOptions {
				if !option.selected {
					selected = true
					break
				}
			}

			for i, option := range m.options.val {
				for j := range m.filteredOptions {
					if option.Key == m.filteredOptions[j].Key {
						m.options.val[i].selected = selected
						m.filteredOptions[j].selected = selected
						break
					}
				}
			}
			m.setSelectAllHelp()
			m.updateValue()
		case key.Matches(msg, m.keymap.Prev):
			m.updateValue()
			m.err = m.validate(m.accessor.Get())
			if m.err != nil {
				return m, nil
			}
			return m, PrevField
		case key.Matches(msg, m.keymap.Next, m.keymap.Submit):
			m.updateValue()
			m.err = m.validate(m.accessor.Get())
			if m.err != nil {
				return m, nil
			}
			return m, NextField
		}

		if m.filtering {
			m.filteredOptions = m.options.val
			if m.filter.Value() != "" {
				m.filteredOptions = nil
				for _, option := range m.options.val {
					if m.filterFunc(option.Key) {
						m.filteredOptions = append(m.filteredOptions, option)
					}
				}
			}
			if len(m.filteredOptions) > 0 {
				m.cursor = min(m.cursor, len(m.filteredOptions)-1)
			}
		}
		m.ensureCursorVisible()
	}

	return m, tea.Batch(cmds...)
}

// updateViewportSize updates the viewport size according to the Height setting
// on this multi-select field.
func (m *MultiSelect[T]) updateViewportSize() {
	yoffset := 0
	if ss := m.titleView(); ss != "" {
		yoffset += lipgloss.Height(ss)
	}
	if ss := m.descriptionView(); ss != "" {
		yoffset += lipgloss.Height(ss)
	}
	v, _, _ := m.optionsView()
	height := m.height
	if height <= 0 {
		height = lipgloss.Height(v)
	}
	width := m.width
	if m.width <= 0 {
		width = lipgloss.Width(v)
	}

	m.viewport.SetWidth(width)
	m.viewport.SetHeight(max(minHeight, height) - yoffset)
}

// numSelected returns the total number of selected options.
func (m *MultiSelect[T]) numSelected() int {
	var count int
	for _, o := range m.options.val {
		if o.selected {
			count++
		}
	}
	return count
}

// numFilteredOptionsSelected returns the number of selected options with the
// current filter applied.
func (m *MultiSelect[T]) numFilteredSelected() int {
	var count int
	for _, o := range m.filteredOptions {
		if o.selected {
			count++
		}
	}
	return count
}

func (m *MultiSelect[T]) updateValue() {
	value := make([]T, 0)
	for _, option := range m.options.val {
		if option.selected {
			value = append(value, option.Value)
		}
	}
	m.accessor.Set(value)
	m.err = m.validate(m.accessor.Get())
}

func (m *MultiSelect[T]) activeStyles() *FieldStyles {
	theme := m.theme
	if theme == nil {
		theme = ThemeFunc(ThemeCharm)
	}
	if m.focused {
		return &theme.Theme(m.hasDarkBg).Focused
	}
	return &theme.Theme(m.hasDarkBg).Blurred
}

func (m *MultiSelect[T]) titleView() string {
	if m.title.val == "" {
		return ""
	}
	var (
		styles   = m.activeStyles()
		sb       = strings.Builder{}
		maxWidth = m.width - styles.Base.GetHorizontalFrameSize()
	)
	if m.filtering {
		sb.WriteString(m.filter.View())
	} else if m.filter.Value() != "" {
		sb.WriteString(styles.Title.Render(wrap(m.title.val, maxWidth)))
		sb.WriteString(styles.Description.Render("/" + m.filter.Value()))
	} else {
		sb.WriteString(styles.Title.Render(wrap(m.title.val, maxWidth)))
	}
	if m.err != nil {
		sb.WriteString(styles.ErrorIndicator.String())
	}
	return sb.String()
}

func (m *MultiSelect[T]) descriptionView() string {
	if m.description.val == "" {
		return ""
	}
	maxWidth := m.width - m.activeStyles().Base.GetHorizontalFrameSize()
	return m.activeStyles().Description.Render(wrap(m.description.val, maxWidth))
}

func (m *MultiSelect[T]) renderOption(option Option[T], cursor, selected bool) string {
	styles := m.activeStyles()
	var parts []string
	if cursor {
		parts = append(parts, styles.MultiSelectSelector.String())
	} else {
		parts = append(parts, strings.Repeat(" ", lipgloss.Width(styles.MultiSelectSelector.String())))
	}
	if selected {
		parts = append(parts, styles.SelectedPrefix.String())
		parts = append(parts, styles.SelectedOption.Render(option.Key))
	} else {
		parts = append(parts, styles.UnselectedPrefix.String())
		parts = append(parts, styles.UnselectedOption.Render(option.Key))
	}
	return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
}

// cursorLineOffset computes the line offset and height (in lines) for the
// current cursor position without rendering the full options string.
func (m *MultiSelect[T]) cursorLineOffset() (offset int, height int) {
	for i, option := range m.filteredOptions {
		line := m.renderOption(option, m.cursor == i, m.filteredOptions[i].selected)
		h := lipgloss.Height(line)
		if i < m.cursor {
			offset += h
		}
		if i == m.cursor {
			height = h
			return offset, height
		}
	}
	return offset, height
}

func (m *MultiSelect[T]) ensureCursorVisible() {
	offset, height := m.cursorLineOffset()
	ensureVisible(&m.viewport, offset, height)
}

func (m *MultiSelect[T]) optionsView() (string, int, int) {
	var sb strings.Builder

	if m.options.loading && time.Since(m.options.loadingStart) > spinnerShowThreshold {
		m.spinner.Style = m.activeStyles().MultiSelectSelector.UnsetString()
		sb.WriteString(m.spinner.View() + " Loading...")
		return sb.String(), -1, 1
	}

	var cursorOffset int
	var cursorHeight int
	for i, option := range m.filteredOptions {
		cursor := m.cursor == i
		line := m.renderOption(option, cursor, m.filteredOptions[i].selected)
		if i < m.cursor {
			cursorOffset += lipgloss.Height(line)
		}
		if cursor {
			cursorHeight = lipgloss.Height(line)
		}
		sb.WriteString(line)
		if i < len(m.options.val)-1 {
			sb.WriteString("\n")
		}
	}

	for i := len(m.filteredOptions); i < len(m.options.val)-1; i++ {
		sb.WriteString("\n")
	}

	return sb.String(), cursorOffset, cursorHeight
}

// View renders the multi-select field.
func (m *MultiSelect[T]) View() string {
	styles := m.activeStyles()

	vpc, _, _ := m.optionsView()
	m.viewport.SetContent(vpc)

	var sb strings.Builder
	if m.title.val != "" || m.title.fn != nil {
		sb.WriteString(m.titleView())
		sb.WriteString("\n")
	}
	if m.description.val != "" || m.description.fn != nil {
		sb.WriteString(m.descriptionView() + "\n")
	}
	sb.WriteString(m.viewport.View())
	return styles.Base.Width(m.width).Height(m.height).
		Render(sb.String())
}

func (m *MultiSelect[T]) printOptions(w io.Writer) {
	styles := m.activeStyles()
	var sb strings.Builder
	for i, option := range m.options.val {
		if option.selected {
			sb.WriteString(styles.SelectedOption.Render(fmt.Sprintf("%d. %s %s", i+1, "✓", option.Key)))
		} else {
			sb.WriteString(fmt.Sprintf("%d.   %s", i+1, option.Key))
		}
		sb.WriteString("\n")
	}
	sb.WriteString("0.   Confirm selection\n")
	_, _ = fmt.Fprint(w, sb.String())
}

// setFilter sets the filter of the select field.
func (m *MultiSelect[T]) setFilter(filter bool) {
	m.filtering = filter
	m.keymap.SetFilter.SetEnabled(filter)
	m.keymap.Filter.SetEnabled(!filter)
	m.keymap.Next.SetEnabled(!filter)
	m.keymap.Submit.SetEnabled(!filter)
	m.keymap.Prev.SetEnabled(!filter)
	m.keymap.ClearFilter.SetEnabled(!filter && m.filter.Value() != "")
}

// filterFunc returns true if the option matches the filter.
func (m *MultiSelect[T]) filterFunc(option string) bool {
	// XXX: remove diacritics or allow customization of filter function.
	return strings.Contains(strings.ToLower(option), strings.ToLower(m.filter.Value()))
}

// setSelectAllHelp enables the appropriate select all or select none keybinding.
func (m *MultiSelect[T]) setSelectAllHelp() {
	if m.limit > 0 {
		m.keymap.SelectAll.SetEnabled(false)
		m.keymap.SelectNone.SetEnabled(false)
		return
	}

	noneSelected := m.numFilteredSelected() <= 0
	allSelected := m.numFilteredSelected() > 0 && m.numFilteredSelected() < len(m.filteredOptions)
	selectAll := noneSelected || allSelected
	m.keymap.SelectAll.SetEnabled(selectAll)
	m.keymap.SelectNone.SetEnabled(!selectAll)
}

// Run runs the multi-select field.
func (m *MultiSelect[T]) Run() error {
	return Run(m)
}

// RunAccessible runs the multi-select field in accessible mode.
func (m *MultiSelect[T]) RunAccessible(w io.Writer, r io.Reader) error {
	styles := m.activeStyles()
	title := styles.Title.
		PaddingRight(1).
		Render(cmp.Or(m.title.val, "Select:"))
	_, _ = fmt.Fprintln(w, title)
	limit := m.limit
	if limit == 0 {
		limit = len(m.options.val)
	}
	_, _ = fmt.Fprintf(w, "Select up to %d options.\n", limit)

	var choice int
	for {
		m.printOptions(w)

		prompt := fmt.Sprintf("Enter a number between %d and %d: ", 0, len(m.options.val))
		choice = accessibility.PromptInt(w, r, prompt, 0, len(m.options.val), nil)
		if choice <= 0 {
			m.updateValue()
			err := m.validate(m.accessor.Get())
			if err != nil {
				_, _ = fmt.Fprintln(w, err)
				continue
			}
			break
		}

		if !m.options.val[choice-1].selected && m.limit > 0 && m.numSelected() >= m.limit {
			_, _ = fmt.Fprintf(w, "You can't select more than %d options.\n", m.limit)
			_, _ = fmt.Fprintln(w)
			continue
		}
		m.options.val[choice-1].selected = !m.options.val[choice-1].selected
		_, _ = fmt.Fprintln(w)
	}

	return nil
}

// WithTheme sets the theme of the multi-select field.
func (m *MultiSelect[T]) WithTheme(theme Theme) Field {
	if m.theme != nil {
		return m
	}
	m.theme = theme
	styles := m.theme.Theme(m.hasDarkBg)

	st := m.filter.Styles()
	st.Cursor.Color = styles.Focused.TextInput.Cursor.GetForeground()
	st.Focused.Prompt = styles.Focused.TextInput.Prompt
	st.Focused.Text = styles.Focused.TextInput.Text
	st.Focused.Placeholder = styles.Focused.TextInput.Placeholder
	m.filter.SetStyles(st)

	m.updateViewportSize()
	return m
}

// WithKeyMap sets the keymap of the multi-select field.
func (m *MultiSelect[T]) WithKeyMap(k *KeyMap) Field {
	m.keymap = k.MultiSelect
	if !m.filterable {
		m.keymap.Filter.SetEnabled(false)
		m.keymap.ClearFilter.SetEnabled(false)
		m.keymap.SetFilter.SetEnabled(false)
	}
	return m
}

// WithWidth sets the width of the multi-select field.
func (m *MultiSelect[T]) WithWidth(width int) Field {
	m.width = width
	m.updateViewportSize()
	return m
}

// WithHeight sets the total height of the multi-select field. Including padding
// and help menu heights.
func (m *MultiSelect[T]) WithHeight(height int) Field {
	m.Height(height)
	return m
}

// WithPosition sets the position of the multi-select field.
func (m *MultiSelect[T]) WithPosition(p FieldPosition) Field {
	if m.filtering {
		return m
	}
	m.keymap.Prev.SetEnabled(!p.IsFirst())
	m.keymap.Next.SetEnabled(!p.IsLast())
	m.keymap.Submit.SetEnabled(p.IsLast())
	return m
}

// GetKey returns the multi-select's key.
func (m *MultiSelect[T]) GetKey() string {
	return m.key
}

// GetValue returns the multi-select's value.
func (m *MultiSelect[T]) GetValue() any {
	return m.accessor.Get()
}

// GetFiltering returns whether the multi-select is filtering.
func (m *MultiSelect[T]) GetFiltering() bool {
	return m.filtering
}
