"""Soft-wrapped multiline text buffer for the commit message editor."""

from __future__ import annotations

from dataclasses import dataclass

from pyratatui import Line, Span, Style, Text

from pygittools.tui.keys import KeyLike, is_backspace, is_enter, printable_char


@dataclass(frozen=True, slots=True)
class _VisualLine:
	text: str
	start: int
	hard_break: bool


class MultilineEdit:
	def __init__(self, initial: str = "") -> None:
		self._text = initial
		self._cursor = len(initial)
		self._width = 40

	def value(self) -> str:
		return self._text

	def set_value(self, value: str) -> None:
		self._text = value
		self._cursor = len(value)

	@property
	def cursor(self) -> int:
		return self._cursor

	def handle_key(self, key: KeyLike) -> bool:
		# Arrow keys only — vim letters must insert as text while editing.
		if key.code == "Left":
			self._cursor = max(0, self._cursor - 1)
			return True
		if key.code == "Right":
			self._cursor = min(len(self._text), self._cursor + 1)
			return True
		if key.code == "Up":
			self._move_vertical(-1)
			return True
		if key.code == "Down":
			self._move_vertical(1)
			return True
		if key.code == "Home":
			line = self._visual_index()
			self._cursor = self._visual_lines()[line].start
			return True
		if key.code == "End":
			lines = self._visual_lines()
			line = self._visual_index()
			visual = lines[line]
			self._cursor = visual.start + len(visual.text)
			return True
		if is_backspace(key):
			if self._cursor > 0:
				self._text = self._text[: self._cursor - 1] + self._text[self._cursor :]
				self._cursor -= 1
			return True
		if key.code == "Delete":
			if self._cursor < len(self._text):
				self._text = self._text[: self._cursor] + self._text[self._cursor + 1 :]
			return True
		if is_enter(key):
			self._insert("\n")
			return True
		char = printable_char(key)
		if char is not None:
			self._insert(char)
			return True
		return False

	def at_last_visual_line(self) -> bool:
		lines = self._visual_lines()
		return self._visual_index() >= len(lines) - 1

	def at_first_visual_line(self) -> bool:
		return self._visual_index() <= 0

	def render(
		self,
		width: int,
		height: int,
		*,
		focused: bool,
		style: Style,
		cursor_style: Style,
	) -> Text:
		self._width = max(1, width)
		lines = self._visual_lines()
		cursor_line = self._visual_index()
		scroll = 0
		if height > 0 and cursor_line >= height:
			scroll = cursor_line - height + 1
		text = Text()
		for row in range(height):
			index = scroll + row
			if index >= len(lines):
				text.push_line(Line([Span(" ", style)]))
				continue
			visual = lines[index]
			if focused and index == cursor_line:
				text.push_line(self._line_with_cursor(visual, style, cursor_style))
			else:
				content = visual.text if visual.text else " "
				text.push_line(Line([Span(content, style)]))
		return text

	def _insert(self, chunk: str) -> None:
		self._text = self._text[: self._cursor] + chunk + self._text[self._cursor :]
		self._cursor += len(chunk)

	def _visual_lines(self) -> list[_VisualLine]:
		width = max(1, self._width)
		lines: list[_VisualLine] = []
		offset = 0
		parts = self._text.split("\n")
		for part_index, part in enumerate(parts):
			hard = part_index < len(parts) - 1
			if part == "":
				lines.append(_VisualLine(text="", start=offset, hard_break=hard))
				offset += 1 if hard else 0
				continue
			start = offset
			while part:
				chunk = part[:width]
				part = part[width:]
				is_last = not part
				lines.append(
					_VisualLine(
						text=chunk,
						start=start,
						hard_break=hard and is_last,
					),
				)
				start += len(chunk)
			offset = start + (1 if hard else 0)
		if not lines:
			lines.append(_VisualLine(text="", start=0, hard_break=False))
		return lines

	def _visual_index(self) -> int:
		lines = self._visual_lines()
		for index, visual in enumerate(lines):
			end = visual.start + len(visual.text)
			if self._cursor < end:
				return index
			if self._cursor == end:
				return index
		return len(lines) - 1

	def _move_vertical(self, delta: int) -> None:
		lines = self._visual_lines()
		index = self._visual_index()
		target = max(0, min(len(lines) - 1, index + delta))
		if target == index:
			return
		col = self._cursor - lines[index].start
		visual = lines[target]
		self._cursor = visual.start + min(col, len(visual.text))

	def _line_with_cursor(self, visual: _VisualLine, style: Style, cursor_style: Style) -> Line:
		col = max(0, self._cursor - visual.start)
		if col < len(visual.text):
			before = visual.text[:col]
			cursor_ch = visual.text[col]
			after = visual.text[col + 1 :]
			spans = []
			if before:
				spans.append(Span(before, style))
			spans.append(Span(cursor_ch, cursor_style))
			if after:
				spans.append(Span(after, style))
			return Line(spans)
		spans = []
		if visual.text:
			spans.append(Span(visual.text, style))
		spans.append(Span(" ", cursor_style))
		return Line(spans)