//! Screen renderer.
//!
//! Renders the editor state through `crossterm` using a dirty-row strategy:
//! only rows the buffer has flagged dirty (or the whole frame after a
//! viewport scroll / mode change) are repainted.

use std::io;

use crossterm::{
    cursor, queue,
    style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
    terminal::{Clear, ClearType},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

use crate::editor::{Editor, Mode};
use crate::highlight::{highlight_line, LineState, Token, TokenKind};
use crate::terminal::Terminal;

/// Rendering controller.  Currently keeps no inter-frame state beyond the
/// dirty flags on each buffer; lifetime kept around so future double-buffer
/// caching can be added without an API change.
pub struct Ui;

impl Ui {
    /// Build a new renderer.
    pub fn new() -> Self {
        Self
    }

    /// Force a complete redraw on the next call.
    pub fn full_redraw(&mut self, editor: &mut Editor, term: &mut Terminal) -> io::Result<()> {
        editor.buf_mut().mark_all_dirty();
        editor.chrome_dirty = true;
        let theme = Theme::for_terminal(term);
        queue!(
            term.out(),
            ResetColor,
            SetBackgroundColor(theme.bg),
            Clear(ClearType::All)
        )?;
        self.redraw(editor, term)
    }

    /// Repaint just the dirty parts of the screen.
    pub fn redraw(&mut self, editor: &mut Editor, term: &mut Terminal) -> io::Result<()> {
        let content_rows = editor.content_rows();
        let content_cols = editor.content_cols();
        let scrolled = editor.buf_mut().ensure_cursor_visible(content_rows, content_cols);
        if scrolled {
            editor.buf_mut().mark_all_dirty();
        }

        queue!(term.out(), cursor::Hide)?;

        let theme = Theme::for_terminal(term);
        let mut row: u16 = 0;
        if editor.buffers.len() > 1 {
            if editor.chrome_dirty {
                draw_tab_bar(editor, term, &theme, row)?;
            }
            row += 1;
        }
        draw_content(editor, term, &theme, row, content_rows)?;
        row += content_rows;
        if editor.chrome_dirty {
            draw_status_bar(editor, term, &theme, row)?;
        }
        row += 2;
        if matches!(
            editor.mode,
            Mode::Search { .. } | Mode::SavePrompt { .. } | Mode::QuitConfirm
        ) {
            draw_mode_bar(editor, term, &theme, row)?;
        }

        if matches!(editor.mode, Mode::Help) {
            draw_help_overlay(editor, term, &theme)?;
        }

        place_cursor(editor, term)?;
        queue!(term.out(), cursor::Show)?;

        term.flush()?;
        editor.chrome_dirty = false;
        editor.buf_mut().viewport_dirty = false;
        for d in &mut editor.buf_mut().line_dirty {
            *d = false;
        }
        Ok(())
    }
}

impl Default for Ui {
    fn default() -> Self {
        Self::new()
    }
}

// ---------------------------------------------------------------------------
// theme
// ---------------------------------------------------------------------------

struct Theme {
    bg: Color,
    fg: Color,
    status_bg: Color,
    status_fg: Color,
    line_number: Color,
    cursor_line_bg: Color,
    selection_bg: Color,
    keyword: Color,
    string: Color,
    comment: Color,
    number: Color,
    type_name: Color,
    function: Color,
    operator: Color,
    heading: Color,
}

impl Theme {
    fn for_terminal(term: &Terminal) -> Self {
        if term.supports_256 {
            Self {
                bg: Color::Rgb { r: 0x1a, g: 0x1b, b: 0x26 },
                fg: Color::Rgb { r: 0xc0, g: 0xca, b: 0xf5 },
                status_bg: Color::Rgb { r: 0x24, g: 0x28, b: 0x3b },
                status_fg: Color::Rgb { r: 0x7a, g: 0xa2, b: 0xf7 },
                line_number: Color::Rgb { r: 0x56, g: 0x5f, b: 0x89 },
                cursor_line_bg: Color::Rgb { r: 0x1f, g: 0x23, b: 0x35 },
                selection_bg: Color::Rgb { r: 0x2e, g: 0x3c, b: 0x64 },
                keyword: Color::Rgb { r: 0xbb, g: 0x9a, b: 0xf7 },
                string: Color::Rgb { r: 0x9e, g: 0xce, b: 0x6a },
                comment: Color::Rgb { r: 0x56, g: 0x5f, b: 0x89 },
                number: Color::Rgb { r: 0xff, g: 0x9e, b: 0x64 },
                type_name: Color::Rgb { r: 0x2a, g: 0xc3, b: 0xde },
                function: Color::Rgb { r: 0x7a, g: 0xa2, b: 0xf7 },
                operator: Color::Rgb { r: 0xc0, g: 0xca, b: 0xf5 },
                heading: Color::Rgb { r: 0xff, g: 0x9e, b: 0x64 },
            }
        } else {
            Self {
                bg: Color::Reset,
                fg: Color::Reset,
                status_bg: Color::Blue,
                status_fg: Color::White,
                line_number: Color::DarkGrey,
                cursor_line_bg: Color::Reset,
                selection_bg: Color::DarkBlue,
                keyword: Color::Magenta,
                string: Color::Green,
                comment: Color::DarkGrey,
                number: Color::Yellow,
                type_name: Color::Cyan,
                function: Color::Blue,
                operator: Color::Reset,
                heading: Color::Yellow,
            }
        }
    }

    fn color_for(&self, kind: TokenKind) -> Color {
        match kind {
            TokenKind::Plain => self.fg,
            TokenKind::Keyword => self.keyword,
            TokenKind::Type => self.type_name,
            TokenKind::Function => self.function,
            TokenKind::String => self.string,
            TokenKind::Number => self.number,
            TokenKind::Comment => self.comment,
            TokenKind::Operator => self.operator,
            TokenKind::Heading => self.heading,
        }
    }
}

// ---------------------------------------------------------------------------
// drawing helpers
// ---------------------------------------------------------------------------

fn draw_tab_bar(editor: &Editor, term: &mut Terminal, theme: &Theme, row: u16) -> io::Result<()> {
    queue!(
        term.out(),
        cursor::MoveTo(0, row),
        SetBackgroundColor(theme.status_bg),
        Clear(ClearType::CurrentLine)
    )?;
    let mut used = 0usize;
    for (i, b) in editor.buffers.iter().enumerate() {
        let mark = if b.modified { "*" } else { "" };
        let label = format!(" {}{} ", b.filename(), mark);
        let w = UnicodeWidthStr::width(label.as_str());
        if used + w > editor.width as usize {
            break;
        }
        if i == editor.current {
            queue!(
                term.out(),
                SetForegroundColor(theme.fg),
                SetBackgroundColor(theme.bg),
                Print(&label)
            )?;
        } else {
            queue!(
                term.out(),
                SetForegroundColor(theme.line_number),
                SetBackgroundColor(theme.status_bg),
                Print(&label)
            )?;
        }
        used += w;
    }
    if used < editor.width as usize {
        queue!(
            term.out(),
            SetBackgroundColor(theme.status_bg),
            Print(" ".repeat(editor.width as usize - used))
        )?;
    }
    queue!(term.out(), ResetColor)?;
    Ok(())
}

fn draw_status_bar(editor: &Editor, term: &mut Terminal, theme: &Theme, row: u16) -> io::Result<()> {
    let buf = editor.buf();
    let cur_row = buf.cursor.row + 1;
    let cur_col = buf.cursor_display_col() + 1;
    let modified = if buf.modified { " [modified]" } else { "" };
    let left = format!(
        "Sintexy  {}{}  [{}]",
        buf.filename(),
        modified,
        buf.language.name()
    );
    let right = format!(
        "Ln {}, Col {}  |  {}  |  {}",
        cur_row,
        cur_col,
        buf.encoding,
        buf.line_ending.as_str()
    );
    let total = editor.width as usize;
    let lw = UnicodeWidthStr::width(left.as_str());
    let rw = UnicodeWidthStr::width(right.as_str());
    let pad = total.saturating_sub(lw + rw);
    let line = if lw + rw <= total {
        format!("{left}{}{right}", " ".repeat(pad))
    } else {
        truncate_to_width(&left, total)
    };
    queue!(
        term.out(),
        cursor::MoveTo(0, row),
        SetBackgroundColor(theme.status_bg),
        SetForegroundColor(theme.status_fg),
        Print(truncate_to_width(&line, total)),
    )?;

    queue!(
        term.out(),
        cursor::MoveTo(0, row + 1),
        SetBackgroundColor(theme.bg),
        SetForegroundColor(theme.fg),
        Clear(ClearType::CurrentLine),
        Print(truncate_to_width(&editor.status_message, total)),
        ResetColor
    )?;
    Ok(())
}

fn draw_mode_bar(editor: &Editor, term: &mut Terminal, theme: &Theme, row: u16) -> io::Result<()> {
    let text = match &editor.mode {
        Mode::Search { query, .. } => format!("Search [Enter=next, Esc=cancel]: {query}"),
        Mode::SavePrompt { input, .. } => format!("Save as: {input}"),
        Mode::QuitConfirm => "Save modified buffers? [y/n]  (Esc to cancel)".to_string(),
        _ => return Ok(()),
    };
    queue!(
        term.out(),
        cursor::MoveTo(0, row),
        SetBackgroundColor(theme.bg),
        Clear(ClearType::CurrentLine),
        SetForegroundColor(theme.status_fg),
        Print(truncate_to_width(&text, editor.width as usize)),
        ResetColor
    )?;
    Ok(())
}

fn draw_content(
    editor: &Editor,
    term: &mut Terminal,
    theme: &Theme,
    start_row: u16,
    rows: u16,
) -> io::Result<()> {
    let gutter = editor.gutter_width() as usize;
    let content_cols = editor.content_cols() as usize;
    let buf = editor.buf();
    let search_q = editor.current_search().filter(|q| !q.is_empty());

    let mut state = LineState::Normal;
    for line in buf.lines.iter().take(buf.viewport_top) {
        let (_, ns) = highlight_line(buf.language, line, state);
        state = ns;
    }

    for screen_row in 0..rows {
        let line_idx = buf.viewport_top + screen_row as usize;
        let terminal_row = start_row + screen_row;
        let dirty = buf.viewport_dirty
            || line_idx >= buf.line_dirty.len()
            || buf.line_dirty[line_idx]
            || line_idx >= buf.lines.len();

        let (tokens, next_state) = if line_idx < buf.lines.len() {
            highlight_line(buf.language, &buf.lines[line_idx], state)
        } else {
            (Vec::new(), state)
        };

        if dirty {
            paint_row(
                term,
                theme,
                buf,
                editor,
                terminal_row,
                line_idx,
                gutter,
                content_cols,
                &tokens,
                search_q,
            )?;
        }
        state = next_state;
    }
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn paint_row(
    term: &mut Terminal,
    theme: &Theme,
    buf: &crate::buffer::Buffer,
    _editor: &Editor,
    terminal_row: u16,
    line_idx: usize,
    gutter: usize,
    content_cols: usize,
    tokens: &[Token],
    search_q: Option<&str>,
) -> io::Result<()> {
    let is_cursor_line = line_idx == buf.cursor.row && line_idx < buf.lines.len();
    let bg = if is_cursor_line {
        theme.cursor_line_bg
    } else {
        theme.bg
    };
    queue!(
        term.out(),
        cursor::MoveTo(0, terminal_row),
        SetBackgroundColor(bg),
        Clear(ClearType::CurrentLine)
    )?;

    if gutter > 0 {
        let pad = gutter.saturating_sub(2);
        queue!(term.out(), SetForegroundColor(theme.line_number))?;
        if line_idx < buf.lines.len() {
            queue!(term.out(), Print(format!("{:>pad$}  ", line_idx + 1, pad = pad)))?;
        } else {
            queue!(term.out(), Print(format!("{:>pad$}  ", "~", pad = pad)))?;
        }
    }

    if line_idx >= buf.lines.len() {
        if content_cols > 0 {
            queue!(
                term.out(),
                SetForegroundColor(theme.line_number),
                Print(" ".repeat(content_cols))
            )?;
        }
        queue!(term.out(), ResetColor)?;
        return Ok(());
    }

    let line = &buf.lines[line_idx];
    let viewport_left = buf.viewport_left;
    let matches: Vec<(usize, usize)> = search_q
        .map(|q| buf.matches_on(line_idx, q))
        .unwrap_or_default();

    let mut display_col = 0usize;
    let mut emitted = 0usize;
    let mut last_kind: Option<TokenKind> = None;
    let mut last_match_state: Option<bool> = None;

    for (i, g) in line.grapheme_indices(true) {
        let w = UnicodeWidthStr::width(g).max(1);
        let next_display = display_col + w;
        if next_display <= viewport_left {
            display_col = next_display;
            continue;
        }
        if display_col >= viewport_left + content_cols {
            break;
        }
        let kind = tokens
            .iter()
            .find(|t| t.start <= i && t.end > i)
            .map(|t| t.kind)
            .unwrap_or(TokenKind::Plain);
        if last_kind != Some(kind) {
            queue!(term.out(), SetForegroundColor(theme.color_for(kind)))?;
            last_kind = Some(kind);
        }
        let in_match = matches.iter().any(|(s, e)| i >= *s && i < *e);
        if last_match_state != Some(in_match) {
            let nb = if in_match {
                theme.selection_bg
            } else if is_cursor_line {
                theme.cursor_line_bg
            } else {
                theme.bg
            };
            queue!(term.out(), SetBackgroundColor(nb))?;
            last_match_state = Some(in_match);
        }

        if display_col < viewport_left {
            let visible = next_display - viewport_left;
            queue!(term.out(), Print(" ".repeat(visible)))?;
            emitted += visible;
        } else if next_display > viewport_left + content_cols {
            let visible = viewport_left + content_cols - display_col;
            queue!(term.out(), Print(" ".repeat(visible)))?;
            emitted += visible;
        } else {
            queue!(term.out(), Print(g))?;
            emitted += w;
        }
        display_col = next_display;
    }

    if last_match_state == Some(true) {
        queue!(term.out(), SetBackgroundColor(bg))?;
    }
    if emitted < content_cols {
        queue!(term.out(), Print(" ".repeat(content_cols - emitted)))?;
    }
    queue!(term.out(), ResetColor)?;
    Ok(())
}

fn draw_help_overlay(editor: &Editor, term: &mut Terminal, theme: &Theme) -> io::Result<()> {
    const BINDINGS: &[(&str, &str)] = &[
        ("Ctrl-Q", "Quit (prompt if unsaved)"),
        ("Ctrl-S", "Save"),
        ("Ctrl-Shift-S", "Save as…"),
        ("Ctrl-F", "Find"),
        ("Ctrl-N", "Toggle line numbers"),
        ("Ctrl-T", "New tab"),
        ("Ctrl-Tab", "Cycle tabs"),
        ("Ctrl-W", "Close tab"),
        ("Ctrl-Z / Ctrl-Y", "Undo / Redo"),
        ("F1 / Ctrl-H", "Help"),
        ("Arrows", "Move cursor"),
        ("Ctrl-←/→", "Word jump"),
        ("Home / End", "Line start / end"),
        ("PgUp / PgDn", "Page up / down"),
        ("Tab", "Indent (spaces)"),
        ("Esc", "Cancel prompt / overlay"),
    ];
    let title = "Sintexy — Keybindings";
    let body_width = BINDINGS
        .iter()
        .map(|(k, d)| k.len() + d.len() + 4)
        .max()
        .unwrap_or(40)
        .max(title.len() + 4);
    let box_w = (body_width + 4).min(editor.width.saturating_sub(2) as usize).max(20);
    let box_h = (BINDINGS.len() + 5).min(editor.height.saturating_sub(2) as usize).max(8);
    let start_x = ((editor.width as usize).saturating_sub(box_w)) / 2;
    let start_y = ((editor.height as usize).saturating_sub(box_h)) / 2;

    for r in 0..box_h {
        queue!(
            term.out(),
            cursor::MoveTo(start_x as u16, (start_y + r) as u16),
            SetBackgroundColor(theme.status_bg),
            SetForegroundColor(theme.status_fg)
        )?;
        if r == 0 {
            let bar = "─".repeat(box_w.saturating_sub(2));
            queue!(term.out(), Print(format!("┌{bar}┐")))?;
        } else if r == box_h - 1 {
            let bar = "─".repeat(box_w.saturating_sub(2));
            queue!(term.out(), Print(format!("└{bar}┘")))?;
        } else if r == 1 {
            let inner = box_w.saturating_sub(2);
            let pad = inner.saturating_sub(title.len());
            let lpad = pad / 2;
            let rpad = pad - lpad;
            queue!(
                term.out(),
                Print(format!("│{}{title}{}│", " ".repeat(lpad), " ".repeat(rpad)))
            )?;
        } else if r == 2 {
            let bar = "─".repeat(box_w.saturating_sub(2));
            queue!(term.out(), Print(format!("├{bar}┤")))?;
        } else if r == box_h - 2 {
            let foot = "  press any key to dismiss  ";
            let inner = box_w.saturating_sub(2);
            let pad = inner.saturating_sub(foot.len());
            let lpad = pad / 2;
            let rpad = pad - lpad;
            queue!(
                term.out(),
                Print(format!("│{}{foot}{}│", " ".repeat(lpad), " ".repeat(rpad)))
            )?;
        } else {
            let idx = r - 3;
            let inner = box_w.saturating_sub(2);
            if let Some((k, d)) = BINDINGS.get(idx) {
                let content = format!("  {k:<16} {d}");
                let trimmed = truncate_to_width(&content, inner);
                let pad = inner.saturating_sub(UnicodeWidthStr::width(trimmed.as_str()));
                queue!(term.out(), Print(format!("│{trimmed}{}│", " ".repeat(pad))))?;
            } else {
                queue!(term.out(), Print(format!("│{}│", " ".repeat(inner))))?;
            }
        }
        queue!(term.out(), ResetColor)?;
    }
    Ok(())
}

fn place_cursor(editor: &Editor, term: &mut Terminal) -> io::Result<()> {
    match &editor.mode {
        Mode::Search { query, .. } => {
            let prefix = "Search [Enter=next, Esc=cancel]: ";
            let col = prefix.chars().count() + query.chars().count();
            queue!(term.out(), cursor::MoveTo(col as u16, editor.height.saturating_sub(1)))?;
        }
        Mode::SavePrompt { input, .. } => {
            let prefix = "Save as: ";
            let col = prefix.chars().count() + input.chars().count();
            queue!(term.out(), cursor::MoveTo(col as u16, editor.height.saturating_sub(1)))?;
        }
        _ => {
            let buf = editor.buf();
            let scr_row = editor.content_start_row()
                + buf.cursor.row.saturating_sub(buf.viewport_top) as u16;
            let scr_col = editor.gutter_width()
                + buf.cursor_display_col().saturating_sub(buf.viewport_left) as u16;
            queue!(term.out(), cursor::MoveTo(scr_col, scr_row))?;
        }
    }
    Ok(())
}

fn truncate_to_width(s: &str, max: usize) -> String {
    let mut w = 0usize;
    let mut out = String::new();
    for g in s.graphemes(true) {
        let gw = UnicodeWidthStr::width(g);
        if w + gw > max {
            break;
        }
        out.push_str(g);
        w += gw;
    }
    out
}
