package viewport

import (
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/ansi"
	"github.com/rivo/uniseg"
)

// parseMatches converts the given matches into highlight ranges.
//
// Assumptions:
// - matches are measured in bytes, e.g. what [regex.FindAllStringIndex] would return
// - matches were made against the given content
// - matches are in order
// - matches do not overlap
// - content is line terminated with \n only
//
// We'll then convert the ranges into [highlightInfo]s, which hold the starting
// line and the grapheme positions.
func parseMatches(
	content string,
	matches [][]int,
) []highlightInfo {
	if len(matches) == 0 {
		return nil
	}

	line := 0
	graphemePos := 0
	previousLinesOffset := 0
	bytePos := 0

	highlights := make([]highlightInfo, 0, len(matches))
	gr := uniseg.NewGraphemes(ansi.Strip(content))

	for _, match := range matches {
		byteStart, byteEnd := match[0], match[1]

		// hilight for this match:
		hi := highlightInfo{
			lines: map[int][2]int{},
		}

		// find the beginning of this byte range, setup current line and
		// grapheme position.
		for byteStart > bytePos {
			if !gr.Next() {
				break
			}
			if content[bytePos] == '\n' {
				previousLinesOffset = graphemePos + 1
				line++
			}
			graphemePos += max(1, gr.Width())
			bytePos += len(gr.Str())
		}

		hi.lineStart = line
		hi.lineEnd = line

		graphemeStart := graphemePos

		// loop until we find the end
		for byteEnd > bytePos {
			if !gr.Next() {
				break
			}

			// if it ends with a new line, add the range, increase line, and continue
			if content[bytePos] == '\n' {
				colstart := max(0, graphemeStart-previousLinesOffset)
				colend := max(graphemePos-previousLinesOffset+1, colstart) // +1 its \n itself

				if colend > colstart {
					hi.lines[line] = [2]int{colstart, colend}
					hi.lineEnd = line
				}

				previousLinesOffset = graphemePos + 1
				line++
			}

			graphemePos += max(1, gr.Width())
			bytePos += len(gr.Str())
		}

		// we found it!, add highlight and continue
		if bytePos == byteEnd {
			colstart := max(0, graphemeStart-previousLinesOffset)
			colend := max(graphemePos-previousLinesOffset, colstart)

			if colend > colstart {
				hi.lines[line] = [2]int{colstart, colend}
				hi.lineEnd = line
			}
		}

		highlights = append(highlights, hi)
	}

	return highlights
}

type highlightInfo struct {
	// in which line this highlight starts and ends
	lineStart, lineEnd int

	// the grapheme highlight ranges for each of these lines
	lines map[int][2]int
}

// coords returns the line x column of this highlight.
func (hi highlightInfo) coords() (int, int, int) {
	for i := hi.lineStart; i <= hi.lineEnd; i++ {
		hl, ok := hi.lines[i]
		if !ok {
			continue
		}
		return i, hl[0], hl[1]
	}
	return hi.lineStart, 0, 0
}

func makeHighlightRanges(
	highlights []highlightInfo,
	line int,
	style lipgloss.Style,
) []lipgloss.Range {
	result := []lipgloss.Range{}
	for _, hi := range highlights {
		lihi, ok := hi.lines[line]
		if !ok {
			continue
		}
		if lihi == [2]int{} {
			continue
		}
		result = append(result, lipgloss.NewRange(lihi[0], lihi[1], style))
	}
	return result
}
