package editor

import (
	"fmt"
	"strings"

	"git.gophernest.net/azpect/TextEditor/internal/action"
)

func posInsideSelection(m Model, col, line int) bool {
	switch m.Mode() {
	case action.VisualLineMode:
		startY := min(m.AnchorY(), m.CursorY())
		endY := max(m.AnchorY(), m.CursorY())
		return line >= startY && line <= endY

	case action.VisualMode:
		ax := m.AnchorX()
		ay := m.AnchorY()

		cx := m.CursorX()
		cy := m.CursorY()

		// Normalize so start is always before end in document order
		var startX, startY, endX, endY int
		if ay < cy || (ay == cy && ax <= cx) {
			startX, startY = ax, ay
			endX, endY = cx, cy
		} else {
			startX, startY = cx, cy
			endX, endY = ax, ay
		}

		// Position is inside if it falls within [start, end] inclusive
		afterStart := line > startY || (line == startY && col >= startX)
		beforeEnd := line < endY || (line == endY && col <= endX)
		return afterStart && beforeEnd

	case action.VisualBlockMode:
		startX := min(m.AnchorX(), m.CursorX())
		startY := min(m.AnchorY(), m.CursorY())
		endX := max(m.AnchorX(), m.CursorX())
		endY := max(m.AnchorY(), m.CursorY())

		return col >= startX && col <= endX &&
			line >= startY && line <= endY

	default:
		return false
	}
}

func posIsAnchor(m Model, col, line int) bool {
	ax := m.AnchorX()
	ay := m.AnchorY()
	return col == ax && line == ay
}

func (m Model) View() string {
	var view strings.Builder

	viewportHeight := m.ViewPortH()
	start := m.ScrollY()
	end := m.ScrollY() + viewportHeight

	for i := start; i < end; i++ {

		if i < m.LineCount() {

			if m.Settings().Number || m.Settings().RelativeNumber {
				var (
					gutter      string
					currentLine bool = false
					lineNumber  int
				)

				if m.Settings().RelativeNumber {
					// Relative line numbers: show distance from cursor, current line shows absolute
					if i > m.CursorY() {
						lineNumber = i - m.CursorY()
						gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
					} else if i < m.CursorY() {
						lineNumber = m.CursorY() - i
						gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
					} else {
						// Current line: show absolute number if Number is also set, otherwise show 0
						currentLine = true
						if m.Settings().Number {
							lineNumber = i + 1
							gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
						} else {
							gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, 0)
						}
					}
				} else if m.Settings().Number {
					// Absolute line numbers only
					lineNumber = i + 1
					currentLine = (i == m.CursorY())
					gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
				}
				view.WriteString(m.gutterStyle(currentLine).Render(gutter))
			}

			runes := []rune(m.Line(i))
			for x := 0; x <= len(runes); x++ {
				if m.CursorY() == i && m.CursorX() == x {
					if x < len(runes) {
						view.WriteString(m.cursorStyle().Render(string(runes[x])))
					} else {
						view.WriteString(m.cursorStyle().Render(" "))
					}
				} else if x < len(runes) {
					if m.Mode().IsVisualMode() && posIsAnchor(m, x, i) {
						view.WriteString(m.visualAnchorStyle().Render(string(runes[x])))
					} else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
						view.WriteString(m.visualHighlightStyle().Render(string(runes[x])))
					} else {
						view.WriteRune(runes[x])
					}
					// To highlight blank lines when in visual mode
				} else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
					view.WriteString(m.visualHighlightStyle().Render(" "))
				}
			}
		} else {
			// Empty lines beyond file content
			if m.Settings().Number || m.Settings().RelativeNumber {
				format := fmt.Sprintf("%%-%ds ", m.Settings().GutterSize-1)
				fmt.Fprintf(&view, format, "~")
			} else {
				view.WriteString("~")
			}
		}

		view.WriteString("\n")
	}

	view.WriteString(drawStatusBar(m))
	view.WriteString("\n")
	view.WriteString(drawCommandBar(m))

	return view.String()
}

func drawStatusBar(m Model) string {
	left := leftBar(m)
	right := rightBar(m)

	diff := m.win_w - (len(left) + len(right))

	// This happens when the terminal spawns
	if diff <= 0 {
		return ""
	}

	middle := strings.Repeat(" ", diff)
	return left + middle + right
}

func leftBar(m Model) string {
	return fmt.Sprintf(" %s", m.Mode().ToString())
}

func rightBar(m Model) (bar string) {
	if m.Mode().IsVisualMode() {
		lineCount := max(m.AnchorY(), m.CursorY()) - min(m.AnchorY(), m.CursorY()) + 1
		bar = fmt.Sprintf("%d:%d  <%d>", m.CursorY(), m.CursorX(), lineCount)
	} else {
		bar = fmt.Sprintf("%d:%d ", m.CursorY(), m.CursorX())
	}
	return
}

func drawCommandBar(m Model) (bar string) {
	if m.Mode() == action.CommandMode {
		bar = ":"
		cmd := m.Command()
		cur := m.CommandCursor()
		for i := 0; i < len(cmd); i++ {
			if i == cur {
				bar += m.cursorStyle().Render(string(cmd[i]))
			} else {
				bar += string(cmd[i])
			}
		}
		// Cursor at end of command
		if cur >= len(cmd) {
			bar += m.cursorStyle().Render(" ")
		}
		// bar = fmt.Sprintf("%s  %d", bar, cur)
	} else if m.CommandError() != nil {
		bar = m.commandErrorStyle().Render(m.CommandError().Error())
	} else if strings.TrimSpace(m.CommandOutput()) != "" {
		bar = m.CommandOutput()
	} else if strings.TrimSpace(m.Command()) != "" {
		bar = fmt.Sprintf(":%s", m.Command())
	}

	return bar
}
