package model

import (
	"strings"

	"charm.land/lipgloss/v2"

	"cliamp/ui"
)

// clampScroll keeps cursor inside [0, count) and adjusts scroll so that
// the cursor sits within the visible window of `visible` rows.
func clampScroll(cursor, scroll *int, count, visible int) {
	if visible <= 0 {
		return
	}
	if *cursor < 0 {
		*cursor = 0
	}
	if *cursor >= count && count > 0 {
		*cursor = count - 1
	}
	if *cursor < *scroll {
		*scroll = *cursor
	} else if *cursor >= *scroll+visible {
		*scroll = *cursor - visible + 1
	}
	if *scroll+visible > count && count > 0 {
		*scroll = max(0, count-visible)
	}
	if *scroll < 0 {
		*scroll = 0
	}
}

// measurePlVisible calculates playlist lines available for a given upper limit.
func (m *Model) measurePlVisible(limit int) int {
	saved := m.plVisible
	m.plVisible = 3 // temporary minimal value for measurement
	defer func() { m.plVisible = saved }()

	// Use mainSections to get all fixed chrome plus any active transient messages.
	probe := strings.Join(m.mainSections("x", true), "\n")
	fixedLines := lipgloss.Height(ui.FrameStyle.Render(probe)) - 1
	return max(3, min(limit, m.height-fixedLines))
}

// collapsedPlVisible returns the natural (non-expanded) playlist height.
func (m *Model) collapsedPlVisible() int {
	return m.measurePlVisible(maxPlVisible)
}

// expandedPlVisible returns the expanded playlist height with no cap.
func (m *Model) expandedPlVisible() int {
	return m.measurePlVisible(m.height)
}

// applyHeightMode sets plVisible based on the current heightExpanded state.
func (m *Model) applyHeightMode() {
	if m.playlist == nil {
		return
	}
	if m.heightExpanded {
		m.plVisible = m.expandedPlVisible()
	} else {
		m.plVisible = m.collapsedPlVisible()
	}
}

// adjustScroll ensures plCursor is visible in the playlist view.
// It accounts for album separator lines that reduce the number of
// tracks that fit in the visible window.
func (m *Model) adjustScroll() {
	if m.playlist == nil {
		return
	}
	tracks := m.playlist.Tracks()
	if len(tracks) == 0 {
		return
	}
	visible := m.effectivePlaylistVisible()
	if visible <= 0 {
		return
	}
	m.plScroll = m.playlistScroll(visible)
}

func (m Model) playlistScroll(visible int) int {
	tracks := m.playlist.Tracks()
	scroll := max(0, m.plScroll)
	if scroll >= len(tracks) {
		scroll = max(0, len(tracks)-1)
	}
	if m.plCursor < scroll {
		return m.plCursor
	}
	for scroll < m.plCursor && m.albumSeparatorRows(tracks, scroll, m.plCursor, m.showAlbumHeaders) > visible {
		scroll++
	}
	return scroll
}

func (m Model) mainFrameFixedLines(includeTransient bool) int {
	if m.chromeOK {
		if !includeTransient {
			return m.chromeHeight
		}
		transientLines := len(m.footerMessages())
		if m.err != nil {
			transientLines++
		}
		return m.chromeHeight + transientLines
	}
	// Fallback: render and measure (only needed until first WindowSizeMsg)
	content := strings.Join(m.mainSections("", includeTransient), "\n")
	return lipgloss.Height(ui.FrameStyle.Render(content))
}

func (m Model) effectivePlaylistVisible() int {
	available := m.height - m.mainFrameFixedLines(true)
	if available <= 0 {
		return 0
	}
	if m.plVisible <= 0 {
		return 0
	}
	return min(m.plVisible, available)
}

// recomputeChrome renders the fixed chrome (without playlist or transients)
// and caches its height. Called when terminal width or compact mode changes.
func (m *Model) recomputeChrome() {
	content := strings.Join(m.mainSections("", false), "\n")
	m.chromeHeight = lipgloss.Height(ui.FrameStyle.Render(content))
	m.chromeOK = true
}

// invalidateChrome marks the chrome height dirty. Until the next recompute,
// mainFrameFixedLines falls back to direct measurement.
func (m *Model) invalidateChrome() {
	m.chromeOK = false
}

func (m *Model) refreshChrome() {
	if m.width > 0 {
		m.recomputeChrome()
		return
	}
	m.invalidateChrome()
}
