"""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)