package operator

import (
	"git.gophernest.net/azpect/TextEditor/internal/action"
	tea "github.com/charmbracelet/bubbletea"
)

// Implements Operator (c)
type ChangeOperator struct{}

func (o ChangeOperator) Operate(m action.Model, start, end action.Position, mtype action.MotionType) tea.Cmd {
	switch m.Mode() {
	case action.VisualMode:
		changeCharSelection(m, start, end)
	case action.VisualLineMode:
		changeLineSelection(m, start, end)
	case action.VisualBlockMode:
		changeBlockSelection(m, start, end)
	case action.NormalMode:
		changeNormalMode(m, start, end, mtype)
	}
	return nil
}

func changeNormalMode(m action.Model, start, end action.Position, mtype action.MotionType) {
	// Normalize so start is always before or equal to end
	if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
		start, end = end, start
	}

	// Linewise motions (j, k, G, gg) always operate on whole lines
	if mtype == action.Linewise {
		changeLineSelection(m, start, end)
		return
	}

	// Charwise motions on same line
	if start.Line == end.Line {
		// No movement = nothing to change
		if start.Col == end.Col && mtype == action.CharwiseExclusive {
			m.SetMode(action.InsertMode)
			return
		}
		// Exclusive motion: end position not included, so back up one
		if mtype == action.CharwiseExclusive {
			end.Col--
		}
		if end.Col >= start.Col {
			changeCharSelection(m, start, end)
		} else {
			m.SetMode(action.InsertMode)
		}
		return
	}

	// Charwise motion spanning multiple lines
	changeCharSelection(m, start, end)
}

func changeCharSelection(m action.Model, start, end action.Position) {
	var deletedText string

	if start.Line == end.Line {
		line := m.Line(start.Line)
		endCol := min(end.Col+1, len(line))
		deletedText = line[start.Col:endCol]
		m.SetLine(start.Line, line[:start.Col]+line[endCol:])
	} else {
		startLine := m.Line(start.Line)
		endLine := m.Line(end.Line)

		// Extract deleted text
		deletedText = startLine[start.Col:] + "\n"
		for y := start.Line + 1; y < end.Line; y++ {
			deletedText += m.Line(y) + "\n"
		}
		endCol := min(end.Col+1, len(endLine))
		deletedText += endLine[:endCol]

		prefix := startLine[:start.Col]
		suffix := ""
		if end.Col+1 < len(endLine) {
			suffix = endLine[end.Col+1:]
		}

		// Delete from end back to start to preserve indices
		for i := end.Line; i >= start.Line; i-- {
			m.DeleteLine(i)
		}
		m.InsertLine(start.Line, prefix+suffix)
	}

	m.SetCursorY(start.Line)
	m.SetCursorX(start.Col)
	m.ClampCursorX()
	m.SetMode(action.InsertMode)

	// Update register with deleted text
	m.UpdateDefaultRegister(action.CharwiseRegister, []string{deletedText})
}

func changeLineSelection(m action.Model, start, end action.Position) {
	var lines []string

	for i := end.Line; i >= start.Line; i-- {
		lines = append([]string{m.Line(i)}, lines...)
		m.DeleteLine(i)
	}

	// Insert an empty line for editing
	insertY := min(start.Line, m.LineCount())
	m.InsertLine(insertY, "")

	m.SetCursorY(insertY)
	m.SetCursorX(0)
	m.SetMode(action.InsertMode)

	// Update registers
	m.UpdateDefaultRegister(action.LinewiseRegister, lines)
}

func changeBlockSelection(m action.Model, start, end action.Position) {
	startCol := min(start.Col, end.Col)
	endCol := max(start.Col, end.Col)

	for y := start.Line; y <= end.Line; y++ {
		line := m.Line(y)
		if startCol >= len(line) {
			continue
		}
		ec := min(endCol+1, len(line))
		m.SetLine(y, line[:startCol]+line[ec:])
	}

	m.SetCursorY(start.Line)
	m.SetCursorX(startCol)
	m.ClampCursorX()
	m.SetMode(action.InsertMode)
}

// Verify ChangeOperator implements DoublePresser
var _ action.DoublePresser = ChangeOperator{}

// Double press handles cc - change the entire line
func (o ChangeOperator) DoublePress(m action.Model, count int) tea.Cmd {
	startY := m.CursorY()

	// If we have a higher value than lines remaining, we can only run so many times
	opCount := min(count, m.LineCount()-startY)

	var lines []string

	// Collect lines to delete (always delete at startY since lines shift up)
	for range opCount {
		lines = append(lines, m.Line(startY))
		m.DeleteLine(startY)
	}

	// Put deleted lines in register
	m.UpdateDefaultRegister(action.LinewiseRegister, lines)

	// Insert empty line at the original position for editing
	// If we deleted everything, startY might be past end, so clamp it
	insertY := min(startY, m.LineCount())
	m.InsertLine(insertY, "")

	// Position cursor on the new empty line
	m.SetCursorY(insertY)
	m.SetCursorX(0)
	m.SetMode(action.InsertMode)

	return nil
}
